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('&','&').replace('<','<').replace('>','>') 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