18.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:06:50 +02:00
parent d72e748793
commit 0a7ae8db93
337 changed files with 399651 additions and 232598 deletions

View file

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ruff: noqa: E402, F401
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
""" OpenERP core library.""" """ OpenERP core library."""
# ----------------------------------------------------------
#----------------------------------------------------------
# odoo must be a namespace package for odoo.addons to become one too # odoo must be a namespace package for odoo.addons to become one too
# https://packaging.python.org/guides/packaging-namespace-packages/ # https://packaging.python.org/guides/packaging-namespace-packages/
#---------------------------------------------------------- # ----------------------------------------------------------
import pkgutil import pkgutil
import os.path import os.path
__path__ = [ __path__ = [
@ -20,76 +20,9 @@ MIN_PY_VERSION = (3, 10)
MAX_PY_VERSION = (3, 13) MAX_PY_VERSION = (3, 13)
assert sys.version_info > MIN_PY_VERSION, f"Outdated python version detected, Odoo requires Python >= {'.'.join(map(str, MIN_PY_VERSION))} to run." assert sys.version_info > MIN_PY_VERSION, f"Outdated python version detected, Odoo requires Python >= {'.'.join(map(str, MIN_PY_VERSION))} to run."
#---------------------------------------------------------- # ----------------------------------------------------------
# Running mode flags (gevent, prefork)
#----------------------------------------------------------
# Is the server running with gevent.
evented = False
if len(sys.argv) > 1 and sys.argv[1] == 'gevent':
sys.argv.remove('gevent')
import gevent.monkey
import psycopg2
from gevent.socket import wait_read, wait_write
gevent.monkey.patch_all()
def gevent_wait_callback(conn, timeout=None):
"""A wait callback useful to allow gevent to work with Psycopg."""
# Copyright (C) 2010-2012 Daniele Varrazzo <daniele.varrazzo@gmail.com>
# This function is borrowed from psycogreen module which is licensed
# under the BSD license (see in odoo/debian/copyright)
while 1:
state = conn.poll()
if state == psycopg2.extensions.POLL_OK:
break
elif state == psycopg2.extensions.POLL_READ:
wait_read(conn.fileno(), timeout=timeout)
elif state == psycopg2.extensions.POLL_WRITE:
wait_write(conn.fileno(), timeout=timeout)
else:
raise psycopg2.OperationalError(
"Bad result from poll: %r" % state)
psycopg2.extensions.set_wait_callback(gevent_wait_callback)
evented = True
# Is the server running in prefork mode (e.g. behind Gunicorn).
# If this is True, the processes have to communicate some events,
# e.g. database update or cache invalidation. Each process has also
# its own copy of the data structure and we don't need to care about
# locks between threads.
multi_process = False
#----------------------------------------------------------
# libc UTC hack
#----------------------------------------------------------
# Make sure the OpenERP server runs in UTC.
import os
os.environ['TZ'] = 'UTC' # Set the timezone
import time
if hasattr(time, 'tzset'):
time.tzset()
# ---------------------------------------------------------
# some charset are known by Python under a different name
# ---------------------------------------------------------
import encodings.aliases # noqa: E402
encodings.aliases.aliases['874'] = 'cp874'
encodings.aliases.aliases['windows_874'] = 'cp874'
#----------------------------------------------------------
# alias hebrew iso-8859-8-i and iso-8859-8-e on iso-8859-8
# https://bugs.python.org/issue18624
#----------------------------------------------------------
import codecs
import re
iso8859_8 = codecs.lookup('iso8859_8')
iso8859_8ie_re = re.compile(r'iso[-_]?8859[-_]8[-_]?[ei]', re.IGNORECASE)
codecs.register(lambda charset: iso8859_8 if iso8859_8ie_re.match(charset) else None)
#----------------------------------------------------------
# Shortcuts # Shortcuts
#---------------------------------------------------------- # ----------------------------------------------------------
# The hard-coded super-user id (a.k.a. administrator, or root user). # The hard-coded super-user id (a.k.a. administrator, or root user).
SUPERUSER_ID = 1 SUPERUSER_ID = 1
@ -100,14 +33,25 @@ def registry(database_name=None):
on the current thread. If the registry does not exist yet, it is created on on the current thread. If the registry does not exist yet, it is created on
the fly. the fly.
""" """
import warnings # noqa: PLC0415
warnings.warn("Use directly odoo.modules.registry.Registry", DeprecationWarning, 2)
if database_name is None: if database_name is None:
import threading import threading
database_name = threading.current_thread().dbname database_name = threading.current_thread().dbname
return modules.registry.Registry(database_name) return modules.registry.Registry(database_name)
#----------------------------------------------------------
# ----------------------------------------------------------
# Import tools to patch code and libraries
# required to do as early as possible for evented and timezone
# ----------------------------------------------------------
from . import _monkeypatches
_monkeypatches.patch_all()
# ----------------------------------------------------------
# Imports # Imports
#---------------------------------------------------------- # ----------------------------------------------------------
from . import upgrade # this namespace must be imported first from . import upgrade # this namespace must be imported first
from . import addons from . import addons
from . import conf from . import conf
@ -120,17 +64,17 @@ from . import service
from . import sql_db from . import sql_db
from . import tools from . import tools
#---------------------------------------------------------- # ----------------------------------------------------------
# Model classes, fields, api decorators, and translations # Model classes, fields, api decorators, and translations
#---------------------------------------------------------- # ----------------------------------------------------------
from . import models from . import models
from . import fields from . import fields
from . import api from . import api
from odoo.tools.translate import _, _lt from odoo.tools.translate import _, _lt
from odoo.fields import Command from odoo.fields import Command
#---------------------------------------------------------- # ----------------------------------------------------------
# Other imports, which may require stuff from above # Other imports, which may require stuff from above
#---------------------------------------------------------- # ----------------------------------------------------------
from . import cli from . import cli
from . import http from . import http

View file

@ -0,0 +1,39 @@
# ruff: noqa: F401, PLC0415
# ignore import not at top of the file
import os
import time
from .evented import patch_evented
def set_timezone_utc():
os.environ['TZ'] = 'UTC' # Set the timezone
if hasattr(time, 'tzset'):
time.tzset()
def patch_all():
patch_evented()
set_timezone_utc()
from .codecs import patch_codecs
patch_codecs()
from .email import patch_email
patch_email()
from .mimetypes import patch_mimetypes
patch_mimetypes()
from .pytz import patch_pytz
patch_pytz()
from .literal_eval import patch_literal_eval
patch_literal_eval()
from .lxml import patch_lxml
patch_lxml()
from .num2words import patch_num2words
patch_num2words()
from .stdnum import patch_stdnum
patch_stdnum()
from .urllib3 import patch_urllib3
patch_urllib3()
from .werkzeug_urls import patch_werkzeug
patch_werkzeug()
from .zeep import patch_zeep
patch_zeep()

View file

@ -0,0 +1,26 @@
import codecs
import encodings.aliases
import re
import babel.core
def patch_codecs():
# ---------------------------------------------------------
# some charset are known by Python under a different name
# ---------------------------------------------------------
encodings.aliases.aliases['874'] = 'cp874'
encodings.aliases.aliases['windows_874'] = 'cp874'
# ---------------------------------------------------------
# alias hebrew iso-8859-8-i and iso-8859-8-e on iso-8859-8
# https://bugs.python.org/issue18624
# ---------------------------------------------------------
iso8859_8 = codecs.lookup('iso8859_8')
iso8859_8ie_re = re.compile(r'iso[-_]?8859[-_]8[-_]?[ei]', re.IGNORECASE)
codecs.register(lambda charset: iso8859_8 if iso8859_8ie_re.match(charset) else None)
# To remove when corrected in Babel
babel.core.LOCALE_ALIASES['nb'] = 'nb_NO'

View file

@ -0,0 +1,12 @@
from email._policybase import _PolicyBase
def patch_email():
def policy_clone(self, **kwargs):
for arg in kwargs:
if arg.startswith("_") or "__" in arg:
raise AttributeError(f"{self.__class__.__name__!r} object has no attribute {arg!r}")
return orig_policy_clone(self, **kwargs)
orig_policy_clone = _PolicyBase.clone
_PolicyBase.clone = policy_clone

View file

@ -0,0 +1,39 @@
"""
Running mode flags (gevent, prefork)
This should be imported as early as possible.
It will initialize the `odoo.evented` variable.
"""
import odoo
import sys
odoo.evented = False
def patch_evented():
if odoo.evented or not (len(sys.argv) > 1 and sys.argv[1] == 'gevent'):
return
sys.argv.remove('gevent')
import gevent.monkey # noqa: PLC0415
import psycopg2 # noqa: PLC0415
from gevent.socket import wait_read, wait_write # noqa: PLC0415
gevent.monkey.patch_all()
def gevent_wait_callback(conn, timeout=None):
"""A wait callback useful to allow gevent to work with Psycopg."""
# Copyright (C) 2010-2012 Daniele Varrazzo <daniele.varrazzo@gmail.com>
# This function is borrowed from psycogreen module which is licensed
# under the BSD license (see in odoo/debian/copyright)
while 1:
state = conn.poll()
if state == psycopg2.extensions.POLL_OK:
break
elif state == psycopg2.extensions.POLL_READ:
wait_read(conn.fileno(), timeout=timeout)
elif state == psycopg2.extensions.POLL_WRITE:
wait_write(conn.fileno(), timeout=timeout)
else:
raise psycopg2.OperationalError(
"Bad result from poll: %r" % state)
psycopg2.extensions.set_wait_callback(gevent_wait_callback)
odoo.evented = True

View file

@ -0,0 +1,32 @@
# ruff: noqa: E402, PLC0415
# ignore import not at top of the file
import ast
import logging
import os
_logger = logging.getLogger(__name__)
orig_literal_eval = ast.literal_eval
def literal_eval(expr):
# limit the size of the expression to avoid segmentation faults
# the default limit is set to 100KiB
# can be overridden by setting the ODOO_LIMIT_LITEVAL_BUFFER buffer_size_environment variable
buffer_size = 102400
buffer_size_env = os.getenv("ODOO_LIMIT_LITEVAL_BUFFER")
if buffer_size_env:
if buffer_size_env.isdigit():
buffer_size = int(buffer_size_env)
else:
_logger.error("ODOO_LIMIT_LITEVAL_BUFFER has to be an integer, defaulting to 100KiB")
if isinstance(expr, str) and len(expr) > buffer_size:
raise ValueError("expression can't exceed buffer limit")
return orig_literal_eval(expr)
def patch_literal_eval():
ast.literal_eval = literal_eval

View file

@ -0,0 +1,13 @@
import lxml.html.clean
import re
from importlib.metadata import version
from odoo.tools import parse_version
def patch_lxml():
# between these versions having a couple data urls in a style attribute
# or style node removes the attribute or node erroneously
if parse_version("4.6.0") <= parse_version(version('lxml')) < parse_version("5.2.0"):
lxml.html.clean._find_image_dataurls = re.compile(r'data:image/(.+?);base64,').findall

View file

@ -0,0 +1,15 @@
import mimetypes
def patch_mimetypes():
# if extension is already knows, the new definition will remplace the existing one
# Add potentially missing (older ubuntu) font mime types
mimetypes.add_type('application/font-woff', '.woff')
mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
mimetypes.add_type('application/x-font-ttf', '.ttf')
mimetypes.add_type('image/webp', '.webp')
# Add potentially wrong (detected on windows) svg mime types
mimetypes.add_type('image/svg+xml', '.svg')
# this one can be present on windows with the value 'text/plain' which
# breaks loading js files from an addon's static folder
mimetypes.add_type('text/javascript', '.js')

View file

@ -1,10 +1,13 @@
import decimal import decimal
import logging
import math import math
import re import re
from collections import OrderedDict from collections import OrderedDict
from decimal import ROUND_HALF_UP, Decimal from decimal import ROUND_HALF_UP, Decimal
from math import floor from math import floor
from odoo import MIN_PY_VERSION
# The following section of the code is used to monkey patch # The following section of the code is used to monkey patch
# the Arabic class of num2words package as there are some problems # the Arabic class of num2words package as there are some problems
# upgrading the package to the newer version that fixed the bugs # upgrading the package to the newer version that fixed the bugs
@ -708,3 +711,274 @@ def to_s(val):
return unicode(val) return unicode(val)
except NameError: except NameError:
return str(val) return str(val)
# Derived from num2cyrillic licensed under LGPL-3.0-only
# Copyright 2018 ClaimCompass, Inc (num2cyrillic authored by Velizar Shulev) https://github.com/ClaimCompass/num2cyrillic
# Copyright 1997 The PHP Group (PEAR::Numbers_Words, authored by Kouber Saparev) https://github.com/pear/Numbers_Words/blob/master/Numbers/Words/Locale/bg.php
class NumberToWords_BG(Num2Word_Base):
locale = 'bg'
lang = 'Bulgarian'
lang_native = 'Български'
_misc_strings = {
'deset': 'десет',
'edinadeset': 'единадесет',
'na': 'на',
'sto': 'сто',
'sta': 'ста',
'stotin': 'стотин',
'hiliadi': 'хиляди',
}
_digits = {
0: [None, 'едно', 'две', 'три', 'четири', 'пет', 'шест', 'седем', 'осем', 'девет'],
}
_digits[1] = [None, 'един', 'два'] + _digits[0][3:]
_digits[-1] = [None, 'една'] + _digits[0][2:]
_last_and = False
_zero = 'нула'
_infinity = 'безкрайност'
_and = 'и'
_sep = ' '
_minus = 'минус'
_plural = 'а'
_exponent = {
0: '',
3: 'хиляда',
6: 'милион',
9: 'милиард',
12: 'трилион',
15: 'квадрилион',
18: 'квинтилион',
21: 'секстилион',
24: 'септилион',
27: 'октилион',
30: 'ноналион',
33: 'декалион',
36: 'ундекалион',
39: 'дуодекалион',
42: 'тредекалион',
45: 'кватордекалион',
48: 'квинтдекалион',
51: 'сексдекалион',
54: 'септдекалион',
57: 'октодекалион',
60: 'новемдекалион',
63: 'вигинтилион',
66: 'унвигинтилион',
69: 'дуовигинтилион',
72: 'тревигинтилион',
75: 'кваторвигинтилион',
78: 'квинвигинтилион',
81: 'сексвигинтилион',
84: 'септенвигинтилион',
87: 'октовигинтилион',
90: 'новемвигинтилион',
93: 'тригинтилион',
96: 'унтригинтилион',
99: 'дуотригинтилион',
102: 'третригинтилион',
105: 'кватортригинтилион',
108: 'квинтригинтилион',
111: 'секстригинтилион',
114: 'септентригинтилион',
117: 'октотригинтилион',
120: 'новемтригинтилион',
123: 'квадрагинтилион',
126: 'унквадрагинтилион',
129: 'дуоквадрагинтилион',
132: 'треквадрагинтилион',
135: 'кваторквадрагинтилион',
138: 'квинквадрагинтилион',
141: 'сексквадрагинтилион',
144: 'септенквадрагинтилион',
147: 'октоквадрагинтилион',
150: 'новемквадрагинтилион',
153: 'квинквагинтилион',
156: 'унквинкагинтилион',
159: 'дуоквинкагинтилион',
162: 'треквинкагинтилион',
165: 'кваторквинкагинтилион',
168: 'квинквинкагинтилион',
171: 'сексквинкагинтилион',
174: 'септенквинкагинтилион',
177: 'октоквинкагинтилион',
180: 'новемквинкагинтилион',
183: 'сексагинтилион',
186: 'унсексагинтилион',
189: 'дуосексагинтилион',
192: 'тресексагинтилион',
195: 'кваторсексагинтилион',
198: 'квинсексагинтилион',
201: 'секссексагинтилион',
204: 'септенсексагинтилион',
207: 'октосексагинтилион',
210: 'новемсексагинтилион',
213: 'септагинтилион',
216: 'унсептагинтилион',
219: 'дуосептагинтилион',
222: 'тресептагинтилион',
225: 'кваторсептагинтилион',
228: 'квинсептагинтилион',
231: 'секссептагинтилион',
234: 'септенсептагинтилион',
237: 'октосептагинтилион',
240: 'новемсептагинтилион',
243: 'октогинтилион',
246: 'уноктогинтилион',
249: 'дуооктогинтилион',
252: 'треоктогинтилион',
255: 'кватороктогинтилион',
258: 'квиноктогинтилион',
261: 'сексоктогинтилион',
264: 'септоктогинтилион',
267: 'октооктогинтилион',
270: 'новемоктогинтилион',
273: 'нонагинтилион',
276: 'уннонагинтилион',
279: 'дуононагинтилион',
282: 'тренонагинтилион',
285: 'кваторнонагинтилион',
288: 'квиннонагинтилион',
291: 'секснонагинтилион',
294: 'септеннонагинтилион',
297: 'октононагинтилион',
300: 'новемнонагинтилион',
303: 'центилион',
}
def to_cardinal(self, value):
return '' if value is None else self._to_words(value).strip()
def to_ordinal(self, _):
raise NotImplementedError
def to_ordinal_num(self, _):
raise NotImplementedError
def to_year(self, _):
raise NotImplementedError
def to_currency(self, _):
raise NotImplementedError
def _split_number(self, num):
if isinstance(num, int):
num = str(num)
first = []
if len(num) % 3 != 0:
if len(num[1:]) % 3 == 0:
first = [num[0:1]]
num = num[1:]
elif len(num[2:]) % 3 == 0:
first = [num[0:2]]
num = num[2:]
ret = [num[i:i + 3] for i in range(0, len(num), 3)]
return first + ret
def _discard_empties(self, ls):
return list(filter(lambda x: x is not None, ls))
def _show_digits_group(self, num, gender=0, last=False):
num = int(num)
e = int(num % 10) # ones
d = int((num - e) % 100 / 10) # tens
s = int((num - d * 10 - e) % 1000 / 100) # hundreds
ret = [None] * 6
if s:
if s == 1:
ret[1] = self._misc_strings['sto']
elif s == 2 or s == 3:
ret[1] = self._digits[0][s] + self._misc_strings['sta']
else:
ret[1] = self._digits[0][s] + self._misc_strings['stotin']
if d:
if d == 1:
if not e:
ret[3] = self._misc_strings['deset']
else:
if e == 1:
ret[3] = self._misc_strings['edinadeset']
else:
ret[3] = self._digits[1][e] + self._misc_strings['na'] + self._misc_strings['deset']
e = 0
else:
ret[3] = self._digits[1][d] + self._misc_strings['deset']
if e:
ret[5] = self._digits[gender][e]
if len(self._discard_empties(ret)) > 1:
if e:
ret[4] = self._and
else:
ret[2] = self._and
if last:
if not s or len(self._discard_empties(ret)) == 1:
ret[0] = self._and
self._last_and = True
return self._sep.join(self._discard_empties(ret))
def _to_words(self, num=0):
num_groups = self._split_number(num)
sizeof_num_groups = len(num_groups)
ret = [None] * (sizeof_num_groups + 1)
ret_minus = ''
if num < 0:
ret_minus = self._minus + self._sep
elif num == 0:
return self._zero
i = sizeof_num_groups - 1
j = 1
while i >= 0:
if ret[j] is None:
ret[j] = ''
_pow = sizeof_num_groups - i
if num_groups[i] != '000':
if int(num_groups[i]) > 1:
if _pow == 1:
ret[j] += self._show_digits_group(num_groups[i], 0, not self._last_and and i) + self._sep
ret[j] += self._exponent[(_pow - 1) * 3]
elif _pow == 2:
ret[j] += self._show_digits_group(num_groups[i], -1, not self._last_and and i) + self._sep
ret[j] += self._misc_strings['hiliadi'] + self._sep
else:
ret[j] += self._show_digits_group(num_groups[i], 1, not self._last_and and i) + self._sep
ret[j] += self._exponent[(_pow - 1) * 3] + self._plural + self._sep
else:
if _pow == 1:
ret[j] += self._show_digits_group(num_groups[i], 0, not self._last_and and i) + self._sep
elif _pow == 2:
ret[j] += self._exponent[(_pow - 1) * 3] + self._sep
else:
ret[j] += self._digits[1][1] + self._sep + self._exponent[(_pow - 1) * 3] + self._sep
i -= 1
j += 1
ret = self._discard_empties(ret)
ret.reverse()
return ret_minus + ''.join(ret)
def patch_num2words():
try:
import num2words # noqa: PLC0415
except ImportError:
_logger = logging.getLogger(__name__)
_logger.warning("num2words is not available, Arabic number to words conversion will not work")
return
if MIN_PY_VERSION >= (3, 12):
raise RuntimeError("The num2words monkey patch is obsolete. Bump the version of the library to the latest available in the official package repository, if it hasn't already been done, and remove the patch.")
num2words.CONVERTER_CLASSES["ar"] = Num2Word_AR_Fixed()
num2words.CONVERTER_CLASSES["bg"] = NumberToWords_BG()

View file

@ -119,14 +119,13 @@ _tz_mapping = {
"Zulu": "Etc/UTC", "Zulu": "Etc/UTC",
} }
original_pytz_timezone = pytz.timezone original_pytz_timezone = pytz.timezone
def timezone(name): def patch_pytz():
if name not in pytz.all_timezones_set and name in _tz_mapping: def timezone(name):
name = _tz_mapping[name] if name not in pytz.all_timezones_set and name in _tz_mapping:
return original_pytz_timezone(name) name = _tz_mapping[name]
return original_pytz_timezone(name)
pytz.timezone = timezone
pytz.timezone = timezone

View file

@ -0,0 +1,57 @@
# ruff: noqa: PLC0415
_soap_clients = {}
def new_get_soap_client(wsdlurl, timeout=30):
# stdnum library does not set the timeout for the zeep Transport class correctly
# (timeout is to fetch the wsdl and operation_timeout is to perform the call),
# requiring us to monkey patch the get_soap_client function.
# Can be removed when https://github.com/arthurdejong/python-stdnum/issues/444 is
# resolved and the version of the dependency is updated.
# The code is a copy of the original apart for the line related to the Transport class.
# This was done to keep the code as similar to the original and to reduce the possibility
# of introducing import errors, even though some imports are not in the requirements.
# See https://github.com/odoo/odoo/pull/173359 for a more thorough explanation.
if (wsdlurl, timeout) not in _soap_clients:
try:
from zeep.transports import Transport
transport = Transport(operation_timeout=timeout, timeout=timeout) # operational_timeout added here
from zeep import CachingClient
client = CachingClient(wsdlurl, transport=transport).service
except ImportError:
# fall back to non-caching zeep client
try:
from zeep import Client
client = Client(wsdlurl, transport=transport).service
except ImportError:
# other implementations require passing the proxy config
try:
from urllib import getproxies
except ImportError:
from urllib.request import getproxies
# fall back to suds
try:
from suds.client import Client
client = Client(
wsdlurl, proxy=getproxies(), timeout=timeout).service
except ImportError:
# use pysimplesoap as last resort
try:
from pysimplesoap.client import SoapClient
client = SoapClient(
wsdl=wsdlurl, proxy=getproxies(), timeout=timeout)
except ImportError:
raise ImportError(
'No SOAP library (such as zeep) found')
_soap_clients[(wsdlurl, timeout)] = client
return _soap_clients[(wsdlurl, timeout)]
def patch_stdnum():
try:
from stdnum import util
except ImportError:
return # nothing to patch
util.get_soap_client = new_get_soap_client

View file

@ -0,0 +1,12 @@
from urllib3 import PoolManager
orig_pool_init = PoolManager.__init__
def pool_init(self, *args, **kwargs):
orig_pool_init(self, *args, **kwargs)
self.pool_classes_by_scheme = {**self.pool_classes_by_scheme}
def patch_urllib3():
PoolManager.__init__ = pool_init

View file

@ -1,14 +1,24 @@
# ruff: noqa: PLC0415 (import in function not at top-level)
from __future__ import annotations from __future__ import annotations
import contextlib
import operator
import os import os
import sys
import re import re
import sys
import typing as t import typing as t
import warnings import warnings
from werkzeug.datastructures import iter_multi_items from shutil import copyfileobj
from werkzeug.urls import _decode_idna from types import CodeType
from werkzeug import urls
from werkzeug.datastructures import FileStorage, MultiDict
from werkzeug.routing import Rule
from werkzeug.urls import _decode_idna
from werkzeug.wrappers import Request, Response
Rule_get_func_code = hasattr(Rule, '_get_func_code') and Rule._get_func_code
import operator
def _check_str_tuple(value: t.Tuple[t.AnyStr, ...]) -> None: def _check_str_tuple(value: t.Tuple[t.AnyStr, ...]) -> None:
"""Ensure tuple items are all strings or all bytes.""" """Ensure tuple items are all strings or all bytes."""
@ -30,8 +40,10 @@ def _make_encode_wrapper(reference: t.AnyStr) -> t.Callable[[str], t.AnyStr]:
return operator.methodcaller("encode", "latin1") return operator.methodcaller("encode", "latin1")
_default_encoding = sys.getdefaultencoding() _default_encoding = sys.getdefaultencoding()
def _to_str( def _to_str(
x: t.Optional[t.Any], x: t.Optional[t.Any],
charset: t.Optional[str] = _default_encoding, charset: t.Optional[str] = _default_encoding,
@ -51,7 +63,6 @@ def _to_str(
return x.decode(charset, errors) # type: ignore return x.decode(charset, errors) # type: ignore
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from werkzeug import datastructures as ds from werkzeug import datastructures as ds
@ -98,7 +109,7 @@ class BaseURL(_URLTuple):
_lbracket: str _lbracket: str
_rbracket: str _rbracket: str
def __new__(cls, *args: t.Any, **kwargs: t.Any) -> BaseURL: def __new__(cls, *args: t.Any, **kwargs: t.Any) -> BaseURL: # noqa: PYI034
return super().__new__(cls, *args, **kwargs) return super().__new__(cls, *args, **kwargs)
def __str__(self) -> str: def __str__(self) -> str:
@ -126,10 +137,8 @@ class BaseURL(_URLTuple):
""" """
rv = self.host rv = self.host
if rv is not None and isinstance(rv, str): if rv is not None and isinstance(rv, str):
try: with contextlib.suppress(UnicodeError):
rv = rv.encode("idna").decode("ascii") rv = rv.encode("idna").decode("ascii")
except UnicodeError:
pass
return rv return rv
@property @property
@ -985,7 +994,7 @@ def url_join(
if not url: if not url:
return base return base
bscheme, bnetloc, bpath, bquery, bfragment = url_parse( bscheme, bnetloc, bpath, bquery, _bfragment = url_parse(
base, allow_fragments=allow_fragments base, allow_fragments=allow_fragments
) )
scheme, netloc, path, query, fragment = url_parse(url, bscheme, allow_fragments) scheme, netloc, path, query, fragment = url_parse(url, bscheme, allow_fragments)
@ -1031,16 +1040,37 @@ def url_join(
return url_unparse((scheme, netloc, path, query, fragment)) return url_unparse((scheme, netloc, path, query, fragment))
from werkzeug import urls def patch_werkzeug():
# see https://github.com/pallets/werkzeug/compare/2.3.0..3.0.0 from ..tools.json import scriptsafe # noqa: PLC0415
# see https://github.com/pallets/werkzeug/blob/2.3.0/src/werkzeug/urls.py for replacement Request.json_module = Response.json_module = scriptsafe
urls.url_decode = url_decode
urls.url_encode = url_encode FileStorage.save = lambda self, dst, buffer_size=(1 << 20): copyfileobj(self.stream, dst, buffer_size)
urls.url_join = url_join
urls.url_parse = url_parse def _multidict_deepcopy(self, memo=None):
urls.url_quote = url_quote return orig_deepcopy(self)
urls.url_unquote = url_unquote
urls.url_quote_plus = url_quote_plus orig_deepcopy = MultiDict.deepcopy
urls.url_unquote_plus = url_unquote_plus MultiDict.deepcopy = _multidict_deepcopy
urls.url_unparse = url_unparse
urls.URL = URL if Rule_get_func_code:
@staticmethod
def _get_func_code(code, name):
assert isinstance(code, CodeType)
return Rule_get_func_code(code, name)
Rule._get_func_code = _get_func_code
if hasattr(urls, 'url_join'):
# URLs are already patched
return
# see https://github.com/pallets/werkzeug/compare/2.3.0..3.0.0
# see https://github.com/pallets/werkzeug/blob/2.3.0/src/werkzeug/urls.py for replacement
urls.url_decode = url_decode
urls.url_encode = url_encode
urls.url_join = url_join
urls.url_parse = url_parse
urls.url_quote = url_quote
urls.url_unquote = url_unquote
urls.url_quote_plus = url_quote_plus
urls.url_unquote_plus = url_unquote_plus
urls.url_unparse = url_unparse
urls.URL = URL

View file

@ -0,0 +1,9 @@
from zeep.xsd import visitor
from zeep.xsd.const import xsd_ns
def patch_zeep():
# see https://github.com/mvantellingen/python-zeep/issues/1185
if visitor.tags.notation.localname != 'notation':
visitor.tags.notation = xsd_ns('notation')
visitor.SchemaVisitor.visitors[visitor.tags.notation] = visitor.SchemaVisitor.visit_notation

View file

@ -3,7 +3,6 @@
from . import controllers from . import controllers
from . import models from . import models
from . import populate
from . import report from . import report
from . import wizard from . import wizard

View file

@ -41,6 +41,7 @@ The kernel of Odoo, needed for all installation.
'views/ir_ui_menu_views.xml', 'views/ir_ui_menu_views.xml',
'views/ir_ui_view_views.xml', 'views/ir_ui_view_views.xml',
'views/ir_default_views.xml', 'views/ir_default_views.xml',
'data/ir_config_parameter_data.xml',
'data/ir_cron_data.xml', 'data/ir_cron_data.xml',
'report/ir_model_report.xml', 'report/ir_model_report.xml',
'report/ir_model_templates.xml', 'report/ir_model_templates.xml',
@ -67,8 +68,8 @@ The kernel of Odoo, needed for all installation.
'views/res_country_views.xml', 'views/res_country_views.xml',
'views/res_currency_views.xml', 'views/res_currency_views.xml',
'views/res_users_views.xml', 'views/res_users_views.xml',
'views/res_device_views.xml',
'views/res_users_identitycheck_views.xml', 'views/res_users_identitycheck_views.xml',
'views/ir_property_views.xml',
'views/res_config_settings_views.xml', 'views/res_config_settings_views.xml',
'views/report_paperformat_views.xml', 'views/report_paperformat_views.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',

View file

@ -10,7 +10,7 @@ from markupsafe import Markup
import odoo import odoo
from odoo.http import Controller, route, dispatch_rpc, request, Response from odoo.http import Controller, route, dispatch_rpc, request, Response
from odoo.fields import Date, Datetime, Command from odoo.fields import Date, Datetime, Command
from odoo.tools import lazy, ustr from odoo.tools import lazy
from odoo.tools.misc import frozendict from odoo.tools.misc import frozendict
# ========================================================== # ==========================================================
@ -45,7 +45,7 @@ def xmlrpc_handle_exception_int(e):
formatted_info = "".join(traceback.format_exception(*info)) formatted_info = "".join(traceback.format_exception(*info))
fault = xmlrpc.client.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info) fault = xmlrpc.client.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
return xmlrpc.client.dumps(fault, allow_none=None) return dumps(fault)
def xmlrpc_handle_exception_string(e): def xmlrpc_handle_exception_string(e):
@ -65,7 +65,7 @@ def xmlrpc_handle_exception_string(e):
formatted_info = "".join(traceback.format_exception(*info)) formatted_info = "".join(traceback.format_exception(*info))
fault = xmlrpc.client.Fault(odoo.tools.exception_to_unicode(e), formatted_info) fault = xmlrpc.client.Fault(odoo.tools.exception_to_unicode(e), formatted_info)
return xmlrpc.client.dumps(fault, allow_none=None, encoding=None) return dumps(fault)
class OdooMarshaller(xmlrpc.client.Marshaller): class OdooMarshaller(xmlrpc.client.Marshaller):
@ -78,9 +78,8 @@ class OdooMarshaller(xmlrpc.client.Marshaller):
# By default, in xmlrpc, bytes are converted to xmlrpc.client.Binary object. # By default, in xmlrpc, bytes are converted to xmlrpc.client.Binary object.
# Historically, odoo is sending binary as base64 string. # Historically, odoo is sending binary as base64 string.
# In python 3, base64.b64{de,en}code() methods now works on bytes. # In python 3, base64.b64{de,en}code() methods now works on bytes.
# Convert them to str to have a consistent behavior between python 2 and python 3.
def dump_bytes(self, value, write): def dump_bytes(self, value, write):
self.dump_unicode(ustr(value), write) self.dump_unicode(value.decode(), write)
def dump_datetime(self, value, write): def dump_datetime(self, value, write):
# override to marshall as a string for backwards compatibility # override to marshall as a string for backwards compatibility
@ -111,21 +110,34 @@ class OdooMarshaller(xmlrpc.client.Marshaller):
dispatch[Markup] = lambda self, value, write: self.dispatch[str](self, str(value), write) dispatch[Markup] = lambda self, value, write: self.dispatch[str](self, str(value), write)
# monkey-patch xmlrpc.client's marshaller def dumps(params: list | tuple | xmlrpc.client.Fault) -> str:
xmlrpc.client.Marshaller = OdooMarshaller response = OdooMarshaller(allow_none=False).dumps(params)
return f"""\
<?xml version="1.0"?>
<methodResponse>
{response}
</methodResponse>
"""
# ========================================================== # ==========================================================
# RPC Controller # RPC Controller
# ========================================================== # ==========================================================
def _check_request():
if request.db:
request.env.cr.close()
class RPC(Controller): class RPC(Controller):
"""Handle RPC connections.""" """Handle RPC connections."""
def _xmlrpc(self, service): def _xmlrpc(self, service):
"""Common method to handle an XML-RPC request.""" """Common method to handle an XML-RPC request."""
_check_request()
data = request.httprequest.get_data() data = request.httprequest.get_data()
params, method = xmlrpc.client.loads(data) params, method = xmlrpc.client.loads(data, use_datetime=True)
result = dispatch_rpc(service, method, params) result = dispatch_rpc(service, method, params)
return xmlrpc.client.dumps((result,), methodresponse=1, allow_none=False) return dumps((result,))
@route("/xmlrpc/<service>", auth="none", methods=["POST"], csrf=False, save_session=False) @route("/xmlrpc/<service>", auth="none", methods=["POST"], csrf=False, save_session=False)
def xmlrpc_1(self, service): def xmlrpc_1(self, service):
@ -134,6 +146,7 @@ class RPC(Controller):
This entrypoint is historical and non-compliant, but kept for This entrypoint is historical and non-compliant, but kept for
backwards-compatibility. backwards-compatibility.
""" """
_check_request()
try: try:
response = self._xmlrpc(service) response = self._xmlrpc(service)
except Exception as error: except Exception as error:
@ -147,6 +160,7 @@ class RPC(Controller):
@route("/xmlrpc/2/<service>", auth="none", methods=["POST"], csrf=False, save_session=False) @route("/xmlrpc/2/<service>", auth="none", methods=["POST"], csrf=False, save_session=False)
def xmlrpc_2(self, service): def xmlrpc_2(self, service):
"""XML-RPC service that returns faultCode as int.""" """XML-RPC service that returns faultCode as int."""
_check_request()
try: try:
response = self._xmlrpc(service) response = self._xmlrpc(service)
except Exception as error: except Exception as error:
@ -160,4 +174,5 @@ class RPC(Controller):
@route('/jsonrpc', type='json', auth="none", save_session=False) @route('/jsonrpc', type='json', auth="none", save_session=False)
def jsonrpc(self, service, method, args): def jsonrpc(self, service, method, args):
""" Method used by client APIs to contact OpenERP. """ """ Method used by client APIs to contact OpenERP. """
_check_request()
return dispatch_rpc(service, method, args) return dispatch_rpc(service, method, args)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="default_max_email_size" model="ir.config_parameter">
<field name="key">base.default_max_email_size</field>
<field name="value">10</field>
</record>
</data>
</odoo>

View file

@ -7,7 +7,6 @@
<field name="code">model._run_vacuum_cleaner()</field> <field name="code">model._run_vacuum_cleaner()</field>
<field name='interval_number'>1</field> <field name='interval_number'>1</field>
<field name='interval_type'>days</field> <field name='interval_type'>days</field>
<field name="numbercall">-1</field>
<field name="priority">3</field> <field name="priority">3</field>
</record> </record>
@ -15,9 +14,9 @@
<field name="name">Base: Portal Users Deletion</field> <field name="name">Base: Portal Users Deletion</field>
<field name="model_id" ref="base.model_res_users_deletion"/> <field name="model_id" ref="base.model_res_users_deletion"/>
<field name="state">code</field> <field name="state">code</field>
<field name="code">model._gc_portal_users()</field> <field name="code">model._gc_portal_users(batch_size=50)</field>
<field name='interval_number'>1</field> <field name='interval_number'>1</field>
<field name='interval_type'>days</field> <field name='interval_type'>days</field>
<field name="numbercall">-1</field> <field name="priority">8</field>
</record> </record>
</odoo> </odoo>

View file

@ -30,10 +30,10 @@ action = {
module(s) failed to install and were disabled module(s) failed to install and were disabled
</h3> </h3>
<field name="failure_ids"> <field name="failure_ids">
<tree> <list>
<field name="module_id"/> <field name="module_id"/>
<field name="error"/> <field name="error"/>
</tree> </list>
</field> </field>
<footer> <footer>
<button string="Ok" class="oe_highlight" type="object" name="done" data-hotkey="q"/> <button string="Ok" class="oe_highlight" type="object" name="done" data-hotkey="q"/>

View file

@ -97,7 +97,7 @@
<record model="ir.module.category" id="module_category_services_helpdesk"> <record model="ir.module.category" id="module_category_services_helpdesk">
<field name="name">Helpdesk</field> <field name="name">Helpdesk</field>
<field name="description">After-sales services</field> <field name="description" />
<field name="sequence">14</field> <field name="sequence">14</field>
</record> </record>

View file

@ -30,8 +30,8 @@
<field name="website">https://www.odoo.com/app/timesheet?utm_source=db&amp;utm_medium=module</field> <field name="website">https://www.odoo.com/app/timesheet?utm_source=db&amp;utm_medium=module</field>
</record> </record>
<record model="ir.module.module" id="base.module_account_accountant"> <record model="ir.module.module" id="base.module_accountant">
<field name="name">account_accountant</field> <field name="name">accountant</field>
<field name="sequence">30</field> <field name="sequence">30</field>
<field name="shortdesc">Accounting</field> <field name="shortdesc">Accounting</field>
<field name="category_id" ref="base.module_category_accounting_accounting"/> <field name="category_id" ref="base.module_category_accounting_accounting"/>
@ -156,20 +156,6 @@
<field name="website">https://www.odoo.com/app/amazon-connector?utm_source=db&amp;utm_medium=module</field> <field name="website">https://www.odoo.com/app/amazon-connector?utm_source=db&amp;utm_medium=module</field>
</record> </record>
<record model="ir.module.module" id="base.module_sale_ebay">
<field name="name">sale_ebay</field>
<field name="shortdesc">eBay Connector</field>
<field name="sequence">325</field>
<field name="category_id" ref="base.module_category_sales_sales"/>
<field name="application" eval="True"/>
<field name="summary">Sell on eBay easily</field>
<field name="license">OEEL-1</field>
<field name="author">Odoo S.A.</field>
<field name="to_buy" eval="True"/>
<field name="icon">/base/static/img/icons/sale_ebay.png</field>
<field name="website">https://www.odoo.com/app/sales?utm_source=db&amp;utm_medium=module</field>
</record>
<record model="ir.module.module" id="base.module_planning"> <record model="ir.module.module" id="base.module_planning">
<field name="name">planning</field> <field name="name">planning</field>
<field name="shortdesc">Planning</field> <field name="shortdesc">Planning</field>
@ -296,17 +282,6 @@
<field name="website">https://play.google.com/store/apps/details?id=com.odoo.mobile</field> <field name="website">https://play.google.com/store/apps/details?id=com.odoo.mobile</field>
</record> </record>
<record model="ir.module.module" id="base.module_website_twitter_wall">
<field name="name">website_twitter_wall</field>
<field name="shortdesc">Twitter Wall</field>
<field name="summary">Interactive twitter wall for events</field>
<field name="category_id" ref="base.module_category_website_website"/>
<field name="license">OEEL-1</field>
<field name="author">Odoo S.A.</field>
<field name="to_buy" eval="True"/>
<field name="icon">/base/static/img/icons/website_twitter_wall.png</field>
</record>
<record model="ir.module.module" id="base.module_payment_sepa_direct_debit"> <record model="ir.module.module" id="base.module_payment_sepa_direct_debit">
<field name="name">payment_sepa_direct_debit</field> <field name="name">payment_sepa_direct_debit</field>
<field name="shortdesc">Sepa Direct Debit Payment Provider</field> <field name="shortdesc">Sepa Direct Debit Payment Provider</field>

View file

@ -8,13 +8,14 @@
<field name="page_height">0</field> <field name="page_height">0</field>
<field name="page_width">0</field> <field name="page_width">0</field>
<field name="orientation">Portrait</field> <field name="orientation">Portrait</field>
<field name="margin_top">40</field> <field name="margin_top">52</field>
<field name="margin_bottom">32</field> <field name="margin_bottom">32</field>
<field name="margin_left">7</field> <field name="margin_left">0</field>
<field name="margin_right">7</field> <field name="margin_right">0</field>
<field name="header_line" eval="False" /> <field name="header_line" eval="False" />
<field name="header_spacing">35</field> <field name="header_spacing">52</field>
<field name="dpi">90</field> <field name="dpi">90</field>
<field name="css_margins" eval="True" />
</record> </record>
<record id="paperformat_us" model="report.paperformat"> <record id="paperformat_us" model="report.paperformat">
@ -24,13 +25,14 @@
<field name="page_height">0</field> <field name="page_height">0</field>
<field name="page_width">0</field> <field name="page_width">0</field>
<field name="orientation">Portrait</field> <field name="orientation">Portrait</field>
<field name="margin_top">40</field> <field name="margin_top">52</field>
<field name="margin_bottom">30</field> <field name="margin_bottom">32</field>
<field name="margin_left">7</field> <field name="margin_left">0</field>
<field name="margin_right">7</field> <field name="margin_right">0</field>
<field name="header_line" eval="False" /> <field name="header_line" eval="False" />
<field name="header_spacing">35</field> <field name="header_spacing">52</field>
<field name="dpi">90</field> <field name="dpi">90</field>
<field name="css_margins" eval="True" />
</record> </record>
<record id="paperformat_batch_deposit" model="report.paperformat"> <record id="paperformat_batch_deposit" model="report.paperformat">

View file

@ -915,7 +915,7 @@ state_et_5,et,"Dire Dawa","DR"
state_et_6,et,"Gambella Peoples","GM" state_et_6,et,"Gambella Peoples","GM"
state_et_7,et,"Harrari Peoples","HR" state_et_7,et,"Harrari Peoples","HR"
state_et_8,et,"Oromia","OR" state_et_8,et,"Oromia","OR"
state_et_9,et,"Somalia","SM" state_et_9,et,"Somali","SM"
state_et_10,et,"Southern Peoples, Nations, and Nationalities","SP" state_et_10,et,"Southern Peoples, Nations, and Nationalities","SP"
state_et_11,et,"Tigray","TG" state_et_11,et,"Tigray","TG"
state_ie_1,ie,"Carlow","CW" state_ie_1,ie,"Carlow","CW"
@ -1748,3 +1748,34 @@ state_jo_ka,jo,"Karak",JO-KA
state_jo_ma,jo,"Mafraq",JO-MA state_jo_ma,jo,"Mafraq",JO-MA
state_jo_md,jo,"Madaba",JO-MD state_jo_md,jo,"Madaba",JO-MD
state_jo_mn,jo,"Maan",JO-MN state_jo_mn,jo,"Maan",JO-MN
state_kr_11,kr,"서울특별시","KR-11"
state_kr_26,kr,"부산광역시","KR-26"
state_kr_27,kr,"대구광역시","KR-27"
state_kr_28,kr,"인천광역시","KR-28"
state_kr_29,kr,"광주광역시","KR-29"
state_kr_30,kr,"대전광역시","KR-30"
state_kr_31,kr,"울산광역시","KR-31"
state_kr_41,kr,"경기도","KR-41"
state_kr_42,kr,"강원도","KR-42"
state_kr_43,kr,"충청북도","KR-43"
state_kr_44,kr,"충청남도","KR-44"
state_kr_45,kr,"전라북도","KR-45"
state_kr_46,kr,"전라남도","KR-46"
state_kr_47,kr,"경상북도","KR-47"
state_kr_48,kr,"경상남도","KR-48"
state_kr_49,kr,"제주특별자치도","KR-49"
state_kr_50,kr,"세종특별자치시","KR-50"
state_be_1,be,"Antwerp","VAN"
state_be_2,be,"Limburg","VLI"
state_be_3,be,"East Flanders","VOV"
state_be_4,be,"Flemish Brabant","VBR"
state_be_5,be,"West Flanders","VWV"
state_be_6,be,"Walloon Brabant","WBR"
state_be_7,be,"Hainaut","WHT"
state_be_8,be,"Liège","WLG"
state_be_9,be,"Luxembourg","WLX"
state_be_10,be,"Namur","WNA"
state_bn_b,bn,"Brunei-Muara","B"
state_bn_k,bn,"Belait","K"
state_bn_t,bn,"Tutong","T"
state_bn_p,bn,"Temburong","P"

1 id country_id:id name code
915 state_et_6 et Gambella Peoples GM
916 state_et_7 et Harrari Peoples HR
917 state_et_8 et Oromia OR
918 state_et_9 et Somalia Somali SM
919 state_et_10 et Southern Peoples, Nations, and Nationalities SP
920 state_et_11 et Tigray TG
921 state_ie_1 ie Carlow CW
1748 state_jo_ma jo Mafraq JO-MA
1749 state_jo_md jo Madaba JO-MD
1750 state_jo_mn jo Maan JO-MN
1751 state_kr_11 kr 서울특별시 KR-11
1752 state_kr_26 kr 부산광역시 KR-26
1753 state_kr_27 kr 대구광역시 KR-27
1754 state_kr_28 kr 인천광역시 KR-28
1755 state_kr_29 kr 광주광역시 KR-29
1756 state_kr_30 kr 대전광역시 KR-30
1757 state_kr_31 kr 울산광역시 KR-31
1758 state_kr_41 kr 경기도 KR-41
1759 state_kr_42 kr 강원도 KR-42
1760 state_kr_43 kr 충청북도 KR-43
1761 state_kr_44 kr 충청남도 KR-44
1762 state_kr_45 kr 전라북도 KR-45
1763 state_kr_46 kr 전라남도 KR-46
1764 state_kr_47 kr 경상북도 KR-47
1765 state_kr_48 kr 경상남도 KR-48
1766 state_kr_49 kr 제주특별자치도 KR-49
1767 state_kr_50 kr 세종특별자치시 KR-50
1768 state_be_1 be Antwerp VAN
1769 state_be_2 be Limburg VLI
1770 state_be_3 be East Flanders VOV
1771 state_be_4 be Flemish Brabant VBR
1772 state_be_5 be West Flanders VWV
1773 state_be_6 be Walloon Brabant WBR
1774 state_be_7 be Hainaut WHT
1775 state_be_8 be Liège WLG
1776 state_be_9 be Luxembourg WLX
1777 state_be_10 be Namur WNA
1778 state_bn_b bn Brunei-Muara B
1779 state_bn_k bn Belait K
1780 state_bn_t bn Tutong T
1781 state_bn_p bn Temburong P

View file

@ -1,92 +1,93 @@
"id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","week_start" "id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","short_time_format","week_start"
"base.lang_en","English (US)","en_US","en","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%H:%M:%S","7" "base.lang_en","English (US)","en_US","en","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%H:%M:%S","%H:%M","7"
"base.lang_am_ET","Amharic / አምሃርኛ","am_ET","am_ET","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S","7" "base.lang_am_ET","Amharic / አምሃርኛ","am_ET","am_ET","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S","%I:%M","7"
"base.lang_ar","Arabic / الْعَرَبيّة","ar_001","ar","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S %p","6" "base.lang_ar","Arabic / الْعَرَبيّة","ar_001","ar","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S %p","%I:%M","6"
"base.lang_ar_SY","Arabic (Syria) / الْعَرَبيّة","ar_SY","ar_SY","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S %p","6" "base.lang_ar_SY","Arabic (Syria) / الْعَرَبيّة","ar_SY","ar_SY","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S %p","%I:%M","6"
"base.lang_az","Azerbaijani / Azərbaycanca","az_AZ","az","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" "base.lang_az","Azerbaijani / Azərbaycanca","az_AZ","az","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_eu_ES","Basque / Euskara","eu_ES","eu_ES","Left-to-Right","[]",",",,"%a, %Y.eko %bren %da","%H:%M:%S","1" "base.lang_eu_ES","Basque / Euskara","eu_ES","eu_ES","Left-to-Right","[]",",",,"%a, %Y.eko %bren %da","%H:%M:%S","%H:%M","1"
"base.lang_bn_IN","Bengali / বাংলা","bn_IN","bn_IN","Left-to-Right","[]",",",,"%A %d %b %Y","%I:%M:%S","1" "base.lang_be","Belarusian / Беларуская мова","be_BY","be","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_bs_BA","Bosnian / bosanski jezik","bs_BA","bs","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","1" "base.lang_bn_IN","Bengali / বাংলা","bn_IN","bn_IN","Left-to-Right","[]",",",,"%A %d %b %Y","%I:%M:%S","%I:%M","1"
"base.lang_bg","Bulgarian / български език","bg_BG","bg","Left-to-Right","[3,0]",",",,"%d.%m.%Y","%H,%M,%S","1" "base.lang_bs_BA","Bosnian / bosanski jezik","bs_BA","bs","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_ca_ES","Catalan / Català","ca_ES","ca_ES","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_bg","Bulgarian / български език","bg_BG","bg","Left-to-Right","[3,0]",",",,"%d.%m.%Y","%H,%M,%S","%H,%M","1"
"base.lang_zh_CN","Chinese (Simplified) / 简体中文","zh_CN","zh_CN","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H时%M分%S秒","7" "base.lang_ca_ES","Catalan / Català","ca_ES","ca_ES","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_zh_HK","Chinese (HK)","zh_HK","zh_HK","Left-to-Right","[3,0]",".",",","%Y年%m月%d日 %A","%I時%M分%S秒","7" "base.lang_zh_CN","Chinese (Simplified) / 简体中文","zh_CN","zh_CN","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H时%M分%S秒","%H时%M分","7"
"base.lang_zh_TW","Chinese (Traditional) / 繁體中文","zh_TW","zh_TW","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H時%M分%S秒","7" "base.lang_zh_HK","Chinese (HK)","zh_HK","zh_HK","Left-to-Right","[3,0]",".",",","%Y年%m月%d日 %A","%I時%M分%S秒","%I時%M分","7"
"base.lang_hr","Croatian / hrvatski jezik","hr_HR","hr","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","1" "base.lang_zh_TW","Chinese (Traditional) / 繁體中文","zh_TW","zh_TW","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H時%M分%S秒","%H時%M分","7"
"base.lang_cs_CZ","Czech / Čeština","cs_CZ","cs_CZ","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" "base.lang_hr","Croatian / hrvatski jezik","hr_HR","hr","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_da_DK","Danish / Dansk","da_DK","da_DK","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","1" "base.lang_cs_CZ","Czech / Čeština","cs_CZ","cs_CZ","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_nl_BE","Dutch (BE) / Nederlands (BE)","nl_BE","nl_BE","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","1" "base.lang_da_DK","Danish / Dansk","da_DK","da_DK","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","%H:%M","1"
"base.lang_nl","Dutch / Nederlands","nl_NL","nl","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","1" "base.lang_nl_BE","Dutch (BE) / Nederlands (BE)","nl_BE","nl_BE","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","%H:%M","1"
"base.lang_en_AU","English (AU)","en_AU","en_AU","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_nl","Dutch / Nederlands","nl_NL","nl","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","%H:%M","1"
"base.lang_en_CA","English (CA)","en_CA","en_CA","Left-to-Right","[3,0]",".",",","%Y-%m-%d","%H:%M:%S","7" "base.lang_en_AU","English (AU)","en_AU","en_AU","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_en_GB","English (UK)","en_GB","en_GB","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","1" "base.lang_en_CA","English (CA)","en_CA","en_CA","Left-to-Right","[3,0]",".",",","%Y-%m-%d","%H:%M:%S","%H:%M","7"
"base.lang_en_IN","English (IN)","en_IN","en_IN","Left-to-Right","[3,2,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_en_GB","English (UK)","en_GB","en_GB","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_en_NZ","English (NZ)","en_NZ","en_NZ","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_en_IN","English (IN)","en_IN","en_IN","Left-to-Right","[3,2,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_et_EE","Estonian / Eesti keel","et_EE","et","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" "base.lang_en_NZ","English (NZ)","en_NZ","en_NZ","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_fi","Finnish / Suomi","fi_FI","fi","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H.%M.%S","1" "base.lang_et_EE","Estonian / Eesti keel","et_EE","et","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_fr_BE","French (BE) / Français (BE)","fr_BE","fr_BE","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_fi","Finnish / Suomi","fi_FI","fi","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H.%M.%S","%H.%M","1"
"base.lang_fr_CA","French (CA) / Français (CA)","fr_CA","fr_CA","Left-to-Right","[3,0]",","," ","%Y-%m-%d","%H:%M:%S","7" "base.lang_fr_BE","French (BE) / Français (BE)","fr_BE","fr_BE","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_fr_CH","French (CH) / Français (CH)","fr_CH","fr_CH","Left-to-Right","[3,0]",".","'","%d.%m.%Y","%H:%M:%S","1" "base.lang_fr_CA","French (CA) / Français (CA)","fr_CA","fr_CA","Left-to-Right","[3,0]",","," ","%Y-%m-%d","%H:%M:%S","%H:%M","7"
"base.lang_fr","French / Français","fr_FR","fr","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","1" "base.lang_fr_CH","French (CH) / Français (CH)","fr_CH","fr_CH","Left-to-Right","[3,0]",".","'","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_gl_ES","Galician / Galego","gl_ES","gl","Left-to-Right","[]",",",,"%d/%m/%Y","%H:%M:%S","1" "base.lang_fr","French / Français","fr_FR","fr","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_ka_GE","Georgian / ქართული ენა","ka_GE","ka","Left-to-Right","[3,0]",",",".","%m/%d/%Y","%H:%M:%S","1" "base.lang_gl_ES","Galician / Galego","gl_ES","gl","Left-to-Right","[]",",",,"%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_de","German / Deutsch","de_DE","de","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","1" "base.lang_ka_GE","Georgian / ქართული ენა","ka_GE","ka","Left-to-Right","[3,0]",",",".","%m/%d/%Y","%H:%M:%S","%H:%M","1"
"base.lang_de_CH","German (CH) / Deutsch (CH)","de_CH","de_CH","Left-to-Right","[3,0]",".","'","%d.%m.%Y","%H:%M:%S","1" "base.lang_de","German / Deutsch","de_DE","de","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_el_GR","Greek / Ελληνικά","el_GR","el_GR","Left-to-Right","[]",",",".","%d/%m/%Y","%I:%M:%S %p","1" "base.lang_de_CH","German (CH) / Deutsch (CH)","de_CH","de_CH","Left-to-Right","[3,0]",".","'","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_gu_IN","Gujarati / ગુજરાતી","gu_IN","gu","Left-to-Right","[]",".",",","%A %d %b %Y","%I:%M:%S","7" "base.lang_el_GR","Greek / Ελληνικά","el_GR","el_GR","Left-to-Right","[]",",",".","%d/%m/%Y","%I:%M:%S %p","%I:%M %p","1"
"base.lang_he_IL","Hebrew / עברית","he_IL","he","Right-to-Left","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_gu_IN","Gujarati / ગુજરાતી","gu_IN","gu","Left-to-Right","[]",".",",","%A %d %b %Y","%I:%M:%S","%I:%M","7"
"base.lang_hi_IN","Hindi / हिंदी","hi_IN","hi","Left-to-Right","[]",".",",","%A %d %b %Y","%I:%M:%S","7" "base.lang_he_IL","Hebrew / עברית","he_IL","he","Right-to-Left","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_hu","Hungarian / Magyar","hu_HU","hu","Left-to-Right","[3,0]",",",".","%Y-%m-%d","%H:%M:%S","1" "base.lang_hi_IN","Hindi / हिंदी","hi_IN","hi","Left-to-Right","[]",".",",","%A %d %b %Y","%I:%M:%S","%I:%M","7"
"base.lang_id","Indonesian / Bahasa Indonesia","id_ID","id","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","7" "base.lang_hu","Hungarian / Magyar","hu_HU","hu","Left-to-Right","[3,0]",",",".","%Y-%m-%d","%H:%M:%S","%H:%M","1"
"base.lang_it","Italian / Italiano","it_IT","it","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_id","Indonesian / Bahasa Indonesia","id_ID","id","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_ja_JP","Japanese / 日本語","ja_JP","ja","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H時%M分%S秒","7" "base.lang_it","Italian / Italiano","it_IT","it","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_kab_DZ","Kabyle / Taqbaylit","kab_DZ","kab","Left-to-Right","[]",".",",","%m/%d/%Y","%I:%M:%S %p","6" "base.lang_ja_JP","Japanese / 日本語","ja_JP","ja","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H時%M分%S秒","%H時%M分","7"
"base.lang_km","Khmer / ភាសាខ្មែរ","km_KH","km","Left-to-Right","[3,0]",".",",","%d %B %Y","%H:%M:%S","7" "base.lang_kab_DZ","Kabyle / Taqbaylit","kab_DZ","kab","Left-to-Right","[]",".",",","%m/%d/%Y","%I:%M:%S %p","%I:%M %p","6"
"base.lang_ko_KP","Korean (KP) / 한국어 (KP)","ko_KP","ko_KP","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%I:%M:%S %p","1" "base.lang_km","Khmer / ភាសាខ្មែរ","km_KH","km","Left-to-Right","[3,0]",".",",","%d %B %Y","%H:%M:%S","%H:%M","7"
"base.lang_ko_KR","Korean (KR) / 한국어 (KR)","ko_KR","ko_KR","Left-to-Right","[3,0]",".",",","%Y년 %m월 %d일","%H시 %M분 %S초","7" "base.lang_ko_KP","Korean (KP) / 한국어 (KP)","ko_KP","ko_KP","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%I:%M:%S %p","%I:%M %p","1"
"base.lang_lo_LA","Lao / ພາສາລາວ","lo_LA","lo","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_ko_KR","Korean (KR) / 한국어 (KR)","ko_KR","ko_KR","Left-to-Right","[3,0]",".",",","%Y년 %m월 %d일","%H시 %M분 %S초","%H시 %M분","7"
"base.lang_lv","Latvian / latviešu valoda","lv_LV","lv","Left-to-Right","[3,0]",","," ","%Y.%m.%d.","%H:%M:%S","1" "base.lang_lo_LA","Lao / ພາສາລາວ","lo_LA","lo","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_lt","Lithuanian / Lietuvių kalba","lt_LT","lt","Left-to-Right","[3,0]",",",".","%Y-%m-%d","%H:%M:%S","1" "base.lang_lv","Latvian / latviešu valoda","lv_LV","lv","Left-to-Right","[3,0]",","," ","%Y.%m.%d.","%H:%M:%S","%H:%M","1"
"base.lang_lb","Luxembourgish","lb_LU","lb","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","1" "base.lang_lt","Lithuanian / Lietuvių kalba","lt_LT","lt","Left-to-Right","[3,0]",",",".","%Y-%m-%d","%H:%M:%S","%H:%M","1"
"base.lang_mk","Macedonian / македонски јазик","mk_MK","mk","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" "base.lang_lb","Luxembourgish","lb_LU","lb","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_ml","Malayalam / മലയാളം","ml_IN","ml","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","1" "base.lang_mk","Macedonian / македонски јазик","mk_MK","mk","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_mn","Mongolian / монгол","mn_MN","mn","Left-to-Right","[3,0]",".","'","%Y-%m-%d","%H:%M:%S","7" "base.lang_ml","Malayalam / മലയാളം","ml_IN","ml","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_ms","Malay / Bahasa Melayu","ms_MY","ms","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","1" "base.lang_mn","Mongolian / монгол","mn_MN","mn","Left-to-Right","[3,0]",".","'","%Y-%m-%d","%H:%M:%S","%H:%M","7"
base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",","%Y %b %d %A","%I:%M:%S %p","7" "base.lang_ms","Malay / Bahasa Melayu","ms_MY","ms","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_nb_NO","Norwegian Bokmål / Norsk bokmål","nb_NO","nb_NO","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",","%Y %b %d %A","%I:%M:%S %p","%I:%M %p","7"
"base.lang_fa_IR","Persian / فارسی","fa_IR","fa","Right-to-Left","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","6" "base.lang_nb_NO","Norwegian Bokmål / Norsk bokmål","nb_NO","nb_NO","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_pl","Polish / Język polski","pl_PL","pl","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","1" "base.lang_fa_IR","Persian / فارسی","fa_IR","fa","Right-to-Left","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","%H:%M","6"
"base.lang_pt_AO","Portuguese (AO) / Português (AO)","pt_AO","pt_AO","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","1" "base.lang_pl","Polish / Język polski","pl_PL","pl","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_pt_BR","Portuguese (BR) / Português (BR)","pt_BR","pt_BR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","7" "base.lang_pt_AO","Portuguese (AO) / Português (AO)","pt_AO","pt_AO","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","%H:%M","1"
"base.lang_pt","Portuguese / Português","pt_PT","pt","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","1" "base.lang_pt_BR","Portuguese (BR) / Português (BR)","pt_BR","pt_BR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_ro","Romanian / română","ro_RO","ro","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","1" "base.lang_pt","Portuguese / Português","pt_PT","pt","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","%H:%M","1"
"base.lang_ru","Russian / русский язык","ru_RU","ru","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" "base.lang_ro","Romanian / română","ro_RO","ro","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_sr_RS","Serbian (Cyrillic) / српски","sr_RS","sr_RS","Left-to-Right","[]",",",,"%d.%m.%Y.","%H:%M:%S","7" "base.lang_ru","Russian / русский язык","ru_RU","ru","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_sr@latin","Serbian (Latin) / srpski","sr@latin","sr@latin","Left-to-Right","[]",".",",","%m/%d/%Y","%I:%M:%S %p","7" "base.lang_sr@Cyrl","Serbian (Cyrillic) / српски","sr@Cyrl","sr@Cyrl","Left-to-Right","[]",",",,"%d.%m.%Y.","%H:%M:%S","%H:%M","7"
"base.lang_sk","Slovak / Slovenský jazyk","sk_SK","sk","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" "base.lang_sr@latin","Serbian (Latin) / srpski","sr@latin","sr@latin","Left-to-Right","[]",".",",","%m/%d/%Y","%I:%M:%S %p","%I:%M %p","7"
"base.lang_sl_SI","Slovenian / slovenščina","sl_SI","sl","Left-to-Right","[]",","," ","%d. %m. %Y","%H:%M:%S","1" "base.lang_sk","Slovak / Slovenský jazyk","sk_SK","sk","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_es_419","Spanish (Latin America) / Español (América Latina)","es_419","es_419","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_sl_SI","Slovenian / slovenščina","sl_SI","sl","Left-to-Right","[]",","," ","%d. %m. %Y","%H:%M:%S","%H:%M","1"
"base.lang_es_AR","Spanish (AR) / Español (AR)","es_AR","es_AR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","7" "base.lang_es_419","Spanish (Latin America) / Español (América Latina)","es_419","es_419","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_es_BO","Spanish (BO) / Español (BO)","es_BO","es_BO","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_es_AR","Spanish (AR) / Español (AR)","es_AR","es_AR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_es_CL","Spanish (CL) / Español (CL)","es_CL","es_CL","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_es_BO","Spanish (BO) / Español (BO)","es_BO","es_BO","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","7" "base.lang_es_CL","Spanish (CL) / Español (CL)","es_CL","es_CL","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_es_CR","Spanish (CR) / Español (CR)","es_CR","es_CR","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","1" "base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","%H:%M","7"
"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S %p","1" "base.lang_es_CR","Spanish (CR) / Español (CR)","es_CR","es_CR","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_es_EC","Spanish (EC) / Español (EC)","es_EC","es_EC","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S %p","%I:%M %p","1"
"base.lang_es_GT","Spanish (GT) / Español (GT)","es_GT","es_GT","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_es_EC","Spanish (EC) / Español (EC)","es_EC","es_EC","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_es_MX","Spanish (MX) / Español (MX)","es_MX","es_MX","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_es_GT","Spanish (GT) / Español (GT)","es_GT","es_GT","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_es_PA","Spanish (PA) / Español (PA)","es_PA","es_PA","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_es_MX","Spanish (MX) / Español (MX)","es_MX","es_MX","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_es_PE","Spanish (PE) / Español (PE)","es_PE","es_PE","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_es_PA","Spanish (PA) / Español (PA)","es_PA","es_PA","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_es_PY","Spanish (PY) / Español (PY)","es_PY","es_PY","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","7" "base.lang_es_PE","Spanish (PE) / Español (PE)","es_PE","es_PE","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_es_UY","Spanish (UY) / Español (UY)","es_UY","es_UY","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_es_PY","Spanish (PY) / Español (PY)","es_PY","es_PY","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_es_VE","Spanish (VE) / Español (VE)","es_VE","es_VE","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","7" "base.lang_es_UY","Spanish (UY) / Español (UY)","es_UY","es_UY","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_es","Spanish / Español","es_ES","es","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_es_VE","Spanish (VE) / Español (VE)","es_VE","es_VE","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_sw","Swahili / Kiswahili","sw","sw","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","1" "base.lang_es","Spanish / Español","es_ES","es","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_sv_SE","Swedish / Svenska","sv_SE","sv","Left-to-Right","[3,0]",","," ","%Y-%m-%d","%H:%M:%S","1" "base.lang_sw","Swahili / Kiswahili","sw","sw","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_th","Thai / ภาษาไทย","th_TH","th","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","7" "base.lang_sv_SE","Swedish / Svenska","sv_SE","sv","Left-to-Right","[3,0]",","," ","%Y-%m-%d","%H:%M:%S","%H:%M","1"
"base.lang_tl","Tagalog / Filipino","tl_PH","tl","Left-to-Right","[3,0]",".",",","%m/%d/%y","%H:%M:%S","1" "base.lang_th","Thai / ภาษาไทย","th_TH","th","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","%H:%M","7"
"base.lang_tr","Turkish / Türkçe","tr_TR","tr","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","1" "base.lang_tl","Tagalog / Filipino","tl_PH","tl","Left-to-Right","[3,0]",".",",","%m/%d/%y","%H:%M:%S","%H:%M","1"
"base.lang_uk_UA","Ukrainian / українська","uk_UA","uk","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","1" "base.lang_tr","Turkish / Türkçe","tr_TR","tr","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","%H:%M","1"
"base.lang_vi_VN","Vietnamese / Tiếng Việt","vi_VN","vi","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","1" "base.lang_uk_UA","Ukrainian / українська","uk_UA","uk","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","%H:%M","1"
"base.lang_sq_AL","Albanian / Shqip","sq_AL","sq","Left-to-Right","[3,0]",",",".","%Y-%b-%d","%I.%M.%S.","1" "base.lang_vi_VN","Vietnamese / Tiếng Việt","vi_VN","vi","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","%H:%M","1"
"base.lang_te_IN","Telugu / తెలుగు","te_IN","te","Left-to-Right","[]",".",",","%B %d %A %Y","%p%I.%M.%S","7" "base.lang_sq_AL","Albanian / Shqip","sq_AL","sq","Left-to-Right","[3,0]",",",".","%Y-%b-%d","%I.%M.%S.","%I.%M.","1"
"base.lang_te_IN","Telugu / తెలుగు","te_IN","te","Left-to-Right","[]",".",",","%B %d %A %Y","%p%I.%M.%S","%p%I.%M","7"

1 id name code iso_code direction grouping decimal_point thousands_sep date_format time_format short_time_format week_start
2 base.lang_en English (US) en_US en Left-to-Right [3,0] . , %m/%d/%Y %H:%M:%S %H:%M 7
3 base.lang_am_ET Amharic / አምሃርኛ am_ET am_ET Left-to-Right [3,0] . , %d/%m/%Y %I:%M:%S %I:%M 7
4 base.lang_ar Arabic / الْعَرَبيّة ar_001 ar Right-to-Left [3,0] . , %d %b, %Y %I:%M:%S %p %I:%M 6
5 base.lang_ar_SY Arabic (Syria) / الْعَرَبيّة ar_SY ar_SY Right-to-Left [3,0] . , %d %b, %Y %I:%M:%S %p %I:%M 6
6 base.lang_az Azerbaijani / Azərbaycanca az_AZ az Left-to-Right [3,0] ,   %d.%m.%Y %H:%M:%S %H:%M 1
7 base.lang_eu_ES Basque / Euskara eu_ES eu_ES Left-to-Right [] , %a, %Y.eko %bren %da %H:%M:%S %H:%M 1
8 base.lang_bn_IN base.lang_be Bengali / বাংলা Belarusian / Беларуская мова bn_IN be_BY bn_IN be Left-to-Right [] [3,0] , %A %d %b %Y %d.%m.%Y %I:%M:%S %H:%M:%S %H:%M 1
9 base.lang_bs_BA base.lang_bn_IN Bosnian / bosanski jezik Bengali / বাংলা bs_BA bn_IN bs bn_IN Left-to-Right [3,0] [] , . %d.%m.%Y %A %d %b %Y %H:%M:%S %I:%M:%S %I:%M 1
10 base.lang_bg base.lang_bs_BA Bulgarian / български език Bosnian / bosanski jezik bg_BG bs_BA bg bs Left-to-Right [3,0] , . %d.%m.%Y %H,%M,%S %H:%M:%S %H:%M 1
11 base.lang_ca_ES base.lang_bg Catalan / Català Bulgarian / български език ca_ES bg_BG ca_ES bg Left-to-Right [3,0] , . %d/%m/%Y %d.%m.%Y %H:%M:%S %H,%M,%S %H,%M 1
12 base.lang_zh_CN base.lang_ca_ES Chinese (Simplified) / 简体中文 Catalan / Català zh_CN ca_ES zh_CN ca_ES Left-to-Right [3,0] . , , . %Y年%m月%d日 %d/%m/%Y %H时%M分%S秒 %H:%M:%S %H:%M 7 1
13 base.lang_zh_HK base.lang_zh_CN Chinese (HK) Chinese (Simplified) / 简体中文 zh_HK zh_CN zh_HK zh_CN Left-to-Right [3,0] . , %Y年%m月%d日 %A %Y年%m月%d日 %I時%M分%S秒 %H时%M分%S秒 %H时%M分 7
14 base.lang_zh_TW base.lang_zh_HK Chinese (Traditional) / 繁體中文 Chinese (HK) zh_TW zh_HK zh_TW zh_HK Left-to-Right [3,0] . , %Y年%m月%d日 %Y年%m月%d日 %A %H時%M分%S秒 %I時%M分%S秒 %I時%M分 7
15 base.lang_hr base.lang_zh_TW Croatian / hrvatski jezik Chinese (Traditional) / 繁體中文 hr_HR zh_TW hr zh_TW Left-to-Right [3,0] , . . , %d.%m.%Y %Y年%m月%d日 %H:%M:%S %H時%M分%S秒 %H時%M分 1 7
16 base.lang_cs_CZ base.lang_hr Czech / Čeština Croatian / hrvatski jezik cs_CZ hr_HR cs_CZ hr Left-to-Right [3,0] ,   . %d.%m.%Y %H:%M:%S %H:%M 1
17 base.lang_da_DK base.lang_cs_CZ Danish / Dansk Czech / Čeština da_DK cs_CZ da_DK cs_CZ Left-to-Right [3,0] , .   %d-%m-%Y %d.%m.%Y %H:%M:%S %H:%M 1
18 base.lang_nl_BE base.lang_da_DK Dutch (BE) / Nederlands (BE) Danish / Dansk nl_BE da_DK nl_BE da_DK Left-to-Right [3,0] , . %d-%m-%Y %H:%M:%S %H:%M 1
19 base.lang_nl base.lang_nl_BE Dutch / Nederlands Dutch (BE) / Nederlands (BE) nl_NL nl_BE nl nl_BE Left-to-Right [3,0] , . %d-%m-%Y %H:%M:%S %H:%M 1
20 base.lang_en_AU base.lang_nl English (AU) Dutch / Nederlands en_AU nl_NL en_AU nl Left-to-Right [3,0] . , , . %d/%m/%Y %d-%m-%Y %H:%M:%S %H:%M 7 1
21 base.lang_en_CA base.lang_en_AU English (CA) English (AU) en_CA en_AU en_CA en_AU Left-to-Right [3,0] . , %Y-%m-%d %d/%m/%Y %H:%M:%S %H:%M 7
22 base.lang_en_GB base.lang_en_CA English (UK) English (CA) en_GB en_CA en_GB en_CA Left-to-Right [3,0] . , %d/%m/%Y %Y-%m-%d %H:%M:%S %H:%M 1 7
23 base.lang_en_IN base.lang_en_GB English (IN) English (UK) en_IN en_GB en_IN en_GB Left-to-Right [3,2,0] [3,0] . , %d/%m/%Y %H:%M:%S %H:%M 7 1
24 base.lang_en_NZ base.lang_en_IN English (NZ) English (IN) en_NZ en_IN en_NZ en_IN Left-to-Right [3,0] [3,2,0] . , %d/%m/%Y %H:%M:%S %H:%M 7
25 base.lang_et_EE base.lang_en_NZ Estonian / Eesti keel English (NZ) et_EE en_NZ et en_NZ Left-to-Right [3,0] , .   , %d.%m.%Y %d/%m/%Y %H:%M:%S %H:%M 1 7
26 base.lang_fi base.lang_et_EE Finnish / Suomi Estonian / Eesti keel fi_FI et_EE fi et Left-to-Right [3,0] ,   %d.%m.%Y %H.%M.%S %H:%M:%S %H:%M 1
27 base.lang_fr_BE base.lang_fi French (BE) / Français (BE) Finnish / Suomi fr_BE fi_FI fr_BE fi Left-to-Right [3,0] , .   %d/%m/%Y %d.%m.%Y %H:%M:%S %H.%M.%S %H.%M 1
28 base.lang_fr_CA base.lang_fr_BE French (CA) / Français (CA) French (BE) / Français (BE) fr_CA fr_BE fr_CA fr_BE Left-to-Right [3,0] ,   . %Y-%m-%d %d/%m/%Y %H:%M:%S %H:%M 7 1
29 base.lang_fr_CH base.lang_fr_CA French (CH) / Français (CH) French (CA) / Français (CA) fr_CH fr_CA fr_CH fr_CA Left-to-Right [3,0] . , '   %d.%m.%Y %Y-%m-%d %H:%M:%S %H:%M 1 7
30 base.lang_fr base.lang_fr_CH French / Français French (CH) / Français (CH) fr_FR fr_CH fr fr_CH Left-to-Right [3,0] , .   ' %d/%m/%Y %d.%m.%Y %H:%M:%S %H:%M 1
31 base.lang_gl_ES base.lang_fr Galician / Galego French / Français gl_ES fr_FR gl fr Left-to-Right [] [3,0] ,   %d/%m/%Y %H:%M:%S %H:%M 1
32 base.lang_ka_GE base.lang_gl_ES Georgian / ქართული ენა Galician / Galego ka_GE gl_ES ka gl Left-to-Right [3,0] [] , . %m/%d/%Y %d/%m/%Y %H:%M:%S %H:%M 1
33 base.lang_de base.lang_ka_GE German / Deutsch Georgian / ქართული ენა de_DE ka_GE de ka Left-to-Right [3,0] , . %d.%m.%Y %m/%d/%Y %H:%M:%S %H:%M 1
34 base.lang_de_CH base.lang_de German (CH) / Deutsch (CH) German / Deutsch de_CH de_DE de_CH de Left-to-Right [3,0] . , ' . %d.%m.%Y %H:%M:%S %H:%M 1
35 base.lang_el_GR base.lang_de_CH Greek / Ελληνικά German (CH) / Deutsch (CH) el_GR de_CH el_GR de_CH Left-to-Right [] [3,0] , . . ' %d/%m/%Y %d.%m.%Y %I:%M:%S %p %H:%M:%S %H:%M 1
36 base.lang_gu_IN base.lang_el_GR Gujarati / ગુજરાતી Greek / Ελληνικά gu_IN el_GR gu el_GR Left-to-Right [] . , , . %A %d %b %Y %d/%m/%Y %I:%M:%S %I:%M:%S %p %I:%M %p 7 1
37 base.lang_he_IL base.lang_gu_IN Hebrew / עברית Gujarati / ગુજરાતી he_IL gu_IN he gu Right-to-Left Left-to-Right [3,0] [] . , %d/%m/%Y %A %d %b %Y %H:%M:%S %I:%M:%S %I:%M 7
38 base.lang_hi_IN base.lang_he_IL Hindi / हिंदी Hebrew / עברית hi_IN he_IL hi he Left-to-Right Right-to-Left [] [3,0] . , %A %d %b %Y %d/%m/%Y %I:%M:%S %H:%M:%S %H:%M 7
39 base.lang_hu base.lang_hi_IN Hungarian / Magyar Hindi / हिंदी hu_HU hi_IN hu hi Left-to-Right [3,0] [] , . . , %Y-%m-%d %A %d %b %Y %H:%M:%S %I:%M:%S %I:%M 1 7
40 base.lang_id base.lang_hu Indonesian / Bahasa Indonesia Hungarian / Magyar id_ID hu_HU id hu Left-to-Right [3,0] , . %d/%m/%Y %Y-%m-%d %H:%M:%S %H:%M 7 1
41 base.lang_it base.lang_id Italian / Italiano Indonesian / Bahasa Indonesia it_IT id_ID it id Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S %H:%M 1 7
42 base.lang_ja_JP base.lang_it Japanese / 日本語 Italian / Italiano ja_JP it_IT ja it Left-to-Right [3,0] . , , . %Y年%m月%d日 %d/%m/%Y %H時%M分%S秒 %H:%M:%S %H:%M 7 1
43 base.lang_kab_DZ base.lang_ja_JP Kabyle / Taqbaylit Japanese / 日本語 kab_DZ ja_JP kab ja Left-to-Right [] [3,0] . , %m/%d/%Y %Y年%m月%d日 %I:%M:%S %p %H時%M分%S秒 %H時%M分 6 7
44 base.lang_km base.lang_kab_DZ Khmer / ភាសាខ្មែរ Kabyle / Taqbaylit km_KH kab_DZ km kab Left-to-Right [3,0] [] . , %d %B %Y %m/%d/%Y %H:%M:%S %I:%M:%S %p %I:%M %p 7 6
45 base.lang_ko_KP base.lang_km Korean (KP) / 한국어 (KP) Khmer / ភាសាខ្មែរ ko_KP km_KH ko_KP km Left-to-Right [3,0] . , %m/%d/%Y %d %B %Y %I:%M:%S %p %H:%M:%S %H:%M 1 7
46 base.lang_ko_KR base.lang_ko_KP Korean (KR) / 한국어 (KR) Korean (KP) / 한국어 (KP) ko_KR ko_KP ko_KR ko_KP Left-to-Right [3,0] . , %Y년 %m월 %d일 %m/%d/%Y %H시 %M분 %S초 %I:%M:%S %p %I:%M %p 7 1
47 base.lang_lo_LA base.lang_ko_KR Lao / ພາສາລາວ Korean (KR) / 한국어 (KR) lo_LA ko_KR lo ko_KR Left-to-Right [3,0] . , %d/%m/%Y %Y년 %m월 %d일 %H:%M:%S %H시 %M분 %S초 %H시 %M분 7
48 base.lang_lv base.lang_lo_LA Latvian / latviešu valoda Lao / ພາສາລາວ lv_LV lo_LA lv lo Left-to-Right [3,0] , .   , %Y.%m.%d. %d/%m/%Y %H:%M:%S %H:%M 1 7
49 base.lang_lt base.lang_lv Lithuanian / Lietuvių kalba Latvian / latviešu valoda lt_LT lv_LV lt lv Left-to-Right [3,0] , .   %Y-%m-%d %Y.%m.%d. %H:%M:%S %H:%M 1
50 base.lang_lb base.lang_lt Luxembourgish Lithuanian / Lietuvių kalba lb_LU lt_LT lb lt Left-to-Right [3,0] ,   . %d/%m/%Y %Y-%m-%d %H:%M:%S %H:%M 1
51 base.lang_mk base.lang_lb Macedonian / македонски јазик Luxembourgish mk_MK lb_LU mk lb Left-to-Right [3,0] ,   %d.%m.%Y %d/%m/%Y %H:%M:%S %H:%M 1
52 base.lang_ml base.lang_mk Malayalam / മലയാളം Macedonian / македонски јазик ml_IN mk_MK ml mk Left-to-Right [3,0] ,   %d/%m/%Y %d.%m.%Y %H:%M:%S %H:%M 1
53 base.lang_mn base.lang_ml Mongolian / монгол Malayalam / മലയാളം mn_MN ml_IN mn ml Left-to-Right [3,0] . , '   %Y-%m-%d %d/%m/%Y %H:%M:%S %H:%M 7 1
54 base.lang_ms base.lang_mn Malay / Bahasa Melayu Mongolian / монгол ms_MY mn_MN ms mn Left-to-Right [3,0] . , ' %d/%m/%Y %Y-%m-%d %H:%M:%S %H:%M 1 7
55 base.lang_my base.lang_ms Burmese / ဗမာစာ Malay / Bahasa Melayu my_MM ms_MY my ms Left-to-Right [3,3] [3,0] . , %Y %b %d %A %d/%m/%Y %I:%M:%S %p %H:%M:%S %H:%M 7 1
56 base.lang_nb_NO base.lang_my Norwegian Bokmål / Norsk bokmål Burmese / ဗမာစာ nb_NO my_MM nb_NO my Left-to-Right [3,0] [3,3] , .   , %d.%m.%Y %Y %b %d %A %H:%M:%S %I:%M:%S %p %I:%M %p 1 7
57 base.lang_fa_IR base.lang_nb_NO Persian / فارسی Norwegian Bokmål / Norsk bokmål fa_IR nb_NO fa nb_NO Right-to-Left Left-to-Right [3,0] . , ,   %Y/%m/%d %d.%m.%Y %H:%M:%S %H:%M 6 1
58 base.lang_pl base.lang_fa_IR Polish / Język polski Persian / فارسی pl_PL fa_IR pl fa Left-to-Right Right-to-Left [] [3,0] , . , %d.%m.%Y %Y/%m/%d %H:%M:%S %H:%M 1 6
59 base.lang_pt_AO base.lang_pl Portuguese (AO) / Português (AO) Polish / Język polski pt_AO pl_PL pt_AO pl Left-to-Right [] , %d-%m-%Y %d.%m.%Y %H:%M:%S %H:%M 1
60 base.lang_pt_BR base.lang_pt_AO Portuguese (BR) / Português (BR) Portuguese (AO) / Português (AO) pt_BR pt_AO pt_BR pt_AO Left-to-Right [3,0] [] , . %d/%m/%Y %d-%m-%Y %H:%M:%S %H:%M 7 1
61 base.lang_pt base.lang_pt_BR Portuguese / Português Portuguese (BR) / Português (BR) pt_PT pt_BR pt pt_BR Left-to-Right [] [3,0] , . %d-%m-%Y %d/%m/%Y %H:%M:%S %H:%M 1 7
62 base.lang_ro base.lang_pt Romanian / română Portuguese / Português ro_RO pt_PT ro pt Left-to-Right [3,0] [] , . %d.%m.%Y %d-%m-%Y %H:%M:%S %H:%M 1
63 base.lang_ru base.lang_ro Russian / русский язык Romanian / română ru_RU ro_RO ru ro Left-to-Right [3,0] ,   . %d.%m.%Y %H:%M:%S %H:%M 1
64 base.lang_sr_RS base.lang_ru Serbian (Cyrillic) / српски Russian / русский язык sr_RS ru_RU sr_RS ru Left-to-Right [] [3,0] ,   %d.%m.%Y. %d.%m.%Y %H:%M:%S %H:%M 7 1
65 base.lang_sr@latin base.lang_sr@Cyrl Serbian (Latin) / srpski Serbian (Cyrillic) / српски sr@latin sr@Cyrl sr@latin sr@Cyrl Left-to-Right [] . , , %m/%d/%Y %d.%m.%Y. %I:%M:%S %p %H:%M:%S %H:%M 7
66 base.lang_sk base.lang_sr@latin Slovak / Slovenský jazyk Serbian (Latin) / srpski sk_SK sr@latin sk sr@latin Left-to-Right [3,0] [] , .   , %d.%m.%Y %m/%d/%Y %H:%M:%S %I:%M:%S %p %I:%M %p 1 7
67 base.lang_sl_SI base.lang_sk Slovenian / slovenščina Slovak / Slovenský jazyk sl_SI sk_SK sl sk Left-to-Right [] [3,0] ,   %d. %m. %Y %d.%m.%Y %H:%M:%S %H:%M 1
68 base.lang_es_419 base.lang_sl_SI Spanish (Latin America) / Español (América Latina) Slovenian / slovenščina es_419 sl_SI es_419 sl Left-to-Right [3,0] [] , .   %d/%m/%Y %d. %m. %Y %H:%M:%S %H:%M 1
69 base.lang_es_AR base.lang_es_419 Spanish (AR) / Español (AR) Spanish (Latin America) / Español (América Latina) es_AR es_419 es_AR es_419 Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S %H:%M 7 1
70 base.lang_es_BO base.lang_es_AR Spanish (BO) / Español (BO) Spanish (AR) / Español (AR) es_BO es_AR es_BO es_AR Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S %H:%M 1 7
71 base.lang_es_CL base.lang_es_BO Spanish (CL) / Español (CL) Spanish (BO) / Español (BO) es_CL es_BO es_CL es_BO Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S %H:%M 1
72 base.lang_es_CO base.lang_es_CL Spanish (CO) / Español (CO) Spanish (CL) / Español (CL) es_CO es_CL es_CO es_CL Left-to-Right [3,0] , . %d-%m-%Y %d/%m/%Y %H:%M:%S %H:%M 7 1
73 base.lang_es_CR base.lang_es_CO Spanish (CR) / Español (CR) Spanish (CO) / Español (CO) es_CR es_CO es_CR es_CO Left-to-Right [3,0] . , , . %d/%m/%Y %d-%m-%Y %H:%M:%S %H:%M 1 7
74 base.lang_es_DO base.lang_es_CR Spanish (DO) / Español (DO) Spanish (CR) / Español (CR) es_DO es_CR es_DO es_CR Left-to-Right [3,0] . , %d/%m/%Y %I:%M:%S %p %H:%M:%S %H:%M 1
75 base.lang_es_EC base.lang_es_DO Spanish (EC) / Español (EC) Spanish (DO) / Español (DO) es_EC es_DO es_EC es_DO Left-to-Right [3,0] , . . , %d/%m/%Y %H:%M:%S %I:%M:%S %p %I:%M %p 1
76 base.lang_es_GT base.lang_es_EC Spanish (GT) / Español (GT) Spanish (EC) / Español (EC) es_GT es_EC es_GT es_EC Left-to-Right [3,0] . , , . %d/%m/%Y %H:%M:%S %H:%M 7 1
77 base.lang_es_MX base.lang_es_GT Spanish (MX) / Español (MX) Spanish (GT) / Español (GT) es_MX es_GT es_MX es_GT Left-to-Right [3,0] . , %d/%m/%Y %H:%M:%S %H:%M 7
78 base.lang_es_PA base.lang_es_MX Spanish (PA) / Español (PA) Spanish (MX) / Español (MX) es_PA es_MX es_PA es_MX Left-to-Right [3,0] . , %d/%m/%Y %H:%M:%S %H:%M 7
79 base.lang_es_PE base.lang_es_PA Spanish (PE) / Español (PE) Spanish (PA) / Español (PA) es_PE es_PA es_PE es_PA Left-to-Right [3,0] . , %d/%m/%Y %H:%M:%S %H:%M 7
80 base.lang_es_PY base.lang_es_PE Spanish (PY) / Español (PY) Spanish (PE) / Español (PE) es_PY es_PE es_PY es_PE Left-to-Right [3,0] , . . , %d/%m/%Y %H:%M:%S %H:%M 7
81 base.lang_es_UY base.lang_es_PY Spanish (UY) / Español (UY) Spanish (PY) / Español (PY) es_UY es_PY es_UY es_PY Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S %H:%M 1 7
82 base.lang_es_VE base.lang_es_UY Spanish (VE) / Español (VE) Spanish (UY) / Español (UY) es_VE es_UY es_VE es_UY Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S %H:%M 7 1
83 base.lang_es base.lang_es_VE Spanish / Español Spanish (VE) / Español (VE) es_ES es_VE es es_VE Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S %H:%M 1 7
84 base.lang_sw base.lang_es Swahili / Kiswahili Spanish / Español sw es_ES sw es Left-to-Right [3,0] . , , . %d/%m/%Y %H:%M:%S %H:%M 1
85 base.lang_sv_SE base.lang_sw Swedish / Svenska Swahili / Kiswahili sv_SE sw sv sw Left-to-Right [3,0] , .   , %Y-%m-%d %d/%m/%Y %H:%M:%S %H:%M 1
86 base.lang_th base.lang_sv_SE Thai / ภาษาไทย Swedish / Svenska th_TH sv_SE th sv Left-to-Right [3,0] . , ,   %d/%m/%Y %Y-%m-%d %H:%M:%S %H:%M 7 1
87 base.lang_tl base.lang_th Tagalog / Filipino Thai / ภาษาไทย tl_PH th_TH tl th Left-to-Right [3,0] . , %m/%d/%y %d/%m/%Y %H:%M:%S %H:%M 1 7
88 base.lang_tr base.lang_tl Turkish / Türkçe Tagalog / Filipino tr_TR tl_PH tr tl Left-to-Right [3,0] , . . , %d-%m-%Y %m/%d/%y %H:%M:%S %H:%M 1
89 base.lang_uk_UA base.lang_tr Ukrainian / українська Turkish / Türkçe uk_UA tr_TR uk tr Left-to-Right [3,0] ,   . %d.%m.%Y %d-%m-%Y %H:%M:%S %H:%M 1
90 base.lang_vi_VN base.lang_uk_UA Vietnamese / Tiếng Việt Ukrainian / українська vi_VN uk_UA vi uk Left-to-Right [3,0] , .   %d/%m/%Y %d.%m.%Y %H:%M:%S %H:%M 1
91 base.lang_sq_AL base.lang_vi_VN Albanian / Shqip Vietnamese / Tiếng Việt sq_AL vi_VN sq vi Left-to-Right [3,0] , . %Y-%b-%d %d/%m/%Y %I.%M.%S. %H:%M:%S %H:%M 1
92 base.lang_te_IN base.lang_sq_AL Telugu / తెలుగు Albanian / Shqip te_IN sq_AL te sq Left-to-Right [] [3,0] . , , . %B %d %A %Y %Y-%b-%d %p%I.%M.%S %I.%M.%S. %I.%M. 7 1
93 base.lang_te_IN Telugu / తెలుగు te_IN te Left-to-Right [] . , %B %d %A %Y %p%I.%M.%S %p%I.%M 7

View file

@ -180,6 +180,7 @@
<record id="bn" model="res.country"> <record id="bn" model="res.country">
<field name="name">Brunei Darussalam</field> <field name="name">Brunei Darussalam</field>
<field name="code">bn</field> <field name="code">bn</field>
<field eval="'%(street)s\n%(street2)s\n%(city)s %(zip)s\n%(country_name)s'" name="address_format" />
<field name="currency_id" ref="BND" /> <field name="currency_id" ref="BND" />
<field eval="673" name="phone_code" /> <field eval="673" name="phone_code" />
</record> </record>
@ -1104,6 +1105,7 @@
<field name="code">om</field> <field name="code">om</field>
<field name="currency_id" ref="OMR" /> <field name="currency_id" ref="OMR" />
<field eval="968" name="phone_code" /> <field eval="968" name="phone_code" />
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s %(state_name)s %(zip)s\n%(country_name)s'" />
</record> </record>
<record id="pa" model="res.country"> <record id="pa" model="res.country">
<field name="name">Panama</field> <field name="name">Panama</field>
@ -1111,6 +1113,7 @@
<field name="currency_id" ref="PAB" /> <field name="currency_id" ref="PAB" />
<field eval="507" name="phone_code" /> <field eval="507" name="phone_code" />
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s %(state_name)s %(zip)s\n%(country_name)s'" /> <field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s %(state_name)s %(zip)s\n%(country_name)s'" />
<field name="vat_label">RUC</field>
</record> </record>
<record id="pe" model="res.country"> <record id="pe" model="res.country">
<field name="name">Peru</field> <field name="name">Peru</field>
@ -1143,6 +1146,7 @@
<field name="code">pk</field> <field name="code">pk</field>
<field name="currency_id" ref="PKR" /> <field name="currency_id" ref="PKR" />
<field eval="92" name="phone_code" /> <field eval="92" name="phone_code" />
<field name="vat_label">NTN</field>
</record> </record>
<record id="pl" model="res.country"> <record id="pl" model="res.country">
<field name="name">Poland</field> <field name="name">Poland</field>

View file

@ -726,12 +726,6 @@
<field name="rate">1105.24376765</field> <field name="rate">1105.24376765</field>
</record> </record>
<record forcecreate="0" id="rateZWL" model="res.currency.rate">
<field name="currency_id" ref="ZWL" />
<field name="name">2010-01-01</field>
<field name="rate">395.80</field>
</record>
<record forcecreate="0" id="rateZIG" model="res.currency.rate"> <record forcecreate="0" id="rateZIG" model="res.currency.rate">
<field name="currency_id" ref="ZIG" /> <field name="currency_id" ref="ZIG" />
<field name="name">2024-04-08</field> <field name="name">2024-04-08</field>

View file

@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<data noupdate="1"> <data noupdate="1">
<!-- es_419 is the new "generic" spanish -->
<record id="base.lang_es" model="res.lang">
<field name="url_code">es_ES</field>
</record>
<record id="base.lang_es_419" model="res.lang">
<field name="url_code">es</field>
</record>
<function name="install_lang" model="res.lang"/> <function name="install_lang" model="res.lang"/>
</data> </data>
<data> <data>
<!-- /my is for the portal -->
<record id="base.lang_my" model="res.lang"> <record id="base.lang_my" model="res.lang">
<field name="url_code">mya</field> <field name="url_code">mya</field>
</record> </record>

View file

@ -100,6 +100,20 @@
<field name="vat">US12345675</field> <field name="vat">US12345675</field>
</record> </record>
<record id="res_partner_5" model="res.partner">
<field name="city">Florenville</field>
<field name="country_id" ref="base.be"/>
<field name="email">wow@example.com</field>
<field name="image_1920" file="base/static/img/partner_open_wood.png" type="base64"/>
<field name="is_company" eval="True"/>
<field name="mobile">+32 987 65 43 21</field>
<field name="name">OpenWood</field>
<field name="phone">+32 987 65 43 21</field>
<field name="street">Orval 1</field>
<field name="website">www.openwood.example.com</field>
<field name="zip">6823</field>
</record>
<record id="res_partner_10" model="res.partner"> <record id="res_partner_10" model="res.partner">
<field name="name">The Jackson Group</field> <field name="name">The Jackson Group</field>
<field name="is_company">1</field> <field name="is_company">1</field>

View file

@ -23,6 +23,10 @@ System</span>]]></field>
Administrator</span>]]></field> Administrator</span>]]></field>
</record> </record>
<record id="user_admin_settings" model="res.users.settings" forcecreate="0">
<field name="user_id" ref="base.user_admin"/>
</record>
<!-- Default user with full access rights for newly created users --> <!-- Default user with full access rights for newly created users -->
<record id="default_user" model="res.users"> <record id="default_user" model="res.users">
<field name="name">Default User Template</field> <field name="name">Default User Template</field>

View file

@ -42,6 +42,10 @@
<field name="image_1920" type="base64" file="base/static/img/user_demo-image.png"/> <field name="image_1920" type="base64" file="base/static/img/user_demo-image.png"/>
</record> </record>
<record id="partner_demo" model="res.partner">
<field name="user_id" ref="base.user_demo"/>
</record>
<record model="res.partner" id="base.partner_root"> <record model="res.partner" id="base.partner_root">
<field name="tz">Europe/Brussels</field> <field name="tz">Europe/Brussels</field>
</record> </record>
@ -57,6 +61,7 @@
<field name="phone">+1 555-555-5555</field> <field name="phone">+1 555-555-5555</field>
<field name="email">admin@yourcompany.example.com</field> <field name="email">admin@yourcompany.example.com</field>
<field name="tz">Europe/Brussels</field> <field name="tz">Europe/Brussels</field>
<field name="user_id" ref="base.user_admin"/>
<field name="image_1920" type="base64" file="base/static/img/partner_root-image.png"/> <field name="image_1920" type="base64" file="base/static/img/partner_root-image.png"/>
</record> </record>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ from . import ir_ui_menu
from . import ir_ui_view from . import ir_ui_view
from . import ir_asset from . import ir_asset
from . import ir_actions from . import ir_actions
from . import ir_embedded_actions
from . import ir_actions_report from . import ir_actions_report
from . import ir_attachment from . import ir_attachment
from . import ir_binary from . import ir_binary
@ -24,7 +25,6 @@ from . import ir_qweb
from . import ir_qweb_fields from . import ir_qweb_fields
from . import ir_http from . import ir_http
from . import ir_logging from . import ir_logging
from . import ir_property
from . import ir_module from . import ir_module
from . import ir_demo from . import ir_demo
from . import ir_demo_failure from . import ir_demo_failure
@ -45,5 +45,6 @@ from . import res_company
from . import res_users from . import res_users
from . import res_users_settings from . import res_users_settings
from . import res_users_deletion from . import res_users_deletion
from . import res_device
from . import decimal_precision from . import decimal_precision

View file

@ -1,22 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from contextlib import closing from contextlib import closing
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime
from lxml import etree from lxml import etree
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import base64
import copy
import hashlib import hashlib
import io import io
import itertools
import json
import logging import logging
import os import os
import re import re
import textwrap import textwrap
import uuid import uuid
import psycopg2
try: try:
import sass as libsass import sass as libsass
except ImportError: except ImportError:
@ -29,11 +23,10 @@ from rjsmin import jsmin as rjsmin
from odoo import release, SUPERUSER_ID, _ from odoo import release, SUPERUSER_ID, _
from odoo.http import request from odoo.http import request
from odoo.tools import (func, misc, transpile_javascript, from odoo.tools import (func, misc, transpile_javascript,
is_odoo_module, SourceMapGenerator, profiler, is_odoo_module, SourceMapGenerator, profiler, OrderedSet)
apply_inheritance_specs) from odoo.tools.json import scriptsafe as json
from odoo.tools.constants import SCRIPT_EXTENSIONS, STYLE_EXTENSIONS from odoo.tools.constants import SCRIPT_EXTENSIONS, STYLE_EXTENSIONS
from odoo.tools.misc import file_open, file_path from odoo.tools.misc import file_open, file_path
from odoo.tools.pycompat import to_text
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -49,6 +42,8 @@ class AssetError(Exception):
class AssetNotFound(AssetError): class AssetNotFound(AssetError):
pass pass
class XMLAssetError(Exception):
pass
class AssetsBundle(object): class AssetsBundle(object):
rx_css_import = re.compile("(@import[^;{]+;?)", re.M) rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
@ -326,22 +321,20 @@ class AssetsBundle(object):
if not js_attachment: if not js_attachment:
template_bundle = '' template_bundle = ''
if self.templates: if self.templates:
content = ['<?xml version="1.0" encoding="UTF-8"?>'] templates = self.generate_xml_bundle()
content.append('<templates xml:space="preserve">')
content.append(self.xml(show_inherit_info=not is_minified))
content.append('</templates>')
templates = '\n'.join(content).replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
template_bundle = textwrap.dedent(f""" template_bundle = textwrap.dedent(f"""
/******************************************* /*******************************************
* Templates * * Templates *
*******************************************/ *******************************************/
odoo.define('{self.name}.bundle.xml', ['@web/core/registry'], function(require){{ odoo.define("{self.name}.bundle.xml", ["@web/core/templates"], function(require) {{
'use strict'; "use strict";
const {{ registry }} = require('@web/core/registry'); const {{ checkPrimaryTemplateParents, registerTemplate, registerTemplateExtension }} = require("@web/core/templates");
registry.category(`xml_templates`).add(`{self.name}`, `{templates}`); /* {self.name} */
}});""") {templates}
}});
""")
if is_minified: if is_minified:
content_bundle = ';\n'.join(asset.minify() for asset in self.javascripts) content_bundle = ';\n'.join(asset.minify() for asset in self.javascripts)
@ -394,29 +387,64 @@ class AssetsBundle(object):
return js_attachment return js_attachment
def xml(self, show_inherit_info=False): def generate_xml_bundle(self):
content = []
blocks = []
try:
blocks = self.xml()
except XMLAssetError as e:
content.append(f'throw new Error({json.dumps(str(e))});')
def get_template(element):
element.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
string = etree.tostring(element, encoding='unicode')
return string.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
names = OrderedSet()
primary_parents = OrderedSet()
extension_parents = OrderedSet()
for block in blocks:
if block["type"] == "templates":
for (element, url, inherit_from) in block["templates"]:
if inherit_from:
primary_parents.add(inherit_from)
name = element.get("t-name")
names.add(name)
template = get_template(element)
content.append(f'registerTemplate("{name}", `{url}`, `{template}`);')
else:
for inherit_from, elements in block["extensions"].items():
extension_parents.add(inherit_from)
for (element, url) in elements:
template = get_template(element)
content.append(f'registerTemplateExtension("{inherit_from}", `{url}`, `{template}`);')
missing_names_for_primary = primary_parents - names
if missing_names_for_primary:
content.append(f'checkPrimaryTemplateParents({json.dumps(list(missing_names_for_primary))});')
missing_names_for_extension = extension_parents - names
if missing_names_for_extension:
content.append(f'console.error("Missing (extension) parent templates: {", ".join(missing_names_for_extension)}");')
return '\n'.join(content)
def xml(self):
""" """
Create the ir.attachment representing the content of the bundle XML. Create a list of blocks. A block can have one of the two types "templates" or "extensions".
The xml contents are loaded and parsed with etree. Inheritances are A template with no parent or template with t-inherit-mode="primary" goes in a block of type "templates".
applied in the order of files and templates. A template with t-inherit-mode="extension" goes in a block of type "extensions".
Used parsed attributes: Used parsed attributes:
* `t-name`: template name * `t-name`: template name
* `t-inherit`: inherited template name. The template use the * `t-inherit`: inherited template name.
`apply_inheritance_specs` method from `ir.ui.view` to apply * 't-inherit-mode': 'primary' or 'extension'.
inheritance (with xpath and position).
* 't-inherit-mode': 'primary' to create a new template with the
update, or 'extension' to apply the update on the inherited
template.
* `t-extend` deprecated attribute, used by the JavaScript Qweb.
:param show_inherit_info: if true add the file url and inherit :return a list of blocks
information in the template.
:return ir.attachment representing the content of the bundle XML
""" """
template_dict = OrderedDict()
parser = etree.XMLParser(ns_clean=True, recover=True, remove_comments=True) parser = etree.XMLParser(ns_clean=True, recover=True, remove_comments=True)
blocks = []
block = None
for asset in self.templates: for asset in self.templates:
# Load content. # Load content.
try: try:
@ -425,106 +453,36 @@ class AssetsBundle(object):
io_content = io.BytesIO(template.encode('utf-8')) io_content = io.BytesIO(template.encode('utf-8'))
content_templates_tree = etree.parse(io_content, parser=parser).getroot() content_templates_tree = etree.parse(io_content, parser=parser).getroot()
except etree.ParseError as e: except etree.ParseError as e:
_logger.error("Could not parse file %s: %s", asset.url, e.msg) return asset.generate_error(f'Could not parse file: {e.msg}')
raise
addon = asset.url.split('/')[1]
template_dict.setdefault(addon, OrderedDict())
# Process every templates. # Process every templates.
for template_tree in list(content_templates_tree): for template_tree in list(content_templates_tree):
template_name = None template_name = template_tree.get("t-name")
if 't-name' in template_tree.attrib: inherit_from = template_tree.get("t-inherit")
template_name = template_tree.attrib['t-name'] inherit_mode = None
dotted_names = template_name.split('.', 1) if inherit_from:
if len(dotted_names) > 1 and dotted_names[0] == addon: inherit_mode = template_tree.get('t-inherit-mode', 'primary')
template_name = dotted_names[1]
if 't-inherit' in template_tree.attrib:
inherit_mode = template_tree.attrib.get('t-inherit-mode', 'primary')
if inherit_mode not in ['primary', 'extension']: if inherit_mode not in ['primary', 'extension']:
raise ValueError(_("Invalid inherit mode. Module %r and template name %r", addon, template_name)) addon = asset.url.split('/')[1]
return asset.generate_error(_(
# Get inherited template, the identifier can be "addon.name", just "name" or (silly) "just.name.with.dots" 'Invalid inherit mode. Module "%(module)s" and template name "%(template_name)s"',
parent_dotted_name = template_tree.attrib['t-inherit'] module=addon,
split_name_attempt = parent_dotted_name.split('.', 1) template_name=template_name,
parent_addon, parent_name = split_name_attempt if len(split_name_attempt) == 2 else (addon, parent_dotted_name) ))
if parent_addon not in template_dict: if inherit_mode == "extension":
if parent_dotted_name in template_dict[addon]: if block is None or block["type"] != "extensions":
parent_addon = addon block = {"type": "extensions", "extensions": OrderedDict()}
parent_name = parent_dotted_name blocks.append(block)
else: block["extensions"].setdefault(inherit_from, [])
raise ValueError(_("Module %r not loaded or inexistent (try to inherit %r), or templates of addon being loaded %r are misordered (template %r)", parent_addon, parent_name, addon, template_name)) block["extensions"][inherit_from].append((template_tree, asset.url))
if parent_name not in template_dict[parent_addon]:
raise ValueError(_("Cannot create %r because the template to inherit %r is not found.", '%s.%s' % (addon, template_name), '%s.%s' % (parent_addon, parent_name)))
# After several performance tests, we found out that deepcopy is the most efficient
# solution in this case (compared with copy, xpath with '.' and stringifying).
parent_tree, parent_urls = template_dict[parent_addon][parent_name]
parent_tree = copy.deepcopy(parent_tree)
if show_inherit_info:
# Add inheritance information as xml comment for debugging.
xpaths = []
for item in template_tree:
position = item.get('position')
attrib = dict(**item.attrib)
attrib.pop('position', None)
comment = etree.Comment(f""" Filepath: {asset.url} ; position="{position}" ; {attrib} """)
if position == "attributes":
if item.get('expr'):
comment_node = etree.Element('xpath', {'expr': item.get('expr'), 'position': 'before'})
else:
comment_node = etree.Element(item.tag, item.attrib)
comment_node.attrib['position'] = 'before'
comment_node.append(comment)
xpaths.append(comment_node)
else:
if len(item) > 0:
item[0].addprevious(comment)
else:
item.append(comment)
xpaths.append(item)
else:
xpaths = list(template_tree)
# Apply inheritance.
if inherit_mode == 'primary':
parent_tree.tag = template_tree.tag
inherited_template = apply_inheritance_specs(parent_tree, xpaths)
if inherit_mode == 'primary': # New template_tree: A' = B(A)
for attr_name, attr_val in template_tree.attrib.items():
if attr_name not in ('t-inherit', 't-inherit-mode'):
inherited_template.set(attr_name, attr_val)
if not template_name:
raise ValueError(_("Template name is missing in file %r.", asset.url))
template_dict[addon][template_name] = (inherited_template, parent_urls + [asset.url])
else: # Modifies original: A = B(A)
template_dict[parent_addon][parent_name] = (inherited_template, parent_urls + [asset.url])
elif template_name: elif template_name:
if template_name in template_dict[addon]: if block is None or block["type"] != "templates":
raise ValueError(_("Template %r already exists in module %r", template_name, addon)) block = {"type": "templates", "templates": []}
template_dict[addon][template_name] = (template_tree, [asset.url]) blocks.append(block)
elif template_tree.attrib.get('t-extend'): block["templates"].append((template_tree, asset.url, inherit_from))
template_name = '%s__extend_%s' % (template_tree.attrib.get('t-extend'), len(template_dict[addon]))
template_dict[addon][template_name] = (template_tree, [asset.url])
else: else:
raise ValueError(_("Template name is missing in file %r.", asset.url)) return asset.generate_error(_("Template name is missing."))
return blocks
# Concat and render inherited templates
root = etree.Element('root')
for addon in template_dict.values():
for template, urls in addon.values():
if show_inherit_info:
tail = "\n"
if len(root) > 0:
tail = root[-1].tail
root[-1].tail = "\n\n"
comment = etree.Comment(f""" Filepath: {' => '.join(urls)} """)
comment.tail = tail
root.append(comment)
root.append(template)
# Returns the string by removing the <root> tag.
return etree.tostring(root, encoding='unicode')[6:-7]
def css(self): def css(self):
is_minified = not self.is_debug_assets is_minified = not self.is_debug_assets
@ -652,7 +610,7 @@ css_error_message {
"""Sanitizes @import rules, remove duplicates @import rules, then compile""" """Sanitizes @import rules, remove duplicates @import rules, then compile"""
imports = [] imports = []
def handle_compile_error(e, source): def handle_compile_error(e, source):
error = self.get_preprocessor_error(e, source=source) error = self.get_preprocessor_error(str(e), source=source)
_logger.warning(error) _logger.warning(error)
self.css_errors.append(error) self.css_errors.append(error)
return '' return ''
@ -668,7 +626,6 @@ css_error_message {
return '' return ''
source = re.sub(self.rx_preprocess_imports, sanitize, source) source = re.sub(self.rx_preprocess_imports, sanitize, source)
compiled = ''
try: try:
compiled = compiler(source) compiled = compiler(source)
except CompileError as e: except CompileError as e:
@ -700,7 +657,7 @@ css_error_message {
cmd = [rtlcss, '-c', file_path("base/data/rtlcss.json"), '-'] cmd = [rtlcss, '-c', file_path("base/data/rtlcss.json"), '-']
try: try:
rtlcss = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) rtlcss = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, encoding='utf-8')
except Exception: except Exception:
# Check the presence of rtlcss, if rtlcss not available then we should return normal less file # Check the presence of rtlcss, if rtlcss not available then we should return normal less file
@ -717,23 +674,20 @@ css_error_message {
self.css_errors.append(msg) self.css_errors.append(msg)
return '' return ''
stdout, stderr = rtlcss.communicate(input=source.encode('utf-8')) out, err = rtlcss.communicate(input=source)
if rtlcss.returncode or (source and not stdout): if rtlcss.returncode or (source and not out):
cmd_output = ''.join(misc.ustr(stderr)) if rtlcss.returncode:
if not cmd_output and rtlcss.returncode: error = self.get_rtlcss_error(err or f"Process exited with return code {rtlcss.returncode}", source=source)
cmd_output = "Process exited with return code %d\n" % rtlcss.returncode else:
elif not cmd_output: error = "rtlcss: error processing payload\n"
cmd_output = "rtlcss: error processing payload\n" _logger.warning("%s", error)
error = self.get_rtlcss_error(cmd_output, source=source)
_logger.warning(error)
self.css_errors.append(error) self.css_errors.append(error)
return '' return ''
rtlcss_result = stdout.strip().decode('utf8') return out.strip()
return rtlcss_result
def get_preprocessor_error(self, stderr, source=None): def get_preprocessor_error(self, stderr, source=None):
"""Improve and remove sensitive information from sass/less compilator error messages""" """Improve and remove sensitive information from sass/less compilator error messages"""
error = misc.ustr(stderr).split('Load paths')[0].replace(' Use --trace for backtrace.', '') error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
if 'Cannot load compass' in error: if 'Cannot load compass' in error:
error += "Maybe you should install the compass gem using this extra argument:\n\n" \ error += "Maybe you should install the compass gem using this extra argument:\n\n" \
" $ sudo gem install compass --pre\n" " $ sudo gem install compass --pre\n"
@ -745,8 +699,8 @@ css_error_message {
def get_rtlcss_error(self, stderr, source=None): def get_rtlcss_error(self, stderr, source=None):
"""Improve and remove sensitive information from sass/less compilator error messages""" """Improve and remove sensitive information from sass/less compilator error messages"""
error = misc.ustr(stderr).split('Load paths')[0].replace(' Use --trace for backtrace.', '') error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
error += "This error occurred while compiling the bundle '%s' containing:" % self.name error = f"{error}This error occurred while compiling the bundle {self.name!r} containing:"
return error return error
@ -765,6 +719,11 @@ class WebAsset(object):
if not inline and not url: if not inline and not url:
raise Exception("An asset should either be inlined or url linked, defined in bundle '%s'" % bundle.name) raise Exception("An asset should either be inlined or url linked, defined in bundle '%s'" % bundle.name)
def generate_error(self, msg):
msg = f'{msg!r} in file {self.url!r}'
_logger.error(msg) # log it in the python console in all cases.
return msg
@func.lazy_property @func.lazy_property
def id(self): def id(self):
if self._id is None: self._id = str(uuid.uuid4()) if self._id is None: self._id = str(uuid.uuid4())
@ -840,6 +799,10 @@ class JavascriptAsset(WebAsset):
self._is_transpiled = None self._is_transpiled = None
self._converted_content = None self._converted_content = None
def generate_error(self, msg):
msg = super().generate_error(msg)
return f'console.error({json.dumps(msg)});'
@property @property
def bundle_version(self): def bundle_version(self):
return self.bundle.get_version('js') return self.bundle.get_version('js')
@ -847,7 +810,7 @@ class JavascriptAsset(WebAsset):
@property @property
def is_transpiled(self): def is_transpiled(self):
if self._is_transpiled is None: if self._is_transpiled is None:
self._is_transpiled = bool(is_odoo_module(super().content)) self._is_transpiled = bool(is_odoo_module(self.url, super().content))
return self._is_transpiled return self._is_transpiled
@property @property
@ -866,7 +829,7 @@ class JavascriptAsset(WebAsset):
try: try:
return super()._fetch_content() return super()._fetch_content()
except AssetError as e: except AssetError as e:
return u"console.error(%s);" % json.dumps(to_text(e)) return self.generate_error(str(e))
def with_header(self, content=None, minimal=True): def with_header(self, content=None, minimal=True):
@ -898,17 +861,21 @@ class XMLAsset(WebAsset):
try: try:
content = super()._fetch_content() content = super()._fetch_content()
except AssetError as e: except AssetError as e:
return u"console.error(%s);" % json.dumps(to_text(e)) return self.generate_error(str(e))
parser = etree.XMLParser(ns_clean=True, remove_comments=True, resolve_entities=False) parser = etree.XMLParser(ns_clean=True, remove_comments=True, resolve_entities=False)
try: try:
root = etree.fromstring(content.encode('utf-8'), parser=parser) root = etree.fromstring(content.encode('utf-8'), parser=parser)
except etree.XMLSyntaxError as e: except etree.XMLSyntaxError as e:
return f'<t t-name="parsing_error{self.url.replace("/","_")}"><parsererror>Invalid XML template: {self.url} \n {e.msg} </parsererror></t>' return self.generate_error(f'Invalid XML template: {e.msg}')
if root.tag in ('templates', 'template'): if root.tag in ('templates', 'template'):
return ''.join(etree.tostring(el, encoding='unicode') for el in root) return ''.join(etree.tostring(el, encoding='unicode') for el in root)
return etree.tostring(root, encoding='unicode') return etree.tostring(root, encoding='unicode')
def generate_error(self, msg):
msg = super().generate_error(msg)
raise XMLAssetError(msg)
@property @property
def bundle_version(self): def bundle_version(self):
return self.bundle.get_version('js') return self.bundle.get_version('js')
@ -1008,17 +975,17 @@ class PreprocessedCSS(StylesheetAsset):
command = self.get_command() command = self.get_command()
try: try:
compiler = Popen(command, stdin=PIPE, stdout=PIPE, compiler = Popen(command, stdin=PIPE, stdout=PIPE,
stderr=PIPE) stderr=PIPE, encoding='utf-8')
except Exception: except Exception:
raise CompileError("Could not execute command %r" % command[0]) raise CompileError("Could not execute command %r" % command[0])
(out, err) = compiler.communicate(input=source.encode('utf-8')) out, err = compiler.communicate(input=source)
if compiler.returncode: if compiler.returncode:
cmd_output = misc.ustr(out) + misc.ustr(err) cmd_output = out + err
if not cmd_output: if not cmd_output:
cmd_output = u"Process exited with return code %d\n" % compiler.returncode cmd_output = u"Process exited with return code %d\n" % compiler.returncode
raise CompileError(cmd_output) raise CompileError(cmd_output)
return out.decode('utf8') return out
class SassStylesheetAsset(PreprocessedCSS): class SassStylesheetAsset(PreprocessedCSS):
rx_indent = re.compile(r'^( +|\t+)', re.M) rx_indent = re.compile(r'^( +|\t+)', re.M)

View file

@ -15,6 +15,7 @@ import logging
from operator import getitem from operator import getitem
import requests import requests
import json import json
import re
import contextlib import contextlib
from pytz import timezone from pytz import timezone
@ -55,9 +56,12 @@ class IrActions(models.Model):
_order = 'name' _order = 'name'
_allow_sudo_commands = False _allow_sudo_commands = False
_sql_constraints = [('path_unique', 'unique(path)', "Path to show in the URL must be unique! Please choose another one.")]
name = fields.Char(string='Action Name', required=True, translate=True) name = fields.Char(string='Action Name', required=True, translate=True)
type = fields.Char(string='Action Type', required=True) type = fields.Char(string='Action Type', required=True)
xml_id = fields.Char(compute='_compute_xml_id', string="External ID") xml_id = fields.Char(compute='_compute_xml_id', string="External ID")
path = fields.Char(string="Path to show in the URL")
help = fields.Html(string='Action Description', help = fields.Html(string='Action Description',
help='Optional help text for the users with a description of the target view, such as its usage and purpose.', help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
translate=True) translate=True)
@ -68,6 +72,30 @@ class IrActions(models.Model):
required=True, default='action') required=True, default='action')
binding_view_types = fields.Char(default='list,form') binding_view_types = fields.Char(default='list,form')
@api.constrains('path')
def _check_path(self):
for action in self:
if action.path:
if not re.fullmatch(r'[a-z][a-z0-9_-]*', action.path):
raise ValidationError(_('The path should contain only lowercase alphanumeric characters, underscore, and dash, and it should start with a letter.'))
if action.path.startswith("m-"):
raise ValidationError(_("'m-' is a reserved prefix."))
if action.path.startswith("action-"):
raise ValidationError(_("'action-' is a reserved prefix."))
if action.path == "new":
raise ValidationError(_("'new' is reserved, and can not be used as path."))
# Tables ir_act_window, ir_act_report_xml, ir_act_url, ir_act_server and ir_act_client
# inherit from table ir_actions (see base_data.sql). The path must be unique across
# all these tables. The unique constraint is not enough because a big limitation of
# the inheritance feature is that unique indexes only apply to single tables, and
# not accross all the tables. So we need to check the uniqueness of the path manually.
# For more information, see: https://www.postgresql.org/docs/14/ddl-inherit.html#DDL-INHERIT-CAVEATS
# Note that, we leave the unique constraint in place to check the uniqueness of the path
# within the same table before checking the uniqueness across all the tables.
if (self.env['ir.actions.actions'].search_count([('path', '=', action.path)]) > 1):
raise ValidationError(_("Path to show in the URL must be unique! Please choose another one."))
def _compute_xml_id(self): def _compute_xml_id(self):
res = self.get_external_id() res = self.get_external_id()
for record in self: for record in self:
@ -132,7 +160,7 @@ class IrActions(models.Model):
for action in all_actions: for action in all_actions:
action = dict(action) action = dict(action)
groups = action.pop('groups_id', None) groups = action.pop('groups_id', None)
if groups and not self.user_has_groups(groups): if groups and not any(self.env.user.has_group(ext_id) for ext_id in groups):
# the user may not perform this action # the user may not perform this action
continue continue
res_model = action.pop('res_model', None) res_model = action.pop('res_model', None)
@ -167,13 +195,16 @@ class IrActions(models.Model):
try: try:
action = self.env[action_model].sudo().browse(action_id) action = self.env[action_model].sudo().browse(action_id)
fields = ['name', 'binding_view_types'] fields = ['name', 'binding_view_types']
for field in ('groups_id', 'res_model', 'sequence'): for field in ('groups_id', 'res_model', 'sequence', 'domain'):
if field in action._fields: if field in action._fields:
fields.append(field) fields.append(field)
action = action.read(fields)[0] action = action.read(fields)[0]
if action.get('groups_id'): if action.get('groups_id'):
# transform the list of ids into a list of xml ids
groups = self.env['res.groups'].browse(action['groups_id']) groups = self.env['res.groups'].browse(action['groups_id'])
action['groups_id'] = ','.join(ext_id for ext_id in groups._ensure_xml_id().values()) action['groups_id'] = list(groups._ensure_xml_id().values())
if 'domain' in action and not action.get('domain'):
action.pop('domain')
result[binding_type].append(frozendict(action)) result[binding_type].append(frozendict(action))
except (MissingError): except (MissingError):
continue continue
@ -217,6 +248,7 @@ class IrActions(models.Model):
return { return {
"binding_model_id", "binding_type", "binding_view_types", "binding_model_id", "binding_type", "binding_view_types",
"display_name", "help", "id", "name", "type", "xml_id", "display_name", "help", "id", "name", "type", "xml_id",
"path",
} }
@ -232,9 +264,9 @@ class IrActionsActWindow(models.Model):
def _check_model(self): def _check_model(self):
for action in self: for action in self:
if action.res_model not in self.env: if action.res_model not in self.env:
raise ValidationError(_('Invalid model name %r in action definition.', action.res_model)) raise ValidationError(_('Invalid model name %s in action definition.', action.res_model))
if action.binding_model_id and action.binding_model_id.model not in self.env: if action.binding_model_id and action.binding_model_id.model not in self.env:
raise ValidationError(_('Invalid model name %r in action definition.', action.binding_model_id.model)) raise ValidationError(_('Invalid model name %s in action definition.', action.binding_model_id.model))
@api.depends('view_ids.view_mode', 'view_mode', 'view_id.type') @api.depends('view_ids.view_mode', 'view_mode', 'view_id.type')
def _compute_views(self): def _compute_views(self):
@ -265,7 +297,7 @@ class IrActionsActWindow(models.Model):
if len(modes) != len(set(modes)): if len(modes) != len(set(modes)):
raise ValidationError(_('The modes in view_mode must not be duplicated: %s', modes)) raise ValidationError(_('The modes in view_mode must not be duplicated: %s', modes))
if ' ' in modes: if ' ' in modes:
raise ValidationError(_('No spaces allowed in view_mode: %r', modes)) raise ValidationError(_('No spaces allowed in view_mode: %s', modes))
type = fields.Char(default="ir.actions.act_window") type = fields.Char(default="ir.actions.act_window")
view_id = fields.Many2one('ir.ui.view', string='View Ref.', ondelete='set null') view_id = fields.Many2one('ir.ui.view', string='View Ref.', ondelete='set null')
@ -277,8 +309,8 @@ class IrActionsActWindow(models.Model):
res_model = fields.Char(string='Destination Model', required=True, res_model = fields.Char(string='Destination Model', required=True,
help="Model name of the object to open in the view window") help="Model name of the object to open in the view window")
target = fields.Selection([('current', 'Current Window'), ('new', 'New Window'), ('inline', 'Inline Edit'), ('fullscreen', 'Full Screen'), ('main', 'Main action of Current Window')], default="current", string='Target Window') target = fields.Selection([('current', 'Current Window'), ('new', 'New Window'), ('inline', 'Inline Edit'), ('fullscreen', 'Full Screen'), ('main', 'Main action of Current Window')], default="current", string='Target Window')
view_mode = fields.Char(required=True, default='tree,form', view_mode = fields.Char(required=True, default='list,form',
help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)") help="Comma-separated list of allowed view modes, such as 'form', 'list', 'calendar', etc. (Default: list,form)")
mobile_view_mode = fields.Char(default="kanban", help="First view mode in mobile and small screen environments (default='kanban'). If it can't be found among available view modes, the same mode as for wider screens is used)") mobile_view_mode = fields.Char(default="kanban", help="First view mode in mobile and small screen environments (default='kanban'). If it can't be found among available view modes, the same mode as for wider screens is used)")
usage = fields.Char(string='Action Usage', usage = fields.Char(string='Action Usage',
help="Used to filter menu and home actions from the user form.") help="Used to filter menu and home actions from the user form.")
@ -291,8 +323,14 @@ class IrActionsActWindow(models.Model):
groups_id = fields.Many2many('res.groups', 'ir_act_window_group_rel', groups_id = fields.Many2many('res.groups', 'ir_act_window_group_rel',
'act_id', 'gid', string='Groups') 'act_id', 'gid', string='Groups')
search_view_id = fields.Many2one('ir.ui.view', string='Search View Ref.') search_view_id = fields.Many2one('ir.ui.view', string='Search View Ref.')
embedded_action_ids = fields.One2many('ir.embedded.actions', compute="_compute_embedded_actions")
filter = fields.Boolean() filter = fields.Boolean()
def _compute_embedded_actions(self):
embedded_actions = self.env["ir.embedded.actions"].search([('parent_action_id', 'in', self.ids)]).filtered(lambda x: x.is_visible)
for action in self:
action.embedded_action_ids = embedded_actions.filtered(lambda rec: rec.parent_action_id == action)
def read(self, fields=None, load='_classic_read'): def read(self, fields=None, load='_classic_read'):
""" call the method get_empty_list_help of the model and set the window action help message """ call the method get_empty_list_help of the model and set the window action help message
""" """
@ -336,20 +374,33 @@ class IrActionsActWindow(models.Model):
def _get_readable_fields(self): def _get_readable_fields(self):
return super()._get_readable_fields() | { return super()._get_readable_fields() | {
"context", "mobile_view_mode", "domain", "filter", "groups_id", "limit", "context", "mobile_view_mode", "domain", "filter", "groups_id", "limit",
"res_id", "res_model", "search_view_id", "target", "view_id", "view_mode", "views", "res_id", "res_model", "search_view_id", "target", "view_id", "view_mode", "views", "embedded_action_ids",
# `flags` is not a real field of ir.actions.act_window but is used # `flags` is not a real field of ir.actions.act_window but is used
# to give the parameters to generate the action # to give the parameters to generate the action
"flags" "flags",
# this is used by frontend, with the document layout wizard before send and print
"close_on_report_download",
} }
def _get_action_dict(self):
""" Override to return action content with detailed embedded actions data if available.
:return: A dict with updated action dictionary including embedded actions information.
"""
result = super()._get_action_dict()
if embedded_action_ids := result["embedded_action_ids"]:
EmbeddedActions = self.env["ir.embedded.actions"]
embedded_fields = EmbeddedActions._get_readable_fields()
result["embedded_action_ids"] = EmbeddedActions.browse(embedded_action_ids).read(embedded_fields)
return result
VIEW_TYPES = [ VIEW_TYPES = [
('tree', 'Tree'), ('list', 'List'),
('form', 'Form'), ('form', 'Form'),
('graph', 'Graph'), ('graph', 'Graph'),
('pivot', 'Pivot'), ('pivot', 'Pivot'),
('calendar', 'Calendar'), ('calendar', 'Calendar'),
('gantt', 'Gantt'),
('kanban', 'Kanban'), ('kanban', 'Kanban'),
] ]
@ -463,6 +514,7 @@ class IrActionsServer(models.Model):
# - records: recordset of all records on which the action is triggered in multi-mode; may be void # - records: recordset of all records on which the action is triggered in multi-mode; may be void
# - time, datetime, dateutil, timezone: useful Python libraries # - time, datetime, dateutil, timezone: useful Python libraries
# - float_compare: utility function to compare floats based on specific precision # - float_compare: utility function to compare floats based on specific precision
# - b64encode, b64decode: functions to encode/decode binary data
# - log: log(message, level='info'): logging function to record debug information in ir.logging table # - log: log(message, level='info'): logging function to record debug information in ir.logging table
# - _logger: _logger.info(message): logger to emit messages in server logs # - _logger: _logger.info(message): logger to emit messages in server logs
# - UserError: exception class for raising user-facing warning messages # - UserError: exception class for raising user-facing warning messages
@ -697,15 +749,15 @@ class IrActionsServer(models.Model):
action.webhook_sample_payload = False action.webhook_sample_payload = False
continue continue
payload = { payload = {
'id': 1, '_id': 1,
'_model': self.model_id.model, '_model': self.model_id.model,
'_name': action.name, '_action': f'{action.name}(#{action.id})',
} }
if self.model_id: if self.model_id:
sample_record = self.env[self.model_id.model].with_context(active_test=False).search([], limit=1) sample_record = self.env[self.model_id.model].with_context(active_test=False).search([], limit=1)
for field in action.webhook_field_ids: for field in action.webhook_field_ids:
if sample_record: if sample_record:
payload['id'] = sample_record.id payload['_id'] = sample_record.id
payload.update(sample_record.read(self.webhook_field_ids.mapped('name'), load=None)[0]) payload.update(sample_record.read(self.webhook_field_ids.mapped('name'), load=None)[0])
else: else:
payload[field.name] = WEBHOOK_SAMPLE_VALUES[field.ttype] if field.ttype in WEBHOOK_SAMPLE_VALUES else WEBHOOK_SAMPLE_VALUES[None] payload[field.name] = WEBHOOK_SAMPLE_VALUES[field.ttype] if field.ttype in WEBHOOK_SAMPLE_VALUES else WEBHOOK_SAMPLE_VALUES[None]
@ -725,8 +777,8 @@ class IrActionsServer(models.Model):
raise ValidationError(msg) raise ValidationError(msg)
@api.constrains('child_ids') @api.constrains('child_ids')
def _check_recursion(self): def _check_child_recursion(self):
if not self._check_m2m_recursion('child_ids'): if self._has_cycle('child_ids'):
raise ValidationError(_('Recursion found in child server actions')) raise ValidationError(_('Recursion found in child server actions'))
def _get_readable_fields(self): def _get_readable_fields(self):
@ -767,7 +819,7 @@ class IrActionsServer(models.Model):
def unlink_action(self): def unlink_action(self):
""" Remove the contextual actions created for the server actions. """ """ Remove the contextual actions created for the server actions. """
self.check_access_rights('write', raise_exception=True) self.check_access('write')
self.filtered('binding_model_id').write({'binding_model_id': False}) self.filtered('binding_model_id').write({'binding_model_id': False})
return True return True
@ -790,7 +842,7 @@ class IrActionsServer(models.Model):
record_cached = self._context['onchange_self'] record_cached = self._context['onchange_self']
for field, new_value in res.items(): for field, new_value in res.items():
record_cached[field] = new_value record_cached[field] = new_value
else: elif self.update_path:
starting_record = self.env[self.model_id.model].browse(self._context.get('active_id')) starting_record = self.env[self.model_id.model].browse(self._context.get('active_id'))
_, _, target_records = self._traverse_path(record=starting_record) _, _, target_records = self._traverse_path(record=starting_record)
target_records.write(res) target_records.write(res)
@ -915,7 +967,7 @@ class IrActionsServer(models.Model):
else: else:
model_name = action.model_id.model model_name = action.model_id.model
try: try:
self.env[model_name].check_access_rights("write") self.env[model_name].check_access("write")
except AccessError: except AccessError:
_logger.warning("Forbidden server action %r executed while the user %s does not have access to %s.", _logger.warning("Forbidden server action %r executed while the user %s does not have access to %s.",
action.name, self.env.user.login, model_name, action.name, self.env.user.login, model_name,
@ -925,11 +977,11 @@ class IrActionsServer(models.Model):
eval_context = self._get_eval_context(action) eval_context = self._get_eval_context(action)
records = eval_context.get('record') or eval_context['model'] records = eval_context.get('record') or eval_context['model']
records |= eval_context.get('records') or eval_context['model'] records |= eval_context.get('records') or eval_context['model']
if records.ids: if not action_groups and records.ids:
# check access rules on real records only; base automations of # check access rules on real records only; base automations of
# type 'onchange' can run server actions on new records # type 'onchange' can run server actions on new records
try: try:
records.check_access_rule('write') records.check_access('write')
except AccessError: except AccessError:
_logger.warning("Forbidden server action %r executed while the user %s does not have access to %s.", _logger.warning("Forbidden server action %r executed while the user %s does not have access to %s.",
action.name, self.env.user.login, records, action.name, self.env.user.login, records,
@ -1016,6 +1068,8 @@ class IrActionsServer(models.Model):
elif action.update_field_id.ttype in ['many2one', 'integer']: elif action.update_field_id.ttype in ['many2one', 'integer']:
try: try:
expr = int(action.value) expr = int(action.value)
if expr == 0 and action.update_field_id.ttype == 'many2one':
expr = False
except Exception: except Exception:
pass pass
elif action.update_field_id.ttype == 'float': elif action.update_field_id.ttype == 'float':
@ -1026,9 +1080,11 @@ class IrActionsServer(models.Model):
def copy_data(self, default=None): def copy_data(self, default=None):
default = default or {} default = default or {}
vals_list = super().copy_data(default=default)
if not default.get('name'): if not default.get('name'):
default['name'] = _('%s (copy)', self.name) for vals in vals_list:
return super().copy_data(default=default) vals['name'] = _('%s (copy)', vals.get('name', ''))
return vals_list
class IrActionsTodo(models.Model): class IrActionsTodo(models.Model):
""" """

Some files were not shown because too many files have changed in this diff Show more