diff --git a/bin/addons/base/i18n/cs.po b/bin/addons/base/i18n/cs.po index cf9121d03d8..c020cab4b42 100644 --- a/bin/addons/base/i18n/cs.po +++ b/bin/addons/base/i18n/cs.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-01-03 19:57+0000\n" +"PO-Revision-Date: 2010-01-04 06:02+0000\n" "Last-Translator: Kuvaly \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-01-04 04:38+0000\n" +"X-Launchpad-Export-Date: 2010-01-05 04:39+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base diff --git a/bin/addons/base/i18n/lv.po b/bin/addons/base/i18n/lv.po index dbfaedc9677..4edc0848282 100644 --- a/bin/addons/base/i18n/lv.po +++ b/bin/addons/base/i18n/lv.po @@ -8,13 +8,13 @@ msgstr "" "Project-Id-Version: openobject-addons\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-01-03 19:31+0000\n" -"Last-Translator: anete \n" +"PO-Revision-Date: 2010-01-04 06:03+0000\n" +"Last-Translator: Fabien (Open ERP) \n" "Language-Team: Latvian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-01-04 04:38+0000\n" +"X-Launchpad-Export-Date: 2010-01-05 04:40+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base diff --git a/bin/addons/base/i18n/uk.po b/bin/addons/base/i18n/uk.po index a5748606c1b..d61a025d1d3 100644 --- a/bin/addons/base/i18n/uk.po +++ b/bin/addons/base/i18n/uk.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-01-03 06:09+0000\n" +"PO-Revision-Date: 2010-01-05 06:38+0000\n" "Last-Translator: Fabien (Open ERP) \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-01-04 04:38+0000\n" +"X-Launchpad-Export-Date: 2010-01-06 04:36+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base diff --git a/bin/addons/base/res/partner/partner.py b/bin/addons/base/res/partner/partner.py index 15af89acae7..be5819ec79b 100644 --- a/bin/addons/base/res/partner/partner.py +++ b/bin/addons/base/res/partner/partner.py @@ -295,7 +295,8 @@ class res_partner_address(osv.osv): 'mobile': fields.char('Mobile', size=64), 'birthdate': fields.char('Birthdate', size=64), 'active': fields.boolean('Active', help="Uncheck the active field to hide the contact."), - 'company_id': fields.related('partner_id','company_id',type='many2one',relation='res.company',string='Company', store=True), +# 'company_id': fields.related('partner_id','company_id',type='many2one',relation='res.company',string='Company', store=True), + 'company_id': fields.many2one('res.company', 'Company',select=1), } _defaults = { 'active': lambda *a: 1, diff --git a/bin/addons/base/res/partner/partner_view.xml b/bin/addons/base/res/partner/partner_view.xml index 6559cd3bcc1..679d395c431 100644 --- a/bin/addons/base/res/partner/partner_view.xml +++ b/bin/addons/base/res/partner/partner_view.xml @@ -96,6 +96,7 @@ + @@ -153,6 +154,7 @@ + diff --git a/bin/addons/base/res/res_company.py b/bin/addons/base/res/res_company.py index 33d39a61003..bfc2b7291ef 100644 --- a/bin/addons/base/res/res_company.py +++ b/bin/addons/base/res/res_company.py @@ -103,15 +103,12 @@ class res_company(osv.osv): args.append(('field_id.name','=',field)) else: args.append(('field_id','=',False)) - print args ids = proxy.search(cr, uid, args, context=context) for rule in proxy.browse(cr, uid, ids, context): user = self.pool.get('res.users').browse(cr, uid, uid) if eval(rule.expression, {'context': context, 'user': user}): - print 'found', object return rule.company_dest_id.id user_company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id - print 'not found', object return user_company_id def _get_child_ids(self, cr, uid, uid2, context={}): diff --git a/bin/service/web_services.py b/bin/service/web_services.py index ec554ac0325..acfe1bfd41e 100644 --- a/bin/service/web_services.py +++ b/bin/service/web_services.py @@ -443,7 +443,10 @@ GNU Public Licence. return info def exp_timezone_get(self, db, login, password): - return time.tzname[0] + #timezone detection is safe in multithread, so lazy init is ok here + if (not tools.config['timezone']): + tools.config['timezone'] = tools.misc.detect_server_timezone() + return tools.config['timezone'] def exp_get_available_updates(self, contract_id, contract_password): diff --git a/bin/tools/config.py b/bin/tools/config.py index 4cdee8736c2..e0ef74dbb56 100644 --- a/bin/tools/config.py +++ b/bin/tools/config.py @@ -77,10 +77,11 @@ class configmanager(object): 'secure' : False, 'syslog' : False, 'log_level': logging.INFO, - 'assert_exit_level': logging.WARNING, # level above which a failed assert will be raise + 'assert_exit_level': logging.WARNING, # level above which a failed assert will be raised 'cache_timeout': 100000, 'login_message': False, 'list_db' : True, + 'timezone' : False, # to override the default TZ } self.misc = {} @@ -111,6 +112,7 @@ class configmanager(object): help="update a module (use \"all\" for all modules)") parser.add_option("--cache-timeout", dest="cache_timeout", help="set the timeout for the cache system", default=100000, type="int") + parser.add_option("-t", "--timezone", dest="timezone", help="specify reference timezone for the server (e.g. Europe/Brussels") # stops the server from launching after initialization parser.add_option("--stop-after-init", action="store_true", dest="stop_after_init", default=False, @@ -227,7 +229,7 @@ class configmanager(object): 'db_port', 'list_db', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout', 'email_from', 'smtp_server', 'smtp_user', 'smtp_password', 'price_accuracy', 'netinterface', 'netport', 'db_maxconn', 'import_partial', 'addons_path', - 'netrpc', 'xmlrpc', 'syslog', 'without_demo'] + 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone',] if hasSSL: keys.extend(['smtp_ssl', 'secure_cert_file', 'secure_pkey_file']) @@ -266,6 +268,17 @@ class configmanager(object): self.options['translate_modules'] = opt.translate_modules and map(lambda m: m.strip(), opt.translate_modules.split(',')) or ['all'] self.options['translate_modules'].sort() + if self.options['timezone']: + # If an explicit TZ was provided in the config, make sure it is known + try: + import pytz + tz = pytz.timezone(self.options['timezone']) + except pytz.UnknownTimeZoneError: + die(True, "The specified timezone (%s) is invalid" % self.options['timezone']) + except: + # If pytz is missing, don't check the provided TZ, it will be ignored anyway. + pass + if opt.pg_path: self.options['pg_path'] = opt.pg_path diff --git a/bin/tools/misc.py b/bin/tools/misc.py index c3394c2ed4f..a237ee1b603 100644 --- a/bin/tools/misc.py +++ b/bin/tools/misc.py @@ -31,6 +31,7 @@ from config import config import zipfile import release import socket +import re if sys.version_info[:2] < (2, 4): from threadinglocal import local @@ -302,8 +303,103 @@ def reverse_enumerate(l): #---------------------------------------------------------- # Emails #---------------------------------------------------------- +email_re = re.compile(r""" + ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part + @ # mandatory @ sign + [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then? + \. + [a-z]{2,3} # TLD + ) + """, re.VERBOSE) +res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE) +command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) +reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE) + +priorities = { + '1': '1 (Highest)', + '2': '2 (High)', + '3': '3 (Normal)', + '4': '4 (Low)', + '5': '5 (Lowest)', + } + +def html2plaintext(html, body_id=None, encoding='utf-8'): + ## (c) Fry-IT, www.fry-it.com, 2007 + ## + ## download here: http://www.peterbe.com/plog/html2plaintext + + + """ from an HTML text, convert the HTML to plain text. + If @body_id is provided then this is the tag where the + body (not necessarily ) starts. + """ + try: + from BeautifulSoup import BeautifulSoup, SoupStrainer, Comment + except: + return html + + urls = [] + if body_id is not None: + strainer = SoupStrainer(id=body_id) + else: + strainer = SoupStrainer('body') + + soup = BeautifulSoup(html, parseOnlyThese=strainer, fromEncoding=encoding) + for link in soup.findAll('a'): + title = link.renderContents() + for url in [x[1] for x in link.attrs if x[0]=='href']: + urls.append(dict(url=url, tag=str(link), title=title)) + + html = soup.__str__() + + url_index = [] + i = 0 + for d in urls: + if d['title'] == d['url'] or 'http://'+d['title'] == d['url']: + html = html.replace(d['tag'], d['url']) + else: + i += 1 + html = html.replace(d['tag'], '%s [%s]' % (d['title'], i)) + url_index.append(d['url']) + + html = html.replace('','*').replace('','*') + html = html.replace('','*').replace('','*') + html = html.replace('

','*').replace('

','*') + html = html.replace('

','**').replace('

','**') + html = html.replace('

','**').replace('

','**') + html = html.replace('','/').replace('','/') + + + # the only line breaks we respect is those of ending tags and + # breaks + + html = html.replace('\n',' ') + html = html.replace('
', '\n') + html = html.replace('', '\n') + html = html.replace('

', '\n\n') + html = re.sub('', '\n', html) + html = html.replace(' ' * 2, ' ') + + + # for all other tags we failed to clean up, just remove then and + # complain about them on the stderr + def desperate_fixer(g): + #print >>sys.stderr, "failed to clean up %s" % str(g.group()) + return ' ' + + html = re.sub('<.*?>', desperate_fixer, html) + + # lstrip all lines + html = '\n'.join([x.lstrip() for x in html.splitlines()]) + + for i, url in enumerate(url_index): + if i == 0: + html += '\n\n' + html += '[%s] %s\n' % (i+1, url) + return html + def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, - attach=None, tinycrm=False, ssl=False, debug=False, subtype='plain', x_headers=None): + attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'): """Send an email.""" import smtplib @@ -314,7 +410,7 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non from email.Utils import formatdate, COMMASPACE from email.Utils import formatdate, COMMASPACE from email import Encoders - import netsvc + import netsvc if x_headers is None: x_headers = {} @@ -325,6 +421,9 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non if not email_from and not config['email_from']: raise Exception("No Email sender by default, see config file") + if not email_from: + email_from = config.get('email_from', False) + if not email_cc: email_cc = [] if not email_bcc: @@ -356,13 +455,15 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)' msg['X-OpenERP-Server-Host'] = socket.gethostname() msg['X-OpenERP-Server-Version'] = release.version + if priority: + msg['X-Priority'] = priorities.get(priority, '3 (Normal)') # Add dynamic X Header for key, value in x_headers.items(): msg['X-OpenERP-%s' % key] = str(value) - if tinycrm: - msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname()) + if openobject_id: + msg['Message-Id'] = "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname()) if attach: try: @@ -393,12 +494,11 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non return True except Exception,e: netsvc.Logger().notifyChannel('email_send (maildir)', netsvc.LOG_ERROR, e) - return False + return False try: oldstderr = smtplib.stderr s = smtplib.SMTP() - try: # in case of debug, the messages are printed to stderr. if debug: @@ -412,13 +512,11 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non s.ehlo() if config['smtp_user'] or config['smtp_password']: - s.login(config['smtp_user'], config['smtp_password']) - + s.login(config['smtp_user'], config['smtp_password']) s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string() ) - finally: s.quit() if debug: @@ -1076,6 +1174,89 @@ def detect_ip_addr(): ip_addr = 'localhost' return ip_addr +# RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT: +# The server side never does any timestamp calculation, always +# sends them in a naive (timezone agnostic) format supposed to be +# expressed within the server timezone, and expects the clients to +# provide timestamps in the server timezone as well. +# It stores all timestamps in the database in naive format as well, +# which also expresses the time in the server timezone. +# For this reason the server makes its timezone name available via the +# common/timezone_get() rpc method, which clients need to read +# to know the appropriate time offset to use when reading/writing +# times. +def get_win32_timezone(): + """Attempt to return the "standard name" of the current timezone on a win32 system. + @return: the standard name of the current win32 timezone, or False if it cannot be found. + """ + res = False + if (sys.platform == "win32"): + try: + import _winreg + hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE) + current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS) + res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code + _winreg.CloseKey(current_tz_key) + _winreg.CloseKey(hklm) + except: + pass + return res + +def detect_server_timezone(): + """Attempt to detect the timezone to use on the server side. + Defaults to UTC if no working timezone can be found. + @return: the timezone identifier as expected by pytz.timezone. + """ + import time + import netsvc + try: + import pytz + except: + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING, + "Python pytz module is not available. Timezone will be set to UTC by default.") + return 'UTC' + + # Option 1: the configuration option (did not exist before, so no backwards compatibility issue) + # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz + # Option 3: the environment variable TZ + sources = [ (config['timezone'], 'OpenERP configuration'), + (time.tzname[0], 'time.tzname'), + (os.environ.get('TZ',False),'TZ environment variable'), ] + # Option 4: OS-specific: /etc/timezone on Unix + if (os.path.exists("/etc/timezone")): + tz_value = False + try: + f = open("/etc/timezone") + tz_value = f.read(128).strip() + except: + pass + finally: + f.close() + sources.append((tz_value,"/etc/timezone file")) + # Option 5: timezone info from registry on Win32 + if (sys.platform == "win32"): + # Timezone info is stored in windows registry. + # However this is not likely to work very well as the standard name + # of timezones in windows is rarely something that is known to pytz. + # But that's ok, it is always possible to use a config option to set + # it explicitly. + sources.append((get_win32_timezone(),"Windows Registry")) + + for (value,source) in sources: + if value: + try: + tz = pytz.timezone(value) + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_INFO, + "Using timezone %s obtained from %s." % (tz.zone,source)) + return value + except pytz.UnknownTimeZoneError: + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING, + "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value)) + + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING, + "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.") + return 'UTC' + if __name__ == '__main__': import doctest