Browse Source

Merge branch '7.0' of https://github.com/OCA/server-tools into 7.0-module_parent_dependencies

pull/8/head
Sylvain LE GAL 10 years ago
parent
commit
0f492f3777
  1. 15
      .coveragerc
  2. 25
      .travis.yml
  3. 12
      README.md
  4. 2
      auth_admin_passkey/__openerp__.py
  5. 2
      auth_admin_passkey/model/res_users.py
  6. 3
      auth_from_http_basic/__init__.py
  7. 2
      base_external_dbsource/__openerp__.py
  8. 22
      base_external_dbsource/base_external_dbsource.py
  9. 6
      base_external_dbsource/test/dbsource_connect.yml
  10. 7
      base_optional_quick_create/__openerp__.py
  11. 15
      base_optional_quick_create/model.py
  12. 22
      configuration_helper/__init__.py
  13. 85
      configuration_helper/__openerp__.py
  14. 118
      configuration_helper/config.py
  15. 2
      cron_run_manually/__openerp__.py
  16. 11
      cron_run_manually/model/ir_cron.py
  17. 1
      dbfilter_from_header/__init__.py
  18. 18
      dbfilter_from_header/__openerp__.py
  19. 4
      disable_openerp_online/data/ir_ui_menu.xml
  20. 31
      email_template_template/__openerp__.py
  21. 6
      email_template_template/model/email_template.py
  22. 2
      fetchmail_attach_from_folder/__openerp__.py
  23. 8
      fetchmail_attach_from_folder/match_algorithm/base.py
  24. 15
      fetchmail_attach_from_folder/match_algorithm/email_domain.py
  25. 9
      fetchmail_attach_from_folder/match_algorithm/email_exact.py
  26. 23
      fetchmail_attach_from_folder/match_algorithm/openerp_standard.py
  27. 87
      fetchmail_attach_from_folder/model/fetchmail_server.py
  28. 99
      fetchmail_attach_from_folder/model/fetchmail_server_folder.py
  29. 97
      fetchmail_attach_from_folder/wizard/attach_mail_manually.py
  30. 29
      import_odbc/__openerp__.py
  31. 159
      import_odbc/import_odbc.py
  32. 15
      mail_environment/__openerp__.py
  33. 230
      mail_environment/env_mail.py
  34. 7
      mass_editing/__init__.py
  35. 15
      mass_editing/mass_editing.py
  36. 6
      mass_editing/wizard/__init__.py
  37. 3
      scheduler_error_mailer/__openerp__.py
  38. 36
      scheduler_error_mailer/ir_cron.py
  39. 19
      server_environment/serv_config.py
  40. 2
      server_environment/system_info.py
  41. 2
      server_environment_files/__init__.py
  42. 2
      super_calendar/__init__.py
  43. 39
      super_calendar/__openerp__.py
  44. BIN
      super_calendar/data/meetings.png
  45. BIN
      super_calendar/data/month_calendar.png
  46. BIN
      super_calendar/data/phone_calls.png
  47. BIN
      super_calendar/data/week_calendar.png
  48. 185
      super_calendar/super_calendar.py
  49. 11
      tree_view_record_id/__init__.py
  50. 58
      tree_view_record_id/__openerp__.py
  51. 64
      tree_view_record_id/orm.py
  52. 41
      users_ldap_groups/__openerp__.py
  53. 138
      users_ldap_groups/users_ldap_groups.py
  54. 51
      users_ldap_groups/users_ldap_groups_operators.py
  55. 30
      users_ldap_mail/__openerp__.py
  56. 26
      users_ldap_mail/users_ldap_model.py
  57. 11
      users_ldap_populate/__openerp__.py
  58. 8
      users_ldap_populate/model/populate_wizard.py
  59. 14
      users_ldap_populate/model/users_ldap.py
  60. 6
      web_context_tunnel/__openerp__.py
  61. 3
      web_context_tunnel/static/src/js/context_tunnel.js

15
.coveragerc

@ -0,0 +1,15 @@
[report]
include =
*/OCA/server-tools/*
omit =
*/tests/*
*__init__.py
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about null context checking
if context is None:

25
.travis.yml

@ -0,0 +1,25 @@
language: python
python:
- "2.7"
env:
- VERSION="7.0" ODOO_REPO="odoo/odoo"
- VERSION="7.0" ODOO_REPO="OCA/OCB"
virtualenv:
system_site_packages: true
install:
- git clone https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly ${VERSION}
- sudo pip install python-ldap
- printf '[options]\n\nrunning_env = dev' > ${HOME}/.openerp_serverrc
script:
- travis_run_flake8
- travis_run_tests ${VERSION}
after_success:
coveralls

12
README.md

@ -0,0 +1,12 @@
[![Build Status](https://travis-ci.org/OCA/server-tools.svg?branch=7.0)](https://travis-ci.org/OCA/server-tools)
[![Coverage Status](https://coveralls.io/repos/OCA/server-tools/badge.png?branch=7.0)](https://coveralls.io/r/OCA/server-tools?branch=7.0)
Server Environment And Tools
============================
This project aim to deal with modules related to manage OpenERP server environment and provide useful tools. You'll find modules that:
- Manage configuration depending on environment (devs, test, prod,..)
- Keep the security on update
- Manage email settings
-...

2
auth_admin_passkey/__openerp__.py

@ -49,7 +49,7 @@ Copyright, Author and Licence :
'license': 'AGPL-3', 'license': 'AGPL-3',
'depends': [ 'depends': [
'mail', 'mail',
],
],
'data': [ 'data': [
'data/ir_config_parameter.xml', 'data/ir_config_parameter.xml',
'view/res_config_view.xml', 'view/res_config_view.xml',

2
auth_admin_passkey/model/res_users.py

@ -35,7 +35,7 @@ class res_users(Model):
# Private Function section # Private Function section
def _get_translation(self, cr, lang, text): def _get_translation(self, cr, lang, text):
context = {'lang': lang}
context = {'lang': lang} # noqa: _() checks page for locals
return _(text) return _(text)
def _send_email_passkey(self, cr, user_id, user_agent_env): def _send_email_passkey(self, cr, user_id, user_agent_env):

3
auth_from_http_basic/__init__.py

@ -36,7 +36,8 @@ def init(self, params):
base_location=self.httprequest.url_root.rstrip('/'), base_location=self.httprequest.url_root.rstrip('/'),
HTTP_HOST=self.httprequest.environ['HTTP_HOST'], HTTP_HOST=self.httprequest.environ['HTTP_HOST'],
REMOTE_ADDR=self.httprequest.environ['REMOTE_ADDR'] REMOTE_ADDR=self.httprequest.environ['REMOTE_ADDR']
))
)
)
WebRequest.init = init WebRequest.init = init

2
base_external_dbsource/__openerp__.py

@ -57,7 +57,7 @@ Contributors
'base_external_dbsource_demo.xml', 'base_external_dbsource_demo.xml',
], ],
'test': [ 'test': [
'dbsource_connect.yml',
'test/dbsource_connect.yml',
], ],
'installable': True, 'installable': True,
'active': False, 'active': False,

22
base_external_dbsource/base_external_dbsource.py

@ -34,13 +34,15 @@ try:
try: try:
import pymssql import pymssql
CONNECTORS.append(('mssql', 'Microsoft SQL Server')) CONNECTORS.append(('mssql', 'Microsoft SQL Server'))
except:
assert pymssql
except (ImportError, AssertionError):
_logger.info('MS SQL Server not available. Please install "pymssql"\ _logger.info('MS SQL Server not available. Please install "pymssql"\
python package.') python package.')
try: try:
import MySQLdb import MySQLdb
CONNECTORS.append(('mysql', 'MySQL')) CONNECTORS.append(('mysql', 'MySQL'))
except:
assert MySQLdb
except (ImportError, AssertionError):
_logger.info('MySQL not available. Please install "mysqldb"\ _logger.info('MySQL not available. Please install "mysqldb"\
python package.') python package.')
except: except:
@ -90,15 +92,15 @@ Sample connection strings:
} }
def conn_open(self, cr, uid, id1): def conn_open(self, cr, uid, id1):
#Get dbsource record
# Get dbsource record
data = self.browse(cr, uid, id1) data = self.browse(cr, uid, id1)
#Build the full connection string
# Build the full connection string
connStr = data.conn_string connStr = data.conn_string
if data.password: if data.password:
if '%s' not in data.conn_string: if '%s' not in data.conn_string:
connStr += ';PWD=%s' connStr += ';PWD=%s'
connStr = connStr % data.password connStr = connStr % data.password
#Try to connect
# Try to connect
if data.connector == 'cx_Oracle': if data.connector == 'cx_Oracle':
os.environ['NLS_LANG'] = 'AMERICAN_AMERICA.UTF8' os.environ['NLS_LANG'] = 'AMERICAN_AMERICA.UTF8'
conn = cx_Oracle.connect(connStr) conn = cx_Oracle.connect(connStr)
@ -134,13 +136,13 @@ Sample connection strings:
for obj in data: for obj in data:
conn = self.conn_open(cr, uid, obj.id) conn = self.conn_open(cr, uid, obj.id)
if obj.connector in ["sqlite", "mysql", "mssql"]: if obj.connector in ["sqlite", "mysql", "mssql"]:
#using sqlalchemy
# using sqlalchemy
cur = conn.execute(sqlquery, sqlparams) cur = conn.execute(sqlquery, sqlparams)
if metadata: if metadata:
cols = cur.keys() cols = cur.keys()
rows = [r for r in cur] rows = [r for r in cur]
else: else:
#using other db connectors
# using other db connectors
cur = conn.cursor() cur = conn.cursor()
cur.execute(sqlquery, sqlparams) cur.execute(sqlquery, sqlparams)
if metadata: if metadata:
@ -157,7 +159,7 @@ Sample connection strings:
conn = False conn = False
try: try:
conn = self.conn_open(cr, uid, obj.id) conn = self.conn_open(cr, uid, obj.id)
except Exception, e:
except Exception as e:
raise orm.except_orm(_("Connection test failed!"), raise orm.except_orm(_("Connection test failed!"),
_("Here is what we got instead:\n %s") _("Here is what we got instead:\n %s")
% tools.ustr(e)) % tools.ustr(e))
@ -168,8 +170,6 @@ Sample connection strings:
except Exception: except Exception:
# ignored, just a consequence of the previous exception # ignored, just a consequence of the previous exception
pass pass
#TODO: if OK a (wizard) message box should be displayed
# TODO: if OK a (wizard) message box should be displayed
raise orm.except_orm(_("Connection test succeeded!"), raise orm.except_orm(_("Connection test succeeded!"),
_("Everything seems properly set up!")) _("Everything seems properly set up!"))
#EOF

6
base_external_dbsource/test/dbsource_connect.yml

@ -2,4 +2,8 @@
Connect to local Postgres. Connect to local Postgres.
- -
!python {model: base.external.dbsource}: | !python {model: base.external.dbsource}: |
self.connection_test(cr, uid, [ref("demo_postgresql")]
from openerp.osv.orm import except_orm
try:
self.connection_test(cr, uid, [ref("demo_postgre")])
except except_orm as e:
assert e.value == u'Everything seems properly set up!'

7
base_optional_quick_create/__openerp__.py

@ -24,8 +24,11 @@
'category': 'Tools', 'category': 'Tools',
'summary': "Avoid 'quick create' on m2o fields, on a 'by model' basis", 'summary': "Avoid 'quick create' on m2o fields, on a 'by model' basis",
'description': """ 'description': """
This module allows to avoid to 'quick create' new records, through many2one fields, for a specific model.
You can configure which models should allow 'quick create'. When specified, the 'quick create' option will always open the standard create form.
This module allows to avoid to 'quick create' new records, through many2one
fields, for a specific model.
You can configure which models should allow 'quick create'.
When specified, the 'quick create' option will always open the standard create
form.
Got the idea from https://twitter.com/nbessi/status/337869826028605441 Got the idea from https://twitter.com/nbessi/status/337869826028605441
""", """,

15
base_optional_quick_create/model.py

@ -22,17 +22,20 @@ from openerp.osv import orm, fields
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.tools.translate import _ from openerp.tools.translate import _
class ir_model(orm.Model): class ir_model(orm.Model):
_inherit = 'ir.model' _inherit = 'ir.model'
_columns = { _columns = {
'avoid_quick_create': fields.boolean('Avoid quick create'), 'avoid_quick_create': fields.boolean('Avoid quick create'),
}
}
def _wrap_name_create(self, old_create, model): def _wrap_name_create(self, old_create, model):
def wrapper(cr, uid, name, context=None): def wrapper(cr, uid, name, context=None):
raise orm.except_orm(_('Error'), _("Can't create quickly. Opening create form"))
raise orm.except_orm(_('Error'),
_("Can't create quickly. "
"Opening create form"))
return wrapper return wrapper
def _register_hook(self, cr, ids=None): def _register_hook(self, cr, ids=None):
@ -42,8 +45,10 @@ class ir_model(orm.Model):
if model.avoid_quick_create: if model.avoid_quick_create:
model_name = model.model model_name = model.model
model_obj = self.pool.get(model_name) model_obj = self.pool.get(model_name)
if not hasattr(model_obj, 'check_quick_create'):
model_obj.name_create = self._wrap_name_create(model_obj.name_create, model_name)
if model_obj and not hasattr(model_obj, 'check_quick_create'):
model_obj.name_create = self._wrap_name_create(
model_obj.name_create,
model_name)
model_obj.check_quick_create = True model_obj.check_quick_create = True
return True return True

22
configuration_helper/__init__.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: David BEAL
# Copyright 2014 Akretion
#
# 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/>.
#
##############################################################################
from . import config # noqa

85
configuration_helper/__openerp__.py

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: David BEAL
# Copyright 2014 Akretion
#
# 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/>.
#
##############################################################################
{
'name': 'Configuration Helper',
'version': '0.8',
'author': 'Akretion',
'maintainer': 'Akretion',
'category': 'server',
'complexity': 'normal',
'depends': ['base'],
'description': """
Configuration Helper
====================
*This module is intended for developer only. It does nothing used alone.*
This module :
* create automatically related fields in 'whatiwant.config.settings' using
those defined in 'res.company' : it avoid duplicated field definitions.
* company_id field with default value is created
* onchange_company_id is defined to update all related fields
* supported fields: char, text, integer, float, datetime, date, boolean, m2o
How to use
----------
.. code-block:: python
from . company import ResCompany
class WhatiwantClassSettings(orm.TransientModel):
_inherit = ['res.config.settings', 'abstract.config.settings']
_name = 'whatiwant.config.settings'
# fields must be defined in ResCompany class
# related fields are automatically generated from previous definitions
_companyObject = ResCompany
Roadmap
-------
* support (or check support) for these field types : o2m, m2m, reference,
property, selection
* automatically generate a default view for 'whatiwant.config.settings'
(in --debug?)
Contributors
------------
* David BEAL <david.beal@akretion.com>
* Sébastien BEAU <sebastien.beau@akretion.com>
* Yannick Vaucher, Camptocamp, (code refactoring from his module
'delivery_carrier_label_postlogistics')
""",
'website': 'http://www.akretion.com/',
'data': [
],
'tests': [],
'installable': True,
'auto_install': False,
'license': 'AGPL-3',
'application': True,
}

118
configuration_helper/config.py

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: David BEAL, Copyright 2014 Akretion
#
# 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 re
from openerp.osv import orm, fields
class AbstractConfigSettings(orm.AbstractModel):
_name = 'abstract.config.settings'
_description = 'Abstract configuration settings'
# prefix field name to differentiate fields in company with those in config
_prefix = 'setting_'
# this is the class name to import in your module
# (it should be ResCompany or res_company, depends of your code)
_companyObject = None
def _filter_field(self, field_key):
"""Inherit in your module to define for which company field
you don't want have a matching related field"""
return True
def __init__(self, pool, cr):
super(AbstractConfigSettings, self).__init__(pool, cr)
if self._companyObject:
company_cols = self._companyObject._columns
for field_key in company_cols:
# allows to exclude some field
if self._filter_field(field_key):
args = ('company_id', field_key)
kwargs = {
'string': company_cols[field_key].string,
'help': company_cols[field_key].help,
'type': company_cols[field_key]._type,
}
if '_obj' in company_cols[field_key].__dict__:
kwargs['relation'] = \
company_cols[field_key]._obj
if '_domain' in \
company_cols[field_key].__dict__:
kwargs['domain'] = \
company_cols[field_key]._domain
field_key = re.sub('^' + self._prefix, '', field_key)
self._columns[field_key] = \
fields.related(*args, **kwargs)
_columns = {
'company_id': fields.many2one(
'res.company',
'Company',
required=True),
}
def _default_company(self, cr, uid, context=None):
user = self.pool['res.users'].browse(cr, uid, uid, context=context)
return user.company_id.id
_defaults = {
'company_id': _default_company,
}
def field_to_populate_as_related(self, cr, uid,
field,
company_cols,
context=None):
"""Only fields which comes from company with the right prefix
must be defined as related"""
if self._prefix + field in company_cols:
return True
return False
def onchange_company_id(self, cr, uid, ids, company_id, context=None):
" update related fields "
values = {}
values['currency_id'] = False
if not company_id:
return {'value': values}
company_m = self.pool['res.company']
company = company_m.browse(
cr, uid, company_id, context=context)
company_cols = company_m._columns.keys()
for field in self._columns:
if self.field_to_populate_as_related(
cr, uid, field, company_cols, context=context):
cpny_field = self._columns[field].arg[-1]
if self._columns[field]._type == 'many2one':
values[field] = company[cpny_field]['id'] or False
else:
values[field] = company[cpny_field]
return {'value': values}
def create(self, cr, uid, values, context=None):
id = super(AbstractConfigSettings, self).create(
cr, uid, values, context=context)
# Hack: to avoid some nasty bug, related fields are not written
# upon record creation. Hence we write on those fields here.
vals = {}
for fname, field in self._columns.iteritems():
if isinstance(field, fields.related) and fname in values:
vals[fname] = values[fname]
self.write(cr, uid, [id], vals, context)
return id

2
cron_run_manually/__openerp__.py

@ -34,4 +34,4 @@ of the scheduler.
'depends': ['base'], 'depends': ['base'],
'data': ['view/ir_cron.xml'], 'data': ['view/ir_cron.xml'],
'installable': True, 'installable': True,
}
}

11
cron_run_manually/model/ir_cron.py

@ -27,9 +27,10 @@ from openerp.tools import SUPERUSER_ID
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.tools.safe_eval import safe_eval from openerp.tools.safe_eval import safe_eval
class irCron(orm.Model): class irCron(orm.Model):
_inherit = 'ir.cron' _inherit = 'ir.cron'
def run_manually(self, cr, uid, ids, context=None): def run_manually(self, cr, uid, ids, context=None):
""" """
Run a job from the cron form view. Run a job from the cron form view.
@ -51,7 +52,7 @@ class irCron(orm.Model):
_('Error'), _('Error'),
_('Only the admin user is allowed to ' _('Only the admin user is allowed to '
'execute inactive cron jobs manually')) 'execute inactive cron jobs manually'))
try: try:
# Try to grab an exclusive lock on the job row # Try to grab an exclusive lock on the job row
# until the end of the transaction # until the end of the transaction
@ -67,9 +68,9 @@ class irCron(orm.Model):
model = self.pool.get(job['model']) model = self.pool.get(job['model'])
method = getattr(model, job['function']) method = getattr(model, job['function'])
args = safe_eval('tuple(%s)' % (job['args'] or '')) args = safe_eval('tuple(%s)' % (job['args'] or ''))
method(cr, job['user_id'], *args)
except psycopg2.OperationalError, e:
method(cr, job['user_id'], *args)
except psycopg2.OperationalError as e:
# User friendly error if the lock could not be claimed # User friendly error if the lock could not be claimed
if e.pgcode == '55P03': if e.pgcode == '55P03':
raise orm.except_orm( raise orm.except_orm(

1
dbfilter_from_header/__init__.py

@ -23,6 +23,7 @@ from openerp.addons.web.controllers import main as web_main
db_list_org = web_main.db_list db_list_org = web_main.db_list
def db_list(req, force=False): def db_list(req, force=False):
db_filter = req.httprequest.environ.get('HTTP_X_OPENERP_DBFILTER', '.*') db_filter = req.httprequest.environ.get('HTTP_X_OPENERP_DBFILTER', '.*')
dbs = db_list_org(req, force=force) dbs = db_list_org(req, force=force)

18
dbfilter_from_header/__openerp__.py

@ -19,14 +19,14 @@
# #
############################################################################## ##############################################################################
{ {
"name" : "dbfilter_from_header",
"version" : "1.0",
"author" : "Therp BV",
"name": "dbfilter_from_header",
"version": "1.0",
"author": "Therp BV",
"complexity": "normal", "complexity": "normal",
"description": """ "description": """
This addon lets you pass a dbfilter as a HTTP header. This addon lets you pass a dbfilter as a HTTP header.
This is interesting for setups where database names can't be mapped to
This is interesting for setups where database names can't be mapped to
proxied host names. proxied host names.
In nginx, use In nginx, use
@ -34,11 +34,11 @@
The addon has to be loaded as server-wide module. The addon has to be loaded as server-wide module.
""", """,
"category" : "Tools",
"depends" : [
"category": "Tools",
"depends": [
'web', 'web',
], ],
"data" : [
"data": [
], ],
"js": [ "js": [
], ],
@ -46,7 +46,7 @@
], ],
"auto_install": False, "auto_install": False,
"installable": True, "installable": True,
"external_dependencies" : {
'python' : [],
"external_dependencies": {
'python': [],
}, },
} }

4
disable_openerp_online/data/ir_ui_menu.xml

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<openerp> <openerp>
<data> <data>
<delete model="ir.ui.menu" id="base.module_mi" />
<delete model="ir.ui.menu" id="base.menu_module_updates" />
<delete model="ir.ui.menu" search="[('parent_id', '=', ref('base.menu_management')), ('name', '=', 'Apps')]" />
<delete model="ir.ui.menu" search="[('parent_id', '=', ref('base.menu_management')), ('name', '=', 'Updates')]" />
</data> </data>
</openerp> </openerp>

31
email_template_template/__openerp__.py

@ -26,11 +26,12 @@
'complexity': "expert", 'complexity': "expert",
"description": """If an organisation's email layout is a bit more "description": """If an organisation's email layout is a bit more
complicated, changes can be tedious when having to do that across several email complicated, changes can be tedious when having to do that across several email
templates. So this addon allows to define templates for mails that is referenced
by other mail templates.
This way we can put the layout parts into the template template and only content
in the other templates. Changing the layout is then only a matter of changing
the template template.
templates. So this addon allows to define templates for mails that is
referenced by other mail templates.
This way we can put the layout parts into the template template and only
content in the other templates. Changing the layout is then only a matter of
changing the template template.
----- -----
Usage Usage
@ -49,7 +50,8 @@ For example, create a template template
Example Corp logo Example Corp logo
Example Corp header Example Corp header
${object.body_text} <- this gets evaluated to the body_text of a template using this template template
${object.body_text} <- this gets evaluated to the body_text of a
template using this template template
Example Corp Example Corp
Example street 42 Example street 42
Example city Example city
@ -60,8 +62,9 @@ Then in your template you write
:: ::
Dear ${object.partner_id.name}, Dear ${object.partner_id.name},
Your order has been booked on date ${object.date} for a total amount of ${object.sum}.
Your order has been booked on date ${object.date}
for a total amount of ${object.sum}.
And it will be evaluated to And it will be evaluated to
@ -71,19 +74,25 @@ And it will be evaluated to
Example Corp header Example Corp header
Dear Jane Doe, Dear Jane Doe,
Your order has been booked on date 04/17/2013 for a total amount of 42.
Your order has been booked on date 04/17/2013
for a total amount of 42.
Example Corp Example Corp
Example street 42 Example street 42
Example city Example city
Example Corp footer Example Corp footer
Given the way evaluation works internally (body_text of the template template is evaluated two times, first with the instance of email.template of your own template, then with the object your template refers to), you can do some trickery if you know that a template template is always used with the same kind of model (that is, models that have the same field name):
Given the way evaluation works internally (body_text of the template template
is evaluated two times, first with the instance of email.template of your own
template, then with the object your template refers to), you can do some
trickery if you know that a template template is always used with the same kind
of model (that is, models that have the same field name):
In your template template: In your template template:
:: ::
Dear ${'${object.name}'}, <-- gets evaluated to "${object.name}" in the first step, then to the content of object.name
Dear ${'${object.name}'}, <-- gets evaluated to "${object.name}" in the
first step, then to the content of object.name
${object.body_html} ${object.body_html}
Best, Best,
Example Corp Example Corp

6
email_template_template/model/email_template.py

@ -28,7 +28,7 @@ class email_template(Model):
def _get_is_template_template(self, cr, uid, ids, fields_name, arg, def _get_is_template_template(self, cr, uid, ids, fields_name, arg,
context=None): context=None):
cr.execute('''select
cr.execute('''select
id, (select count(*) > 0 from email_template e id, (select count(*) > 0 from email_template e
where email_template_id=email_template.id) where email_template_id=email_template.id)
from email_template from email_template
@ -40,12 +40,12 @@ class email_template(Model):
'is_template_template': fields.function( 'is_template_template': fields.function(
_get_is_template_template, type='boolean', _get_is_template_template, type='boolean',
string='Is a template template'), string='Is a template template'),
}
}
def get_email_template(self, cr, uid, template_id=False, record_id=None, def get_email_template(self, cr, uid, template_id=False, record_id=None,
context=None): context=None):
this = super(email_template, self).get_email_template( this = super(email_template, self).get_email_template(
cr, uid, template_id, record_id, context)
cr, uid, template_id, record_id, context)
if this.email_template_id and not this.is_template_template: if this.email_template_id and not this.is_template_template:
for field in ['body_html']: for field in ['body_html']:

2
fetchmail_attach_from_folder/__openerp__.py

@ -38,7 +38,7 @@ client integration.
'view/fetchmail_server.xml', 'view/fetchmail_server.xml',
'wizard/attach_mail_manually.xml', 'wizard/attach_mail_manually.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
],
],
'js': [], 'js': [],
'installable': True, 'installable': True,
'active': False, 'active': False,

8
fetchmail_attach_from_folder/match_algorithm/base.py

@ -20,16 +20,16 @@
# #
############################################################################## ##############################################################################
class base(object): class base(object):
name = None name = None
'''Name shown to the user''' '''Name shown to the user'''
required_fields = [] required_fields = []
'''Fields on fetchmail_server folder that are required for this algorithm'''
'''Fields on fetchmail_server folder required for this algorithm'''
readonly_fields = [] readonly_fields = []
'''Fields on fetchmail_server folder that are readonly for this algorithm'''
'''Fields on fetchmail_server folder readonly for this algorithm'''
def search_matches(self, cr, uid, conf, mail_message, mail_message_org): def search_matches(self, cr, uid, conf, mail_message, mail_message_org):
'''Returns ids found for model with mail_message''' '''Returns ids found for model with mail_message'''
@ -40,4 +40,4 @@ class base(object):
mail_message, mail_message_org, msgid, context=None): mail_message, mail_message_org, msgid, context=None):
'''Do whatever it takes to handle a match''' '''Do whatever it takes to handle a match'''
return folder.server_id.attach_mail(connection, object_id, folder, return folder.server_id.attach_mail(connection, object_id, folder,
mail_message, msgid)
mail_message, msgid)

15
fetchmail_attach_from_folder/match_algorithm/email_domain.py

@ -22,6 +22,7 @@
from email_exact import email_exact from email_exact import email_exact
class email_domain(email_exact): class email_domain(email_exact):
'''Search objects by domain name of email address. '''Search objects by domain name of email address.
Beware of match_first here, this is most likely to get it wrong (gmail)''' Beware of match_first here, this is most likely to get it wrong (gmail)'''
@ -29,16 +30,16 @@ class email_domain(email_exact):
def search_matches(self, cr, uid, conf, mail_message, mail_message_org): def search_matches(self, cr, uid, conf, mail_message, mail_message_org):
ids = super(email_domain, self).search_matches( ids = super(email_domain, self).search_matches(
cr, uid, conf, mail_message, mail_message_org)
cr, uid, conf, mail_message, mail_message_org)
if not ids: if not ids:
domains = [] domains = []
for addr in self._get_mailaddresses(conf, mail_message): for addr in self._get_mailaddresses(conf, mail_message):
domains.append(addr.split('@')[-1]) domains.append(addr.split('@')[-1])
ids = conf.pool.get(conf.model_id.model).search( ids = conf.pool.get(conf.model_id.model).search(
cr, uid,
self._get_mailaddress_search_domain(
conf, mail_message,
operator='like',
values=['%@'+domain for domain in set(domains)]),
order=conf.model_order)
cr, uid,
self._get_mailaddress_search_domain(
conf, mail_message,
operator='like',
values=['%@' + domain for domain in set(domains)]),
order=conf.model_order)
return ids return ids

9
fetchmail_attach_from_folder/match_algorithm/email_exact.py

@ -24,6 +24,7 @@ from base import base
from openerp.tools.safe_eval import safe_eval from openerp.tools.safe_eval import safe_eval
from openerp.tools.mail import email_split from openerp.tools.mail import email_split
class email_exact(base): class email_exact(base):
'''Search for exactly the mailadress as noted in the email''' '''Search for exactly the mailadress as noted in the email'''
@ -36,17 +37,17 @@ class email_exact(base):
for field in fields: for field in fields:
if field in mail_message: if field in mail_message:
mailaddresses += email_split(mail_message[field]) mailaddresses += email_split(mail_message[field])
return [ addr.lower() for addr in mailaddresses ]
return [addr.lower() for addr in mailaddresses]
def _get_mailaddress_search_domain( def _get_mailaddress_search_domain(
self, conf, mail_message, operator='=', values=None): self, conf, mail_message, operator='=', values=None):
mailaddresses = values or self._get_mailaddresses( mailaddresses = values or self._get_mailaddresses(
conf, mail_message)
conf, mail_message)
if not mailaddresses: if not mailaddresses:
return [(0, '=', 1)] return [(0, '=', 1)]
search_domain = ((['|'] * (len(mailaddresses) - 1)) + [ search_domain = ((['|'] * (len(mailaddresses) - 1)) + [
(conf.model_field, operator, addr) for addr in mailaddresses] +
safe_eval(conf.domain or '[]'))
(conf.model_field, operator, addr) for addr in mailaddresses] +
safe_eval(conf.domain or '[]'))
return search_domain return search_domain
def search_matches(self, cr, uid, conf, mail_message, mail_message_org): def search_matches(self, cr, uid, conf, mail_message, mail_message_org):

23
fetchmail_attach_from_folder/match_algorithm/openerp_standard.py

@ -21,15 +21,21 @@
############################################################################## ##############################################################################
from base import base from base import base
from openerp.tools.safe_eval import safe_eval
class openerp_standard(base): class openerp_standard(base):
'''No search at all. Use OpenERP's standard mechanism to attach mails to '''No search at all. Use OpenERP's standard mechanism to attach mails to
mail.thread objects. Note that this algorithm always matches.''' mail.thread objects. Note that this algorithm always matches.'''
name = 'OpenERP standard' name = 'OpenERP standard'
readonly_fields = ['model_field', 'mail_field', 'match_first', 'domain',
'model_order', 'flag_nonmatching']
readonly_fields = [
'model_field',
'mail_field',
'match_first',
'domain',
'model_order',
'flag_nonmatching',
]
def search_matches(self, cr, uid, conf, mail_message, mail_message_org): def search_matches(self, cr, uid, conf, mail_message, mail_message_org):
'''Always match. Duplicates will be fished out by message_id''' '''Always match. Duplicates will be fished out by message_id'''
@ -39,11 +45,12 @@ class openerp_standard(base):
self, cr, uid, connection, object_id, folder, self, cr, uid, connection, object_id, folder,
mail_message, mail_message_org, msgid, context): mail_message, mail_message_org, msgid, context):
result = folder.pool.get('mail.thread').message_process( result = folder.pool.get('mail.thread').message_process(
cr, uid,
folder.model_id.model, mail_message_org,
save_original=folder.server_id.original,
strip_attachments=(not folder.server_id.attach),
context=context)
cr, uid,
folder.model_id.model, mail_message_org,
save_original=folder.server_id.original,
strip_attachments=(not folder.server_id.attach),
context=context
)
if folder.delete_matching: if folder.delete_matching:
connection.store(msgid, '+FLAGS', '\\DELETED') connection.store(msgid, '+FLAGS', '\\DELETED')

87
fetchmail_attach_from_folder/model/fetchmail_server.py

@ -23,12 +23,11 @@
import base64 import base64
import simplejson import simplejson
from lxml import etree from lxml import etree
from openerp.osv.orm import Model, except_orm, browse_null
from openerp.osv.orm import Model, except_orm
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.osv import fields from openerp.osv import fields
from openerp.addons.fetchmail.fetchmail import _logger as logger from openerp.addons.fetchmail.fetchmail import _logger as logger
from openerp.tools.misc import UnquoteEvalContext from openerp.tools.misc import UnquoteEvalContext
from openerp.tools.safe_eval import safe_eval
class fetchmail_server(Model): class fetchmail_server(Model):
@ -36,7 +35,7 @@ class fetchmail_server(Model):
_columns = { _columns = {
'folder_ids': fields.one2many( 'folder_ids': fields.one2many(
'fetchmail.server.folder', 'server_id', 'Folders'),
'fetchmail.server.folder', 'server_id', 'Folders'),
} }
_defaults = { _defaults = {
@ -45,7 +44,7 @@ class fetchmail_server(Model):
def __init__(self, pool, cr): def __init__(self, pool, cr):
self._columns['object_id'].required = False self._columns['object_id'].required = False
return super(fetchmail_server, self).__init__(pool, cr)
super(fetchmail_server, self).__init__(pool, cr)
def onchange_server_type( def onchange_server_type(
self, cr, uid, ids, server_type=False, ssl=False, self, cr, uid, ids, server_type=False, ssl=False,
@ -94,21 +93,21 @@ class fetchmail_server(Model):
match_algorithm = folder.get_algorithm() match_algorithm = folder.get_algorithm()
if connection.select(folder.path)[0] != 'OK': if connection.select(folder.path)[0] != 'OK':
logger.error(
'Could not open mailbox %s on %s' % (
folder.path, this.server))
logger.error('Could not open mailbox %s on %s',
folder.path,
this.server)
connection.select() connection.select()
continue continue
result, msgids = this.get_msgids(connection) result, msgids = this.get_msgids(connection)
if result != 'OK': if result != 'OK':
logger.error(
'Could not search mailbox %s on %s' % (
folder.path, this.server))
logger.error('Could not search mailbox %s on %s',
folder.path,
this.server)
continue continue
for msgid in msgids[0].split(): for msgid in msgids[0].split():
matched_object_ids += this.apply_matching( matched_object_ids += this.apply_matching(
connection, folder, msgid, match_algorithm)
connection, folder, msgid, match_algorithm)
logger.info('finished checking for emails in %s server %s', logger.info('finished checking for emails in %s server %s',
folder.path, this.name) folder.path, this.name)
@ -129,17 +128,19 @@ class fetchmail_server(Model):
result, msgdata = connection.fetch(msgid, '(RFC822)') result, msgdata = connection.fetch(msgid, '(RFC822)')
if result != 'OK': if result != 'OK':
logger.error(
'Could not fetch %s in %s on %s' % (
msgid, folder.path, this.server))
logger.error('Could not fetch %s in %s on %s',
msgid,
folder.path,
this.server)
continue continue
mail_message = self.pool.get('mail.thread').message_parse( mail_message = self.pool.get('mail.thread').message_parse(
cr, uid, msgdata[0][1], save_original=this.original,
context=context)
cr, uid, msgdata[0][1], save_original=this.original,
context=context)
if self.pool.get('mail.message').search(cr, uid, [
('message_id', '=', mail_message['message_id'])]):
if self.pool.get('mail.message').search(
cr, uid, [
('message_id', '=', mail_message['message_id'])]):
continue continue
found_ids = match_algorithm.search_matches( found_ids = match_algorithm.search_matches(
@ -156,7 +157,7 @@ class fetchmail_server(Model):
msgdata[0][1], msgid, context) msgdata[0][1], msgid, context)
cr.execute('release savepoint apply_matching') cr.execute('release savepoint apply_matching')
matched_object_ids += found_ids[:1] matched_object_ids += found_ids[:1]
except Exception, e:
except Exception:
cr.execute('rollback to savepoint apply_matching') cr.execute('rollback to savepoint apply_matching')
logger.exception( logger.exception(
"Failed to fetch mail %s from %s", "Failed to fetch mail %s from %s",
@ -181,42 +182,42 @@ class fetchmail_server(Model):
partner_id = self.pool.get( partner_id = self.pool.get(
folder.model_id.model).browse( folder.model_id.model).browse(
cr, uid, object_id, context cr, uid, object_id, context
).partner_id.id
).partner_id.id
attachments=[]
attachments = []
if this.attach and mail_message.get('attachments'): if this.attach and mail_message.get('attachments'):
for attachment in mail_message['attachments']: for attachment in mail_message['attachments']:
fname, fcontent = attachment fname, fcontent = attachment
if isinstance(fcontent, unicode): if isinstance(fcontent, unicode):
fcontent = fcontent.encode('utf-8') fcontent = fcontent.encode('utf-8')
data_attach = { data_attach = {
'name': fname,
'datas': base64.b64encode(str(fcontent)),
'datas_fname': fname,
'description': _('Mail attachment'),
'res_model': folder.model_id.model,
'res_id': object_id,
}
'name': fname,
'datas': base64.b64encode(str(fcontent)),
'datas_fname': fname,
'description': _('Mail attachment'),
'res_model': folder.model_id.model,
'res_id': object_id,
}
attachments.append( attachments.append(
self.pool.get('ir.attachment').create( self.pool.get('ir.attachment').create(
cr, uid, data_attach, context=context)) cr, uid, data_attach, context=context))
mail_message_ids.append( mail_message_ids.append(
self.pool.get('mail.message').create(
cr, uid,
{
'author_id': partner_id,
'model': folder.model_id.model,
'res_id': object_id,
'type': 'email',
'body': mail_message.get('body'),
'subject': mail_message.get('subject'),
'email_from': mail_message.get('from'),
'date': mail_message.get('date'),
'message_id': mail_message.get('message_id'),
'attachment_ids': [(6, 0, attachments)],
},
context))
self.pool.get('mail.message').create(
cr, uid,
{
'author_id': partner_id,
'model': folder.model_id.model,
'res_id': object_id,
'type': 'email',
'body': mail_message.get('body'),
'subject': mail_message.get('subject'),
'email_from': mail_message.get('from'),
'date': mail_message.get('date'),
'message_id': mail_message.get('message_id'),
'attachment_ids': [(6, 0, attachments)],
},
context))
if folder.delete_matching: if folder.delete_matching:
connection.store(msgid, '+FLAGS', '\\DELETED') connection.store(msgid, '+FLAGS', '\\DELETED')

99
fetchmail_attach_from_folder/model/fetchmail_server_folder.py

@ -31,11 +31,13 @@ class fetchmail_server_folder(Model):
def _get_match_algorithms(self): def _get_match_algorithms(self):
def get_all_subclasses(cls): def get_all_subclasses(cls):
return cls.__subclasses__() + [subsub
for sub in cls.__subclasses__()
for subsub in get_all_subclasses(sub)]
return dict([(cls.__name__, cls) for cls in get_all_subclasses(
match_algorithm.base.base)])
return (cls.__subclasses__() +
[subsub
for sub in cls.__subclasses__()
for subsub in get_all_subclasses(sub)])
return dict([(cls.__name__, cls)
for cls in get_all_subclasses(
match_algorithm.base.base)])
def _get_match_algorithms_sel(self, cr, uid, context=None): def _get_match_algorithms_sel(self, cr, uid, context=None):
algorithms = [] algorithms = []
@ -47,55 +49,66 @@ class fetchmail_server_folder(Model):
_columns = { _columns = {
'sequence': fields.integer('Sequence'), 'sequence': fields.integer('Sequence'),
'path': fields.char( 'path': fields.char(
'Path', size=256, help='The path to your mail '
"folder. Typically would be something like 'INBOX.myfolder'",
required=True),
'Path', size=256, help='The path to your mail '
"folder. Typically would be something like 'INBOX.myfolder'",
required=True
),
'model_id': fields.many2one( 'model_id': fields.many2one(
'ir.model', 'Model', required=True,
help='The model to attach emails to'),
'ir.model', 'Model', required=True,
help='The model to attach emails to'
),
'model_field': fields.char( 'model_field': fields.char(
'Field (model)', size=128,
help='The field in your model that contains the field to match '
'against.\n'
'Examples:\n'
"'email' if your model is res.partner, or "
"'partner_id.email' if you're matching sale orders"),
'Field (model)', size=128,
help='The field in your model that contains the field to match '
'against.\n'
'Examples:\n'
"'email' if your model is res.partner, or "
"'partner_id.email' if you're matching sale orders"
),
'model_order': fields.char( 'model_order': fields.char(
'Order (model)', size=128,
help='Fields to order by, this mostly useful in conjunction '
"with 'Use 1st match'"),
'Order (model)', size=128,
help='Fields to order by, this mostly useful in conjunction '
"with 'Use 1st match'"
),
'match_algorithm': fields.selection( 'match_algorithm': fields.selection(
_get_match_algorithms_sel,
'Match algorithm', required=True, translate=True,
help='The algorithm used to determine which object an email '
'matches.'),
_get_match_algorithms_sel,
'Match algorithm', required=True, translate=True,
help='The algorithm used to determine which object an email '
'matches.'
),
'mail_field': fields.char( 'mail_field': fields.char(
'Field (email)', size=128,
help='The field in the email used for matching. Typically '
"this is 'to' or 'from'"),
'Field (email)', size=128,
help='The field in the email used for matching. Typically '
"this is 'to' or 'from'"
),
'server_id': fields.many2one('fetchmail.server', 'Server'), 'server_id': fields.many2one('fetchmail.server', 'Server'),
'delete_matching': fields.boolean( 'delete_matching': fields.boolean(
'Delete matches',
help='Delete matched emails from server'),
'Delete matches',
help='Delete matched emails from server'
),
'flag_nonmatching': fields.boolean( 'flag_nonmatching': fields.boolean(
'Flag nonmatching',
help="Flag emails in the server that don't match any object "
'in OpenERP'),
'Flag nonmatching',
help="Flag emails in the server that don't match any object "
'in OpenERP'
),
'match_first': fields.boolean( 'match_first': fields.boolean(
'Use 1st match',
help='If there are multiple matches, use the first one. If '
'not checked, multiple matches count as no match at all'),
'Use 1st match',
help='If there are multiple matches, use the first one. If '
'not checked, multiple matches count as no match at all'
),
'domain': fields.char( 'domain': fields.char(
'Domain', size=128, help='Fill in a search '
'filter to narrow down objects to match'),
'Domain', size=128, help='Fill in a search '
'filter to narrow down objects to match'
),
'msg_state': fields.selection( 'msg_state': fields.selection(
[
('sent', 'Sent'),
('received', 'Received'),
],
'Message state',
help='The state messages fetched from this folder should be '
'assigned in OpenERP'),
[
('sent', 'Sent'),
('received', 'Received'),
],
'Message state',
help='The state messages fetched from this folder should be '
'assigned in OpenERP'
),
} }
_defaults = { _defaults = {

97
fetchmail_attach_from_folder/wizard/attach_mail_manually.py

@ -22,54 +22,60 @@
from openerp.osv import fields from openerp.osv import fields
from openerp.osv.orm import TransientModel from openerp.osv.orm import TransientModel
import logging
logger = logging.getLogger(__name__)
class attach_mail_manually(TransientModel): class attach_mail_manually(TransientModel):
_name = 'fetchmail.attach.mail.manually' _name = 'fetchmail.attach.mail.manually'
_columns = { _columns = {
'folder_id': fields.many2one('fetchmail.server.folder', 'Folder',
readonly=True),
'mail_ids': fields.one2many(
'fetchmail.attach.mail.manually.mail', 'wizard_id', 'Emails'),
}
'folder_id': fields.many2one('fetchmail.server.folder', 'Folder',
readonly=True),
'mail_ids': fields.one2many(
'fetchmail.attach.mail.manually.mail', 'wizard_id', 'Emails'),
}
def default_get(self, cr, uid, fields_list, context=None): def default_get(self, cr, uid, fields_list, context=None):
if context is None: if context is None:
context = {} context = {}
defaults = super(attach_mail_manually, self).default_get(cr, uid,
fields_list, context)
defaults = super(attach_mail_manually, self).default_get(
cr, uid, fields_list, context
)
for folder in self.pool.get('fetchmail.server.folder').browse(cr, uid,
for folder in self.pool.get('fetchmail.server.folder').browse(
cr, uid,
[context.get('default_folder_id')], context): [context.get('default_folder_id')], context):
defaults['mail_ids']=[]
defaults['mail_ids'] = []
connection = folder.server_id.connect() connection = folder.server_id.connect()
connection.select(folder.path) connection.select(folder.path)
result, msgids = connection.search(None,
'FLAGGED' if folder.flag_nonmatching else 'UNDELETED')
result, msgids = connection.search(
None,
'FLAGGED' if folder.flag_nonmatching else 'UNDELETED')
if result != 'OK': if result != 'OK':
logger.error('Could not search mailbox %s on %s' % ( logger.error('Could not search mailbox %s on %s' % (
folder.path, this.server))
folder.path, folder.server_id.name))
continue continue
attach_mail_manually_mail._columns['object_id'].selection=[
(folder.model_id.model, folder.model_id.name)]
attach_mail_manually_mail._columns['object_id'].selection = [
(folder.model_id.model, folder.model_id.name)]
for msgid in msgids[0].split(): for msgid in msgids[0].split():
result, msgdata = connection.fetch(msgid, '(RFC822)') result, msgdata = connection.fetch(msgid, '(RFC822)')
if result != 'OK': if result != 'OK':
logger.error('Could not fetch %s in %s on %s' % ( logger.error('Could not fetch %s in %s on %s' % (
msgid, folder.path, this.server))
msgid, folder.path, folder.server_id.name))
continue continue
mail_message = self.pool.get('mail.thread').message_parse( mail_message = self.pool.get('mail.thread').message_parse(
cr, uid, msgdata[0][1],
save_original=folder.server_id.original,
context=context)
cr, uid, msgdata[0][1],
save_original=folder.server_id.original,
context=context
)
defaults['mail_ids'].append((0, 0, { defaults['mail_ids'].append((0, 0, {
'msgid': msgid, 'msgid': msgid,
'subject': mail_message.get('subject', ''), 'subject': mail_message.get('subject', ''),
'date': mail_message.get('date', ''), 'date': mail_message.get('date', ''),
'object_id': folder.model_id.model+',False'
}))
'object_id': folder.model_id.model + ',False'
}))
connection.close() connection.close()
return defaults return defaults
@ -79,36 +85,45 @@ class attach_mail_manually(TransientModel):
for mail in this.mail_ids: for mail in this.mail_ids:
connection = this.folder_id.server_id.connect() connection = this.folder_id.server_id.connect()
connection.select(this.folder_id.path) connection.select(this.folder_id.path)
result, msgdata = connection.fetch(mail.msgid, '(RFC822)')
if result != 'OK':
logger.error('Could not fetch %s in %s on %s' % (
msgid, folder.path, this.server))
continue
result, msgdata = connection.fetch(mail.msgid, '(RFC822)')
if result != 'OK':
logger.error('Could not fetch %s in %s on %s' % (
mail.msgid, this.folder_id.path, this.server))
continue
mail_message = self.pool.get('mail.thread').message_parse( mail_message = self.pool.get('mail.thread').message_parse(
cr, uid, msgdata[0][1], cr, uid, msgdata[0][1],
save_original=this.folder_id.server_id.original, save_original=this.folder_id.server_id.original,
context=context) context=context)
this.folder_id.server_id.attach_mail(connection,
mail.object_id.id, this.folder_id, mail_message,
mail.msgid)
this.folder_id.server_id.attach_mail(
connection,
mail.object_id.id, this.folder_id, mail_message,
mail.msgid
)
connection.close() connection.close()
return {'type': 'ir.actions.act_window_close'} return {'type': 'ir.actions.act_window_close'}
class attach_mail_manually_mail(TransientModel): class attach_mail_manually_mail(TransientModel):
_name = 'fetchmail.attach.mail.manually.mail' _name = 'fetchmail.attach.mail.manually.mail'
_columns = { _columns = {
'wizard_id': fields.many2one('fetchmail.attach.mail.manually',
readonly=True),
'msgid': fields.char('Message id', size=16, readonly=True),
'subject': fields.char('Subject', size=128, readonly=True),
'date': fields.datetime('Date', readonly=True),
'object_id': fields.reference('Object',
selection=lambda self, cr, uid, context:
[(m.model, m.name) for m in
self.pool.get('ir.model').browse(cr, uid,
self.pool.get('ir.model').search(cr, uid, []),
context)], size=128),
}
'wizard_id': fields.many2one('fetchmail.attach.mail.manually',
readonly=True),
'msgid': fields.char('Message id', size=16, readonly=True),
'subject': fields.char('Subject', size=128, readonly=True),
'date': fields.datetime('Date', readonly=True),
'object_id': fields.reference(
'Object',
selection=lambda self, cr, uid, context: [
(m.model, m.name)
for m in self.pool.get('ir.model').browse(
cr, uid,
self.pool.get('ir.model').search(cr, uid, []),
context
)
],
size=128,
),
}

29
import_odbc/__openerp__.py

@ -29,11 +29,21 @@ Import data directly from other databases.
Installed in the Administration module, menu Configuration -> Import from SQL. Installed in the Administration module, menu Configuration -> Import from SQL.
Features: Features:
* Fetched data from the databases are used to build lines equivalent to regular import files. These are imported using the standard "import_data()" ORM method, benefiting from all its features, including xml_ids.
* Each table import is defined by an SQL statement, used to build the equivalent for an import file. Each column's name should match the column names you would use in an import file. The first column must provide an unique identifier for the record, and will be used to build its xml_id.
* SQL columns named "none" are ignored. This can be used for the first column of the SQL, so that it's used to build the XML Id but it's not imported to any OpenERP field.
* The last sync date is the last successfull execution can be used in the SQL using "%(sync)s", or ":sync" in the case of Oracle.
* When errors are found, only the record with the error fails import. The other correct records are commited. However, the "last sync date" will only be automaticaly updated when no errors are found.
* Fetched data from the databases are used to build lines equivalent to
regular import files. These are imported using the standard "import_data()"
ORM method, benefiting from all its features, including xml_ids.
* Each table import is defined by an SQL statement, used to build the
equivalent for an import file. Each column's name should match the column
names you would use in an import file. The first column must provide an
unique identifier for the record, and will be used to build its xml_id.
* SQL columns named "none" are ignored. This can be used for the first column
of the SQL, so that it's used to build the XML Id but it's not imported to
any OpenERP field.
* The last sync date is the last successfull execution can be used in the SQL
using "%(sync)s", or ":sync" in the case of Oracle.
* When errors are found, only the record with the error fails import. The
other correct records are commited. However, the "last sync date" will only
be automaticaly updated when no errors are found.
* The import execution can be scheduled to run automatically. * The import execution can be scheduled to run automatically.
Examples: Examples:
@ -54,9 +64,12 @@ Examples:
WHERE DATE_CHANGED >= %(sync)s WHERE DATE_CHANGED >= %(sync)s
Improvements ideas waiting for a contributor: Improvements ideas waiting for a contributor:
* Allow to import many2one fields (currently not supported). Done by adding a second SQL sentence to get child record list?
* Allow "import sets" that can be executed at different time intervals using different scheduler jobs.
* Allow to inactivate/delete OpenERP records when not present in an SQL result set.
* Allow to import many2one fields (currently not supported). Done by adding a
second SQL sentence to get child record list?
* Allow "import sets" that can be executed at different time intervals using
different scheduler jobs.
* Allow to inactivate/delete OpenERP records when not present in an SQL
result set.
Contributors Contributors
============ ============

159
import_odbc/import_odbc.py

@ -35,25 +35,43 @@ class import_odbc_dbtable(orm.Model):
_columns = { _columns = {
'name': fields.char('Datasource name', required=True, size=64), 'name': fields.char('Datasource name', required=True, size=64),
'enabled': fields.boolean('Execution enabled'), 'enabled': fields.boolean('Execution enabled'),
'dbsource_id': fields.many2one('base.external.dbsource', 'Database source', required=True),
'sql_source': fields.text('SQL', required=True, help='Column names must be valid "import_data" columns.'),
'dbsource_id': fields.many2one('base.external.dbsource',
'Database source',
required=True),
'sql_source': fields.text('SQL',
required=True,
help='Column names must be valid '
'"import_data" columns.'),
'model_target': fields.many2one('ir.model', 'Target object'), 'model_target': fields.many2one('ir.model', 'Target object'),
'noupdate': fields.boolean('No updates', help="Only create new records; disable updates to existing records."),
'exec_order': fields.integer('Execution order', help="Defines the order to perform the import"),
'last_sync': fields.datetime('Last sync date',
help="Datetime for the last succesfull sync."
"\nLater changes on the source may not be replicated on the destination"),
'start_run': fields.datetime('Time started', readonly=True),
'last_run': fields.datetime('Time ended', readonly=True),
'last_record_count': fields.integer('Last record count', readonly=True),
'last_error_count': fields.integer('Last error count', readonly=True),
'last_warn_count': fields.integer('Last warning count', readonly=True),
'noupdate': fields.boolean('No updates',
help="Only create new records; disable "
"updates to existing records."),
'exec_order': fields.integer('Execution order',
help="Defines the order to perform "
"the import"),
'last_sync': fields.datetime(
'Last sync date',
help="Datetime for the last succesfull sync.\n"
"Later changes on the source may not be replicated "
"on the destination"),
'start_run': fields.datetime('Time started',
readonly=True),
'last_run': fields.datetime('Time ended',
readonly=True),
'last_record_count': fields.integer('Last record count',
readonly=True),
'last_error_count': fields.integer('Last error count',
readonly=True),
'last_warn_count': fields.integer('Last warning count',
readonly=True),
'last_log': fields.text('Last run log', readonly=True), 'last_log': fields.text('Last run log', readonly=True),
'ignore_rel_errors': fields.boolean('Ignore relationship errors',
help="On error try to reimport rows ignoring relationships."),
'raise_import_errors': fields.boolean('Raise import errors',
help="Import errors not handled, intended for debugging purposes."
"\nAlso forces debug messages to be written to the server log."),
'ignore_rel_errors': fields.boolean(
'Ignore relationship errors',
help="On error try to reimport rows ignoring relationships."),
'raise_import_errors': fields.boolean(
'Raise import errors',
help="Import errors not handled, intended for debugging purposes."
"\nAlso forces debug messages to be written to the server log."),
} }
_defaults = { _defaults = {
'enabled': True, 'enabled': True,
@ -64,7 +82,7 @@ class import_odbc_dbtable(orm.Model):
"""Import data and returns error msg or empty string""" """Import data and returns error msg or empty string"""
def find_m2o(field_list): def find_m2o(field_list):
""""Find index of first column with a one2many field"""
"""Find index of first column with a one2many field"""
for i, x in enumerate(field_list): for i, x in enumerate(field_list):
if len(x) > 3 and x[-3:] == ':id' or x[-3:] == '/id': if len(x) > 3 and x[-3:] == ':id' or x[-3:] == '/id':
return i return i
@ -72,42 +90,57 @@ class import_odbc_dbtable(orm.Model):
def append_to_log(log, level, obj_id='', msg='', rel_id=''): def append_to_log(log, level, obj_id='', msg='', rel_id=''):
if '_id_' in obj_id: if '_id_' in obj_id:
obj_id = '.'.join(obj_id.split('_')[:-2]) + ': ' + obj_id.split('_')[-1]
obj_id = ('.'.join(obj_id.split('_')[:-2])
+ ': '
+ obj_id.split('_')[-1])
if ': .' in msg and not rel_id: if ': .' in msg and not rel_id:
rel_id = msg[msg.find(': .')+3:]
rel_id = msg[msg.find(': .') + 3:]
if '_id_' in rel_id: if '_id_' in rel_id:
rel_id = '.'.join(rel_id.split('_')[:-2]) + ': ' + rel_id.split('_')[-1]
rel_id = ('.'.join(rel_id.split('_')[:-2])
+ ': '
+ rel_id.split('_')[-1])
msg = msg[:msg.find(': .')] msg = msg[:msg.find(': .')]
log['last_log'].append('%s|%s\t|%s\t|%s' % (level.ljust(5), obj_id, rel_id, msg))
log['last_log'].append('%s|%s\t|%s\t|%s' % (level.ljust(5),
obj_id,
rel_id,
msg))
_logger.debug(data) _logger.debug(data)
cols = list(flds) # copy to avoid side effects cols = list(flds) # copy to avoid side effects
errmsg = str() errmsg = str()
if table_obj.raise_import_errors: if table_obj.raise_import_errors:
model_obj.import_data(cr, uid, cols, [data], noupdate=table_obj.noupdate)
model_obj.import_data(cr, uid, cols, [data],
noupdate=table_obj.noupdate)
else: else:
try: try:
model_obj.import_data(cr, uid, cols, [data], noupdate=table_obj.noupdate)
model_obj.import_data(cr, uid, cols, [data],
noupdate=table_obj.noupdate)
except: except:
errmsg = str(sys.exc_info()[1]) errmsg = str(sys.exc_info()[1])
if errmsg and not table_obj.ignore_rel_errors: if errmsg and not table_obj.ignore_rel_errors:
#Fail
# Fail
append_to_log(log, 'ERROR', data, errmsg) append_to_log(log, 'ERROR', data, errmsg)
log['last_error_count'] += 1 log['last_error_count'] += 1
return False return False
if errmsg and table_obj.ignore_rel_errors: if errmsg and table_obj.ignore_rel_errors:
#Warn and retry ignoring many2one fields...
# Warn and retry ignoring many2one fields...
append_to_log(log, 'WARN', data, errmsg) append_to_log(log, 'WARN', data, errmsg)
log['last_warn_count'] += 1 log['last_warn_count'] += 1
#Try ignoring each many2one (tip: in the SQL sentence select more problematic FKs first)
# Try ignoring each many2one (tip: in the SQL sentence select more
# problematic FKs first)
i = find_m2o(cols) i = find_m2o(cols)
if i >= 0: if i >= 0:
#Try again without the [i] column
# Try again without the [i] column
del cols[i] del cols[i]
del data[i] del data[i]
self._import_data(cr, uid, cols, data, model_obj, table_obj, log)
self._import_data(cr, uid, cols,
data,
model_obj,
table_obj,
log)
else: else:
#Fail
append_to_log(log, 'ERROR', data, 'Removed all m2o keys and still fails.')
# Fail
append_to_log(log, 'ERROR', data,
'Removed all m2o keys and still fails.')
log['last_error_count'] += 1 log['last_error_count'] += 1
return False return False
return True return True
@ -117,18 +150,21 @@ class import_odbc_dbtable(orm.Model):
actions = self.read(cr, uid, ids, ['id', 'exec_order']) actions = self.read(cr, uid, ids, ['id', 'exec_order'])
actions.sort(key=lambda x: (x['exec_order'], x['id'])) actions.sort(key=lambda x: (x['exec_order'], x['id']))
#Consider each dbtable:
# Consider each dbtable:
for action_ref in actions: for action_ref in actions:
obj = self.browse(cr, uid, action_ref['id']) obj = self.browse(cr, uid, action_ref['id'])
if not obj.enabled: if not obj.enabled:
continue # skip continue # skip
_logger.setLevel(obj.raise_import_errors and logging.DEBUG or _loglvl)
_logger.debug('Importing %s...' % obj.name)
_logger.setLevel(obj.raise_import_errors and
logging.DEBUG or
_loglvl)
_logger.debug('Importing %s...', obj.name)
#now() microseconds are stripped to avoid problem with SQL smalldate
#TODO: convert UTC Now to local timezone
#http://stackoverflow.com/questions/4770297/python-convert-utc-datetime-string-to-local-datetime
# now() microseconds are stripped to avoid problem with SQL
# smalldate
# TODO: convert UTC Now to local timezone
# http://stackoverflow.com/questions/4770297
model_name = obj.model_target.model model_name = obj.model_target.model
model_obj = self.pool.get(model_name) model_obj = self.pool.get(model_name)
xml_prefix = model_name.replace('.', '_') + "_id_" xml_prefix = model_name.replace('.', '_') + "_id_"
@ -140,7 +176,7 @@ class import_odbc_dbtable(orm.Model):
'last_log': list()} 'last_log': list()}
self.write(cr, uid, [obj.id], log) self.write(cr, uid, [obj.id], log)
#Prepare SQL sentence; replace "%s" with the last_sync date
# Prepare SQL sentence; replace "%s" with the last_sync date
if obj.last_sync: if obj.last_sync:
sync = datetime.strptime(obj.last_sync, "%Y-%m-%d %H:%M:%S") sync = datetime.strptime(obj.last_sync, "%Y-%m-%d %H:%M:%S")
else: else:
@ -149,30 +185,35 @@ class import_odbc_dbtable(orm.Model):
res = db_model.execute(cr, uid, [obj.dbsource_id.id], res = db_model.execute(cr, uid, [obj.dbsource_id.id],
obj.sql_source, params, metadata=True) obj.sql_source, params, metadata=True)
#Exclude columns titled "None"; add (xml_)"id" column
cidx = [i for i, x in enumerate(res['cols']) if x.upper() != 'NONE']
cols = [x for i, x in enumerate(res['cols']) if x.upper() != 'NONE'] + ['id']
# Exclude columns titled "None"; add (xml_)"id" column
cidx = [i for i, x in enumerate(res['cols'])
if x.upper() != 'NONE']
cols = [x for i, x in enumerate(res['cols'])
if x.upper() != 'NONE'] + ['id']
#Import each row:
# Import each row:
for row in res['rows']: for row in res['rows']:
#Build data row; import only columns present in the "cols" list
# Build data row; import only columns present in the "cols"
# list
data = list() data = list()
for i in cidx: for i in cidx:
#TODO: Handle imported datetimes properly - convert from localtime to UTC!
# TODO: Handle imported datetimes properly - convert from
# localtime to UTC!
v = row[i] v = row[i]
if isinstance(v, str): if isinstance(v, str):
v = v.strip() v = v.strip()
data.append(v) data.append(v)
data.append(xml_prefix + str(row[0]).strip()) data.append(xml_prefix + str(row[0]).strip())
#Import the row; on error, write line to the log
# Import the row; on error, write line to the log
log['last_record_count'] += 1 log['last_record_count'] += 1
self._import_data(cr, uid, cols, data, model_obj, obj, log) self._import_data(cr, uid, cols, data, model_obj, obj, log)
if log['last_record_count'] % 500 == 0: if log['last_record_count'] % 500 == 0:
_logger.info('...%s rows processed...' % (log['last_record_count']))
_logger.info('...%s rows processed...',
(log['last_record_count']))
#Finished importing all rows
#If no errors, write new sync date
# Finished importing all rows
# If no errors, write new sync date
if not (log['last_error_count'] or log['last_warn_count']): if not (log['last_error_count'] or log['last_warn_count']):
log['last_sync'] = log['start_run'] log['last_sync'] = log['start_run']
level = logging.DEBUG level = logging.DEBUG
@ -180,17 +221,21 @@ class import_odbc_dbtable(orm.Model):
level = logging.WARN level = logging.WARN
if log['last_error_count']: if log['last_error_count']:
level = logging.ERROR level = logging.ERROR
_logger.log(level, 'Imported %s , %d rows, %d errors, %d warnings.' % (
model_name, log['last_record_count'], log['last_error_count'],
log['last_warn_count']))
#Write run log, either if the table import is active or inactive
_logger.log(level,
'Imported %s , %d rows, %d errors, %d warnings.',
model_name,
log['last_record_count'],
log['last_error_count'],
log['last_warn_count'])
# Write run log, either if the table import is active or inactive
if log['last_log']: if log['last_log']:
log['last_log'].insert(0, 'LEVEL|== Line == |== Relationship ==|== Message ==')
_line = 'LEVEL|== Line == |== Relationship ==|== Message =='
log['last_log'].insert(0, _line)
log.update({'last_log': '\n'.join(log['last_log'])}) log.update({'last_log': '\n'.join(log['last_log'])})
log.update({'last_run': datetime.now().replace(microsecond=0)}) log.update({'last_run': datetime.now().replace(microsecond=0)})
self.write(cr, uid, [obj.id], log) self.write(cr, uid, [obj.id], log)
#Finished
# Finished
_logger.debug('Import job FINISHED.') _logger.debug('Import job FINISHED.')
return True return True
@ -205,7 +250,7 @@ class import_odbc_dbtable(orm.Model):
'function': 'import_run', 'function': 'import_run',
'doall': False, 'doall': False,
'active': True 'active': True
})
})
return { return {
'name': 'Import ODBC tables', 'name': 'Import ODBC tables',
'view_type': 'form', 'view_type': 'form',
@ -213,6 +258,4 @@ class import_odbc_dbtable(orm.Model):
'res_model': 'ir.cron', 'res_model': 'ir.cron',
'res_id': new_create_id, 'res_id': new_create_id,
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
}
#EOF
}

15
mail_environment/__openerp__.py

@ -26,15 +26,17 @@
'description': """ 'description': """
Extend mail and fetch mail with server environment module. Extend mail and fetch mail with server environment module.
In config files, sections outgoint_mail and incoming_mails are default values for all Outgoing Mail Servers and Fetchmail Servers.
For each server, you can (re)define values with a section named "outgoing_mail.resource_name" where resource_name is the name of your server.
In config files, sections outgoint_mail and incoming_mails are default values
for all Outgoing Mail Servers and Fetchmail Servers.
For each server, you can (re)define values with a section named
"outgoing_mail.resource_name" where resource_name is the name of your server.
Exemple of config file : Exemple of config file :
[outgoing_mail] [outgoing_mail]
smtp_host = smtp.myserver.com smtp_host = smtp.myserver.com
smtp_port = 587 smtp_port = 587
smtp_user =
smtp_user =
smtp_pass = smtp_pass =
smtp_encryption = ssl smtp_encryption = ssl
@ -57,11 +59,14 @@ password = openerp
'author': 'Camptocamp', 'author': 'Camptocamp',
'license': 'AGPL-3', 'license': 'AGPL-3',
'website': 'http://openerp.camptocamp.com', 'website': 'http://openerp.camptocamp.com',
'depends': ['mail', 'fetchmail', 'server_environment', 'server_environment_files', 'crm'],
'depends': ['mail',
'fetchmail',
'server_environment',
'server_environment_files',
'crm'],
'init_xml': [], 'init_xml': [],
'update_xml': ['mail_view.xml'], 'update_xml': ['mail_view.xml'],
'demo_xml': [], 'demo_xml': [],
'installable': True, 'installable': True,
'active': False, 'active': False,
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

230
mail_environment/env_mail.py

@ -27,7 +27,7 @@ from server_environment import serv_config
class IrMail(osv.osv): class IrMail(osv.osv):
_inherit = "ir.mail_server" _inherit = "ir.mail_server"
def _get_smtp_conf(self, cursor, uid, ids, name, args, context=None): def _get_smtp_conf(self, cursor, uid, ids, name, args, context=None):
""" """
Return configuration Return configuration
@ -41,7 +41,8 @@ class IrMail(osv.osv):
if serv_config.has_section(global_section_name): if serv_config.has_section(global_section_name):
config_vals.update((serv_config.items(global_section_name))) config_vals.update((serv_config.items(global_section_name)))
custom_section_name = '.'.join((global_section_name, mail_server.name))
custom_section_name = '.'.join((global_section_name,
mail_server.name))
if serv_config.has_section(custom_section_name): if serv_config.has_section(custom_section_name):
config_vals.update(serv_config.items(custom_section_name)) config_vals.update(serv_config.items(custom_section_name))
@ -52,44 +53,52 @@ class IrMail(osv.osv):
return res return res
_columns = { _columns = {
'smtp_host': fields.function(_get_smtp_conf,
method=True,
string='SMTP Server',
type="char",
multi='outgoing_mail_config',
size=128),
'smtp_port': fields.function(_get_smtp_conf,
method=True,
string='SMTP Port',
type="integer",
multi='outgoing_mail_config',
help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases.",
size=5),
'smtp_user': fields.function(_get_smtp_conf,
method=True,
string='Username',
type="char",
multi='outgoing_mail_config',
help="Optional username for SMTP authentication",
size=64),
'smtp_pass': fields.function(_get_smtp_conf,
method=True,
string='Password',
type="char",
multi='outgoing_mail_config',
help="Optional password for SMTP authentication",
size=64),
'smtp_encryption' :fields.function(_get_smtp_conf,
method=True,
string='smtp_encryption',
type="char",
multi='outgoing_mail_config',
help="Choose the connection encryption scheme:\n"
"- none: SMTP sessions are done in cleartext.\n"
"- starttls: TLS encryption is requested at start of SMTP session (Recommended)\n"
"- ssl: SMTP sessions are encrypted with SSL/TLS through a dedicated port (default: 465)",
size=64)}
'smtp_host': fields.function(
_get_smtp_conf,
method=True,
string='SMTP Server',
type="char",
multi='outgoing_mail_config',
size=128),
'smtp_port': fields.function(
_get_smtp_conf,
method=True,
string='SMTP Port',
type="integer",
multi='outgoing_mail_config',
help=("SMTP Port. Usually 465 for SSL, "
"and 25 or 587 for other cases."),
size=5),
'smtp_user': fields.function(
_get_smtp_conf,
method=True,
string='Username',
type="char",
multi='outgoing_mail_config',
help="Optional username for SMTP authentication",
size=64),
'smtp_pass': fields.function(
_get_smtp_conf,
method=True,
string='Password',
type="char",
multi='outgoing_mail_config',
help="Optional password for SMTP authentication",
size=64),
'smtp_encryption': fields.function(
_get_smtp_conf,
method=True,
string='smtp_encryption',
type="char",
multi='outgoing_mail_config',
help="Choose the connection encryption scheme:\n"
"- none: SMTP sessions are done in cleartext.\n"
"- starttls: TLS encryption is requested at start "
"of SMTP session (Recommended)\n"
"- ssl: SMTP sessions are encrypted with SSL/TLS "
"through a dedicated port (default: 465)",
size=64)}
IrMail() IrMail()
@ -108,17 +117,20 @@ class FetchmailServer(osv.osv):
key_types = {'port': int, key_types = {'port': int,
'is_ssl': lambda a: bool(int(a)), 'is_ssl': lambda a: bool(int(a)),
'attach': lambda a: bool(int(a)), 'attach': lambda a: bool(int(a)),
'original': lambda a: bool(int(a)),}
'original': lambda a: bool(int(a)),
}
# default vals # default vals
config_vals = {'port': 993, config_vals = {'port': 993,
'is_ssl': 0, 'is_ssl': 0,
'attach': 0, 'attach': 0,
'original': 0}
'original': 0,
}
if serv_config.has_section(global_section_name): if serv_config.has_section(global_section_name):
config_vals.update(serv_config.items(global_section_name)) config_vals.update(serv_config.items(global_section_name))
custom_section_name = '.'.join((global_section_name, fetchmail.name))
custom_section_name = '.'.join((global_section_name,
fetchmail.name))
if serv_config.has_section(custom_section_name): if serv_config.has_section(custom_section_name):
config_vals.update(serv_config.items(custom_section_name)) config_vals.update(serv_config.items(custom_section_name))
@ -128,81 +140,95 @@ class FetchmailServer(osv.osv):
res[fetchmail.id] = config_vals res[fetchmail.id] = config_vals
return res return res
def _type_search(self, cr, uid, obj, name, args, context={}):
def _type_search(self, cr, uid, obj, name, args, context=None):
if context is None:
context = self.pool['res.users'].context_get(cr, uid)
result_ids = [] result_ids = []
# read all incomming servers values # read all incomming servers values
all_ids = self.search(cr, uid, [], context=context) all_ids = self.search(cr, uid, [], context=context)
results = self.read(cr, uid, all_ids, ['id','type'], context=context)
results = self.read(cr, uid, all_ids, ['id', 'type'], context=context)
args = args[:] args = args[:]
i = 0 i = 0
while i < len(args): while i < len(args):
operator = args[i][1] operator = args[i][1]
if operator == '=': if operator == '=':
for res in results: for res in results:
if (res['type'] == args[i][2]) and (res['id'] not in result_ids):
if (res['type'] == args[i][2]) and \
(res['id'] not in result_ids):
result_ids.append(res['id']) result_ids.append(res['id'])
elif operator == 'in': elif operator == 'in':
for search_vals in args[i][2]: for search_vals in args[i][2]:
for res in results: for res in results:
if (res['type'] == search_vals) and (res['id'] not in result_ids):
result_ids.append(res['id'])
if (res['type'] == search_vals) and \
(res['id'] not in result_ids):
result_ids.append(res['id'])
else: else:
continue continue
i += 1 i += 1
return [('id', 'in', result_ids)] return [('id', 'in', result_ids)]
_columns = { _columns = {
'server': fields.function(_get_incom_conf,
method=True,
string='Server',
type="char",
multi='income_mail_config',
size=256, help="Hostname or IP of the mail server"),
'port': fields.function(_get_incom_conf,
method=True,
string='Port',
type="integer",
multi='income_mail_config',
help="Hostname or IP of the mail server"),
'type': fields.function(_get_incom_conf,
method=True,
string='Type',
type="char",
multi='income_mail_config',
fnct_search=_type_search,
size=64,
help="pop, imap, local"),
'is_ssl': fields.function(_get_incom_conf,
method=True,
string='Is SSL',
type="boolean",
multi='income_mail_config',
help='Connections are encrypted with SSL/TLS through'
' a dedicated port (default: IMAPS=993, POP3S=995)'),
'attach': fields.function(_get_incom_conf,
method=True,
string='Keep Attachments',
type="boolean",
multi='income_mail_config',
help="Whether attachments should be downloaded. "
"If not enabled, incoming emails will be stripped of any attachments before being processed"),
'original': fields.function(_get_incom_conf,
method=True,
string='Keep Original',
type="boolean",
multi='income_mail_config',
help="Whether a full original copy of each email should be kept for reference"
"and attached to each processed message. This will usually double the size of your message database."),
'user': fields.function(_get_incom_conf,
method=True,
string='Username',
type="char",
multi='income_mail_config',
size=64),
'password': fields.function(_get_incom_conf,
method=True,
string='password',
type="char",
multi='income_mail_config',
size=64)}
FetchmailServer()
'server': fields.function(
_get_incom_conf,
method=True,
string='Server',
type="char",
multi='income_mail_config',
size=256, help="Hostname or IP of the mail server"),
'port': fields.function(
_get_incom_conf,
method=True,
string='Port',
type="integer",
multi='income_mail_config',
help="Hostname or IP of the mail server"),
'type': fields.function(
_get_incom_conf,
method=True,
string='Type',
type="char",
multi='income_mail_config',
fnct_search=_type_search,
size=64,
help="pop, imap, local"),
'is_ssl': fields.function(
_get_incom_conf,
method=True,
string='Is SSL',
type="boolean",
multi='income_mail_config',
help='Connections are encrypted with SSL/TLS through'
' a dedicated port (default: IMAPS=993, POP3S=995)'),
'attach': fields.function(
_get_incom_conf,
method=True,
string='Keep Attachments',
type="boolean",
multi='income_mail_config',
help="Whether attachments should be downloaded. "
"If not enabled, incoming emails will be stripped of any "
"attachments before being processed"),
'original': fields.function(
_get_incom_conf,
method=True,
string='Keep Original',
type="boolean",
multi='income_mail_config',
help="Whether a full original copy of each email should be kept "
"for reference and attached to each processed message. This "
"will usually double the size of your message database."),
'user': fields.function(
_get_incom_conf,
method=True,
string='Username',
type="char",
multi='income_mail_config',
size=64),
'password': fields.function(
_get_incom_conf,
method=True,
string='password',
type="char",
multi='income_mail_config',
size=64)}
FetchmailServer()

7
mass_editing/__init__.py

@ -2,7 +2,8 @@
############################################################################## ##############################################################################
# #
# This module uses OpenERP, Open Source Management Solution Framework. # This module uses OpenERP, Open Source Management Solution Framework.
# Copyright (C) 2012-Today Serpent Consulting Services (<http://www.serpentcs.com>)
# Copyright (C) 2012-Today Serpent Consulting Services
# (<http://www.serpentcs.com>)
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,9 +19,5 @@
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
# #
############################################################################## ##############################################################################
import mass_editing import mass_editing
import wizard import wizard
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

15
mass_editing/mass_editing.py

@ -32,10 +32,11 @@ class ir_model_fields(orm.Model):
count=False): count=False):
model_domain = [] model_domain = []
for domain in args: for domain in args:
if domain[0] == 'model_id' and domain[2]\
and type(domain[2]) != list:
model_domain += [(
'model_id', 'in', map(int, domain[2][1:-1].split(',')))]
if (len(domain) > 2 and domain[0] == 'model_id'
and isinstance(domain[2], basestring)):
model_domain += [
('model_id', 'in', map(int, domain[2][1:-1].split(',')))
]
else: else:
model_domain.append(domain) model_domain.append(domain)
return super(ir_model_fields, self).search( return super(ir_model_fields, self).search(
@ -102,7 +103,7 @@ class mass_object(orm.Model):
'view_mode': 'form,tree', 'view_mode': 'form,tree',
'target': 'new', 'target': 'new',
'auto_refresh': 1, 'auto_refresh': 1,
}, context)
}, context)
vals['ref_ir_value'] = ir_values_obj.create(cr, uid, { vals['ref_ir_value'] = ir_values_obj.create(cr, uid, {
'name': button_name, 'name': button_name,
'model': src_obj, 'model': src_obj,
@ -111,11 +112,11 @@ class mass_object(orm.Model):
"ir.actions.act_window," "ir.actions.act_window,"
+ str(vals['ref_ir_act_window'])), + str(vals['ref_ir_act_window'])),
'object': True, 'object': True,
}, context)
}, context)
self.write(cr, uid, ids, { self.write(cr, uid, ids, {
'ref_ir_act_window': vals.get('ref_ir_act_window', False), 'ref_ir_act_window': vals.get('ref_ir_act_window', False),
'ref_ir_value': vals.get('ref_ir_value', False), 'ref_ir_value': vals.get('ref_ir_value', False),
}, context)
}, context)
return True return True
def unlink_action(self, cr, uid, ids, context=None): def unlink_action(self, cr, uid, ids, context=None):

6
mass_editing/wizard/__init__.py

@ -2,7 +2,8 @@
############################################################################## ##############################################################################
# #
# This module uses OpenERP, Open Source Management Solution Framework. # This module uses OpenERP, Open Source Management Solution Framework.
# Copyright (C) 2012-Today Serpent Consulting Services (<http://www.serpentcs.com>)
# Copyright (C) 2012-Today Serpent Consulting Services
# (<http://www.serpentcs.com>)
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,7 +19,4 @@
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
# #
############################################################################## ##############################################################################
import mass_editing_wizard import mass_editing_wizard
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

3
scheduler_error_mailer/__openerp__.py

@ -33,7 +33,8 @@
Scheduler Error Mailer Scheduler Error Mailer
====================== ======================
This module adds the possibility to send an e-mail when a scheduler raises an error.""",
This module adds the possibility to send an e-mail when a scheduler raises an
error.""",
'author': 'Akretion', 'author': 'Akretion',
'website': 'http://www.akretion.com/', 'website': 'http://www.akretion.com/',
'depends': ['email_template'], 'depends': ['email_template'],

36
scheduler_error_mailer/ir_cron.py

@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
#################################################################################
##############################################################################
# #
# Scheduler Error Mailer module for OpenERP # Scheduler Error Mailer module for OpenERP
# Copyright (C) 2012-2013 Akretion (http://www.akretion.com/) # Copyright (C) 2012-2013 Akretion (http://www.akretion.com/)
@ -28,20 +28,27 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ir_cron(orm.Model): class ir_cron(orm.Model):
_inherit = "ir.cron" _inherit = "ir.cron"
_columns = { _columns = {
'email_template': fields.many2one('email.template',
'email_template': fields.many2one(
'email.template',
'Error E-mail Template', 'Error E-mail Template',
help="Select the email template that will be sent when this scheduler fails."),
help="Select the email template that will be sent "
"when this scheduler fails."),
} }
def _handle_callback_exception(self, cr, uid,
model_name,
method_name,
args,
job_id,
job_exception):
def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
res = super(ir_cron, self)._handle_callback_exception(cr, uid,
model_name, method_name, args, job_id, job_exception)
res = super(ir_cron, self)._handle_callback_exception(
cr, uid, model_name, method_name, args, job_id, job_exception)
my_cron = self.browse(cr, uid, job_id) my_cron = self.browse(cr, uid, job_id)
@ -51,12 +58,13 @@ class ir_cron(orm.Model):
context = { context = {
'job_exception': job_exception, 'job_exception': job_exception,
'dbname': cr.dbname, 'dbname': cr.dbname,
}
}
logger.debug("Sending scheduler error email with context=%s" % context)
self.pool['email.template'].send_mail(cr, uid,
my_cron.email_template.id, my_cron.id, force_send=True,
context=context)
logger.debug("Sending scheduler error email with context=%s",
context)
self.pool['email.template'].send_mail(
cr, uid, my_cron.email_template.id, my_cron.id,
force_send=True, context=context)
return res return res
@ -66,5 +74,5 @@ class res_users(orm.Model):
def test_scheduler_failure(self, cr, uid, context=None): def test_scheduler_failure(self, cr, uid, context=None):
"""This function is used to test and debug this module""" """This function is used to test and debug this module"""
raise orm.except_orm(_('Error :'), _("Task failure with UID = %d." % uid))
raise orm.except_orm(_('Error :'),
_("Task failure with UID = %d.") % uid)

19
server_environment/serv_config.py

@ -23,7 +23,7 @@ import os
import ConfigParser import ConfigParser
from lxml import etree from lxml import etree
from openerp.osv import osv, fields, orm
from openerp.osv import fields, orm
from openerp.tools.config import config as system_base_config from openerp.tools.config import config as system_base_config
from .system_info import get_server_environment from .system_info import get_server_environment
@ -46,14 +46,17 @@ if not system_base_config.get('running_env', False):
ck_path = os.path.join(_dir, system_base_config['running_env']) ck_path = os.path.join(_dir, system_base_config['running_env'])
if not os.path.exists(ck_path) :
if not os.path.exists(ck_path):
raise Exception( raise Exception(
"Provided server environment does not exist, " "Provided server environment does not exist, "
"please add a folder %s" % ck_path "please add a folder %s" % ck_path
) )
def setboolean(obj, attr, _bool=_boolean_states):
def setboolean(obj, attr, _bool=None):
"""Replace the attribute with a boolean.""" """Replace the attribute with a boolean."""
if _bool is None:
_bool = dict(_boolean_states) # we copy original dict
res = _bool[getattr(obj, attr).lower()] res = _bool[getattr(obj, attr).lower()]
setattr(obj, attr, res) setattr(obj, attr, res)
return res return res
@ -92,7 +95,7 @@ def _load_config():
config_p.optionxform = str config_p.optionxform = str
try: try:
config_p.read(conf_files) config_p.read(conf_files)
except Exception, e:
except Exception as e:
raise Exception('Cannot read config files "%s": %s' % (conf_files, e)) raise Exception('Cannot read config files "%s": %s' % (conf_files, e))
return config_p return config_p
@ -115,7 +118,7 @@ class ServerConfiguration(orm.TransientModel):
_conf_defaults = _Defaults() _conf_defaults = _Defaults()
def __init__(self, pool, cr): def __init__(self, pool, cr):
res = super(ServerConfiguration, self).__init__(pool, cr)
super(ServerConfiguration, self).__init__(pool, cr)
self.running_env = system_base_config['running_env'] self.running_env = system_base_config['running_env']
# Only show passwords in development # Only show passwords in development
self.show_passwords = self.running_env in ('dev',) self.show_passwords = self.running_env in ('dev',)
@ -169,8 +172,9 @@ class ServerConfiguration(orm.TransientModel):
arch += '</notebook></form>' arch += '</notebook></form>'
self._arch = etree.fromstring(arch) self._arch = etree.fromstring(arch)
def fields_view_get(self, cr, uid, view_id=None, view_type='form',
context=None, toolbar=False, submenu=False):
def fields_view_get(
self, cr, uid, view_id=None, view_type='form', context=None,
toolbar=False, submenu=False):
"""Overwrite the default method to render the custom view.""" """Overwrite the default method to render the custom view."""
res = super(ServerConfiguration, self).fields_view_get(cr, uid, res = super(ServerConfiguration, self).fields_view_get(cr, uid,
view_id, view_id,
@ -187,7 +191,6 @@ class ServerConfiguration(orm.TransientModel):
res['fields'] = xfields res['fields'] = xfields
return res return res
def default_get(self, cr, uid, fields_list, context=None): def default_get(self, cr, uid, fields_list, context=None):
res = {} res = {}
for key in self._conf_defaults: for key in self._conf_defaults:

2
server_environment/system_info.py

@ -38,7 +38,7 @@ def get_server_environment():
# inspired by server/bin/service/web_services.py # inspired by server/bin/service/web_services.py
try: try:
rev_id = _get_output('bzr revision-info') rev_id = _get_output('bzr revision-info')
except Exception, e:
except Exception as e:
rev_id = 'Exception: %s' % (e,) rev_id = 'Exception: %s' % (e,)
os_lang = '.'.join([x for x in locale.getdefaultlocale() if x]) os_lang = '.'.join([x for x in locale.getdefaultlocale() if x])

2
server_environment_files/__init__.py

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
##############################################################################
##############################################################################

2
super_calendar/__init__.py

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## ##############################################################################
#
#
# Copyright (C) 2012 Agile Business Group sagl (<http://www.agilebg.com>) # Copyright (C) 2012 Agile Business Group sagl (<http://www.agilebg.com>)
# Copyright (C) 2012 Domsense srl (<http://www.domsense.com>) # Copyright (C) 2012 Domsense srl (<http://www.domsense.com>)
# #

39
super_calendar/__openerp__.py

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## ##############################################################################
#
#
# Copyright (C) 2012 Agile Business Group sagl (<http://www.agilebg.com>) # Copyright (C) 2012 Agile Business Group sagl (<http://www.agilebg.com>)
# Copyright (C) 2012 Domsense srl (<http://www.domsense.com>) # Copyright (C) 2012 Domsense srl (<http://www.domsense.com>)
# #
@ -26,9 +26,13 @@
'description': """ 'description': """
This module allows to create configurable calendars. This module allows to create configurable calendars.
Through the 'calendar configurator' object, you can specify which models have to be merged in the super calendar. For each model, you have to define the 'description' and 'date_start' fields at least. Then you can define 'duration' and the 'user_id' fields.
Through the 'calendar configurator' object, you can specify which models have
to be merged in the super calendar. For each model, you have to define the
'description' and 'date_start' fields at least. Then you can define 'duration'
and the 'user_id' fields.
The 'super.calendar' object contains the the merged calendars. The 'super.calendar' can be updated by 'ir.cron' or manually.
The 'super.calendar' object contains the the merged calendars. The
'super.calendar' can be updated by 'ir.cron' or manually.
Configuration Configuration
============= =============
@ -37,39 +41,46 @@ After installing the module you can go to
Super calendar Configuration Configurators Super calendar Configuration Configurators
and create a new configurator. For instance, if you want to see meetings and phone calls, you can create the following lines
and create a new configurator. For instance, if you want to see meetings and
phone calls, you can create the following lines
.. image:: http://planet.domsense.com/wp-content/uploads/2012/04/meetings.png
.. image:: data/meetings.png
:width: 400 px :width: 400 px
.. image:: http://planet.domsense.com/wp-content/uploads/2012/04/phone_calls.png
.. image:: data/phone_calls.png
:width: 400 px :width: 400 px
Then, you can use the Generate Calendar button or wait for the scheduled action (Generate Calendar Records) to be run.
Then, you can use the Generate Calendar button or wait for the scheduled
action (Generate Calendar Records) to be run.
When the calendar is generated, you can visualize it by the super calendar main menu.
When the calendar is generated, you can visualize it by the super calendar
main menu.
Here is a sample monthly calendar: Here is a sample monthly calendar:
.. image:: http://planet.domsense.com/wp-content/uploads/2012/04/month_calendar.png
.. image:: data/month_calendar.png
:width: 400 px :width: 400 px
And here is the weekly one: And here is the weekly one:
.. image:: http://planet.domsense.com/wp-content/uploads/2012/04/week_calendar.png
.. image:: data/week_calendar.png
:width: 400 px :width: 400 px
As you can see, several filters are available. A typical usage consists in filtering by Configurator (if you have several configurators, Scheduled calls and meetings can be one of them) and by your user. Once you filtered, you can save the filter as Advanced filter or even add it to a dashboard.
As you can see, several filters are available. A typical usage consists in
filtering by Configurator (if you have several configurators,
Scheduled calls and meetings can be one of them) and by your user.
Once you filtered, you can save the filter as Advanced filter or even
add it to a dashboard.
""", """,
'author': 'Agile Business Group', 'author': 'Agile Business Group',
'website': 'http://www.agilebg.com', 'website': 'http://www.agilebg.com',
'license': 'AGPL-3', 'license': 'AGPL-3',
'depends' : ['base'],
"data" : [
'depends': ['base'],
"data": [
'super_calendar_view.xml', 'super_calendar_view.xml',
'cron_data.xml', 'cron_data.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
],
],
'demo': [], 'demo': [],
'test': [], 'test': [],
'installable': True, 'installable': True,

BIN
super_calendar/data/meetings.png

After

Width: 962  |  Height: 374  |  Size: 25 KiB

BIN
super_calendar/data/month_calendar.png

After

Width: 1064  |  Height: 536  |  Size: 70 KiB

BIN
super_calendar/data/phone_calls.png

After

Width: 960  |  Height: 322  |  Size: 26 KiB

BIN
super_calendar/data/week_calendar.png

After

Width: 1062  |  Height: 566  |  Size: 78 KiB

185
super_calendar/super_calendar.py

@ -19,7 +19,7 @@
# #
############################################################################## ##############################################################################
from openerp.osv import fields, osv, orm
from openerp.osv import fields, orm
from openerp.tools.translate import _ from openerp.tools.translate import _
import logging import logging
from mako.template import Template from mako.template import Template
@ -27,111 +27,162 @@ from datetime import datetime
from openerp import tools from openerp import tools
from openerp.tools.safe_eval import safe_eval from openerp.tools.safe_eval import safe_eval
def _models_get(self, cr, uid, context=None): def _models_get(self, cr, uid, context=None):
obj = self.pool.get('ir.model') obj = self.pool.get('ir.model')
ids = obj.search(cr, uid, []) ids = obj.search(cr, uid, [])
res = obj.read(cr, uid, ids, ['model', 'name'], context) res = obj.read(cr, uid, ids, ['model', 'name'], context)
return [(r['model'], r['name']) for r in res] return [(r['model'], r['name']) for r in res]
class super_calendar_configurator(orm.Model): class super_calendar_configurator(orm.Model):
_logger = logging.getLogger('super.calendar') _logger = logging.getLogger('super.calendar')
_name = 'super.calendar.configurator' _name = 'super.calendar.configurator'
_columns = {
_columns = {
'name': fields.char('Name', size=64, required=True), 'name': fields.char('Name', size=64, required=True),
'line_ids': fields.one2many('super.calendar.configurator.line', 'configurator_id', 'Lines'),
}
'line_ids': fields.one2many('super.calendar.configurator.line',
'configurator_id', 'Lines'),
}
def generate_calendar_records(self, cr, uid, ids, context=None): def generate_calendar_records(self, cr, uid, ids, context=None):
configurator_ids = self.search(cr, uid, []) configurator_ids = self.search(cr, uid, [])
super_calendar_pool = self.pool.get('super.calendar') super_calendar_pool = self.pool.get('super.calendar')
# removing old records # removing old records
super_calendar_ids = super_calendar_pool.search(cr, uid, [], context=context)
super_calendar_pool.unlink(cr, uid, super_calendar_ids, context=context)
super_calendar_ids = super_calendar_pool.search(cr, uid, [],
context=context)
super_calendar_pool.unlink(cr, uid,
super_calendar_ids,
context=context)
for configurator in self.browse(cr, uid, configurator_ids, context): for configurator in self.browse(cr, uid, configurator_ids, context):
for line in configurator.line_ids: for line in configurator.line_ids:
current_pool = self.pool.get(line.name.model)
current_record_ids = current_pool.search(
cr,
uid,
line.domain and safe_eval(line.domain) or [],
context=context)
for current_record_id in current_record_ids:
current_record = current_pool.browse(cr, uid, current_record_id, context=context)
if line.user_field_id and \
current_record[line.user_field_id.name] and current_record[line.user_field_id.name]._table_name != 'res.users':
raise osv.except_osv(_('Error'),
_("The 'User' field of record %s (%s) does not refer to res.users")
% (current_record[line.description_field_id.name], line.name.model))
if (((line.description_field_id
and current_record[line.description_field_id.name])
or line.description_code)
and current_record[line.date_start_field_id.name]):
duration = False
if not line.duration_field_id and line.date_stop_field_id and current_record[line.date_start_field_id.name] and current_record[line.date_stop_field_id.name]:
date_start= datetime.strptime(current_record[line.date_start_field_id.name], tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_stop= datetime.strptime(current_record[line.date_stop_field_id.name], tools.DEFAULT_SERVER_DATETIME_FORMAT)
duration = (date_stop - date_start).total_seconds() / 3600
elif line.duration_field_id:
duration = current_record[line.duration_field_id.name]
if line.description_type != 'code':
name = current_record[line.description_field_id.name]
else:
parse_dict = {'o': current_record}
mytemplate = Template(line.description_code)
name= mytemplate.render(**parse_dict)
super_calendar_values = {
'name': name,
'model_description': line.description,
'date_start': current_record[line.date_start_field_id.name],
'duration': duration,
'user_id': line.user_field_id and current_record[line.user_field_id.name] and current_record[line.user_field_id.name].id or False,
'configurator_id': configurator.id,
'res_id': line.name.model+','+str(current_record['id']),
'model_id': line.name.id,
}
super_calendar_pool.create(cr, uid, super_calendar_values, context=context)
values = self._generate_record_from_line(cr, uid,
configurator,
line,
context)
super_calendar_pool.create(cr, uid, values, context=context)
self._logger.info('Calendar generated') self._logger.info('Calendar generated')
return True return True
def _generate_record_from_line(self, cr, uid, configurator, line, context):
current_pool = self.pool.get(line.name.model)
current_record_ids = current_pool.search(
cr,
uid,
line.domain and safe_eval(line.domain) or [],
context=context)
for current_record_id in current_record_ids:
record = current_pool.browse(cr, uid,
current_record_id,
context=context)
if line.user_field_id and \
record[line.user_field_id.name] and \
record[line.user_field_id.name]._table_name != 'res.users':
raise orm.except_orm(
_('Error'),
_("The 'User' field of record %s (%s) "
"does not refer to res.users")
% (record[line.description_field_id.name],
line.name.model))
if (((line.description_field_id and
record[line.description_field_id.name]) or
line.description_code) and
record[line.date_start_field_id.name]):
duration = False
if (not line.duration_field_id and
line.date_stop_field_id and
record[line.date_start_field_id.name] and
record[line.date_stop_field_id.name]):
date_start = datetime.strptime(
record[line.date_start_field_id.name],
tools.DEFAULT_SERVER_DATETIME_FORMAT
)
date_stop = datetime.strptime(
record[line.date_stop_field_id.name],
tools.DEFAULT_SERVER_DATETIME_FORMAT
)
duration = (date_stop - date_start).total_seconds() / 3600
elif line.duration_field_id:
duration = record[line.duration_field_id.name]
if line.description_type != 'code':
name = record[line.description_field_id.name]
else:
parse_dict = {'o': record}
mytemplate = Template(line.description_code)
name = mytemplate.render(**parse_dict)
super_calendar_values = {
'name': name,
'model_description': line.description,
'date_start': record[line.date_start_field_id.name],
'duration': duration,
'user_id': (
line.user_field_id and
record[line.user_field_id.name] and
record[line.user_field_id.name].id or
False
),
'configurator_id': configurator.id,
'res_id': line.name.model + ',' + str(record['id']),
'model_id': line.name.id,
}
return super_calendar_values
class super_calendar_configurator_line(orm.Model): class super_calendar_configurator_line(orm.Model):
_name = 'super.calendar.configurator.line' _name = 'super.calendar.configurator.line'
_columns = {
_columns = {
'name': fields.many2one('ir.model', 'Model', required=True), 'name': fields.many2one('ir.model', 'Model', required=True),
'description': fields.char('Description', size=128, required=True), 'description': fields.char('Description', size=128, required=True),
'domain': fields.char('Domain', size=512), 'domain': fields.char('Domain', size=512),
'configurator_id': fields.many2one('super.calendar.configurator', 'Configurator'),
'configurator_id': fields.many2one('super.calendar.configurator',
'Configurator'),
'description_type': fields.selection([ 'description_type': fields.selection([
('field', 'Field'), ('field', 'Field'),
('code', 'Code'), ('code', 'Code'),
], string="Description Type"),
'description_field_id': fields.many2one('ir.model.fields', 'Description field',
], string="Description Type"),
'description_field_id': fields.many2one(
'ir.model.fields', 'Description field',
domain="[('model_id', '=', name),('ttype', '=', 'char')]"), domain="[('model_id', '=', name),('ttype', '=', 'char')]"),
'description_code': fields.text('Description field', help="Use '${o}' to refer to the involved object. E.g.: '${o.project_id.name}'"),
'date_start_field_id': fields.many2one('ir.model.fields', 'Start date field',
domain="['&','|',('ttype', '=', 'datetime'),('ttype', '=', 'date'),('model_id', '=', name)]",
'description_code': fields.text(
'Description field',
help="Use '${o}' to refer to the involved object. "
"E.g.: '${o.project_id.name}'"),
'date_start_field_id': fields.many2one(
'ir.model.fields', 'Start date field',
domain="['&','|',('ttype', '=', 'datetime'),"
"('ttype', '=', 'date'),"
"('model_id', '=', name)]",
required=True), required=True),
'date_stop_field_id': fields.many2one('ir.model.fields', 'End date field',
domain="['&',('ttype', '=', 'datetime'),('model_id', '=', name)]"),
'duration_field_id': fields.many2one('ir.model.fields', 'Duration field',
'date_stop_field_id': fields.many2one(
'ir.model.fields', 'End date field',
domain="['&',('ttype', '=', 'datetime'),('model_id', '=', name)]"
),
'duration_field_id': fields.many2one(
'ir.model.fields', 'Duration field',
domain="['&',('ttype', '=', 'float'),('model_id', '=', name)]"), domain="['&',('ttype', '=', 'float'),('model_id', '=', name)]"),
'user_field_id': fields.many2one('ir.model.fields', 'User field',
'user_field_id': fields.many2one(
'ir.model.fields', 'User field',
domain="['&',('ttype', '=', 'many2one'),('model_id', '=', name)]"), domain="['&',('ttype', '=', 'many2one'),('model_id', '=', name)]"),
}
}
class super_calendar(orm.Model): class super_calendar(orm.Model):
_name = 'super.calendar' _name = 'super.calendar'
_columns = {
_columns = {
'name': fields.char('Description', size=512, required=True), 'name': fields.char('Description', size=512, required=True),
'model_description': fields.char('Model Description', size=128, required=True),
'date_start':fields.datetime('Start date', required=True),
'duration':fields.float('Duration'),
'model_description': fields.char('Model Description',
size=128,
required=True),
'date_start': fields.datetime('Start date', required=True),
'duration': fields.float('Duration'),
'user_id': fields.many2one('res.users', 'User'), 'user_id': fields.many2one('res.users', 'User'),
'configurator_id': fields.many2one('super.calendar.configurator', 'Configurator'),
'res_id': fields.reference('Resource', selection=_models_get, size=128),
'configurator_id': fields.many2one('super.calendar.configurator',
'Configurator'),
'res_id': fields.reference('Resource',
selection=_models_get,
size=128),
'model_id': fields.many2one('ir.model', 'Model'), 'model_id': fields.many2one('ir.model', 'Model'),
}
}

11
tree_view_record_id/__init__.py

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# licence AGPL version 3 or later
# see licence in __openerp__.py or http://www.gnu.org/licenses/agpl-3.0.txt
# Copyright (C) 2014 Akretion (http://www.akretion.com).
# @author David BEAL <david.beal@akretion.com>
#
##############################################################################
import orm

58
tree_view_record_id/__openerp__.py

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Copyright (C) 2012-TODAY Akretion <http://www.akretion.com>.
# All Rights Reserved
# @author David BEAL <david.beal@akretion.com>
# 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/>.
#
###############################################################################
{
'name': 'Tree View Record Id',
'version': '0.1',
'category': 'Other modules',
'sequence': 10,
'author': 'Akretion',
'summary': "Adds id field to tree views",
'description': """
Adds Id field in all tree views of any modules/models, except:
* Arborescent tree views like 'Products by Category', 'Chart of accounts', etc.
* Tree views (like in wizard 'Change password') built on transient models
which don't have this column in their table.
Id field is the primary key of standard sql tables
defined by the orm (Odoo model).
""",
'website': 'http://www.akretion.com',
'depends': [
'base',
],
'data': [
],
'demo': [
],
'installable': True,
'auto_install': False,
'application': False,
'images': [
],
'css': [
],
'js': [
],
'qweb': [
],
}

64
tree_view_record_id/orm.py

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# licence AGPL version 3 or later
# see licence in __openerp__.py or http://www.gnu.org/licenses/agpl-3.0.txt
# Copyright (C) 2014 Akretion (http://www.akretion.com).
# @author David BEAL <david.beal@akretion.com>
#
##############################################################################
from openerp.osv import orm
from lxml import etree
import os
""" procedural code is executed even if the module is not installed
TRY TO SWITCH to _register_hook() in the model to avoid
execution when not installed in V 8.0 version
"""
module_name = os.path.basename(os.path.dirname(os.path.abspath(__file__)))
DUMMY_MODEL = 'module.%s.installed' % module_name.replace('_', '.')
class DummyModel(orm.Model):
""" Allow to check if module is installed or not
in fields_view_get method to avoid code execution if not
Only executed if the module is installed
"""
_name = DUMMY_MODEL
fields_view_get_orginal = orm.BaseModel.fields_view_get
def fields_view_get(self, cr, uid, view_id=None, view_type='form',
context=None, toolbar=False, submenu=False):
res = fields_view_get_orginal(
self, cr, uid, view_id=view_id, view_type=view_type, context=context,
toolbar=toolbar, submenu=submenu)
if view_type == 'tree':
compatible_tree = res.get('field_parent', True) is False
# Tree views with res['field_parent'] different from False
# looks like 'Products by Category'.
# We don't modify these views
# to avoid to break them (js error)
if '_transient_max_count' in self.pool[res['model']].__dict__.keys():
# model with '_transient_max_count' key are transient model
# transient models haven't 'id' column in mostly case
compatible_tree = False
if compatible_tree and DUMMY_MODEL in self.pool.models.keys():
doc = etree.XML(res['arch'])
if (doc.xpath("//tree") and
len(doc.xpath("//field[@name='id']"))) == 0:
node = doc.xpath("//tree")[0]
node.append(etree.Element("field", name="id", string="Id"))
res['arch'] = etree.tostring(node)
return res
orm.BaseModel.fields_view_get = fields_view_get

41
users_ldap_groups/__openerp__.py

@ -20,22 +20,22 @@
############################################################################## ##############################################################################
{ {
"name" : "Groups assignment",
"version" : "1.2",
"depends" : ["auth_ldap"],
"author" : "Therp BV",
"description": """
"name": "Groups assignment",
"version": "1.2",
"depends": ["auth_ldap"],
"author": "Therp BV",
"description": """
Adds user accounts to groups based on rules defined by the administrator. Adds user accounts to groups based on rules defined by the administrator.
Usage: Usage:
Define mappings in Settings->Companies->[your company]->tab configuration->[your
ldap server].
Define mappings in Settings->Companies->[your company]->tab
configuration->[your ldap server].
Decide whether you want only groups mapped from ldap (Only ldap groups=y) or a Decide whether you want only groups mapped from ldap (Only ldap groups=y) or a
mix of manually set groups and ldap groups (Only ldap groups=n). Setting this to
'no' will result in users never losing privileges when you remove them from a
ldap group, so that's a potential security issue. It is still the default to
mix of manually set groups and ldap groups (Only ldap groups=n). Setting this
to 'no' will result in users never losing privileges when you remove them from
a ldap group, so that's a potential security issue. It is still the default to
prevent losing group information by accident. prevent losing group information by accident.
For active directory, use LDAP attribute 'memberOf' and operator 'contains'. For active directory, use LDAP attribute 'memberOf' and operator 'contains'.
@ -46,17 +46,16 @@ For posix accounts, use operator 'query' and a value like
(&(cn=bzr)(objectClass=posixGroup)(memberUid=$uid)) (&(cn=bzr)(objectClass=posixGroup)(memberUid=$uid))
The operator query matches if the filter in value returns something, and value The operator query matches if the filter in value returns something, and value
can contain $[attribute] which will be replaced by the first value of the
can contain $[attribute] which will be replaced by the first value of the
user's ldap record's attribute named [attribute]. user's ldap record's attribute named [attribute].
""", """,
"category" : "Tools",
"data" : [
'users_ldap_groups.xml',
'security/ir.model.access.csv',
],
"installable": True,
"external_dependencies" : {
'python' : ['ldap'],
},
"category": "Tools",
"data": [
'users_ldap_groups.xml',
'security/ir.model.access.csv',
],
"installable": True,
"external_dependencies": {
'python': ['ldap'],
},
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

138
users_ldap_groups/users_ldap_groups.py

@ -23,76 +23,100 @@ from openerp.osv import fields, orm
import logging import logging
import users_ldap_groups_operators import users_ldap_groups_operators
import inspect import inspect
import sys
class CompanyLDAPGroupMapping(orm.Model): class CompanyLDAPGroupMapping(orm.Model):
_name='res.company.ldap.group_mapping'
_rec_name='ldap_attribute'
_order='ldap_attribute'
_name = 'res.company.ldap.group_mapping'
_rec_name = 'ldap_attribute'
_order = 'ldap_attribute'
def _get_operators(self, cr, uid, context=None):
operators=[]
for name, operator in inspect.getmembers(users_ldap_groups_operators,
lambda cls: inspect.isclass(cls)
and cls!=users_ldap_groups_operators.LDAPOperator):
operators.append((name, name))
return tuple(operators)
def _get_operators(self, cr, uid, context=None):
operators = []
members = inspect.getmembers(
users_ldap_groups_operators,
lambda cls: inspect.isclass(cls)
and cls != users_ldap_groups_operators.LDAPOperator)
for name, operator in members:
operators.append((name, name))
return tuple(operators)
_columns={
'ldap_id': fields.many2one('res.company.ldap', 'LDAP server',
required=True),
'ldap_attribute': fields.char('LDAP attribute', size=64,
_columns = {
'ldap_id': fields.many2one('res.company.ldap', 'LDAP server',
required=True),
'ldap_attribute': fields.char(
'LDAP attribute', size=64,
help='The LDAP attribute to check.\n' help='The LDAP attribute to check.\n'
'For active directory, use memberOf.'),
'operator': fields.selection(_get_operators, 'Operator',
'For active directory, use memberOf.'),
'operator': fields.selection(
_get_operators, 'Operator',
help='The operator to check the attribute against the value\n' help='The operator to check the attribute against the value\n'
'For active directory, use \'contains\'', required=True), 'For active directory, use \'contains\'', required=True),
'value': fields.char('Value', size=1024,
'value': fields.char(
'Value', size=1024,
help='The value to check the attribute against.\n' help='The value to check the attribute against.\n'
'For active directory, use the dn of the desired group',
'For active directory, use the dn of the desired group',
required=True), required=True),
'group': fields.many2one('res.groups', 'OpenERP group',
'group': fields.many2one(
'res.groups', 'OpenERP group',
help='The OpenERP group to assign', required=True), help='The OpenERP group to assign', required=True),
}
}
class CompanyLDAP(orm.Model): class CompanyLDAP(orm.Model):
_inherit='res.company.ldap'
_inherit = 'res.company.ldap'
_columns={
'group_mappings': fields.one2many('res.company.ldap.group_mapping',
'ldap_id', 'Group mappings',
help='Define how OpenERP groups are assigned to ldap users'),
'only_ldap_groups': fields.boolean('Only ldap groups',
help='If this is checked, manual changes to group membership are '
'undone on every login (so OpenERP groups are always synchronous '
'with LDAP groups). If not, manually added groups are preserved.')
}
_columns = {
'group_mappings': fields.one2many(
'res.company.ldap.group_mapping',
'ldap_id', 'Group mappings',
help='Define how OpenERP groups are assigned to ldap users'),
'only_ldap_groups': fields.boolean(
'Only ldap groups',
help='If this is checked, manual changes to group membership are '
'undone on every login (so OpenERP groups are always '
'synchronous with LDAP groups). If not, manually added '
'groups are preserved.')
}
_default={
'only_ldap_groups': False
}
_default = {
'only_ldap_groups': False,
}
def get_or_create_user(self, cr, uid, conf, login, ldap_entry, context=None):
user_id=super(CompanyLDAP, self).get_or_create_user(cr, uid, conf, login,
ldap_entry, context)
if not user_id:
def get_or_create_user(self, cr, uid,
conf,
login,
ldap_entry,
context=None):
_super = super(CompanyLDAP, self)
user_id = _super.get_or_create_user(cr, uid, conf, login,
ldap_entry, context)
if not user_id:
return user_id
logger = logging.getLogger('users_ldap_groups')
mappingobj = self.pool.get('res.company.ldap.group_mapping')
userobj = self.pool.get('res.users')
conf_all = self.read(cr, uid, conf['id'], ['only_ldap_groups'])
if(conf_all['only_ldap_groups']):
logger.debug('deleting all groups from user %d', user_id)
userobj.write(cr, uid,
[user_id],
{'groups_id': [(5, )]},
context=context)
mapping_ids = mappingobj.search(cr, uid,
[('ldap_id', '=', conf['id'])])
for mapping in mappingobj.read(cr, uid, mapping_ids, []):
operator = getattr(users_ldap_groups_operators,
mapping['operator'])()
logger.debug('checking mapping %s', mapping)
if operator.check_value(ldap_entry,
mapping['ldap_attribute'],
mapping['value'],
conf,
self,
logger):
logger.debug('adding user %d to group %s',
(user_id, mapping['group'][1]))
userobj.write(cr, uid, [user_id],
{'groups_id': [(4, mapping['group'][0])]},
context=context)
return user_id return user_id
logger=logging.getLogger('users_ldap_groups')
mappingobj=self.pool.get('res.company.ldap.group_mapping')
userobj=self.pool.get('res.users')
conf_all=self.read(cr, uid, conf['id'], ['only_ldap_groups'])
if(conf_all['only_ldap_groups']):
logger.debug('deleting all groups from user %d' % user_id)
userobj.write(cr, uid, [user_id], {'groups_id': [(5, )]})
for mapping in mappingobj.read(cr, uid, mappingobj.search(cr, uid,
[('ldap_id', '=', conf['id'])]), []):
operator=getattr(users_ldap_groups_operators, mapping['operator'])()
logger.debug('checking mapping %s' % mapping)
if operator.check_value(ldap_entry, mapping['ldap_attribute'],
mapping['value'], conf, self, logger):
logger.debug('adding user %d to group %s' %
(user_id, mapping['group'][1]))
userobj.write(cr, uid, [user_id],
{'groups_id': [(4, mapping['group'][0])]})
return user_id

51
users_ldap_groups/users_ldap_groups_operators.py

@ -20,25 +20,46 @@
############################################################################## ##############################################################################
from string import Template from string import Template
class LDAPOperator: class LDAPOperator:
pass
pass
class contains(LDAPOperator): class contains(LDAPOperator):
def check_value(self, ldap_entry, attribute, value, ldap_config, company,
logger):
return (attribute in ldap_entry[1]) and (value in ldap_entry[1][attribute])
def check_value(self,
ldap_entry,
attribute, value,
ldap_config,
company,
logger):
return (attribute in ldap_entry[1] and
value in ldap_entry[1][attribute])
class equals(LDAPOperator): class equals(LDAPOperator):
def check_value(self, ldap_entry, attribute, value, ldap_config, company,
logger):
return (attribute in ldap_entry[1]) and (str(value)==str(ldap_entry[1][attribute]))
def check_value(self,
ldap_entry,
attribute, value,
ldap_config,
company,
logger):
return (attribute in ldap_entry[1] and
unicode(value) == unicode(ldap_entry[1][attribute]))
class query(LDAPOperator): class query(LDAPOperator):
def check_value(self, ldap_entry, attribute, value, ldap_config, company,
logger):
query_string=Template(value).safe_substitute(dict([(attribute,
ldap_entry[1][attribute][0]) for attribute in ldap_entry[1]]))
logger.debug('evaluating query group mapping, filter: %s'%query_string)
results=company.query(ldap_config, query_string)
logger.debug(results)
return bool(results)
def check_value(self,
ldap_entry,
attribute,
value,
ldap_config,
company,
logger):
query_string = Template(value).safe_substitute(dict(
[(attr, ldap_entry[1][attribute][0]) for attr in ldap_entry[1]]
))
logger.debug('evaluating query group mapping, filter: %s',
query_string)
results = company.query(ldap_config, query_string)
logger.debug(results)
return bool(results)

30
users_ldap_mail/__openerp__.py

@ -20,21 +20,21 @@
############################################################################## ##############################################################################
{ {
'name': "LDAP mapping for user name and e-mail",
'version': "1.0",
'depends': ["auth_ldap"],
'author': "Daniel Reis (https://launchpad.com/~dreis-pt)",
'description': """\
Allows to define the LDAP attributes to use to retrieve user name and e-mail address.
'name': "LDAP mapping for user name and e-mail",
'version': "1.0",
'depends': ["auth_ldap"],
'author': "Daniel Reis (https://launchpad.com/~dreis-pt)",
'description': """\
Allows to define the LDAP attributes to use to retrieve user name and e-mail
address.
The default attribute used for the name is "cn".
For Active Directory, you might prefer to use "displayName" instead.
AD also supports the "mail" attribute, so it can be mapped into OpenERP.
The default attribute used for the name is `cn`. For Active Directory, you
might prefer to use `displayName` instead. AD also supports the `mail`
attribute, so it can be mapped into OpenERP.
""", """,
'category': "Tools",
'data': [
'users_ldap_view.xml',
],
'installable': True,
'category': "Tools",
'data': [
'users_ldap_view.xml',
],
'installable': True,
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

26
users_ldap_mail/users_ldap_model.py

@ -24,22 +24,25 @@ from openerp.osv import fields, orm
import logging import logging
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
class CompanyLDAP(orm.Model): class CompanyLDAP(orm.Model):
_inherit = 'res.company.ldap' _inherit = 'res.company.ldap'
_columns = { _columns = {
'name_attribute': fields.char('Name Attribute', size=64,
'name_attribute': fields.char(
'Name Attribute', size=64,
help="By default 'cn' is used. " help="By default 'cn' is used. "
"For ActiveDirectory you might use 'displayName' instead."), "For ActiveDirectory you might use 'displayName' instead."),
'mail_attribute': fields.char('E-mail attribute', size=64,
'mail_attribute': fields.char(
'E-mail attribute', size=64,
help="LDAP attribute to use to retrieve em-mail address."), help="LDAP attribute to use to retrieve em-mail address."),
}
}
_defaults = { _defaults = {
'name_attribute': 'cn', 'name_attribute': 'cn',
'mail_attribute': 'mail', 'mail_attribute': 'mail',
}
}
def get_ldap_dicts(self, cr, ids=None): def get_ldap_dicts(self, cr, ids=None):
"""
"""
Copy of auth_ldap's funtion, changing only the SQL, so that it returns Copy of auth_ldap's funtion, changing only the SQL, so that it returns
all fields in the table. all fields in the table.
""" """
@ -57,18 +60,19 @@ class CompanyLDAP(orm.Model):
return cr.dictfetchall() return cr.dictfetchall()
def map_ldap_attributes(self, cr, uid, conf, login, ldap_entry): def map_ldap_attributes(self, cr, uid, conf, login, ldap_entry):
values = super(CompanyLDAP, self).map_ldap_attributes(cr, uid, conf,
login, ldap_entry)
_super = super(CompanyLDAP, self)
values = _super.map_ldap_attributes(cr, uid, conf,
login, ldap_entry)
mapping = [ mapping = [
('name', 'name_attribute'), ('name', 'name_attribute'),
('email', 'mail_attribute'), ('email', 'mail_attribute'),
]
]
for value_key, conf_name in mapping: for value_key, conf_name in mapping:
try: try:
if conf[conf_name]: if conf[conf_name]:
values[value_key] = ldap_entry[1][conf[conf_name]][0] values[value_key] = ldap_entry[1][conf[conf_name]][0]
except KeyError: except KeyError:
_log.warning('No LDAP attribute "%s" found for login "%s"' % (
conf.get(conf_name), values.get('login')))
_log.warning('No LDAP attribute "%s" found for login "%s"',
conf.get(conf_name),
values.get('login'))
return values return values

11
users_ldap_populate/__openerp__.py

@ -24,8 +24,8 @@
"author": "Therp BV", "author": "Therp BV",
"category": 'Tools', "category": 'Tools',
"description": """ "description": """
This module allows to prepopulate the user database with all entries in the LDAP
database.
This module allows to prepopulate the user database with all entries in the
LDAP database.
In order to schedule the population of the user database on a regular basis, In order to schedule the population of the user database on a regular basis,
create a new scheduled action with the following properties: create a new scheduled action with the following properties:
@ -34,16 +34,17 @@ create a new scheduled action with the following properties:
- Function: action_populate - Function: action_populate
- Arguments: [res.company.ldap.id] - Arguments: [res.company.ldap.id]
Substitute res.company.ldap.id with the actual id of the res.company.ldap object you want to query.
Substitute res.company.ldap.id with the actual id of the res.company.ldap
object you want to query.
""", """,
"depends": [ "depends": [
'auth_ldap', 'auth_ldap',
],
],
"data": [ "data": [
'view/users_ldap.xml', 'view/users_ldap.xml',
'view/populate_wizard.xml', 'view/populate_wizard.xml',
],
],
'installable': True, 'installable': True,
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

8
users_ldap_populate/model/populate_wizard.py

@ -19,9 +19,10 @@
# #
############################################################################## ##############################################################################
from osv import osv, fields
from osv import orm, fields
class CompanyLDAPPopulateWizard(osv.TransientModel):
class CompanyLDAPPopulateWizard(orm.TransientModel):
_name = 'res.company.ldap.populate_wizard' _name = 'res.company.ldap.populate_wizard'
_description = 'Populate users from LDAP' _description = 'Populate users from LDAP'
_columns = { _columns = {
@ -30,11 +31,10 @@ class CompanyLDAPPopulateWizard(osv.TransientModel):
'res.company.ldap', 'LDAP Configuration'), 'res.company.ldap', 'LDAP Configuration'),
'users_created': fields.integer( 'users_created': fields.integer(
'Number of users created', readonly=True), 'Number of users created', readonly=True),
}
}
def create(self, cr, uid, vals, context=None): def create(self, cr, uid, vals, context=None):
ldap_pool = self.pool.get('res.company.ldap') ldap_pool = self.pool.get('res.company.ldap')
users_pool = self.pool.get('res.users')
if 'ldap_id' in vals: if 'ldap_id' in vals:
vals['users_created'] = ldap_pool.action_populate( vals['users_created'] = ldap_pool.action_populate(
cr, uid, vals['ldap_id'], context=context) cr, uid, vals['ldap_id'], context=context)

14
users_ldap_populate/model/users_ldap.py

@ -21,18 +21,18 @@
import re import re
from ldap.filter import filter_format from ldap.filter import filter_format
from openerp.osv import orm, fields
import openerp.exceptions
from openerp.osv import orm
import logging import logging
class CompanyLDAP(orm.Model): class CompanyLDAP(orm.Model):
_inherit = 'res.company.ldap' _inherit = 'res.company.ldap'
def action_populate(self, cr, uid, ids, context=None): def action_populate(self, cr, uid, ids, context=None):
""" """
Prepopulate the user table from one or more LDAP resources. Prepopulate the user table from one or more LDAP resources.
Obviously, the option to create users must be toggled in
Obviously, the option to create users must be toggled in
the LDAP configuration. the LDAP configuration.
Return the number of users created (as far as we can tell). Return the number of users created (as far as we can tell).
@ -54,7 +54,7 @@ class CompanyLDAP(orm.Model):
if attribute_match: if attribute_match:
login_attr = attribute_match.group(1) login_attr = attribute_match.group(1)
else: else:
raise osv.except_osv(
raise orm.except_orm(
"No login attribute found", "No login attribute found",
"Could not extract login attribute from filter %s" % "Could not extract login attribute from filter %s" %
conf['ldap_filter']) conf['ldap_filter'])
@ -93,4 +93,4 @@ class CompanyLDAP(orm.Model):
'target': 'new', 'target': 'new',
'res_id': res_id, 'res_id': res_id,
'nodestroy': True, 'nodestroy': True,
}
}

6
web_context_tunnel/__openerp__.py

@ -3,7 +3,7 @@
'category': 'Hidden', 'category': 'Hidden',
'author': 'Akretion', 'author': 'Akretion',
'license': 'AGPL-3', 'license': 'AGPL-3',
'description':"""
'description': """
Web Context Tunnel. Web Context Tunnel.
=================== ===================
@ -15,7 +15,7 @@ arguments. This is annoying as modules often need to pass extra arguments
that are not present in the base on_change signatures. As soon as two modules that are not present in the base on_change signatures. As soon as two modules
try to alter this signature to add their extra arguments, they are incompatible try to alter this signature to add their extra arguments, they are incompatible
between them unless some extra glue module make them compatible again by between them unless some extra glue module make them compatible again by
taking all extra arguments into account. But this leads to a combinatorial
taking all extra arguments into account. But this leads to a combinatorial
explosion to make modules compatible again. explosion to make modules compatible again.
The solution The solution
@ -78,7 +78,7 @@ a !python statement like context.update({'my_extra_field': my_extra_field}).
You can see an example of module conversion to use web_context_tunnel here You can see an example of module conversion to use web_context_tunnel here
for instance: for instance:
https://github.com/openerpbrasil/l10n_br_core/compare/develop...feature%2Fsale-web-context-tunnel
https://github.com/openerpbrasil/l10n_br_core/commit/33065366726a83dbc69b9f0031c81d82362fbfae
""", """,
'version': '2.0', 'version': '2.0',
'depends': ['web'], 'depends': ['web'],

3
web_context_tunnel/static/src/js/context_tunnel.js

@ -22,6 +22,3 @@ openerp.web_context_tunnel = function(instance) {
return v_context; return v_context;
}; };
}; };
// vim:et fdc=0 fdl=0:
Loading…
Cancel
Save