[MERGE]merge with trunk

bzr revid: kbh@tinyerp.com-20121109052642-2nte0ax6pu7oktuy
This commit is contained in:
Khushboo Bhatt (Open ERP)
2012-11-09 10:56:42 +05:30
20 changed files with 365 additions and 68 deletions
+9 -17
View File
@@ -6,17 +6,9 @@ Customer Relationship Management software. More info at:
http://www.openerp.com
Installation on Debian Ubuntu
Installation on Debian/Ubuntu
-----------------------------
Download the deb file and type:
$ sudo dpkg -i <openerp-deb-filename>
$ sudo apt-get install install -f
Installation on Debian Ubuntu from nightly build
------------------------------------------------
Add the the apt repository
deb http://nightly.openerp.com/6.1/deb/ ./
@@ -26,6 +18,11 @@ in your source.list and type:
$ sudo apt-get update
$ sudo apt-get install openerp
Or download the deb file and type:
$ sudo dpkg -i <openerp-deb-filename>
$ sudo apt-get install install -f
Installation on RedHat, Fedora, CentOS
--------------------------------------
@@ -42,6 +39,8 @@ Install the openerp rpm
Installation on Windows
-----------------------
Check the notes in setup.py
Installation on MacOSX
-----------------------
@@ -54,14 +53,7 @@ default master password is "admin".
Detailed System Requirements
----------------------------
You need the following software installed:
postgresql-client, python-dateutil, python-feedparser, python-gdata,
python-ldap, python-libxslt1, python-lxml, python-mako, python-openid,
python-psycopg2, python-pybabel, python-pychart, python-pydot,
python-pyparsing, python-reportlab, python-simplejson, python-tz,
python-vatnumber, python-vobject, python-webdav, python-werkzeug, python-xlwt,
python-yaml, python-zsi
The dependencies are listed in setup.py
For Luxembourg localization, you also need:
+1
View File
@@ -7,6 +7,7 @@ OpenERP Server
import
module-versioning
on_change_tips
test-framework
Changed in 7.0
+51
View File
@@ -0,0 +1,51 @@
.. _on_change_tips:
On Change Methods
=================
Definition of on change methods in a view looks like this:
::
<field name="name" on_change="name_change(name, address, city)"/>
And here is the corresponding method in the model:
::
def name_change(self, cr, uid, ids, name, address, city, context=None):
...
return {
'value': {
'address': ...
'city': ...
}
}
On change methods can be confusing when people use them, here are a list of clarifications to avoid any misconception:
- On change methods can be executed during the creation of a row, long before it is effectively saved into the database.
- Fields are *not* validated before going through a on change methods. As an example, a field marqued as required can be False.
- On change methods can read data in the database but should *never* attempt to write anything, this is always a strong conception
problem.
- The format of the values passed to an on change method is exactly the same than the one passed to the write() method. So
the on change method must be able to handle any format used for all the fields it process. The following list describe some fields
that can have an unusual format.
- *float*: Due to the way JSON represents numbers and the way the JSON library of Python handles it, a float field will not always
be represented as a python float type. When the number can be represented as an integer it will appear as a python integer type.
This can be a problem when using some mathematical operations (example: price / 2), so it is a good practice to always cast any number
to float when you want to handle floats in on change methods.
- *one2many and many2many*: There are plenty of misconception about x2many fields in on change methods. The reality is, in fact, quite
complex. x2many are defined by a list of operations, each operation was given a number (0 -> create, 1 -> write, ect...) and has
its own semantic. To be able to use one2many and many2many in on change methods, you are strongly encourage to use the
resolve_2many_commands() method. Here is a sample usage:
::
values = self.resolve_2many_commands(cr, uid, 'my_o2m', my_o2m_values, ['price', 'tax'], context)
This code will convert the complex list of operations that makes the o2m value into a simple list of dictionaries containing the fields
'price' and 'tax', which is way simpler to handle in most on change methods. Please note that you can also return a list of
dictionaries as the new value of a one2many, it will replace the actual rows contained in that one2many (but it will also remove the
previous ones).
+1
View File
@@ -69,6 +69,7 @@
<field name="name">Administrator</field>
<field name="company_id" ref="main_company"/>
<field name="customer" eval="False"/>
<field name="email">admin@example.com</field>
</record>
<record model="res.users" id="base.user_root">
+5
View File
@@ -5,6 +5,7 @@
<field name="name">Demo User</field>
<field name="company_id" ref="main_company"/>
<field name="customer" eval="False"/>
<field name="email">demo@example.com</field>
</record>
<record id="user_demo" model="res.users">
<field name="partner_id" ref="base.partner_demo"/>
@@ -15,6 +16,10 @@
<field name="groups_id" eval="[(6,0,[ref('base.group_user')])]"/>
</record>
<record model="res.partner" id="base.partner_root">
<field name="email">admin@example.com</field>
</record>
<!-- new rate for demo transactions in multi currency -->
<record id="rateUSDbis" model="res.currency.rate">
<field name="rate">1.5289</field>
+67 -1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import collections
import datetime
import functools
import operator
@@ -31,9 +32,70 @@ REPLACE_WITH = lambda ids: (6, False, ids)
class ConversionNotFound(ValueError): pass
class ColumnWrapper(object):
def __init__(self, column, cr, uid, pool, fromtype, context=None):
self._converter = None
self._column = column
if column._obj:
self._pool = pool
self._converter_args = {
'cr': cr,
'uid': uid,
'model': pool[column._obj],
'fromtype': fromtype,
'context': context
}
@property
def converter(self):
if not self._converter:
self._converter = self._pool['ir.fields.converter'].for_model(
**self._converter_args)
return self._converter
def __getattr__(self, item):
return getattr(self._column, item)
class ir_fields_converter(orm.Model):
_name = 'ir.fields.converter'
def for_model(self, cr, uid, model, fromtype=str, context=None):
""" Returns a converter object for the model. A converter is a
callable taking a record-ish (a dictionary representing an openerp
record with values of typetag ``fromtype``) and returning a converted
records matching what :meth:`openerp.osv.orm.Model.write` expects.
:param model: :class:`openerp.osv.orm.Model` for the conversion base
:returns: a converter callable
:rtype: (record: dict, logger: (field, error) -> None) -> dict
"""
columns = dict(
(k, ColumnWrapper(v.column, cr, uid, self.pool, fromtype, context))
for k, v in model._all_columns.iteritems())
converters = dict(
(k, self.to_field(cr, uid, model, column, fromtype, context))
for k, column in columns.iteritems())
def fn(record, log):
converted = {}
for field, value in record.iteritems():
if field in (None, 'id', '.id'): continue
if not value:
converted[field] = False
continue
try:
converted[field], ws = converters[field](value)
for w in ws:
if isinstance(w, basestring):
# wrap warning string in an ImportWarning for
# uniform handling
w = ImportWarning(w)
log(field, w)
except ValueError, e:
log(field, e)
return converted
return fn
def to_field(self, cr, uid, model, column, fromtype=str, context=None):
""" Fetches a converter for the provided column object, from the
specified type.
@@ -343,6 +405,10 @@ class ir_fields_converter(orm.Model):
# [{subfield:ref1},{subfield:ref2},{subfield:ref3}]
records = ({subfield:item} for item in record[subfield].split(','))
def log(_, e):
if not isinstance(e, Warning):
raise e
warnings.append(e)
for record in records:
id = None
refs = only_ref_fields(record)
@@ -355,7 +421,7 @@ class ir_fields_converter(orm.Model):
cr, uid, model, column, subfield, reference, context=context)
warnings.extend(w2)
writable = exclude_ref_fields(record)
writable = column.converter(exclude_ref_fields(record), log)
if id:
commands.append(LINK_TO(id))
commands.append(UPDATE(id, writable))
+3
View File
@@ -24,6 +24,7 @@ import logging
import openerp.modules
from openerp.osv import fields, osv
from tools.translate import _
_logger = logging.getLogger(__name__)
@@ -336,6 +337,8 @@ class ir_translation(osv.osv):
trans_model = self.pool.get(model)
domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')]
langs_ids = self.pool.get('res.lang').search(cr, uid, [('code', '!=', 'en_US')], context=context)
if not langs_ids:
raise osv.except_osv(_('Error'), _("Translation features are unavailable until you install an extra OpenERP translation."))
langs = [lg.code for lg in self.pool.get('res.lang').browse(cr, uid, langs_ids, context=context)]
main_lang = 'en_US'
translatable_fields = []
+9 -4
View File
@@ -264,10 +264,15 @@ class res_partner(osv.osv, format_address):
return False
def _get_default_image(self, cr, uid, is_company, context=None, colorize=False):
if is_company:
image = open(openerp.modules.get_module_resource('base', 'static/src/img', 'company_image.png')).read()
else:
image = tools.image_colorize(open(openerp.modules.get_module_resource('base', 'static/src/img', 'avatar.png')).read())
img_path = openerp.modules.get_module_resource('base', 'static/src/img',
('company_image.png' if is_company else 'avatar.png'))
with open(img_path, 'rb') as f:
image = f.read()
# colorize user avatars
if not is_company:
image = tools.image_colorize(image)
return tools.image_resize_image_big(image.encode('base64'))
def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
+1 -1
View File
@@ -147,7 +147,7 @@
<group>
<group>
<label for="type" attrs="{'invisible': [('parent_id','=', False)]}" invisible="1"/>
<label for="type" attrs="{'invisible': [('parent_id','=', False)]}"/>
<div attrs="{'invisible': [('parent_id','=', False)]}" invisible="1" name="div_type">
<field class="oe_inline"
name="type"/>
+1 -1
View File
@@ -202,6 +202,7 @@
<field name="name" readonly="1" class="oe_inline"/>
(<field name="login" readonly="1" class="oe_inline"/>)
</h1>
<button name="preference_change_password" type="object" string="Change password" class="oe_link"/>
<group name="preferences" col="4">
<field name="lang" readonly="0"/>
<field name="tz" readonly="0"/>
@@ -213,7 +214,6 @@
<field name="signature" readonly="0"/>
</group>
<footer>
<button name="preference_change_password" type="object" string="Change password"/>
<button name="preference_save" type="object" string="Save"/>
or
<button name="preference_cancel" string="Cancel" special="cancel" class="oe_link"/>
+1 -2
View File
@@ -38,7 +38,6 @@ import openerp.osv as osv
import openerp.pooler as pooler
import openerp.release as release
import openerp.tools as tools
import openerp.tools.assertion_report as assertion_report
from openerp import SUPERUSER_ID
from openerp import SUPERUSER_ID
@@ -285,7 +284,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
# processed_modules: for cleanup step after install
# loaded_modules: to avoid double loading
report = assertion_report.assertion_report()
report = pool._assertion_report
loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
if tools.config['load_language']:
+2
View File
@@ -32,6 +32,7 @@ import openerp.cron
import openerp.tools
import openerp.modules.db
import openerp.tools.config
from openerp.tools import assertion_report
_logger = logging.getLogger(__name__)
@@ -49,6 +50,7 @@ class Registry(object):
self._store_function = {}
self._init = True
self._init_parent = {}
self._assertion_report = assertion_report.assertion_report()
# modules fully loaded (maintained during init phase by `loading` module)
self._init_modules = set()
+14 -29
View File
@@ -1352,9 +1352,11 @@ class BaseModel(object):
noupdate=noupdate, res_id=id, context=context))
cr.execute('RELEASE SAVEPOINT model_load_save')
except psycopg2.Warning, e:
_logger.exception('Failed to import record %s', record)
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
messages.append(dict(info, type='warning', message=str(e)))
except psycopg2.Error, e:
_logger.exception('Failed to import record %s', record)
# Failed to write, log to messages, rollback savepoint (to
# avoid broken transaction) and keep going
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
@@ -1458,15 +1460,16 @@ class BaseModel(object):
field_names = dict(
(f, (Translation._get_source(cr, uid, self._name + ',' + f, 'field',
context.get('lang'))
or column.string or f))
or column.string))
for f, column in columns.iteritems())
converters = dict(
(k, Converter.to_field(cr, uid, self, column, context=context))
for k, column in columns.iteritems())
convert = Converter.for_model(cr, uid, self, context=context)
def _log(base, field, exception):
type = 'warning' if isinstance(exception, Warning) else 'error'
record = dict(base, field=field, type=type,
# logs the logical (not human-readable) field name for automated
# processing of response, but injects human readable in message
record = dict(base, type=type, field=field,
message=unicode(exception.args[0]) % base)
if len(exception.args) > 1 and exception.args[1]:
record.update(exception.args[1])
@@ -1476,7 +1479,6 @@ class BaseModel(object):
for record, extras in stream:
dbid = False
xid = False
converted = {}
# name_get/name_create
if None in record: pass
# xid
@@ -1497,27 +1499,8 @@ class BaseModel(object):
message=_(u"Unknown database identifier '%s'") % dbid))
dbid = False
for field, strvalue in record.iteritems():
if field in (None, 'id', '.id'): continue
if not strvalue:
converted[field] = False
continue
# In warnings and error messages, use translated string as
# field name
message_base = dict(
extras, record=stream.index, field=field_names[field])
try:
converted[field], ws = converters[field](strvalue)
for w in ws:
if isinstance(w, basestring):
# wrap warning string in an ImportWarning for
# uniform handling
w = ImportWarning(w)
_log(message_base, field, w)
except ValueError, e:
_log(message_base, field, e)
converted = convert(record, lambda field, err:\
_log(dict(extras, record=stream.index, field=field_names[field]), field, err))
yield dbid, xid, converted, dict(extras, record=stream.index)
@@ -3878,10 +3861,12 @@ class BaseModel(object):
getattr(wf_service, trigger)(uid, self._name, res_id, cr)
def _workflow_signal(self, cr, uid, ids, signal, context=None):
"""Send given workflow signal"""
"""Send given workflow signal and return a dict mapping ids to workflow results"""
wf_service = netsvc.LocalService("workflow")
result = {}
for res_id in ids:
wf_service.trg_validate(uid, self._name, res_id, signal, cr)
result[res_id] = wf_service.trg_validate(uid, self._name, res_id, signal, cr)
return result
def unlink(self, cr, uid, ids, context=None):
"""
+2 -1
View File
@@ -188,7 +188,8 @@ class object_proxy(object):
object = pooler.get_pool(cr.dbname).get(obj)
if not object:
raise except_osv('Object Error', 'Object %s doesn\'t exist' % str(obj))
return object._workflow_signal(cr, uid, [args[0]], signal)
res_id = args[0]
return object._workflow_signal(cr, uid, [res_id], signal)[res_id]
@check
def exec_workflow(self, db, uid, obj, signal, *args):
+1 -1
View File
@@ -34,7 +34,6 @@ import openerp.netsvc
import openerp.osv
import openerp.tools
import openerp.service.wsgi_server
import openerp.service.workers
#.apidoc title: RPC Services
@@ -119,6 +118,7 @@ def stop_services():
openerp.modules.registry.RegistryManager.delete_all()
def start_services_workers():
import openerp.service.workers
openerp.multi_process = True # Nah!
openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run()
+5 -8
View File
@@ -3,20 +3,17 @@
# TODO rename class: Multicorn -> Arbiter ?
#-----------------------------------------------------------
import errno
try:
import fcntl
except ImportError:
fcntl = None
import fcntl
import logging
import os
import psutil
import random
import resource
import select
import socket
import time
import logging
import os
import signal
import socket
import sys
import time
import werkzeug.serving
@@ -79,6 +79,7 @@ class One2ManyMultiple(orm.Model):
_name = 'export.one2many.multiple'
_columns = {
'parent_id': fields.many2one('export.one2many.recursive'),
'const': fields.integer(),
'child1': fields.one2many('export.one2many.child.1', 'parent_id'),
'child2': fields.one2many('export.one2many.child.2', 'parent_id'),
@@ -135,3 +136,11 @@ class SelectionWithDefault(orm.Model):
'const': 4,
'value': 2,
}
class RecO2M(orm.Model):
_name = 'export.one2many.recursive'
_columns = {
'value': fields.integer(),
'child': fields.one2many('export.one2many.multiple', 'parent_id')
}
@@ -0,0 +1 @@
[["Wood y Wood Pecker", "", "Snow Street, 25", "Kainuu", "Finland", "Supplier", "1", "0", "1", ""], ["Roger Pecker", "Default", "Snow Street, 27", "Kainuu", "Finland", "Supplier", "1", "0", "0", "Wood y Wood Pecker"], ["Sharon Pecker", "Shipping", "Snow Street, 28", "Kainuu", "Finland", "Supplier", "1", "0", "0", "Wood y Wood Pecker"], ["Thomas Pecker", "Contact", "Snow Street, 27", "Kainuu", "Finland", "Supplier", "1", "0", "0", "Wood y Wood Pecker"], ["Norseman Roundabout", "", "Atonium Street, 45a", "Brussels", "Belgium", "Supplier", "1", "0", "1", ""], ["Yvan Holiday", "Invoice", "Atonium Street, 45b", "Brussels", "Belgium", "Supplier", "1", "0", "0", "Norseman Roundabout"], ["Jack Unsworth", "Contact", "Atonium Street, 45a", "Brussels", "Belgium", "Supplier", "1", "0", "0", "Norseman Roundabout"]]
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import json
import pkgutil
import unittest2
import openerp.modules.registry
import openerp
@@ -246,7 +247,7 @@ class test_integer_field(ImporterCase):
-1, -42, -(2**31 - 1), -(2**31), -12345678
], values(self.read()))
@mute_logger('openerp.sql_db')
@mute_logger('openerp.sql_db', 'openerp.osv.orm')
def test_out_of_range(self):
result = self.import_(['value'], [[str(2**31)]])
self.assertIs(result['ids'], False)
@@ -388,14 +389,14 @@ class test_unbound_string_field(ImporterCase):
class test_required_string_field(ImporterCase):
model_name = 'export.string.required'
@mute_logger('openerp.sql_db')
@mute_logger('openerp.sql_db', 'openerp.osv.orm')
def test_empty(self):
result = self.import_(['value'], [[]])
self.assertEqual(result['messages'], [message(
u"Missing required value for the field 'unknown'")])
self.assertIs(result['ids'], False)
@mute_logger('openerp.sql_db')
@mute_logger('openerp.sql_db', 'openerp.osv.orm')
def test_not_provided(self):
result = self.import_(['const'], [['12']])
self.assertEqual(result['messages'], [message(
@@ -1007,6 +1008,46 @@ class test_realworld(common.TransactionCase):
self.assertFalse(result['messages'])
self.assertEqual(len(result['ids']), len(data))
def test_backlink(self):
data = json.loads(pkgutil.get_data(self.__module__, 'contacts.json'))
result = self.registry('res.partner').load(
self.cr, openerp.SUPERUSER_ID,
["name", "type", "street", "city", "country_id", "category_id",
"supplier", "customer", "is_company", "parent_id"],
data)
self.assertFalse(result['messages'])
self.assertEqual(len(result['ids']), len(data))
def test_recursive_o2m(self):
""" The content of the o2m field's dict needs to go through conversion
as it may be composed of convertables or other relational fields
"""
self.registry('ir.model.data').clear_caches()
Model = self.registry('export.one2many.recursive')
result = Model.load(self.cr, openerp.SUPERUSER_ID,
['value', 'child/const', 'child/child1/str', 'child/child2/value'],
[
['4', '42', 'foo', '55'],
['', '43', 'bar', '56'],
['', '', 'baz', ''],
['', '55', 'qux', '57'],
['5', '99', 'wheee', ''],
['', '98', '', '12'],
],
context=None)
self.assertFalse(result['messages'])
self.assertEqual(len(result['ids']), 2)
b = Model.browse(self.cr, openerp.SUPERUSER_ID, result['ids'], context=None)
self.assertEqual((b[0].value, b[1].value), (4, 5))
self.assertEqual([child.str for child in b[0].child[1].child1],
['bar', 'baz'])
self.assertFalse(len(b[1].child[1].child1))
self.assertEqual([child.value for child in b[1].child[1].child2],
[12])
class test_date(ImporterCase):
model_name = 'export.date'
Executable
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import glob, os, re, setuptools, sys
from os.path import join, isfile
# List all data files
def data():
files = []
for root, dirnames, filenames in os.walk('openerp'):
for filename in filenames:
if not re.match(r'.*(\.pyc|\.pyo|\~)$',filename):
files.append(os.path.join(root, filename))
d = {}
for v in files:
k=os.path.dirname(v)
if k in d:
d[k].append(v)
else:
d[k]=[v]
r = d.items()
if os.name == 'nt':
r.append(("Microsoft.VC90.CRT", glob.glob('C:\Microsoft.VC90.CRT\*.*')))
import babel
r.append(("localedata",
glob.glob(os.path.join(os.path.dirname(babel.__file__), "localedata" , '*'))))
return r
def gen_manifest():
file_list="\n".join(data())
open('MANIFEST','w').write(file_list)
if os.name == 'nt':
sys.path.append("C:\Microsoft.VC90.CRT")
def py2exe_options():
if os.name == 'nt':
import py2exe
return {
"console" : [ { "script": "openerp-server", "icon_resources": [(1, join("install","openerp-icon.ico"))], }],
'options' : {
"py2exe": {
"skip_archive": 1,
"optimize": 2,
"dist_dir": 'dist',
"packages": [ "DAV", "HTMLParser", "PIL", "asynchat", "asyncore", "commands", "dateutil", "decimal", "email", "encodings", "imaplib", "lxml", "lxml._elementpath", "lxml.builder", "lxml.etree", "lxml.objectify", "mako", "openerp", "poplib", "pychart", "pydot", "pyparsing", "reportlab", "select", "simplejson", "smtplib", "uuid", "vatnumber", "vobject", "xml", "xml.dom", "yaml", ],
"excludes" : ["Tkconstants","Tkinter","tcl"],
}
}
}
else:
return {}
execfile(join(os.path.dirname(__file__), 'openerp', 'release.py'))
# Notes for OpenERP developer on windows:
#
# To setup a windows developer evironement install python2.7 then pip and use
# "pip install <depencey>" for every dependency listed below.
#
# Dependecies that requires DLLs are not installable with pip install, for
# them we added comments with links where you can find the installers.
#
# OpenERP on windows also require the pywin32, the binary can be found at
# http://pywin32.sf.net
#
# Both python2.7 32bits and 64bits are known to work.
setuptools.setup(
name = 'openerp',
version = version,
description = description,
long_description = long_desc,
url = url,
author = author,
author_email = author_email,
classifiers = filter(None, classifiers.split("\n")),
license = license,
scripts = ['openerp-server'],
data_files = data(),
packages = setuptools.find_packages(),
dependency_links = ['http://download.gna.org/pychart/'],
#include_package_data = True,
install_requires = [
'pychart', # not on pypi, use: pip install http://download.gna.org/pychart/PyChart-1.39.tar.gz
'babel',
'docutils',
'feedparser',
'gdata',
'lxml < 3', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'mako',
'PIL', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'psutil', # windows binary code.google.com/p/psutil/downloads/list
'psycopg2',
'pydot',
'python-dateutil < 2',
'python-ldap', # optional
'python-openid',
'pytz',
'pywebdav',
'pyyaml',
'reportlab', # windows binary pypi.python.org/pypi/reportlab
'simplejson',
'vatnumber',
'vobject',
'werkzeug',
'xlwt',
],
extras_require = {
'SSL' : ['pyopenssl'],
},
tests_require = ['unittest2'],
**py2exe_options()
)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: