mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 15:52:09 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
|
|
@ -5,5 +5,12 @@ from . import ir_qweb_fields
|
|||
from . import ir_http
|
||||
from . import ir_model
|
||||
from . import ir_ui_menu
|
||||
from . import ir_ui_view
|
||||
from . import models
|
||||
from . import base_document_layout
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import res_users_settings_embedded_action
|
||||
from . import res_users_settings
|
||||
from . import res_users
|
||||
from . import properties_base_definition
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import markupsafe
|
||||
import os
|
||||
from markupsafe import Markup
|
||||
from math import ceil
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.base.models.assetsbundle import ScssStylesheetAsset
|
||||
from odoo.addons.base.models.ir_qweb_fields import nl2br
|
||||
from odoo.modules import get_resource_path
|
||||
from odoo.tools import file_path, html2plaintext, is_html_empty
|
||||
from odoo.tools import html2plaintext, is_html_empty, image as tools
|
||||
|
||||
try:
|
||||
import sass as libsass
|
||||
except ImportError:
|
||||
# If the `sass` python library isn't found, we fallback on the
|
||||
# `sassc` executable in the path.
|
||||
libsass = None
|
||||
try:
|
||||
from PIL.Image import Resampling
|
||||
except ImportError:
|
||||
|
|
@ -47,7 +38,7 @@ class BaseDocumentLayout(models.TransientModel):
|
|||
if 'company_name' not in address_format:
|
||||
address_format = '%(company_name)s\n' + address_format
|
||||
company_data['company_name'] = company_data['company_name'] or company.name
|
||||
return Markup(nl2br(address_format)) % company_data
|
||||
return nl2br(address_format) % company_data
|
||||
|
||||
def _clean_address_format(self, address_format, company_data):
|
||||
missing_company_data = [k for k, v in company_data.items() if not v]
|
||||
|
|
@ -113,7 +104,7 @@ class BaseDocumentLayout(models.TransientModel):
|
|||
@api.depends('logo')
|
||||
def _compute_logo_colors(self):
|
||||
for wizard in self:
|
||||
if wizard._context.get('bin_size'):
|
||||
if wizard.env.context.get('bin_size'):
|
||||
wizard_for_image = wizard.with_context(bin_size=False)
|
||||
else:
|
||||
wizard_for_image = wizard
|
||||
|
|
@ -126,22 +117,29 @@ class BaseDocumentLayout(models.TransientModel):
|
|||
|
||||
for wizard in self:
|
||||
if wizard.report_layout_id:
|
||||
# guarantees that bin_size is always set to False,
|
||||
# so the logo always contains the bin data instead of the binary size
|
||||
if wizard.env.context.get('bin_size'):
|
||||
wizard_with_logo = wizard.with_context(bin_size=False)
|
||||
else:
|
||||
wizard_with_logo = wizard
|
||||
preview_css = markupsafe.Markup(self._get_css_for_preview(styles, wizard_with_logo.id))
|
||||
ir_ui_view = wizard_with_logo.env['ir.ui.view']
|
||||
wizard.preview = ir_ui_view._render_template('web.report_invoice_wizard_preview', {
|
||||
'company': wizard_with_logo,
|
||||
'preview_css': preview_css,
|
||||
'is_html_empty': is_html_empty,
|
||||
})
|
||||
# guarantees that bin_size is always set to False,
|
||||
# so the logo always contains the bin data instead of the binary size
|
||||
wizard = wizard.with_context(bin_size=False)
|
||||
wizard.preview = wizard.env['ir.ui.view']._render_template(
|
||||
wizard._get_preview_template(),
|
||||
wizard._get_render_information(styles),
|
||||
)
|
||||
else:
|
||||
wizard.preview = False
|
||||
|
||||
def _get_preview_template(self):
|
||||
return 'web.report_invoice_wizard_preview'
|
||||
|
||||
def _get_render_information(self, styles):
|
||||
self.ensure_one()
|
||||
preview_css = self._get_css_for_preview(styles, self.id)
|
||||
return {
|
||||
'company': self,
|
||||
'preview_css': preview_css,
|
||||
'is_html_empty': is_html_empty,
|
||||
}
|
||||
|
||||
@api.onchange('company_id')
|
||||
def _onchange_company_id(self):
|
||||
for wizard in self:
|
||||
|
|
@ -204,7 +202,7 @@ class BaseDocumentLayout(models.TransientModel):
|
|||
:param white_threshold: arbitrary value defining the maximum value a color can reach
|
||||
:param mitigate: arbitrary value defining the maximum value a band can reach
|
||||
|
||||
:return colors: hex values of primary and secondary colors
|
||||
:return: a 2-value tuple with hex values of primary and secondary colors
|
||||
"""
|
||||
if not logo:
|
||||
return False, False
|
||||
|
|
@ -217,7 +215,7 @@ class BaseDocumentLayout(models.TransientModel):
|
|||
return False, False
|
||||
|
||||
base_w, base_h = image.size
|
||||
w = int(50 * base_w / base_h)
|
||||
w = ceil(50 * base_w / base_h)
|
||||
h = 50
|
||||
|
||||
# Converts to RGBA (if already RGBA, this is a noop)
|
||||
|
|
@ -251,14 +249,6 @@ class BaseDocumentLayout(models.TransientModel):
|
|||
|
||||
return tools.rgb_to_hex(primary), tools.rgb_to_hex(secondary)
|
||||
|
||||
@api.model
|
||||
def action_open_base_document_layout(self, action_ref=None):
|
||||
if not action_ref:
|
||||
action_ref = 'web.action_base_document_layout_configurator'
|
||||
res = self.env["ir.actions.actions"]._for_xml_id(action_ref)
|
||||
self.env[res["res_model"]].check_access_rights('write')
|
||||
return res
|
||||
|
||||
def document_layout_save(self):
|
||||
# meant to be overridden
|
||||
return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -281,45 +271,11 @@ class BaseDocumentLayout(models.TransientModel):
|
|||
"""
|
||||
Compile the scss into css.
|
||||
"""
|
||||
css_code = self._compile_scss(scss)
|
||||
return css_code
|
||||
|
||||
@api.model
|
||||
def _compile_scss(self, scss_source):
|
||||
"""
|
||||
This code will compile valid scss into css.
|
||||
Parameters are the same from odoo/addons/base/models/assetsbundle.py
|
||||
Simply copied and adapted slightly
|
||||
"""
|
||||
|
||||
def scss_importer(path, *args):
|
||||
*parent_path, file = os.path.split(path)
|
||||
try:
|
||||
parent_path = file_path(os.path.join(*parent_path))
|
||||
except FileNotFoundError:
|
||||
parent_path = file_path(os.path.join(bootstrap_path, *parent_path))
|
||||
return [(os.path.join(parent_path, file),)]
|
||||
|
||||
# No scss ? still valid, returns empty css
|
||||
if not scss_source.strip():
|
||||
if not scss.strip():
|
||||
return ""
|
||||
|
||||
precision = 8
|
||||
output_style = 'expanded'
|
||||
bootstrap_path = get_resource_path('web', 'static', 'lib', 'bootstrap', 'scss')
|
||||
|
||||
try:
|
||||
return libsass.compile(
|
||||
string=scss_source,
|
||||
include_paths=[
|
||||
bootstrap_path,
|
||||
],
|
||||
importers=[(0, scss_importer)],
|
||||
output_style=output_style,
|
||||
precision=precision,
|
||||
)
|
||||
except libsass.CompileError as e:
|
||||
raise libsass.CompileError(e.args[0])
|
||||
asset = ScssStylesheetAsset(None, inline='// css_for_preview')
|
||||
css_code = asset.compile(scss)
|
||||
return Markup(css_code) if isinstance(scss, Markup) else css_code
|
||||
|
||||
@api.depends('company_details')
|
||||
def _compute_empty_company_details(self):
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
import odoo
|
||||
from odoo import api, http, models
|
||||
from odoo.http import request
|
||||
from odoo.tools import file_open, image_process, ustr
|
||||
from odoo.tools.misc import str2bool
|
||||
from odoo import api, models, fields
|
||||
from odoo.http import request, DEFAULT_MAX_CONTENT_LENGTH
|
||||
from odoo.tools import config
|
||||
from odoo.tools.misc import hmac, str2bool
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
Debug mode is stored in session and should always be a string.
|
||||
It can be activated with an URL query string `debug=<mode>` where mode
|
||||
|
|
@ -27,10 +21,10 @@ You can use any truthy/falsy value from `str2bool` (eg: 'on', 'f'..)
|
|||
Multiple debug modes can be activated simultaneously, separated with a
|
||||
comma (eg: 'tests, assets').
|
||||
"""
|
||||
ALLOWED_DEBUG_MODES = ['', '1', 'assets', 'tests', 'disable-t-cache']
|
||||
ALLOWED_DEBUG_MODES = ['', '1', 'assets', 'tests']
|
||||
|
||||
|
||||
class Http(models.AbstractModel):
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
bots = ["bot", "crawl", "slurp", "spider", "curl", "wget", "facebookexternalhit", "whatsapp", "trendsmapresolver", "pinterest", "instagram", "google-pagerenderer", "preview"]
|
||||
|
|
@ -42,6 +36,12 @@ class Http(models.AbstractModel):
|
|||
# timeit has been done to check the optimum method
|
||||
return any(bot in user_agent for bot in cls.bots)
|
||||
|
||||
@classmethod
|
||||
def _sanitize_cookies(cls, cookies):
|
||||
super()._sanitize_cookies(cookies)
|
||||
if cids := cookies.get('cids'):
|
||||
cookies['cids'] = '-'.join(cids.split(','))
|
||||
|
||||
@classmethod
|
||||
def _handle_debug(cls):
|
||||
debug = request.httprequest.args.get('debug')
|
||||
|
|
@ -58,12 +58,24 @@ class Http(models.AbstractModel):
|
|||
super()._pre_dispatch(rule, args)
|
||||
cls._handle_debug()
|
||||
|
||||
@classmethod
|
||||
def _post_logout(cls):
|
||||
super()._post_logout()
|
||||
request.future_response.set_cookie('cids', max_age=0)
|
||||
|
||||
def webclient_rendering_context(self):
|
||||
return {
|
||||
'menu_data': request.env['ir.ui.menu'].load_menus(request.session.debug),
|
||||
'color_scheme': self.color_scheme(),
|
||||
'session_info': self.session_info(),
|
||||
}
|
||||
|
||||
def color_scheme(self):
|
||||
return "light"
|
||||
|
||||
@api.model
|
||||
def lazy_session_info(self):
|
||||
return {}
|
||||
|
||||
def session_info(self):
|
||||
user = self.env.user
|
||||
session_uid = request.session.uid
|
||||
|
|
@ -79,55 +91,53 @@ class Http(models.AbstractModel):
|
|||
IrConfigSudo = self.env['ir.config_parameter'].sudo()
|
||||
max_file_upload_size = int(IrConfigSudo.get_param(
|
||||
'web.max_file_upload_size',
|
||||
default=128 * 1024 * 1024, # 128MiB
|
||||
default=DEFAULT_MAX_CONTENT_LENGTH,
|
||||
))
|
||||
mods = odoo.conf.server_wide_modules or []
|
||||
if request.db:
|
||||
mods = list(request.registry._init_modules) + mods
|
||||
is_internal_user = user._is_internal()
|
||||
session_info = {
|
||||
"uid": session_uid,
|
||||
"is_system": user._is_system() if session_uid else False,
|
||||
"is_admin": user._is_admin() if session_uid else False,
|
||||
"is_public": user._is_public(),
|
||||
"is_internal_user": is_internal_user,
|
||||
"user_context": user_context,
|
||||
"db": self.env.cr.dbname,
|
||||
"registry_hash": hmac(self.env(su=True), "webclient-cache", self.env.registry.registry_sequence),
|
||||
"user_settings": self.env['res.users.settings']._find_or_create_for_user(user)._res_users_settings_format(),
|
||||
"server_version": version_info.get('server_version'),
|
||||
"server_version_info": version_info.get('server_version_info'),
|
||||
"support_url": "https://www.odoo.com/buy",
|
||||
"name": user.name,
|
||||
"username": user.login,
|
||||
"quick_login": str2bool(IrConfigSudo.get_param('web.quick_login', default=True), True),
|
||||
"partner_write_date": fields.Datetime.to_string(user.partner_id.write_date),
|
||||
"partner_display_name": user.partner_id.display_name,
|
||||
"company_id": user.company_id.id if session_uid else None, # YTI TODO: Remove this from the user context
|
||||
"partner_id": user.partner_id.id if session_uid and user.partner_id else None,
|
||||
"web.base.url": IrConfigSudo.get_param('web.base.url', default=''),
|
||||
"active_ids_limit": int(IrConfigSudo.get_param('web.active_ids_limit', default='20000')),
|
||||
'profile_session': request.session.profile_session,
|
||||
'profile_collectors': request.session.profile_collectors,
|
||||
'profile_params': request.session.profile_params,
|
||||
'profile_session': request.session.get('profile_session'),
|
||||
'profile_collectors': request.session.get('profile_collectors'),
|
||||
'profile_params': request.session.get('profile_params'),
|
||||
"max_file_upload_size": max_file_upload_size,
|
||||
"home_action_id": user.action_id.id,
|
||||
"cache_hashes": {
|
||||
"translations": self.env['ir.http'].sudo().get_web_translations_hash(
|
||||
mods, request.session.context['lang']
|
||||
) if session_uid else None,
|
||||
},
|
||||
"currencies": self.sudo().get_currencies(),
|
||||
"currencies": self.env['res.currency'].get_all_currencies(),
|
||||
'bundle_params': {
|
||||
'lang': request.session.context['lang'],
|
||||
},
|
||||
'test_mode': config['test_enable'],
|
||||
'view_info': self.env['ir.ui.view'].get_view_info(),
|
||||
'groups': {
|
||||
'base.group_allow_export': user.has_group('base.group_allow_export') if session_uid else False,
|
||||
},
|
||||
}
|
||||
if request.session.debug:
|
||||
session_info['bundle_params']['debug'] = request.session.debug
|
||||
if self.env.user.has_group('base.group_user'):
|
||||
# the following is only useful in the context of a webclient bootstrapping
|
||||
# but is still included in some other calls (e.g. '/web/session/authenticate')
|
||||
# to avoid access errors and unnecessary information, it is only included for users
|
||||
# with access to the backend ('internal'-type users)
|
||||
menus = self.env['ir.ui.menu'].with_context(lang=request.session.context['lang']).load_menus(request.session.debug)
|
||||
ordered_menus = {str(k): v for k, v in menus.items()}
|
||||
menu_json_utf8 = json.dumps(ordered_menus, default=ustr, sort_keys=True).encode()
|
||||
session_info['cache_hashes'].update({
|
||||
"load_menus": hashlib.sha512(menu_json_utf8).hexdigest()[:64], # sha512/256
|
||||
})
|
||||
if is_internal_user:
|
||||
# We need sudo since a user may not have access to ancestor companies
|
||||
# We use `_get_company_ids` because it is cached and we sudo it because env.user return a sudo user.
|
||||
user_companies = self.env['res.company'].browse(user._get_company_ids()).sudo()
|
||||
disallowed_ancestor_companies_sudo = user_companies.parent_ids - user_companies
|
||||
all_companies_in_hierarchy_sudo = disallowed_ancestor_companies_sudo + user_companies
|
||||
session_info.update({
|
||||
# current_company should be default_company
|
||||
"user_companies": {
|
||||
|
|
@ -137,11 +147,22 @@ class Http(models.AbstractModel):
|
|||
'id': comp.id,
|
||||
'name': comp.name,
|
||||
'sequence': comp.sequence,
|
||||
} for comp in user.company_ids
|
||||
'child_ids': (comp.child_ids & user_companies).ids,
|
||||
'parent_id': comp.parent_id.id,
|
||||
'currency_id': comp.currency_id.id,
|
||||
} for comp in user_companies
|
||||
},
|
||||
'disallowed_ancestor_companies': {
|
||||
comp.id: {
|
||||
'id': comp.id,
|
||||
'name': comp.name,
|
||||
'sequence': comp.sequence,
|
||||
'child_ids': (comp.child_ids & all_companies_in_hierarchy_sudo).ids,
|
||||
'parent_id': comp.parent_id.id,
|
||||
} for comp in disallowed_ancestor_companies_sudo
|
||||
},
|
||||
},
|
||||
"show_effect": True,
|
||||
"display_switch_company_menu": user.has_group('base.group_multi_company') and len(user.company_ids) > 1,
|
||||
})
|
||||
return session_info
|
||||
|
||||
|
|
@ -152,16 +173,22 @@ class Http(models.AbstractModel):
|
|||
session_info = {
|
||||
'is_admin': user._is_admin() if session_uid else False,
|
||||
'is_system': user._is_system() if session_uid else False,
|
||||
'is_public': user._is_public(),
|
||||
"is_internal_user": user._is_internal(),
|
||||
'is_website_user': user._is_public() if session_uid else False,
|
||||
'user_id': user.id if session_uid else False,
|
||||
'uid': session_uid,
|
||||
"registry_hash": hmac(self.env(su=True), "webclient-cache", self.env.registry.registry_sequence),
|
||||
'is_frontend': True,
|
||||
'profile_session': request.session.profile_session,
|
||||
'profile_collectors': request.session.profile_collectors,
|
||||
'profile_params': request.session.profile_params,
|
||||
'profile_session': request.session.get('profile_session'),
|
||||
'profile_collectors': request.session.get('profile_collectors'),
|
||||
'profile_params': request.session.get('profile_params'),
|
||||
'show_effect': bool(request.env['ir.config_parameter'].sudo().get_param('base_setup.show_effect')),
|
||||
'currencies': self.env['res.currency'].get_all_currencies(),
|
||||
'quick_login': str2bool(request.env['ir.config_parameter'].sudo().get_param('web.quick_login', default=True), True),
|
||||
'bundle_params': {
|
||||
'lang': request.session.context['lang'],
|
||||
},
|
||||
'test_mode': config['test_enable'],
|
||||
}
|
||||
if request.session.debug:
|
||||
session_info['bundle_params']['debug'] = request.session.debug
|
||||
|
|
@ -173,7 +200,6 @@ class Http(models.AbstractModel):
|
|||
})
|
||||
return session_info
|
||||
|
||||
@api.deprecated("Deprecated since 19.0, use get_all_currencies on 'res.currency'")
|
||||
def get_currencies(self):
|
||||
Currency = self.env['res.currency']
|
||||
currencies = Currency.search([]).read(['symbol', 'position', 'decimal_places'])
|
||||
return {c['id']: {'symbol': c['symbol'], 'position': c['position'], 'digits': [69,c['decimal_places']]} for c in currencies}
|
||||
return self.env['res.currency'].get_all_currencies()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class IrModel(models.Model):
|
|||
accessible_models = []
|
||||
not_accessible_models = []
|
||||
for model in models:
|
||||
if self._check_model_access(model):
|
||||
if self._is_valid_for_model_selector(model):
|
||||
accessible_models.append(model)
|
||||
else:
|
||||
not_accessible_models.append({"display_name": model, "model": model})
|
||||
|
|
@ -34,9 +34,15 @@ class IrModel(models.Model):
|
|||
} for model in records]
|
||||
|
||||
@api.model
|
||||
def _check_model_access(self, model):
|
||||
return (self.env.user._is_internal() and model in self.env
|
||||
and self.env[model].check_access_rights("read", raise_exception=False))
|
||||
def _is_valid_for_model_selector(self, model):
|
||||
model = self.env.get(model)
|
||||
return (
|
||||
self.env.user._is_internal()
|
||||
and model is not None
|
||||
and model.has_access("read")
|
||||
and not model._transient
|
||||
and not model._abstract
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_available_models(self):
|
||||
|
|
@ -44,5 +50,49 @@ class IrModel(models.Model):
|
|||
Return the list of models the current user has access to, with their
|
||||
corresponding display name.
|
||||
"""
|
||||
accessible_models = [model for model in self.pool.keys() if self._check_model_access(model)]
|
||||
accessible_models = [model for model in self.pool if self._is_valid_for_model_selector(model)]
|
||||
return self._display_name_for(accessible_models)
|
||||
|
||||
def _get_definitions(self, model_names):
|
||||
model_definitions = {}
|
||||
for model_name in model_names:
|
||||
model = self.env[model_name]
|
||||
# get fields, relational fields are kept only if the related model is in model_names
|
||||
fields_data_by_fname = {
|
||||
fname: field_data
|
||||
for fname, field_data in model.fields_get(
|
||||
attributes={
|
||||
'definition_record_field', 'definition_record', 'aggregator',
|
||||
'name', 'readonly', 'related', 'relation', 'required', 'searchable',
|
||||
'selection', 'sortable', 'store', 'string', 'tracking', 'type',
|
||||
},
|
||||
).items()
|
||||
if field_data.get('selectable', True) and (
|
||||
not field_data.get('relation') or field_data['relation'] in model_names
|
||||
)
|
||||
}
|
||||
fields_data_by_fname = {
|
||||
fname: field_data
|
||||
for fname, field_data in fields_data_by_fname.items()
|
||||
if not field_data.get('related') or field_data['related'].split('.')[0] in fields_data_by_fname
|
||||
}
|
||||
for fname, field_data in fields_data_by_fname.items():
|
||||
if fname in model._fields:
|
||||
inverse_fields = [
|
||||
field for field in model.pool.field_inverses[model._fields[fname]]
|
||||
if field.model_name in model_names
|
||||
and model.env[field.model_name]._has_field_access(field, 'read')
|
||||
]
|
||||
if inverse_fields:
|
||||
field_data['inverse_fname_by_model_name'] = {field.model_name: field.name for field in inverse_fields}
|
||||
if field_data['type'] == 'many2one_reference':
|
||||
field_data['model_name_ref_fname'] = model._fields[fname].model_field
|
||||
model_definitions[model_name] = {
|
||||
'description': model._description,
|
||||
'fields': fields_data_by_fname,
|
||||
'inherit': [model_name for model_name in model._inherit_module if model_name in model_names],
|
||||
'order': model._order,
|
||||
'parent_name': model._parent_name,
|
||||
'rec_name': model._rec_name,
|
||||
}
|
||||
return model_definitions
|
||||
|
|
|
|||
|
|
@ -6,20 +6,17 @@ from collections import OrderedDict
|
|||
from werkzeug.urls import url_quote
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools import pycompat
|
||||
from odoo import api, models, fields
|
||||
from odoo.tools import html_escape as escape
|
||||
|
||||
|
||||
class Image(models.AbstractModel):
|
||||
class IrQwebFieldImage(models.AbstractModel):
|
||||
"""
|
||||
Widget options:
|
||||
|
||||
``class``
|
||||
set as attribute on the generated <img> tag
|
||||
"""
|
||||
_name = 'ir.qweb.field.image'
|
||||
_description = 'Qweb Field Image'
|
||||
_inherit = 'ir.qweb.field.image'
|
||||
|
||||
def _get_src_urls(self, record, field_name, options):
|
||||
|
|
@ -36,7 +33,7 @@ class Image(models.AbstractModel):
|
|||
if max_width or max_height:
|
||||
max_size = '%sx%s' % (max_width, max_height)
|
||||
|
||||
sha = hashlib.sha512(str(getattr(record, '__last_update')).encode('utf-8')).hexdigest()[:7]
|
||||
sha = hashlib.sha512(str(getattr(record, 'write_date', fields.Datetime.now())).encode('utf-8')).hexdigest()[:7]
|
||||
max_size = '' if max_size is None else '/%s' % max_size
|
||||
|
||||
if options.get('filename-field') and options['filename-field'] in record and record[options['filename-field']]:
|
||||
|
|
@ -106,16 +103,16 @@ class Image(models.AbstractModel):
|
|||
for name, value in atts.items():
|
||||
if value:
|
||||
img.append(' ')
|
||||
img.append(escape(pycompat.to_text(name)))
|
||||
img.append(escape(name))
|
||||
img.append('="')
|
||||
img.append(escape(pycompat.to_text(value)))
|
||||
img.append(escape(value))
|
||||
img.append('"')
|
||||
img.append('/>')
|
||||
|
||||
return Markup(''.join(img))
|
||||
|
||||
class ImageUrlConverter(models.AbstractModel):
|
||||
_description = 'Qweb Field Image'
|
||||
|
||||
class IrQwebFieldImage_Url(models.AbstractModel):
|
||||
_inherit = 'ir.qweb.field.image_url'
|
||||
|
||||
def _get_src_urls(self, record, field_name, options):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
|
|
@ -30,22 +32,43 @@ class IrUiMenu(models.Model):
|
|||
"xmlid": "",
|
||||
"actionID": False,
|
||||
"actionModel": False,
|
||||
"actionPath": False,
|
||||
"webIcon": None,
|
||||
"webIconData": None,
|
||||
"webIconDataMimetype": None,
|
||||
"backgroundImage": menu.get('backgroundImage'),
|
||||
}
|
||||
else:
|
||||
action = menu['action']
|
||||
action_id = menu['action_id']
|
||||
action_model = menu['action_model']
|
||||
action_path = menu['action_path']
|
||||
web_icon = menu['web_icon']
|
||||
web_icon_data = menu['web_icon_data']
|
||||
|
||||
if menu['id'] == menu['app_id']:
|
||||
# if it's an app take action of first (sub)child having one defined
|
||||
child = menu
|
||||
while child and not action:
|
||||
action = child['action']
|
||||
while child and not action_id:
|
||||
action_id = child['action_id']
|
||||
action_model = child['action_model']
|
||||
action_path = child['action_path']
|
||||
child = menus[child['children'][0]] if child['children'] else False
|
||||
|
||||
action_model, action_id = action.split(',') if action else (False, False)
|
||||
action_id = int(action_id) if action_id else False
|
||||
webIcon = menu.get('web_icon', '')
|
||||
webIconlist = webIcon and webIcon.split(',')
|
||||
iconClass = color = backgroundColor = None
|
||||
if webIconlist:
|
||||
if len(webIconlist) >= 2:
|
||||
iconClass, color = webIconlist[:2]
|
||||
if len(webIconlist) == 3:
|
||||
backgroundColor = webIconlist[2]
|
||||
|
||||
if menu.get('web_icon_data'):
|
||||
web_icon_data = re.sub(r'\s/g', "", ('data:%s;base64,%s' % (menu['web_icon_data_mimetype'], menu['web_icon_data'])))
|
||||
elif backgroundColor is not None: # Could split in three parts?
|
||||
web_icon = ",".join([iconClass or "", color or "", backgroundColor])
|
||||
else:
|
||||
web_icon_data = '/web/static/img/default_icon_app.png'
|
||||
|
||||
web_menus[menu['id']] = {
|
||||
"id": menu['id'],
|
||||
|
|
@ -55,8 +78,10 @@ class IrUiMenu(models.Model):
|
|||
"xmlid": menu['xmlid'],
|
||||
"actionID": action_id,
|
||||
"actionModel": action_model,
|
||||
"webIcon": menu['web_icon'],
|
||||
"webIconData": menu['web_icon_data'],
|
||||
"actionPath": action_path,
|
||||
"webIcon": web_icon,
|
||||
"webIconData": web_icon_data,
|
||||
"webIconDataMimetype": menu['web_icon_data_mimetype'],
|
||||
}
|
||||
|
||||
return web_menus
|
||||
|
|
|
|||
31
odoo-bringout-oca-ocb-web/web/models/ir_ui_view.py
Normal file
31
odoo-bringout-oca-ocb-web/web/models/ir_ui_view.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrUiView(models.Model):
|
||||
_inherit = 'ir.ui.view'
|
||||
|
||||
def get_view_info(self):
|
||||
_view_info = self._get_view_info()
|
||||
return {
|
||||
type_: {
|
||||
'display_name': display_name,
|
||||
'icon': _view_info[type_]['icon'],
|
||||
'multi_record': _view_info[type_].get('multi_record', True),
|
||||
}
|
||||
for (type_, display_name)
|
||||
in self.fields_get(['type'], ['selection'])['type']['selection']
|
||||
if type_ != 'qweb' and type_ in _view_info
|
||||
}
|
||||
|
||||
def _get_view_info(self):
|
||||
return {
|
||||
'list': {'icon': 'oi oi-view-list'},
|
||||
'form': {'icon': 'fa fa-address-card', 'multi_record': False},
|
||||
'graph': {'icon': 'fa fa-area-chart'},
|
||||
'pivot': {'icon': 'oi oi-view-pivot'},
|
||||
'kanban': {'icon': 'oi oi-view-kanban'},
|
||||
'calendar': {'icon': 'fa fa-calendar'},
|
||||
'search': {'icon': 'oi oi-search'},
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,21 @@
|
|||
from odoo import _, api, models
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class PropertiesBaseDefinition(models.Model):
|
||||
_inherit = "properties.base.definition"
|
||||
|
||||
@api.model
|
||||
def get_properties_base_definition(self, model_name, field_name):
|
||||
"""Return the base properties definition if we can read the model."""
|
||||
model = self.env[model_name]
|
||||
model.check_access("read")
|
||||
if model._fields[field_name].type != "properties":
|
||||
raise AccessError(_("You can not read that field definition."))
|
||||
return self.sudo().web_search_read(
|
||||
[
|
||||
["properties_field_id.name", "=", field_name],
|
||||
["properties_field_id.model", "=", model_name],
|
||||
],
|
||||
specification={"display_name": {}, "properties_definition": {}},
|
||||
)
|
||||
10
odoo-bringout-oca-ocb-web/web/models/res_config_settings.py
Normal file
10
odoo-bringout-oca-ocb-web/web/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
web_app_name = fields.Char('Web App Name', config_parameter='web.web_app_name')
|
||||
97
odoo-bringout-oca-ocb-web/web/models/res_partner.py
Normal file
97
odoo-bringout-oca-ocb-web/web/models/res_partner.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
from base64 import b64decode
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools.facade import Proxy, ProxyAttr, ProxyFunc
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import vobject.vcard
|
||||
except ImportError:
|
||||
_logger.warning("`vobject` Python module not found, vcard file generation disabled. Consider installing this module if you want to generate vcard files")
|
||||
vobject = None
|
||||
|
||||
|
||||
if vobject is not None:
|
||||
|
||||
class VBaseProxy(Proxy):
|
||||
_wrapped__ = vobject.base.VBase
|
||||
|
||||
encoding_param = ProxyAttr()
|
||||
type_param = ProxyAttr()
|
||||
value = ProxyAttr(None)
|
||||
|
||||
class VCardContentsProxy(Proxy):
|
||||
_wrapped__ = dict
|
||||
|
||||
__delitem__ = ProxyFunc()
|
||||
__contains__ = ProxyFunc()
|
||||
get = ProxyFunc(lambda lines: [VBaseProxy(line) for line in lines])
|
||||
|
||||
class VComponentProxy(Proxy):
|
||||
_wrapped__ = vobject.base.Component
|
||||
|
||||
add = ProxyFunc(VBaseProxy)
|
||||
contents = ProxyAttr(VCardContentsProxy)
|
||||
serialize = ProxyFunc()
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def _build_vcard(self):
|
||||
""" Build the partner's vCard.
|
||||
:returns a vobject.vCard object
|
||||
"""
|
||||
if not vobject:
|
||||
return False
|
||||
vcard = vobject.vCard()
|
||||
# Name
|
||||
n = vcard.add('n')
|
||||
n.value = vobject.vcard.Name(family=self.name or self.complete_name or '')
|
||||
# Formatted Name
|
||||
fn = vcard.add('fn')
|
||||
fn.value = self.name or self.complete_name or ''
|
||||
# Address
|
||||
adr = vcard.add('adr')
|
||||
adr.value = vobject.vcard.Address(street=self.street or '', city=self.city or '', code=self.zip or '')
|
||||
if self.state_id:
|
||||
adr.value.region = self.state_id.name
|
||||
if self.country_id:
|
||||
adr.value.country = self.country_id.name
|
||||
# Email
|
||||
if self.email:
|
||||
email = vcard.add('email')
|
||||
email.value = self.email
|
||||
email.type_param = 'INTERNET'
|
||||
# Telephone numbers
|
||||
if self.phone:
|
||||
tel = vcard.add('tel')
|
||||
tel.type_param = 'work'
|
||||
tel.value = self.phone
|
||||
# URL
|
||||
if self.website:
|
||||
url = vcard.add('url')
|
||||
url.value = self.website
|
||||
# Organisation
|
||||
if self.commercial_company_name:
|
||||
org = vcard.add('org')
|
||||
org.value = [self.commercial_company_name]
|
||||
if self.function:
|
||||
function = vcard.add('title')
|
||||
function.value = self.function
|
||||
# Photo
|
||||
photo = vcard.add('photo')
|
||||
photo.value = b64decode(self.avatar_512)
|
||||
photo.encoding_param = 'B'
|
||||
photo.type_param = 'JPG'
|
||||
return VComponentProxy(vcard)
|
||||
|
||||
def _get_vcard_file(self):
|
||||
vcard = self._build_vcard()
|
||||
if vcard:
|
||||
return vcard.serialize().encode()
|
||||
return False
|
||||
36
odoo-bringout-oca-ocb-web/web/models/res_users.py
Normal file
36
odoo-bringout-oca-ocb-web/web/models/res_users.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.fields import Domain
|
||||
from odoo.http import request
|
||||
|
||||
SKIP_CAPTCHA_LOGIN = object()
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
@api.model
|
||||
def name_search(self, name='', domain=None, operator='ilike', limit=100):
|
||||
# if we have a search with a limit, move current user as the first result
|
||||
domain = Domain(domain or Domain.TRUE)
|
||||
user_list = super().name_search(name, domain, operator, limit)
|
||||
uid = self.env.uid
|
||||
# index 0 is correct not Falsy in this case, use None to avoid ignoring it
|
||||
if (index := next((i for i, (user_id, _name) in enumerate(user_list) if user_id == uid), None)) is not None:
|
||||
# move found user first
|
||||
user_tuple = user_list.pop(index)
|
||||
user_list.insert(0, user_tuple)
|
||||
elif limit is not None and len(user_list) == limit:
|
||||
# user not found and limit reached, try to find the user again
|
||||
if user_tuple := super().name_search(name, domain & Domain('id', '=', uid), operator, limit=1):
|
||||
user_list = [user_tuple[0], *user_list[:-1]]
|
||||
return user_list
|
||||
|
||||
def _on_webclient_bootstrap(self):
|
||||
self.ensure_one()
|
||||
|
||||
def _should_captcha_login(self, credential):
|
||||
if request and request.env.context.get('skip_captcha_login') is SKIP_CAPTCHA_LOGIN:
|
||||
return False
|
||||
return credential['type'] == 'password'
|
||||
36
odoo-bringout-oca-ocb-web/web/models/res_users_settings.py
Normal file
36
odoo-bringout-oca-ocb-web/web/models/res_users_settings.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
_inherit = 'res.users.settings'
|
||||
|
||||
embedded_actions_config_ids = fields.One2many('res.users.settings.embedded.action', 'user_setting_id')
|
||||
|
||||
@api.model
|
||||
def _format_settings(self, fields_to_format):
|
||||
res = super()._format_settings(fields_to_format)
|
||||
if 'embedded_actions_config_ids' in fields_to_format:
|
||||
res['embedded_actions_config_ids'] = self.embedded_actions_config_ids._embedded_action_settings_format()
|
||||
return res
|
||||
|
||||
def get_embedded_actions_settings(self):
|
||||
self.ensure_one()
|
||||
return self.embedded_actions_config_ids._embedded_action_settings_format()
|
||||
|
||||
def set_embedded_actions_setting(self, action_id, res_id, vals):
|
||||
self.ensure_one()
|
||||
embedded_actions_config = self.env['res.users.settings.embedded.action'].search([
|
||||
('user_setting_id', '=', self.id), ('action_id', '=', action_id), ('res_id', '=', res_id)
|
||||
], limit=1)
|
||||
for field, value in vals.items():
|
||||
if field in ('embedded_actions_order', 'embedded_actions_visibility'):
|
||||
vals[field] = ','.join('false' if action_id is False else str(action_id) for action_id in value)
|
||||
if embedded_actions_config:
|
||||
embedded_actions_config.write(vals)
|
||||
else:
|
||||
self.env['res.users.settings.embedded.action'].create({
|
||||
**vals,
|
||||
'user_setting_id': self.id,
|
||||
'action_id': action_id,
|
||||
'res_id': res_id,
|
||||
})
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ResUsersSettingsEmbeddedAction(models.Model):
|
||||
_name = 'res.users.settings.embedded.action'
|
||||
_description = 'User Settings for Embedded Actions'
|
||||
|
||||
user_setting_id = fields.Many2one('res.users.settings', required=True, ondelete='cascade', index='btree_not_null', export_string_translation=False)
|
||||
action_id = fields.Many2one('ir.actions.act_window', required=True, ondelete='cascade', export_string_translation=False)
|
||||
res_model = fields.Char(required=True, export_string_translation=False)
|
||||
res_id = fields.Integer(export_string_translation=False)
|
||||
embedded_actions_order = fields.Char('List order of embedded action ids', export_string_translation=False)
|
||||
embedded_actions_visibility = fields.Char('List visibility of embedded actions ids', export_string_translation=False)
|
||||
embedded_visibility = fields.Boolean('Is top bar visible', export_string_translation=False)
|
||||
|
||||
_res_user_settings_embedded_action_unique = models.Constraint(
|
||||
'UNIQUE (user_setting_id, action_id, res_id)',
|
||||
'The user should have one unique embedded action setting per user setting, action and record id.',
|
||||
)
|
||||
|
||||
@api.constrains('embedded_actions_order')
|
||||
def _check_embedded_actions_order(self):
|
||||
self._check_embedded_actions_field_format('embedded_actions_order')
|
||||
|
||||
@api.constrains('embedded_actions_visibility')
|
||||
def _check_embedded_actions_visibility(self):
|
||||
self._check_embedded_actions_field_format('embedded_actions_visibility')
|
||||
|
||||
def _check_embedded_actions_field_format(self, field_name):
|
||||
for setting in self:
|
||||
value = setting[field_name]
|
||||
if not value:
|
||||
return
|
||||
action_ids = value.split(',')
|
||||
if len(action_ids) != len(set(action_ids)):
|
||||
raise ValidationError(
|
||||
self.env._(
|
||||
'The ids in %(field_name)s must not be duplicated: “%(action_ids)s”',
|
||||
field_name=field_name,
|
||||
action_ids=action_ids,
|
||||
)
|
||||
)
|
||||
for action_id in action_ids:
|
||||
if not (action_id.isdigit() or action_id == 'false'):
|
||||
raise ValidationError(
|
||||
self.env._(
|
||||
'The ids in %(field_name)s must only be integers or "false": “%(action_ids)s”',
|
||||
field_name=field_name,
|
||||
action_ids=action_ids,
|
||||
)
|
||||
)
|
||||
|
||||
def _embedded_action_settings_format(self):
|
||||
return {
|
||||
f'{setting.action_id.id}+{setting.res_id or ""}': {
|
||||
'embedded_actions_order': [
|
||||
False if action_id == 'false' else int(action_id) for action_id in setting.embedded_actions_order.split(',')
|
||||
] if setting.embedded_actions_order else [],
|
||||
'embedded_actions_visibility': [
|
||||
False if action_id == 'false' else int(action_id) for action_id in setting.embedded_actions_visibility.split(',')
|
||||
] if setting.embedded_actions_visibility else [],
|
||||
'embedded_visibility': setting.embedded_visibility,
|
||||
}
|
||||
for setting in self
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue