1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4
5"""
6Miscellaneous tools used by OpenERP.
7"""
8import cProfile
9import collections
10import datetime
11import hmac as hmac_lib
12import hashlib
13import io
14import os
15import pickle as pickle_
16import re
17import socket
18import subprocess
19import sys
20import threading
21import time
22import traceback
23import types
24import unicodedata
25import zipfile
26from collections import OrderedDict
27from collections.abc import Iterable, Mapping, MutableMapping, MutableSet
28from contextlib import contextmanager
29from difflib import HtmlDiff
30from functools import wraps
31from itertools import islice, groupby as itergroupby
32from operator import itemgetter
33
34import babel
35import babel.dates
36import passlib.utils
37import pytz
38import werkzeug.utils
39from lxml import etree
40
41import odoo
42import odoo.addons
43# get_encodings, ustr and exception_to_unicode were originally from tools.misc.
44# There are moved to loglevels until we refactor tools.
45from odoo.loglevels import get_encodings, ustr, exception_to_unicode     # noqa
46from . import pycompat
47from .cache import *
48from .config import config
49from .parse_version import parse_version
50from .which import which
51
52_logger = logging.getLogger(__name__)
53
54# List of etree._Element subclasses that we choose to ignore when parsing XML.
55# We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
56SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase, etree._Entity)
57
58# Configure default global parser
59etree.set_default_parser(etree.XMLParser(resolve_entities=False))
60
61#----------------------------------------------------------
62# Subprocesses
63#----------------------------------------------------------
64
65def find_in_path(name):
66    path = os.environ.get('PATH', os.defpath).split(os.pathsep)
67    if config.get('bin_path') and config['bin_path'] != 'None':
68        path.append(config['bin_path'])
69    return which(name, path=os.pathsep.join(path))
70
71def _exec_pipe(prog, args, env=None):
72    cmd = (prog,) + args
73    # on win32, passing close_fds=True is not compatible
74    # with redirecting std[in/err/out]
75    close_fds = os.name=="posix"
76    pop = subprocess.Popen(cmd, bufsize=-1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=close_fds, env=env)
77    return pop.stdin, pop.stdout
78
79def exec_command_pipe(name, *args):
80    prog = find_in_path(name)
81    if not prog:
82        raise Exception('Command `%s` not found.' % name)
83    return _exec_pipe(prog, args)
84
85#----------------------------------------------------------
86# Postgres subprocesses
87#----------------------------------------------------------
88
89def find_pg_tool(name):
90    path = None
91    if config['pg_path'] and config['pg_path'] != 'None':
92        path = config['pg_path']
93    try:
94        return which(name, path=path)
95    except IOError:
96        raise Exception('Command `%s` not found.' % name)
97
98def exec_pg_environ():
99    """
100    Force the database PostgreSQL environment variables to the database
101    configuration of Odoo.
102
103    Note: On systems where pg_restore/pg_dump require an explicit password
104    (i.e.  on Windows where TCP sockets are used), it is necessary to pass the
105    postgres user password in the PGPASSWORD environment variable or in a
106    special .pgpass file.
107
108    See also http://www.postgresql.org/docs/8.4/static/libpq-envars.html
109    """
110    env = os.environ.copy()
111    if odoo.tools.config['db_host']:
112        env['PGHOST'] = odoo.tools.config['db_host']
113    if odoo.tools.config['db_port']:
114        env['PGPORT'] = str(odoo.tools.config['db_port'])
115    if odoo.tools.config['db_user']:
116        env['PGUSER'] = odoo.tools.config['db_user']
117    if odoo.tools.config['db_password']:
118        env['PGPASSWORD'] = odoo.tools.config['db_password']
119    return env
120
121def exec_pg_command(name, *args):
122    prog = find_pg_tool(name)
123    env = exec_pg_environ()
124    with open(os.devnull) as dn:
125        args2 = (prog,) + args
126        rc = subprocess.call(args2, env=env, stdout=dn, stderr=subprocess.STDOUT)
127        if rc:
128            raise Exception('Postgres subprocess %s error %s' % (args2, rc))
129
130def exec_pg_command_pipe(name, *args):
131    prog = find_pg_tool(name)
132    env = exec_pg_environ()
133    return _exec_pipe(prog, args, env)
134
135#----------------------------------------------------------
136# File paths
137#----------------------------------------------------------
138#file_path_root = os.getcwd()
139#file_path_addons = os.path.join(file_path_root, 'addons')
140
141def file_open(name, mode="r", subdir='addons', pathinfo=False):
142    """Open a file from the OpenERP root, using a subdir folder.
143
144    Example::
145
146    >>> file_open('hr/report/timesheer.xsl')
147    >>> file_open('addons/hr/report/timesheet.xsl')
148
149    @param name name of the file
150    @param mode file open mode
151    @param subdir subdirectory
152    @param pathinfo if True returns tuple (fileobject, filepath)
153
154    @return fileobject if pathinfo is False else (fileobject, filepath)
155    """
156    adps = odoo.addons.__path__
157    rtp = os.path.normcase(os.path.abspath(config['root_path']))
158
159    basename = name
160
161    if os.path.isabs(name):
162        # It is an absolute path
163        # Is it below 'addons_path' or 'root_path'?
164        name = os.path.normcase(os.path.normpath(name))
165        for root in adps + [rtp]:
166            root = os.path.normcase(os.path.normpath(root)) + os.sep
167            if name.startswith(root):
168                base = root.rstrip(os.sep)
169                name = name[len(base) + 1:]
170                break
171        else:
172            # It is outside the OpenERP root: skip zipfile lookup.
173            base, name = os.path.split(name)
174        return _fileopen(name, mode=mode, basedir=base, pathinfo=pathinfo, basename=basename)
175
176    if name.replace(os.sep, '/').startswith('addons/'):
177        subdir = 'addons'
178        name2 = name[7:]
179    elif subdir:
180        name = os.path.join(subdir, name)
181        if name.replace(os.sep, '/').startswith('addons/'):
182            subdir = 'addons'
183            name2 = name[7:]
184        else:
185            name2 = name
186
187    # First, try to locate in addons_path
188    if subdir:
189        for adp in adps:
190            try:
191                return _fileopen(name2, mode=mode, basedir=adp,
192                                 pathinfo=pathinfo, basename=basename)
193            except IOError:
194                pass
195
196    # Second, try to locate in root_path
197    return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename)
198
199
200def _fileopen(path, mode, basedir, pathinfo, basename=None):
201    name = os.path.normpath(os.path.normcase(os.path.join(basedir, path)))
202
203    paths = odoo.addons.__path__ + [config['root_path']]
204    for addons_path in paths:
205        addons_path = os.path.normpath(os.path.normcase(addons_path)) + os.sep
206        if name.startswith(addons_path):
207            break
208    else:
209        raise ValueError("Unknown path: %s" % name)
210
211    if basename is None:
212        basename = name
213    # Give higher priority to module directories, which is
214    # a more common case than zipped modules.
215    if os.path.isfile(name):
216        if 'b' in mode:
217            fo = open(name, mode)
218        else:
219            fo = io.open(name, mode, encoding='utf-8')
220        if pathinfo:
221            return fo, name
222        return fo
223
224    # Support for loading modules in zipped form.
225    # This will not work for zipped modules that are sitting
226    # outside of known addons paths.
227    head = os.path.normpath(path)
228    zipname = False
229    while os.sep in head:
230        head, tail = os.path.split(head)
231        if not tail:
232            break
233        if zipname:
234            zipname = os.path.join(tail, zipname)
235        else:
236            zipname = tail
237        zpath = os.path.join(basedir, head + '.zip')
238        if zipfile.is_zipfile(zpath):
239            zfile = zipfile.ZipFile(zpath)
240            try:
241                fo = io.BytesIO()
242                fo.write(zfile.read(os.path.join(
243                    os.path.basename(head), zipname).replace(
244                        os.sep, '/')))
245                fo.seek(0)
246                if pathinfo:
247                    return fo, name
248                return fo
249            except Exception:
250                pass
251    # Not found
252    if name.endswith('.rml'):
253        raise IOError('Report %r does not exist or has been deleted' % basename)
254    raise IOError('File not found: %s' % basename)
255
256
257#----------------------------------------------------------
258# iterables
259#----------------------------------------------------------
260def flatten(list):
261    """Flatten a list of elements into a unique list
262    Author: Christophe Simonis (christophe@tinyerp.com)
263
264    Examples::
265    >>> flatten(['a'])
266    ['a']
267    >>> flatten('b')
268    ['b']
269    >>> flatten( [] )
270    []
271    >>> flatten( [[], [[]]] )
272    []
273    >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
274    ['a', 'b', 'c', 'd', 'e', 'f']
275    >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
276    >>> flatten(t)
277    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
278    """
279    r = []
280    for e in list:
281        if isinstance(e, (bytes, str)) or not isinstance(e, collections.Iterable):
282            r.append(e)
283        else:
284            r.extend(flatten(e))
285    return r
286
287def reverse_enumerate(l):
288    """Like enumerate but in the other direction
289
290    Usage::
291    >>> a = ['a', 'b', 'c']
292    >>> it = reverse_enumerate(a)
293    >>> it.next()
294    (2, 'c')
295    >>> it.next()
296    (1, 'b')
297    >>> it.next()
298    (0, 'a')
299    >>> it.next()
300    Traceback (most recent call last):
301      File "<stdin>", line 1, in <module>
302    StopIteration
303    """
304    return zip(range(len(l)-1, -1, -1), reversed(l))
305
306def partition(pred, elems):
307    """ Return a pair equivalent to:
308        ``filter(pred, elems), filter(lambda x: not pred(x), elems)` """
309    yes, nos = [], []
310    for elem in elems:
311        (yes if pred(elem) else nos).append(elem)
312    return yes, nos
313
314def topological_sort(elems):
315    """ Return a list of elements sorted so that their dependencies are listed
316    before them in the result.
317
318    :param elems: specifies the elements to sort with their dependencies; it is
319        a dictionary like `{element: dependencies}` where `dependencies` is a
320        collection of elements that must appear before `element`. The elements
321        of `dependencies` are not required to appear in `elems`; they will
322        simply not appear in the result.
323
324    :returns: a list with the keys of `elems` sorted according to their
325        specification.
326    """
327    # the algorithm is inspired by [Tarjan 1976],
328    # http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
329    result = []
330    visited = set()
331
332    def visit(n):
333        if n not in visited:
334            visited.add(n)
335            if n in elems:
336                # first visit all dependencies of n, then append n to result
337                for it in elems[n]:
338                    visit(it)
339                result.append(n)
340
341    for el in elems:
342        visit(el)
343
344    return result
345
346
347def merge_sequences(*iterables):
348    """ Merge several iterables into a list. The result is the union of the
349        iterables, ordered following the partial order given by the iterables,
350        with a bias towards the end for the last iterable::
351
352            seq = merge_sequences(['A', 'B', 'C'])
353            assert seq == ['A', 'B', 'C']
354
355            seq = merge_sequences(
356                ['A', 'B', 'C'],
357                ['Z'],                  # 'Z' can be anywhere
358                ['Y', 'C'],             # 'Y' must precede 'C';
359                ['A', 'X', 'Y'],        # 'X' must follow 'A' and precede 'Y'
360            )
361            assert seq == ['A', 'B', 'X', 'Y', 'C', 'Z']
362    """
363    # we use an OrderedDict to keep elements in order by default
364    deps = OrderedDict()                # {item: elems_before_item}
365    for iterable in iterables:
366        prev = None
367        for index, item in enumerate(iterable):
368            if not index:
369                deps.setdefault(item, [])
370            else:
371                deps.setdefault(item, []).append(prev)
372            prev = item
373    return topological_sort(deps)
374
375
376try:
377    import xlwt
378
379    # add some sanitization to respect the excel sheet name restrictions
380    # as the sheet name is often translatable, can not control the input
381    class PatchedWorkbook(xlwt.Workbook):
382        def add_sheet(self, name, cell_overwrite_ok=False):
383            # invalid Excel character: []:*?/\
384            name = re.sub(r'[\[\]:*?/\\]', '', name)
385
386            # maximum size is 31 characters
387            name = name[:31]
388            return super(PatchedWorkbook, self).add_sheet(name, cell_overwrite_ok=cell_overwrite_ok)
389
390    xlwt.Workbook = PatchedWorkbook
391
392except ImportError:
393    xlwt = None
394
395try:
396    import xlsxwriter
397
398    # add some sanitization to respect the excel sheet name restrictions
399    # as the sheet name is often translatable, can not control the input
400    class PatchedXlsxWorkbook(xlsxwriter.Workbook):
401
402        # TODO when xlsxwriter bump to 0.9.8, add worksheet_class=None parameter instead of kw
403        def add_worksheet(self, name=None, **kw):
404            if name:
405                # invalid Excel character: []:*?/\
406                name = re.sub(r'[\[\]:*?/\\]', '', name)
407
408                # maximum size is 31 characters
409                name = name[:31]
410            return super(PatchedXlsxWorkbook, self).add_worksheet(name, **kw)
411
412    xlsxwriter.Workbook = PatchedXlsxWorkbook
413
414except ImportError:
415    xlsxwriter = None
416
417
418def to_xml(s):
419    return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
420
421def get_iso_codes(lang):
422    if lang.find('_') != -1:
423        if lang.split('_')[0] == lang.split('_')[1].lower():
424            lang = lang.split('_')[0]
425    return lang
426
427def scan_languages():
428    """ Returns all languages supported by OpenERP for translation
429
430    :returns: a list of (lang_code, lang_name) pairs
431    :rtype: [(str, unicode)]
432    """
433    csvpath = odoo.modules.module.get_resource_path('base', 'data', 'res.lang.csv')
434    try:
435        # read (code, name) from languages in base/data/res.lang.csv
436        with open(csvpath, 'rb') as csvfile:
437            reader = pycompat.csv_reader(csvfile, delimiter=',', quotechar='"')
438            fields = next(reader)
439            code_index = fields.index("code")
440            name_index = fields.index("name")
441            result = [
442                (row[code_index], row[name_index])
443                for row in reader
444            ]
445    except Exception:
446        _logger.error("Could not read %s", csvpath)
447        result = []
448
449    return sorted(result or [('en_US', u'English')], key=itemgetter(1))
450
451def mod10r(number):
452    """
453    Input number : account or invoice number
454    Output return: the same number completed with the recursive mod10
455    key
456    """
457    codec=[0,9,4,6,8,2,7,1,3,5]
458    report = 0
459    result=""
460    for digit in number:
461        result += digit
462        if digit.isdigit():
463            report = codec[ (int(digit) + report) % 10 ]
464    return result + str((10 - report) % 10)
465
466def str2bool(s, default=None):
467    s = ustr(s).lower()
468    y = 'y yes 1 true t on'.split()
469    n = 'n no 0 false f off'.split()
470    if s not in (y + n):
471        if default is None:
472            raise ValueError('Use 0/1/yes/no/true/false/on/off')
473        return bool(default)
474    return s in y
475
476def human_size(sz):
477    """
478    Return the size in a human readable format
479    """
480    if not sz:
481        return False
482    units = ('bytes', 'Kb', 'Mb', 'Gb', 'Tb')
483    if isinstance(sz, str):
484        sz=len(sz)
485    s, i = float(sz), 0
486    while s >= 1024 and i < len(units)-1:
487        s /= 1024
488        i += 1
489    return "%0.2f %s" % (s, units[i])
490
491def logged(f):
492    @wraps(f)
493    def wrapper(*args, **kwargs):
494        from pprint import pformat
495
496        vector = ['Call -> function: %r' % f]
497        for i, arg in enumerate(args):
498            vector.append('  arg %02d: %s' % (i, pformat(arg)))
499        for key, value in kwargs.items():
500            vector.append('  kwarg %10s: %s' % (key, pformat(value)))
501
502        timeb4 = time.time()
503        res = f(*args, **kwargs)
504
505        vector.append('  result: %s' % pformat(res))
506        vector.append('  time delta: %s' % (time.time() - timeb4))
507        _logger.debug('\n'.join(vector))
508        return res
509
510    return wrapper
511
512class profile(object):
513    def __init__(self, fname=None):
514        self.fname = fname
515
516    def __call__(self, f):
517        @wraps(f)
518        def wrapper(*args, **kwargs):
519            profile = cProfile.Profile()
520            result = profile.runcall(f, *args, **kwargs)
521            profile.dump_stats(self.fname or ("%s.cprof" % (f.__name__,)))
522            return result
523
524        return wrapper
525
526def detect_ip_addr():
527    """Try a very crude method to figure out a valid external
528       IP or hostname for the current machine. Don't rely on this
529       for binding to an interface, but it could be used as basis
530       for constructing a remote URL to the server.
531    """
532    def _detect_ip_addr():
533        from array import array
534        from struct import pack, unpack
535
536        try:
537            import fcntl
538        except ImportError:
539            fcntl = None
540
541        ip_addr = None
542
543        if not fcntl: # not UNIX:
544            host = socket.gethostname()
545            ip_addr = socket.gethostbyname(host)
546        else: # UNIX:
547            # get all interfaces:
548            nbytes = 128 * 32
549            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
550            names = array('B', '\0' * nbytes)
551            #print 'names: ', names
552            outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
553            namestr = names.tostring()
554
555            # try 64 bit kernel:
556            for i in range(0, outbytes, 40):
557                name = namestr[i:i+16].split('\0', 1)[0]
558                if name != 'lo':
559                    ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
560                    break
561
562            # try 32 bit kernel:
563            if ip_addr is None:
564                ifaces = [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)]
565
566                for ifname in [iface for iface in ifaces if iface if iface != 'lo']:
567                    ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
568                    break
569
570        return ip_addr or 'localhost'
571
572    try:
573        ip_addr = _detect_ip_addr()
574    except Exception:
575        ip_addr = 'localhost'
576    return ip_addr
577
578DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
579DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
580DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
581    DEFAULT_SERVER_DATE_FORMAT,
582    DEFAULT_SERVER_TIME_FORMAT)
583
584DATE_LENGTH = len(datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT))
585
586# Python's strftime supports only the format directives
587# that are available on the platform's libc, so in order to
588# be cross-platform we map to the directives required by
589# the C standard (1989 version), always available on platforms
590# with a C standard implementation.
591DATETIME_FORMATS_MAP = {
592        '%C': '', # century
593        '%D': '%m/%d/%Y', # modified %y->%Y
594        '%e': '%d',
595        '%E': '', # special modifier
596        '%F': '%Y-%m-%d',
597        '%g': '%Y', # modified %y->%Y
598        '%G': '%Y',
599        '%h': '%b',
600        '%k': '%H',
601        '%l': '%I',
602        '%n': '\n',
603        '%O': '', # special modifier
604        '%P': '%p',
605        '%R': '%H:%M',
606        '%r': '%I:%M:%S %p',
607        '%s': '', #num of seconds since epoch
608        '%T': '%H:%M:%S',
609        '%t': ' ', # tab
610        '%u': ' %w',
611        '%V': '%W',
612        '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
613        '%+': '%Y-%m-%d %H:%M:%S',
614
615        # %Z is a special case that causes 2 problems at least:
616        #  - the timezone names we use (in res_user.context_tz) come
617        #    from pytz, but not all these names are recognized by
618        #    strptime(), so we cannot convert in both directions
619        #    when such a timezone is selected and %Z is in the format
620        #  - %Z is replaced by an empty string in strftime() when
621        #    there is not tzinfo in a datetime value (e.g when the user
622        #    did not pick a context_tz). The resulting string does not
623        #    parse back if the format requires %Z.
624        # As a consequence, we strip it completely from format strings.
625        # The user can always have a look at the context_tz in
626        # preferences to check the timezone.
627        '%z': '',
628        '%Z': '',
629}
630
631POSIX_TO_LDML = {
632    'a': 'E',
633    'A': 'EEEE',
634    'b': 'MMM',
635    'B': 'MMMM',
636    #'c': '',
637    'd': 'dd',
638    'H': 'HH',
639    'I': 'hh',
640    'j': 'DDD',
641    'm': 'MM',
642    'M': 'mm',
643    'p': 'a',
644    'S': 'ss',
645    'U': 'w',
646    'w': 'e',
647    'W': 'w',
648    'y': 'yy',
649    'Y': 'yyyy',
650    # see comments above, and babel's format_datetime assumes an UTC timezone
651    # for naive datetime objects
652    #'z': 'Z',
653    #'Z': 'z',
654}
655
656def posix_to_ldml(fmt, locale):
657    """ Converts a posix/strftime pattern into an LDML date format pattern.
658
659    :param fmt: non-extended C89/C90 strftime pattern
660    :param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
661    :return: unicode
662    """
663    buf = []
664    pc = False
665    quoted = []
666
667    for c in fmt:
668        # LDML date format patterns uses letters, so letters must be quoted
669        if not pc and c.isalpha():
670            quoted.append(c if c != "'" else "''")
671            continue
672        if quoted:
673            buf.append("'")
674            buf.append(''.join(quoted))
675            buf.append("'")
676            quoted = []
677
678        if pc:
679            if c == '%': # escaped percent
680                buf.append('%')
681            elif c == 'x': # date format, short seems to match
682                buf.append(locale.date_formats['short'].pattern)
683            elif c == 'X': # time format, seems to include seconds. short does not
684                buf.append(locale.time_formats['medium'].pattern)
685            else: # look up format char in static mapping
686                buf.append(POSIX_TO_LDML[c])
687            pc = False
688        elif c == '%':
689            pc = True
690        else:
691            buf.append(c)
692
693    # flush anything remaining in quoted buffer
694    if quoted:
695        buf.append("'")
696        buf.append(''.join(quoted))
697        buf.append("'")
698
699    return ''.join(buf)
700
701def split_every(n, iterable, piece_maker=tuple):
702    """Splits an iterable into length-n pieces. The last piece will be shorter
703       if ``n`` does not evenly divide the iterable length.
704
705       :param int n: maximum size of each generated chunk
706       :param Iterable iterable: iterable to chunk into pieces
707       :param piece_maker: callable taking an iterable and collecting each
708                           chunk from its slice, *must consume the entire slice*.
709    """
710    iterator = iter(iterable)
711    piece = piece_maker(islice(iterator, n))
712    while piece:
713        yield piece
714        piece = piece_maker(islice(iterator, n))
715
716def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
717    """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
718
719       :param string field: name of the field we want to read and group by
720       :return: mapping of field values to the list of ids that have it
721       :rtype: dict
722    """
723    res = {}
724    for record in obj.read(cr, uid, ids, [field], context=context):
725        key = record[field]
726        res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
727    return res
728
729def get_and_group_by_company(cr, uid, obj, ids, context=None):
730    return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
731
732# port of python 2.6's attrgetter with support for dotted notation
733def resolve_attr(obj, attr):
734    for name in attr.split("."):
735        obj = getattr(obj, name)
736    return obj
737
738def attrgetter(*items):
739    if len(items) == 1:
740        attr = items[0]
741        def g(obj):
742            return resolve_attr(obj, attr)
743    else:
744        def g(obj):
745            return tuple(resolve_attr(obj, attr) for attr in items)
746    return g
747
748# ---------------------------------------------
749# String management
750# ---------------------------------------------
751
752# Inspired by http://stackoverflow.com/questions/517923
753def remove_accents(input_str):
754    """Suboptimal-but-better-than-nothing way to replace accented
755    latin letters by an ASCII equivalent. Will obviously change the
756    meaning of input_str and work only for some cases"""
757    if not input_str:
758        return input_str
759    input_str = ustr(input_str)
760    nkfd_form = unicodedata.normalize('NFKD', input_str)
761    return u''.join([c for c in nkfd_form if not unicodedata.combining(c)])
762
763class unquote(str):
764    """A subclass of str that implements repr() without enclosing quotation marks
765       or escaping, keeping the original string untouched. The name come from Lisp's unquote.
766       One of the uses for this is to preserve or insert bare variable names within dicts during eval()
767       of a dict's repr(). Use with care.
768
769       Some examples (notice that there are never quotes surrounding
770       the ``active_id`` name:
771
772       >>> unquote('active_id')
773       active_id
774       >>> d = {'test': unquote('active_id')}
775       >>> d
776       {'test': active_id}
777       >>> print d
778       {'test': active_id}
779    """
780    def __repr__(self):
781        return self
782
783class UnquoteEvalContext(defaultdict):
784    """Defaultdict-based evaluation context that returns
785       an ``unquote`` string for any missing name used during
786       the evaluation.
787       Mostly useful for evaluating OpenERP domains/contexts that
788       may refer to names that are unknown at the time of eval,
789       so that when the context/domain is converted back to a string,
790       the original names are preserved.
791
792       **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
793       ``safe_eval()`` will shadow the builtins, which may cause other
794       failures, depending on what is evaluated.
795
796       Example (notice that ``section_id`` is preserved in the final
797       result) :
798
799       >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
800       >>> eval(context_str, UnquoteEvalContext(uid=1))
801       {'default_user_id': 1, 'default_section_id': section_id}
802
803       """
804    def __init__(self, *args, **kwargs):
805        super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
806
807    def __missing__(self, key):
808        return unquote(key)
809
810
811class mute_logger(object):
812    """Temporary suppress the logging.
813    Can be used as context manager or decorator.
814
815        @mute_logger('odoo.plic.ploc')
816        def do_stuff():
817            blahblah()
818
819        with mute_logger('odoo.foo.bar'):
820            do_suff()
821
822    """
823    def __init__(self, *loggers):
824        self.loggers = loggers
825
826    def filter(self, record):
827        return 0
828
829    def __enter__(self):
830        for logger in self.loggers:
831            assert isinstance(logger, str),\
832                "A logger name must be a string, got %s" % type(logger)
833            logging.getLogger(logger).addFilter(self)
834
835    def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
836        for logger in self.loggers:
837            logging.getLogger(logger).removeFilter(self)
838
839    def __call__(self, func):
840        @wraps(func)
841        def deco(*args, **kwargs):
842            with self:
843                return func(*args, **kwargs)
844        return deco
845
846_ph = object()
847class CountingStream(object):
848    """ Stream wrapper counting the number of element it has yielded. Similar
849    role to ``enumerate``, but for use when the iteration process of the stream
850    isn't fully under caller control (the stream can be iterated from multiple
851    points including within a library)
852
853    ``start`` allows overriding the starting index (the index before the first
854    item is returned).
855
856    On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
857    by one.
858
859    .. attribute:: index
860
861        ``int``, index of the last yielded element in the stream. If the stream
862        has ended, will give an index 1-past the stream
863    """
864    def __init__(self, stream, start=-1):
865        self.stream = iter(stream)
866        self.index = start
867        self.stopped = False
868    def __iter__(self):
869        return self
870    def next(self):
871        if self.stopped: raise StopIteration()
872        self.index += 1
873        val = next(self.stream, _ph)
874        if val is _ph:
875            self.stopped = True
876            raise StopIteration()
877        return val
878    __next__ = next
879
880def stripped_sys_argv(*strip_args):
881    """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
882    strip_args = sorted(set(strip_args) | set(['-s', '--save', '-u', '--update', '-i', '--init', '--i18n-overwrite']))
883    assert all(config.parser.has_option(s) for s in strip_args)
884    takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)
885
886    longs, shorts = list(tuple(y) for _, y in itergroupby(strip_args, lambda x: x.startswith('--')))
887    longs_eq = tuple(l + '=' for l in longs if takes_value[l])
888
889    args = sys.argv[:]
890
891    def strip(args, i):
892        return args[i].startswith(shorts) \
893            or args[i].startswith(longs_eq) or (args[i] in longs) \
894            or (i >= 1 and (args[i - 1] in strip_args) and takes_value[args[i - 1]])
895
896    return [x for i, x in enumerate(args) if not strip(args, i)]
897
898class ConstantMapping(Mapping):
899    """
900    An immutable mapping returning the provided value for every single key.
901
902    Useful for default value to methods
903    """
904    __slots__ = ['_value']
905    def __init__(self, val):
906        self._value = val
907
908    def __len__(self):
909        """
910        defaultdict updates its length for each individually requested key, is
911        that really useful?
912        """
913        return 0
914
915    def __iter__(self):
916        """
917        same as len, defaultdict updates its iterable keyset with each key
918        requested, is there a point for this?
919        """
920        return iter([])
921
922    def __getitem__(self, item):
923        return self._value
924
925
926def dumpstacks(sig=None, frame=None, thread_idents=None):
927    """ Signal handler: dump a stack trace for each existing thread or given
928    thread(s) specified through the ``thread_idents`` sequence.
929    """
930    code = []
931
932    def extract_stack(stack):
933        for filename, lineno, name, line in traceback.extract_stack(stack):
934            yield 'File: "%s", line %d, in %s' % (filename, lineno, name)
935            if line:
936                yield "  %s" % (line.strip(),)
937
938    # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
939    # modified for python 2.5 compatibility
940    threads_info = {th.ident: {'repr': repr(th),
941                               'uid': getattr(th, 'uid', 'n/a'),
942                               'dbname': getattr(th, 'dbname', 'n/a'),
943                               'url': getattr(th, 'url', 'n/a')}
944                    for th in threading.enumerate()}
945    for threadId, stack in sys._current_frames().items():
946        if not thread_idents or threadId in thread_idents:
947            thread_info = threads_info.get(threadId, {})
948            code.append("\n# Thread: %s (db:%s) (uid:%s) (url:%s)" %
949                        (thread_info.get('repr', threadId),
950                         thread_info.get('dbname', 'n/a'),
951                         thread_info.get('uid', 'n/a'),
952                         thread_info.get('url', 'n/a')))
953            for line in extract_stack(stack):
954                code.append(line)
955
956    if odoo.evented:
957        # code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
958        import gc
959        from greenlet import greenlet
960        for ob in gc.get_objects():
961            if not isinstance(ob, greenlet) or not ob:
962                continue
963            code.append("\n# Greenlet: %r" % (ob,))
964            for line in extract_stack(ob.gr_frame):
965                code.append(line)
966
967    _logger.info("\n".join(code))
968
969def freehash(arg):
970    try:
971        return hash(arg)
972    except Exception:
973        if isinstance(arg, Mapping):
974            return hash(frozendict(arg))
975        elif isinstance(arg, Iterable):
976            return hash(frozenset(freehash(item) for item in arg))
977        else:
978            return id(arg)
979
980def clean_context(context):
981    """ This function take a dictionary and remove each entry with its key starting with 'default_' """
982    return {k: v for k, v in context.items() if not k.startswith('default_')}
983
984class frozendict(dict):
985    """ An implementation of an immutable dictionary. """
986    def __delitem__(self, key):
987        raise NotImplementedError("'__delitem__' not supported on frozendict")
988    def __setitem__(self, key, val):
989        raise NotImplementedError("'__setitem__' not supported on frozendict")
990    def clear(self):
991        raise NotImplementedError("'clear' not supported on frozendict")
992    def pop(self, key, default=None):
993        raise NotImplementedError("'pop' not supported on frozendict")
994    def popitem(self):
995        raise NotImplementedError("'popitem' not supported on frozendict")
996    def setdefault(self, key, default=None):
997        raise NotImplementedError("'setdefault' not supported on frozendict")
998    def update(self, *args, **kwargs):
999        raise NotImplementedError("'update' not supported on frozendict")
1000    def __hash__(self):
1001        return hash(frozenset((key, freehash(val)) for key, val in self.items()))
1002
1003class Collector(Mapping):
1004    """ A mapping from keys to lists. This is essentially a space optimization
1005        for ``defaultdict(list)``.
1006    """
1007    __slots__ = ['_map']
1008    def __init__(self):
1009        self._map = {}
1010    def add(self, key, val):
1011        vals = self._map.setdefault(key, [])
1012        if val not in vals:
1013            vals.append(val)
1014    def __getitem__(self, key):
1015        return self._map.get(key, ())
1016    def __iter__(self):
1017        return iter(self._map)
1018    def __len__(self):
1019        return len(self._map)
1020
1021
1022class StackMap(MutableMapping):
1023    """ A stack of mappings behaving as a single mapping, and used to implement
1024        nested scopes. The lookups search the stack from top to bottom, and
1025        returns the first value found. Mutable operations modify the topmost
1026        mapping only.
1027    """
1028    __slots__ = ['_maps']
1029
1030    def __init__(self, m=None):
1031        self._maps = [] if m is None else [m]
1032
1033    def __getitem__(self, key):
1034        for mapping in reversed(self._maps):
1035            try:
1036                return mapping[key]
1037            except KeyError:
1038                pass
1039        raise KeyError(key)
1040
1041    def __setitem__(self, key, val):
1042        self._maps[-1][key] = val
1043
1044    def __delitem__(self, key):
1045        del self._maps[-1][key]
1046
1047    def __iter__(self):
1048        return iter({key for mapping in self._maps for key in mapping})
1049
1050    def __len__(self):
1051        return sum(1 for key in self)
1052
1053    def __str__(self):
1054        return u"<StackMap %s>" % self._maps
1055
1056    def pushmap(self, m=None):
1057        self._maps.append({} if m is None else m)
1058
1059    def popmap(self):
1060        return self._maps.pop()
1061
1062
1063class OrderedSet(MutableSet):
1064    """ A set collection that remembers the elements first insertion order. """
1065    __slots__ = ['_map']
1066    def __init__(self, elems=()):
1067        self._map = OrderedDict((elem, None) for elem in elems)
1068    def __contains__(self, elem):
1069        return elem in self._map
1070    def __iter__(self):
1071        return iter(self._map)
1072    def __len__(self):
1073        return len(self._map)
1074    def add(self, elem):
1075        self._map[elem] = None
1076    def discard(self, elem):
1077        self._map.pop(elem, None)
1078
1079
1080class LastOrderedSet(OrderedSet):
1081    """ A set collection that remembers the elements last insertion order. """
1082    def add(self, elem):
1083        OrderedSet.discard(self, elem)
1084        OrderedSet.add(self, elem)
1085
1086
1087class Callbacks:
1088    """ A simple queue of callback functions.  Upon run, every function is
1089    called (in addition order), and the queue is emptied.
1090
1091        callbacks = Callbacks()
1092
1093        # add foo
1094        def foo():
1095            print("foo")
1096
1097        callbacks.add(foo)
1098
1099        # add bar
1100        callbacks.add
1101        def bar():
1102            print("bar")
1103
1104        # add foo again
1105        callbacks.add(foo)
1106
1107        # call foo(), bar(), foo(), then clear the callback queue
1108        callbacks.run()
1109
1110    The queue also provides a ``data`` dictionary, that may be freely used to
1111    store anything, but is mostly aimed at aggregating data for callbacks.  The
1112    dictionary is automatically cleared by ``run()`` once all callback functions
1113    have been called.
1114
1115        # register foo to process aggregated data
1116        @callbacks.add
1117        def foo():
1118            print(sum(callbacks.data['foo']))
1119
1120        callbacks.data.setdefault('foo', []).append(1)
1121        ...
1122        callbacks.data.setdefault('foo', []).append(2)
1123        ...
1124        callbacks.data.setdefault('foo', []).append(3)
1125
1126        # call foo(), which prints 6
1127        callbacks.run()
1128
1129    Given the global nature of ``data``, the keys should identify in a unique
1130    way the data being stored.  It is recommended to use strings with a
1131    structure like ``"{module}.{feature}"``.
1132    """
1133    __slots__ = ['_funcs', 'data']
1134
1135    def __init__(self):
1136        self._funcs = collections.deque()
1137        self.data = {}
1138
1139    def add(self, func):
1140        """ Add the given function. """
1141        self._funcs.append(func)
1142
1143    def run(self):
1144        """ Call all the functions (in addition order), then clear associated data.
1145        """
1146        while self._funcs:
1147            func = self._funcs.popleft()
1148            func()
1149        self.clear()
1150
1151    def clear(self):
1152        """ Remove all callbacks and data from self. """
1153        self._funcs.clear()
1154        self.data.clear()
1155
1156
1157class IterableGenerator:
1158    """ An iterable object based on a generator function, which is called each
1159        time the object is iterated over.
1160    """
1161    __slots__ = ['func', 'args']
1162
1163    def __init__(self, func, *args):
1164        self.func = func
1165        self.args = args
1166
1167    def __iter__(self):
1168        return self.func(*self.args)
1169
1170
1171def groupby(iterable, key=None):
1172    """ Return a collection of pairs ``(key, elements)`` from ``iterable``. The
1173        ``key`` is a function computing a key value for each element. This
1174        function is similar to ``itertools.groupby``, but aggregates all
1175        elements under the same key, not only consecutive elements.
1176    """
1177    if key is None:
1178        key = lambda arg: arg
1179    groups = defaultdict(list)
1180    for elem in iterable:
1181        groups[key(elem)].append(elem)
1182    return groups.items()
1183
1184def unique(it):
1185    """ "Uniquifier" for the provided iterable: will output each element of
1186    the iterable once.
1187
1188    The iterable's elements must be hashahble.
1189
1190    :param Iterable it:
1191    :rtype: Iterator
1192    """
1193    seen = set()
1194    for e in it:
1195        if e not in seen:
1196            seen.add(e)
1197            yield e
1198
1199class Reverse(object):
1200    """ Wraps a value and reverses its ordering, useful in key functions when
1201    mixing ascending and descending sort on non-numeric data as the
1202    ``reverse`` parameter can not do piecemeal reordering.
1203    """
1204    __slots__ = ['val']
1205
1206    def __init__(self, val):
1207        self.val = val
1208
1209    def __eq__(self, other): return self.val == other.val
1210    def __ne__(self, other): return self.val != other.val
1211
1212    def __ge__(self, other): return self.val <= other.val
1213    def __gt__(self, other): return self.val < other.val
1214    def __le__(self, other): return self.val >= other.val
1215    def __lt__(self, other): return self.val > other.val
1216
1217@contextmanager
1218def ignore(*exc):
1219    try:
1220        yield
1221    except exc:
1222        pass
1223
1224# Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
1225if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0'):
1226    def html_escape(text):
1227        return werkzeug.utils.escape(text, quote=True)
1228else:
1229    def html_escape(text):
1230        return werkzeug.utils.escape(text)
1231
1232def get_lang(env, lang_code=False):
1233    """
1234    Retrieve the first lang object installed, by checking the parameter lang_code,
1235    the context and then the company. If no lang is installed from those variables,
1236    fallback on the first lang installed in the system.
1237    :param str lang_code: the locale (i.e. en_US)
1238    :return res.lang: the first lang found that is installed on the system.
1239    """
1240    langs = [code for code, _ in env['res.lang'].get_installed()]
1241    lang = langs[0]
1242    if lang_code and lang_code in langs:
1243        lang = lang_code
1244    elif env.context.get('lang') in langs:
1245        lang = env.context.get('lang')
1246    elif env.user.company_id.partner_id.lang in langs:
1247        lang = env.user.company_id.partner_id.lang
1248    return env['res.lang']._lang_get(lang)
1249
1250def babel_locale_parse(lang_code):
1251    try:
1252        return babel.Locale.parse(lang_code)
1253    except:
1254        try:
1255            return babel.Locale.default()
1256        except:
1257            return babel.Locale.parse("en_US")
1258
1259def formatLang(env, value, digits=None, grouping=True, monetary=False, dp=False, currency_obj=False):
1260    """
1261        Assuming 'Account' decimal.precision=3:
1262            formatLang(value) -> digits=2 (default)
1263            formatLang(value, digits=4) -> digits=4
1264            formatLang(value, dp='Account') -> digits=3
1265            formatLang(value, digits=5, dp='Account') -> digits=5
1266    """
1267
1268    if digits is None:
1269        digits = DEFAULT_DIGITS = 2
1270        if dp:
1271            decimal_precision_obj = env['decimal.precision']
1272            digits = decimal_precision_obj.precision_get(dp)
1273        elif currency_obj:
1274            digits = currency_obj.decimal_places
1275
1276    if isinstance(value, str) and not value:
1277        return ''
1278
1279    lang_obj = get_lang(env)
1280
1281    res = lang_obj.format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
1282
1283    if currency_obj and currency_obj.symbol:
1284        if currency_obj.position == 'after':
1285            res = '%s %s' % (res, currency_obj.symbol)
1286        elif currency_obj and currency_obj.position == 'before':
1287            res = '%s %s' % (currency_obj.symbol, res)
1288    return res
1289
1290
1291def format_date(env, value, lang_code=False, date_format=False):
1292    '''
1293        Formats the date in a given format.
1294
1295        :param env: an environment.
1296        :param date, datetime or string value: the date to format.
1297        :param string lang_code: the lang code, if not specified it is extracted from the
1298            environment context.
1299        :param string date_format: the format or the date (LDML format), if not specified the
1300            default format of the lang.
1301        :return: date formatted in the specified format.
1302        :rtype: string
1303    '''
1304    if not value:
1305        return ''
1306    if isinstance(value, str):
1307        if len(value) < DATE_LENGTH:
1308            return ''
1309        if len(value) > DATE_LENGTH:
1310            # a datetime, convert to correct timezone
1311            value = odoo.fields.Datetime.from_string(value)
1312            value = odoo.fields.Datetime.context_timestamp(env['res.lang'], value)
1313        else:
1314            value = odoo.fields.Datetime.from_string(value)
1315
1316    lang = get_lang(env, lang_code)
1317    locale = babel_locale_parse(lang.code)
1318    if not date_format:
1319        date_format = posix_to_ldml(lang.date_format, locale=locale)
1320
1321    return babel.dates.format_date(value, format=date_format, locale=locale)
1322
1323def parse_date(env, value, lang_code=False):
1324    '''
1325        Parse the date from a given format. If it is not a valid format for the
1326        localization, return the original string.
1327
1328        :param env: an environment.
1329        :param string value: the date to parse.
1330        :param string lang_code: the lang code, if not specified it is extracted from the
1331            environment context.
1332        :return: date object from the localized string
1333        :rtype: datetime.date
1334    '''
1335    lang = get_lang(env, lang_code)
1336    locale = babel_locale_parse(lang.code)
1337    try:
1338        return babel.dates.parse_date(value, locale=locale)
1339    except:
1340        return value
1341
1342
1343def format_datetime(env, value, tz=False, dt_format='medium', lang_code=False):
1344    """ Formats the datetime in a given format.
1345
1346        :param {str, datetime} value: naive datetime to format either in string or in datetime
1347        :param {str} tz: name of the timezone  in which the given datetime should be localized
1348        :param {str} dt_format: one of “full”, “long”, “medium”, or “short”, or a custom date/time pattern compatible with `babel` lib
1349        :param {str} lang_code: ISO code of the language to use to render the given datetime
1350    """
1351    if not value:
1352        return ''
1353    if isinstance(value, str):
1354        timestamp = odoo.fields.Datetime.from_string(value)
1355    else:
1356        timestamp = value
1357
1358    tz_name = tz or env.user.tz or 'UTC'
1359    utc_datetime = pytz.utc.localize(timestamp, is_dst=False)
1360    try:
1361        context_tz = pytz.timezone(tz_name)
1362        localized_datetime = utc_datetime.astimezone(context_tz)
1363    except Exception:
1364        localized_datetime = utc_datetime
1365
1366    lang = get_lang(env, lang_code)
1367
1368    locale = babel_locale_parse(lang.code or lang_code)  # lang can be inactive, so `lang`is empty
1369    if not dt_format:
1370        date_format = posix_to_ldml(lang.date_format, locale=locale)
1371        time_format = posix_to_ldml(lang.time_format, locale=locale)
1372        dt_format = '%s %s' % (date_format, time_format)
1373
1374    # Babel allows to format datetime in a specific language without change locale
1375    # So month 1 = January in English, and janvier in French
1376    # Be aware that the default value for format is 'medium', instead of 'short'
1377    #     medium:  Jan 5, 2016, 10:20:31 PM |   5 janv. 2016 22:20:31
1378    #     short:   1/5/16, 10:20 PM         |   5/01/16 22:20
1379    # Formatting available here : http://babel.pocoo.org/en/latest/dates.html#date-fields
1380    return babel.dates.format_datetime(localized_datetime, dt_format, locale=locale)
1381
1382
1383def format_time(env, value, tz=False, time_format='medium', lang_code=False):
1384    """ Format the given time (hour, minute and second) with the current user preference (language, format, ...)
1385
1386        :param value: the time to format
1387        :type value: `datetime.time` instance. Could be timezoned to display tzinfo according to format (e.i.: 'full' format)
1388        :param format: one of “full”, “long”, “medium”, or “short”, or a custom date/time pattern
1389        :param lang_code: ISO
1390
1391        :rtype str
1392    """
1393    if not value:
1394        return ''
1395
1396    lang = get_lang(env, lang_code)
1397    locale = babel_locale_parse(lang.code)
1398    if not time_format:
1399        time_format = posix_to_ldml(lang.time_format, locale=locale)
1400
1401    return babel.dates.format_time(value, format=time_format, locale=locale)
1402
1403
1404def _format_time_ago(env, time_delta, lang_code=False, add_direction=True):
1405    if not lang_code:
1406        langs = [code for code, _ in env['res.lang'].get_installed()]
1407        lang_code = env.context['lang'] if env.context.get('lang') in langs else (env.user.company_id.partner_id.lang or langs[0])
1408    locale = babel_locale_parse(lang_code)
1409    return babel.dates.format_timedelta(-time_delta, add_direction=add_direction, locale=locale)
1410
1411
1412def format_decimalized_number(number, decimal=1):
1413    """Format a number to display to nearest metrics unit next to it.
1414
1415    Do not display digits if all visible digits are null.
1416    Do not display units higher then "Tera" because most of people don't know what
1417    a "Yotta" is.
1418
1419    >>> format_decimalized_number(123_456.789)
1420    123.5k
1421    >>> format_decimalized_number(123_000.789)
1422    123k
1423    >>> format_decimalized_number(-123_456.789)
1424    -123.5k
1425    >>> format_decimalized_number(0.789)
1426    0.8
1427    """
1428    for unit in ['', 'k', 'M', 'G']:
1429        if abs(number) < 1000.0:
1430            return "%g%s" % (round(number, decimal), unit)
1431        number /= 1000.0
1432    return "%g%s" % (round(number, decimal), 'T')
1433
1434
1435def format_decimalized_amount(amount, currency=None):
1436    """Format a amount to display the currency and also display the metric unit of the amount.
1437
1438    >>> format_decimalized_amount(123_456.789, res.currency("$"))
1439    $123.5k
1440    """
1441    formated_amount = format_decimalized_number(amount)
1442
1443    if not currency:
1444        return formated_amount
1445
1446    if currency.position == 'before':
1447        return "%s%s" % (currency.symbol or '', formated_amount)
1448
1449    return "%s %s" % (formated_amount, currency.symbol or '')
1450
1451
1452def format_amount(env, amount, currency, lang_code=False):
1453    fmt = "%.{0}f".format(currency.decimal_places)
1454    lang = get_lang(env, lang_code)
1455
1456    formatted_amount = lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
1457        .replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
1458
1459    pre = post = u''
1460    if currency.position == 'before':
1461        pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=currency.symbol or '')
1462    else:
1463        post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=currency.symbol or '')
1464
1465    return u'{pre}{0}{post}'.format(formatted_amount, pre=pre, post=post)
1466
1467
1468def format_duration(value):
1469    """ Format a float: used to display integral or fractional values as
1470        human-readable time spans (e.g. 1.5 as "01:30").
1471    """
1472    hours, minutes = divmod(abs(value) * 60, 60)
1473    minutes = round(minutes)
1474    if minutes == 60:
1475        minutes = 0
1476        hours += 1
1477    if value < 0:
1478        return '-%02d:%02d' % (hours, minutes)
1479    return '%02d:%02d' % (hours, minutes)
1480
1481
1482def _consteq(str1, str2):
1483    """ Constant-time string comparison. Suitable to compare bytestrings of fixed,
1484        known length only, because length difference is optimized. """
1485    return len(str1) == len(str2) and sum(ord(x)^ord(y) for x, y in zip(str1, str2)) == 0
1486
1487consteq = getattr(passlib.utils, 'consteq', _consteq)
1488
1489# forbid globals entirely: str/unicode, int/long, float, bool, tuple, list, dict, None
1490class Unpickler(pickle_.Unpickler, object):
1491    find_global = None # Python 2
1492    find_class = None # Python 3
1493def _pickle_load(stream, encoding='ASCII', errors=False):
1494    if sys.version_info[0] == 3:
1495        unpickler = Unpickler(stream, encoding=encoding)
1496    else:
1497        unpickler = Unpickler(stream)
1498    try:
1499        return unpickler.load()
1500    except Exception:
1501        _logger.warning('Failed unpickling data, returning default: %r',
1502                        errors, exc_info=True)
1503        return errors
1504pickle = types.ModuleType(__name__ + '.pickle')
1505pickle.load = _pickle_load
1506pickle.loads = lambda text, encoding='ASCII': _pickle_load(io.BytesIO(text), encoding=encoding)
1507pickle.dump = pickle_.dump
1508pickle.dumps = pickle_.dumps
1509
1510
1511class DotDict(dict):
1512    """Helper for dot.notation access to dictionary attributes
1513
1514        E.g.
1515          foo = DotDict({'bar': False})
1516          return foo.bar
1517    """
1518    def __getattr__(self, attrib):
1519        val = self.get(attrib)
1520        return DotDict(val) if type(val) is dict else val
1521
1522
1523def get_diff(data_from, data_to, custom_style=False):
1524    """
1525    Return, in an HTML table, the diff between two texts.
1526
1527    :param tuple data_from: tuple(text, name), name will be used as table header
1528    :param tuple data_to: tuple(text, name), name will be used as table header
1529    :param tuple custom_style: string, style css including <style> tag.
1530    :return: a string containing the diff in an HTML table format.
1531    """
1532    def handle_style(html_diff, custom_style):
1533        """ The HtmlDiff lib will add some usefull classes on the DOM to
1534        identify elements. Simply append to those classes some BS4 ones.
1535        For the table to fit the modal width, some custom style is needed.
1536        """
1537        to_append = {
1538            'diff_header': 'bg-600 text-center align-top px-2',
1539            'diff_next': 'd-none',
1540            'diff_add': 'bg-success',
1541            'diff_chg': 'bg-warning',
1542            'diff_sub': 'bg-danger',
1543        }
1544        for old, new in to_append.items():
1545            html_diff = html_diff.replace(old, "%s %s" % (old, new))
1546        html_diff = html_diff.replace('nowrap', '')
1547        html_diff += custom_style or '''
1548            <style>
1549                table.diff { width: 100%; }
1550                table.diff th.diff_header { width: 50%; }
1551                table.diff td.diff_header { white-space: nowrap; }
1552                table.diff td { word-break: break-all; }
1553            </style>
1554        '''
1555        return html_diff
1556
1557    diff = HtmlDiff(tabsize=2).make_table(
1558        data_from[0].splitlines(),
1559        data_to[0].splitlines(),
1560        data_from[1],
1561        data_to[1],
1562        context=True,  # Show only diff lines, not all the code
1563        numlines=3,
1564    )
1565    return handle_style(diff, custom_style)
1566
1567
1568def traverse_containers(val, type_):
1569    """ Yields atoms filtered by specified type_ (or type tuple), traverses
1570    through standard containers (non-string mappings or sequences) *unless*
1571    they're selected by the type filter
1572    """
1573    from odoo.models import BaseModel
1574    if isinstance(val, type_):
1575        yield val
1576    elif isinstance(val, (str, bytes, BaseModel)):
1577        return
1578    elif isinstance(val, Mapping):
1579        for k, v in val.items():
1580            yield from traverse_containers(k, type_)
1581            yield from traverse_containers(v, type_)
1582    elif isinstance(val, collections.abc.Sequence):
1583        for v in val:
1584            yield from traverse_containers(v, type_)
1585
1586
1587def hmac(env, scope, message, hash_function=hashlib.sha256):
1588    """Compute HMAC with `database.secret` config parameter as key.
1589
1590    :param env: sudo environment to use for retrieving config parameter
1591    :param message: message to authenticate
1592    :param scope: scope of the authentication, to have different signature for the same
1593        message in different usage
1594    :param hash_function: hash function to use for HMAC (default: SHA-256)
1595    """
1596    if not scope:
1597        raise ValueError('Non-empty scope required')
1598
1599    secret = env['ir.config_parameter'].get_param('database.secret')
1600    message = repr((scope, message))
1601    return hmac_lib.new(
1602        secret.encode(),
1603        message.encode(),
1604        hash_function,
1605    ).hexdigest()
1606