diff --git a/addons/account/wizard/account_chart.py b/addons/account/wizard/account_chart.py
index 38df2f7484d..de652a947f0 100644
--- a/addons/account/wizard/account_chart.py
+++ b/addons/account/wizard/account_chart.py
@@ -62,9 +62,10 @@ class account_chart(osv.osv_memory):
ORDER BY p.date_stop DESC
LIMIT 1) AS period_stop''', (fiscalyear_id, fiscalyear_id))
periods = [i[0] for i in cr.fetchall()]
- if periods and len(periods) > 1:
+ if periods:
start_period = periods[0]
- end_period = periods[1]
+ if len(periods) > 1:
+ end_period = periods[1]
res['value'] = {'period_from': start_period, 'period_to': end_period}
else:
res['value'] = {'period_from': False, 'period_to': False}
diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py
index 519660fb708..9550f77bcab 100644
--- a/addons/account_analytic_analysis/account_analytic_analysis.py
+++ b/addons/account_analytic_analysis/account_analytic_analysis.py
@@ -22,7 +22,6 @@ from dateutil.relativedelta import relativedelta
import datetime
import logging
import time
-import traceback
from openerp.osv import osv, fields
from openerp.osv.orm import intersect, except_orm
@@ -73,6 +72,7 @@ class account_analytic_invoice_line(osv.osv):
result = {}
res = self.pool.get('product.product').browse(cr, uid, product, context=local_context)
+ price = False
if price_unit is not False:
price = price_unit
elif pricelist_id:
@@ -746,29 +746,32 @@ class account_analytic_account(osv.osv):
contract_ids = ids
else:
contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True), ('type', '=', 'contract')])
- for contract in self.browse(cr, uid, contract_ids, context=context):
- try:
- invoice_values = self._prepare_invoice(cr, uid, contract, context=context)
- invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context))
- next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
- interval = contract.recurring_interval
- if contract.recurring_rule_type == 'daily':
- new_date = next_date+relativedelta(days=+interval)
- elif contract.recurring_rule_type == 'weekly':
- new_date = next_date+relativedelta(weeks=+interval)
- elif contract.recurring_rule_type == 'monthly':
- new_date = next_date+relativedelta(months=+interval)
- else:
- new_date = next_date+relativedelta(years=+interval)
- self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
- if automatic:
- cr.commit()
- except Exception:
- if automatic:
- cr.rollback()
- _logger.error(traceback.format_exc())
- else:
- raise
+ if contract_ids:
+ cr.execute('SELECT company_id, array_agg(id) as ids FROM account_analytic_account WHERE id IN %s GROUP BY company_id', (tuple(contract_ids),))
+ for company_id, ids in cr.fetchall():
+ for contract in self.browse(cr, uid, ids, context=dict(context, company_id=company_id, force_company=company_id)):
+ try:
+ invoice_values = self._prepare_invoice(cr, uid, contract, context=context)
+ invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context))
+ next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
+ interval = contract.recurring_interval
+ if contract.recurring_rule_type == 'daily':
+ new_date = next_date+relativedelta(days=+interval)
+ elif contract.recurring_rule_type == 'weekly':
+ new_date = next_date+relativedelta(weeks=+interval)
+ elif contract.recurring_rule_type == 'monthly':
+ new_date = next_date+relativedelta(months=+interval)
+ else:
+ new_date = next_date+relativedelta(years=+interval)
+ self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
+ if automatic:
+ cr.commit()
+ except Exception:
+ if automatic:
+ cr.rollback()
+ _logger.exception('Fail to create recurring invoice for contract %s', contract.code)
+ else:
+ raise
return invoice_ids
class account_analytic_account_summary_user(osv.osv):
diff --git a/addons/account_voucher/voucher_sales_purchase_view.xml b/addons/account_voucher/voucher_sales_purchase_view.xml
index d0064d6336c..f8c2d65f63b 100644
--- a/addons/account_voucher/voucher_sales_purchase_view.xml
+++ b/addons/account_voucher/voucher_sales_purchase_view.xml
@@ -248,7 +248,7 @@
-
+
diff --git a/addons/auth_crypt/auth_crypt.py b/addons/auth_crypt/auth_crypt.py
index 4651d27fe7d..6c9deb51e92 100644
--- a/addons/auth_crypt/auth_crypt.py
+++ b/addons/auth_crypt/auth_crypt.py
@@ -1,137 +1,41 @@
-#
-# Implements encrypting functions.
-#
-# Copyright (c) 2008, F S 3 Consulting Inc.
-#
-# Maintainer:
-# Alec Joseph Rivera (agifs3.ph)
-# refactored by Antony Lesuisse openerp.com>
-#
-
-import hashlib
-import hmac
import logging
-from random import sample
-from string import ascii_letters, digits
+
+from passlib.context import CryptContext
import openerp
from openerp.osv import fields, osv
_logger = logging.getLogger(__name__)
-magic_md5 = '$1$'
-magic_sha256 = '$5$'
-
-def gen_salt(length=8, symbols=None):
- if symbols is None:
- symbols = ascii_letters + digits
- return ''.join(sample(symbols, length))
-
-def md5crypt( raw_pw, salt, magic=magic_md5 ):
- """ md5crypt FreeBSD crypt(3) based on but different from md5
-
- The md5crypt is based on Mark Johnson's md5crypt.py, which in turn is
- based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
- Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
- Poul and Mark. -agi
-
- Original license:
-
- * "THE BEER-WARE LICENSE" (Revision 42):
- *
- * wrote this file. As long as you retain this
- * notice you can do whatever you want with this stuff. If we meet some
- * day, and you think this stuff is worth it, you can buy me a beer in
- * return.
- *
- * Poul-Henning Kamp
- """
- raw_pw = raw_pw.encode('utf-8')
- salt = salt.encode('utf-8')
- hash = hashlib.md5()
- hash.update( raw_pw + magic + salt )
- st = hashlib.md5()
- st.update( raw_pw + salt + raw_pw)
- stretch = st.digest()
-
- for i in range( 0, len( raw_pw ) ):
- hash.update( stretch[i % 16] )
-
- i = len( raw_pw )
-
- while i:
- if i & 1:
- hash.update('\x00')
- else:
- hash.update( raw_pw[0] )
- i >>= 1
-
- saltedmd5 = hash.digest()
-
- for i in range( 1000 ):
- hash = hashlib.md5()
-
- if i & 1:
- hash.update( raw_pw )
- else:
- hash.update( saltedmd5 )
-
- if i % 3:
- hash.update( salt )
- if i % 7:
- hash.update( raw_pw )
- if i & 1:
- hash.update( saltedmd5 )
- else:
- hash.update( raw_pw )
-
- saltedmd5 = hash.digest()
-
- itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
-
- rearranged = ''
- for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
- v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
-
- for i in range(4):
- rearranged += itoa64[v & 0x3f]
- v >>= 6
-
- v = ord( saltedmd5[11] )
-
- for i in range( 2 ):
- rearranged += itoa64[v & 0x3f]
- v >>= 6
-
- return magic + salt + '$' + rearranged
-
-def sh256crypt(cls, password, salt, magic=magic_sha256):
- iterations = 1000
- # see http://en.wikipedia.org/wiki/PBKDF2
- result = password.encode('utf8')
- for i in xrange(cls.iterations):
- result = hmac.HMAC(result, salt, hashlib.sha256).digest() # uses HMAC (RFC 2104) to apply salt
- result = result.encode('base64') # doesnt seem to be crypt(3) compatible
- return '%s%s$%s' % (magic_sha256, salt, result)
+default_crypt_context = CryptContext(
+ # kdf which can be verified by the context. The default encryption kdf is
+ # the first of the list
+ ['pbkdf2_sha512', 'md5_crypt'],
+ # deprecated algorithms are still verified as usual, but ``needs_update``
+ # will indicate that the stored hash should be replaced by a more recent
+ # algorithm. Passlib 1.6 supports an `auto` value which deprecates any
+ # algorithm but the default, but Debian only provides 1.5 so...
+ deprecated=['md5_crypt'],
+)
class res_users(osv.osv):
_inherit = "res.users"
+ def init(self, cr):
+ _logger.info("Hashing passwords, may be slow for databases with many users...")
+ cr.execute("SELECT id, password FROM res_users"
+ " WHERE password IS NOT NULL"
+ " AND password != ''")
+ for uid, pwd in cr.fetchall():
+ self._set_password(cr, openerp.SUPERUSER_ID, uid, pwd)
+
def set_pw(self, cr, uid, id, name, value, args, context):
if value:
- encrypted = md5crypt(value, gen_salt())
- cr.execute("update res_users set password='', password_crypt=%s where id=%s", (encrypted, id))
- del value
+ self._set_password(cr, uid, id, value, context=context)
def get_pw( self, cr, uid, ids, name, args, context ):
cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
- stored_pws = cr.fetchall()
- res = {}
-
- for id, stored_pw in stored_pws:
- res[id] = stored_pw
-
- return res
+ return dict(cr.fetchall())
_columns = {
'password': fields.function(get_pw, fnct_inv=set_pw, type='char', string='Password', invisible=True, store=True),
@@ -141,27 +45,51 @@ class res_users(osv.osv):
def check_credentials(self, cr, uid, password):
# convert to base_crypt if needed
cr.execute('SELECT password, password_crypt FROM res_users WHERE id=%s AND active', (uid,))
+ encrypted = None
if cr.rowcount:
- stored_password, stored_password_crypt = cr.fetchone()
- if stored_password and not stored_password_crypt:
- salt = gen_salt()
- stored_password_crypt = md5crypt(stored_password, salt)
- cr.execute("UPDATE res_users SET password='', password_crypt=%s WHERE id=%s", (stored_password_crypt, uid))
+ stored, encrypted = cr.fetchone()
+ if stored and not encrypted:
+ self._set_password(cr, uid, uid, stored)
try:
return super(res_users, self).check_credentials(cr, uid, password)
except openerp.exceptions.AccessDenied:
- # check md5crypt
- if stored_password_crypt:
- if stored_password_crypt[:len(magic_md5)] == magic_md5:
- salt = stored_password_crypt[len(magic_md5):11]
- if stored_password_crypt == md5crypt(password, salt):
- return
- elif stored_password_crypt[:len(magic_md5)] == magic_sha256:
- salt = stored_password_crypt[len(magic_md5):11]
- if stored_password_crypt == md5crypt(password, salt):
- return
- # Reraise password incorrect
+ if encrypted:
+ valid_pass, replacement = self._crypt_context(cr, uid, uid)\
+ .verify_and_update(password, encrypted)
+ if replacement is not None:
+ self._set_encrypted_password(cr, uid, uid, replacement)
+ if valid_pass:
+ return
+
raise
+ def _set_password(self, cr, uid, id, password, context=None):
+ """ Encrypts then stores the provided plaintext password for the user
+ ``id``
+ """
+ encrypted = self._crypt_context(cr, uid, id, context=context).encrypt(password)
+ self._set_encrypted_password(cr, uid, id, encrypted, context=context)
+
+ def _set_encrypted_password(self, cr, uid, id, encrypted, context=None):
+ """ Store the provided encrypted password to the database, and clears
+ any plaintext password
+
+ :param uid: id of the current user
+ :param id: id of the user on which the password should be set
+ """
+ cr.execute(
+ "UPDATE res_users SET password='', password_crypt=%s WHERE id=%s",
+ (encrypted, id))
+
+ def _crypt_context(self, cr, uid, id, context=None):
+ """ Passlib CryptContext instance used to encrypt and verify
+ passwords. Can be overridden if technical, legal or political matters
+ require different kdfs than the provided default.
+
+ Requires a CryptContext as deprecation and upgrade notices are used
+ internally
+ """
+ return default_crypt_context
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py
index 99f134a5cbd..7652420d7c6 100644
--- a/addons/auth_oauth/controllers/main.py
+++ b/addons/auth_oauth/controllers/main.py
@@ -74,7 +74,7 @@ class OAuthLogin(Home):
state = dict(
d=request.session.db,
p=provider['id'],
- r=redirect,
+ r=werkzeug.url_quote_plus(redirect),
)
token = request.params.get('token')
if token:
@@ -143,7 +143,7 @@ class OAuthController(http.Controller):
cr.commit()
action = state.get('a')
menu = state.get('m')
- redirect = state.get('r')
+ redirect = werkzeug.url_unquote_plus(state['r']) if state.get('r') else False
url = '/web'
if redirect:
url = redirect
diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js
index eed958e20d9..37669840818 100644
--- a/addons/calendar/static/src/js/base_calendar.js
+++ b/addons/calendar/static/src/js/base_calendar.js
@@ -102,7 +102,7 @@ openerp.calendar = function(instance) {
var self = this;
var action_url = '';
- action_url = _.str.sprintf('/?db=%s#id=%s&view_type=form&model=calendar.event', db, meeting_id);
+ action_url = _.str.sprintf('/web?db=%s#id=%s&view_type=form&model=calendar.event', db, meeting_id);
var reload_page = function(){
return location.replace(action_url);
diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py
index 641c490cabb..e38bf973cdf 100644
--- a/addons/crm_claim/crm_claim.py
+++ b/addons/crm_claim/crm_claim.py
@@ -45,13 +45,10 @@ class crm_claim_stage(osv.osv):
help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
'case_default': fields.boolean('Common to All Teams',
help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
- 'fold': fields.boolean('Hide in Views when Empty',
- help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
}
_defaults = {
'sequence': lambda *args: 1,
- 'fold': False,
}
class crm_claim(osv.osv):
diff --git a/addons/crm_claim/crm_claim_data.xml b/addons/crm_claim/crm_claim_data.xml
index fca7ac51178..86cf988ff45 100644
--- a/addons/crm_claim/crm_claim_data.xml
+++ b/addons/crm_claim/crm_claim_data.xml
@@ -61,7 +61,6 @@
Rejected29
-
diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml
index 43df3d069bf..6180b85660d 100644
--- a/addons/crm_claim/crm_claim_view.xml
+++ b/addons/crm_claim/crm_claim_view.xml
@@ -51,7 +51,6 @@
-
diff --git a/addons/event/report/report_event_registration.py b/addons/event/report/report_event_registration.py
index e76b5c60d1f..c4a4dfd6b2d 100644
--- a/addons/event/report/report_event_registration.py
+++ b/addons/event/report/report_event_registration.py
@@ -32,7 +32,7 @@ class report_event_registration(osv.osv):
'draft_state': fields.integer(' # No of Draft Registrations', size=20),
'confirm_state': fields.integer(' # No of Confirmed Registrations', size=20),
'seats_max': fields.integer('Max Seats'),
- 'nbevent': fields.integer('Number Of Events'),
+ 'nbevent': fields.integer('Number of Registrations'),
'event_type': fields.many2one('event.type', 'Event Type'),
'registration_state': fields.selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Attended'), ('cancel', 'Cancelled')], 'Registration State', readonly=True, required=True),
'event_state': fields.selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Event State', readonly=True, required=True),
@@ -59,7 +59,7 @@ class report_event_registration(osv.osv):
r.name AS name_registration,
e.company_id AS company_id,
e.date_begin AS event_date,
- count(e.id) AS nbevent,
+ count(r.id) AS nbevent,
CASE WHEN r.state IN ('draft') THEN r.nb_register ELSE 0 END AS draft_state,
CASE WHEN r.state IN ('open','done') THEN r.nb_register ELSE 0 END AS confirm_state,
e.type AS event_type,
diff --git a/addons/event/static/src/css/event.css b/addons/event/static/src/css/event.css
index ff5bfaba19b..53c03cc03f0 100644
--- a/addons/event/static/src/css/event.css
+++ b/addons/event/static/src/css/event.css
@@ -1,7 +1,7 @@
.oe_event_date{
border-top-left-radius:3px;
border-top-right-radius:3px;
- font-size: 48px;
+ font-size: 36px;
height: auto;
font-weight: bold;
text-align: center;
diff --git a/addons/event_sale/event_sale.py b/addons/event_sale/event_sale.py
index ed99e6a39cc..69ad0cb6af6 100644
--- a/addons/event_sale/event_sale.py
+++ b/addons/event_sale/event_sale.py
@@ -245,7 +245,8 @@ class event_ticket(osv.osv):
]
def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
- return {'value': {'price': self.pool.get("product.product").browse(cr, uid, product_id).list_price or 0}}
+ price = self.pool.get("product.product").browse(cr, uid, product_id).list_price if product_id else 0
+ return {'value': {'price': price}}
class event_registration(osv.osv):
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py
index ddc08336293..faed62f8da9 100644
--- a/addons/gamification/models/challenge.py
+++ b/addons/gamification/models/challenge.py
@@ -58,7 +58,7 @@ def start_end_date_for_period(period, default_start_date=False, default_end_date
end_date = default_end_date
if start_date and end_date:
- return (start_date.strftime(DF), end_date.strftime(DF))
+ return (datetime.strftime(start_date, DF), datetime.strftime(end_date, DF))
else:
return (start_date, end_date)
diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py
index 543c81977aa..dd1d45c05fe 100644
--- a/addons/google_calendar/google_calendar.py
+++ b/addons/google_calendar/google_calendar.py
@@ -699,7 +699,7 @@ class google_calendar(osv.AbstractModel):
for att in att_obj.browse(cr, uid, my_att_ids, context=context):
event = att.event_id
- base_event_id = att.google_internal_event_id.split('_')[0]
+ base_event_id = att.google_internal_event_id.rsplit('_', 1)[0]
if base_event_id not in event_to_synchronize:
event_to_synchronize[base_event_id] = {}
@@ -721,7 +721,7 @@ class google_calendar(osv.AbstractModel):
for event in all_event_from_google.values():
event_id = event.get('id')
- base_event_id = event_id.split('_')[0]
+ base_event_id = event_id.rsplit('_', 1)[0]
if base_event_id not in event_to_synchronize:
event_to_synchronize[base_event_id] = {}
@@ -786,7 +786,7 @@ class google_calendar(osv.AbstractModel):
if actSrc == 'OE':
self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'GG':
- new_google_event_id = event.GG.event['id'].split('_')[1]
+ new_google_event_id = event.GG.event['id'].rsplit('_', 1)[1]
if 'T' in new_google_event_id:
new_google_event_id = new_google_event_id.replace('T', '')[:-1]
else:
@@ -795,7 +795,8 @@ class google_calendar(osv.AbstractModel):
if event.GG.status:
parent_event = {}
if not event_to_synchronize[base_event][0][1].OE.event_id:
- event_to_synchronize[base_event][0][1].OE.event_id = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].split('_')[0])], ['event_id'], context=context_novirtual)[0].get('event_id')[0]
+ main_ev = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], fields=['event_id'], context=context_novirtual)
+ event_to_synchronize[base_event][0][1].OE.event_id = main_ev[0].get('event_id')[0]
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)
diff --git a/addons/hr/hr.py b/addons/hr/hr.py
index 85366adfe3b..f4719f572f0 100644
--- a/addons/hr/hr.py
+++ b/addons/hr/hr.py
@@ -225,7 +225,7 @@ class hr_employee(osv.osv):
"resized as a 128x128px image, with aspect ratio preserved. "\
"Use this field in form views or some kanban views."),
'image_small': fields.function(_get_image, fnct_inv=_set_image,
- string="Smal-sized photo", type="binary", multi="_get_image",
+ string="Small-sized photo", type="binary", multi="_get_image",
store = {
'hr.employee': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
},
diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml
index 09e7373384e..0c356706407 100644
--- a/addons/hr/hr_view.xml
+++ b/addons/hr/hr_view.xml
@@ -40,7 +40,9 @@
-
+
@@ -68,7 +70,9 @@
-
+
diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py
index 3d467e0b065..ed25b75aa83 100644
--- a/addons/hr_holidays/hr_holidays.py
+++ b/addons/hr_holidays/hr_holidays.py
@@ -99,7 +99,7 @@ class hr_holidays_status(osv.osv):
for record in self.browse(cr, uid, ids, context=context):
name = record.name
if not record.limit:
- name = name + (' (%d/%d)' % (record.leaves_taken or 0.0, record.max_leaves or 0.0))
+ name = name + (' (%g/%g)' % (record.leaves_taken or 0.0, record.max_leaves or 0.0))
res.append((record.id, name))
return res
diff --git a/addons/l10n_be_coda/__openerp__.py b/addons/l10n_be_coda/__openerp__.py
index ea1ce59eb88..6ecdb04f1d6 100644
--- a/addons/l10n_be_coda/__openerp__.py
+++ b/addons/l10n_be_coda/__openerp__.py
@@ -59,25 +59,7 @@ A removal of one object in the CODA processing results in the removal of the
associated objects. The removal of a CODA File containing multiple Bank
Statements will also remove those associated statements.
-The following reconciliation logic has been implemented in the CODA processing:
--------------------------------------------------------------------------------
- 1) The Company's Bank Account Number of the CODA statement is compared against
- the Bank Account Number field of the Company's CODA Bank Account
- configuration records (whereby bank accounts defined in type='info'
- configuration records are ignored). If this is the case an 'internal transfer'
- transaction is generated using the 'Internal Transfer Account' field of the
- CODA File Import wizard.
- 2) As a second step the 'Structured Communication' field of the CODA transaction
- line is matched against the reference field of in- and outgoing invoices
- (supported : Belgian Structured Communication Type).
- 3) When the previous step doesn't find a match, the transaction counterparty is
- located via the Bank Account Number configured on the OpenERP Customer and
- Supplier records.
- 4) In case the previous steps are not successful, the transaction is generated
- by using the 'Default Account for Unrecognized Movement' field of the CODA
- File Import wizard in order to allow further manual processing.
-
-In stead of a manual adjustment of the generated Bank Statements, you can also
+Instead of a manual adjustment of the generated Bank Statements, you can also
re-import the CODA after updating the OpenERP database with the information that
was missing to allow automatic reconciliation.
diff --git a/addons/l10n_be_coda/l10n_be_coda.py b/addons/l10n_be_coda/l10n_be_coda.py
index 0dfa9bf9db4..c4cd8ab7c2b 100644
--- a/addons/l10n_be_coda/l10n_be_coda.py
+++ b/addons/l10n_be_coda/l10n_be_coda.py
@@ -28,46 +28,4 @@ class account_bank_statement(osv.osv):
}
-class account_bank_statement_line(osv.osv):
- _inherit = 'account.bank.statement.line'
- _columns = {
- 'coda_account_number': fields.char('Account Number', help="The Counter Party Account Number")
- }
-
- def create(self, cr, uid, data, context=None):
- """
- This function creates a Bank Account Number if, for a bank statement line,
- the partner_id field and the coda_account_number field are set,
- and the account number does not exist in the database
- """
- if 'partner_id' in data and data['partner_id'] and 'coda_account_number' in data and data['coda_account_number']:
- acc_number_ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', data['coda_account_number'])])
- if len(acc_number_ids) == 0:
- try:
- type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
- type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
- self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': data['coda_account_number'], 'partner_id': data['partner_id'], 'state': type_id.code}, context=context)
- except ValueError:
- pass
- return super(account_bank_statement_line, self).create(cr, uid, data, context=context)
-
- def write(self, cr, uid, ids, vals, context=None):
- super(account_bank_statement_line, self).write(cr, uid, ids, vals, context)
- """
- Same as create function above, but for write function
- """
- if 'partner_id' in vals:
- for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
- if line.coda_account_number:
- acc_number_ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', line.coda_account_number)])
- if len(acc_number_ids) == 0:
- try:
- type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
- type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
- self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': line.coda_account_number, 'partner_id': vals['partner_id'], 'state': type_id.code}, context=context)
- except ValueError:
- pass
- return True
-
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/l10n_be_coda/l10n_be_coda_demo.xml b/addons/l10n_be_coda/l10n_be_coda_demo.xml
index 63436a60a92..ce44539a715 100644
--- a/addons/l10n_be_coda/l10n_be_coda_demo.xml
+++ b/addons/l10n_be_coda/l10n_be_coda_demo.xml
@@ -32,5 +32,27 @@
+
+
+
+
+
+
+
+ draft
+ out_invoice
+
+
+ bba
+ +++240/2838/42818+++
+
+
+ Otpez Laptop without OS
+
+ 608.89
+ 10
+
+
+
diff --git a/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt b/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt
index bc3af59e322..d0add859663 100644
--- a/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt
+++ b/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt
@@ -4,7 +4,7 @@
2200010000 GKCCBEBB 1 0
2300010000BE41063012345610 PARTNER 1 0 1
3100010001OL44483FW SCTOFBIONLO001010001001PARTNER 1 0 0
-2100020000OL4414AC8BOVSOVSOVERS00000000030444501101110015000002010237 11011113501 0
+2100020000OL4414AC8BOVSOVSOVERS0000000003044450110111001500001101240283842818 11011113501 0
2200020000 BBRUBEBB 1 0
2300020000BE61310126985517 PARTNER 2 0 1
3100020001OL4414AC8BOVSOVSOVERS001500001001PARTNER 2 1 0
diff --git a/addons/l10n_be_coda/wizard/account_coda_import.py b/addons/l10n_be_coda/wizard/account_coda_import.py
index e50f8537881..cac4cce452c 100644
--- a/addons/l10n_be_coda/wizard/account_coda_import.py
+++ b/addons/l10n_be_coda/wizard/account_coda_import.py
@@ -291,79 +291,38 @@ class account_coda_import(osv.osv_memory):
if 'counterpartyAddress' in line and line['counterpartyAddress'] != '':
note.append(_('Counter Party Address') + ': ' + line['counterpartyAddress'])
line['name'] = "\n".join(filter(None, [line['counterpartyName'], line['communication']]))
- partner = None
partner_id = None
- invoice = False
+ structured_com = ""
+ bank_account_id = False
if line['communication_struct'] and 'communication_type' in line and line['communication_type'] == '101':
- ids = self.pool.get('account.invoice').search(cr, uid, [('reference', '=', line['communication']), ('reference_type', '=', 'bba')])
-
-# Gère les communications structurées
-# TODO : à faire primer sur resolution_proposition : si la communication indique une facture, on la sélectionne
-
-# if ids:
-# invoice = self.pool.get('account.invoice').browse(cr, uid, ids[0])
-# partner = invoice.partner_id
-# partner_id = partner.id
-# if invoice.type in ['in_invoice', 'in_refund'] and line['debit'] == '1':
-# line['transaction_type'] = 'supplier'
-# elif invoice.type in ['out_invoice', 'out_refund'] and line['debit'] == '0':
-# line['transaction_type'] = 'customer'
-# line['account'] = invoice.account_id.id
-# line['reconcile'] = False
-# if invoice.type in ['in_invoice', 'out_invoice']:
-# iml_ids = self.pool.get('account.move.line').search(cr, uid, [('move_id', '=', invoice.move_id.id), ('reconcile_id', '=', False), ('account_id.reconcile', '=', True)])
-# if iml_ids:
-# line['reconcile'] = iml_ids[0]
-# if line['reconcile']:
-# voucher_vals = {
-# 'type': line['transaction_type'] == 'supplier' and 'payment' or 'receipt',
-# 'name': line['name'],
-# 'partner_id': partner_id,
-# 'journal_id': statement['journal_id'].id,
-# 'account_id': statement['journal_id'].default_credit_account_id.id,
-# 'company_id': statement['journal_id'].company_id.id,
-# 'currency_id': statement['journal_id'].company_id.currency_id.id,
-# 'date': line['entryDate'],
-# 'amount': abs(line['amount']),
-# 'period_id': statement['period_id'],
-# 'invoice_id': invoice.id,
-# }
-# context['invoice_id'] = invoice.id
-# voucher_vals.update(self.pool.get('account.voucher').onchange_partner_id(cr, uid, [],
-# partner_id=partner_id,
-# journal_id=statement['journal_id'].id,
-# amount=abs(line['amount']),
-# currency_id=statement['journal_id'].company_id.currency_id.id,
-# ttype=line['transaction_type'] == 'supplier' and 'payment' or 'receipt',
-# date=line['transactionDate'],
-# context=context
-# )['value'])
-# line_drs = []
-# for line_dr in voucher_vals['line_dr_ids']:
-# line_drs.append((0, 0, line_dr))
-# voucher_vals['line_dr_ids'] = line_drs
-# line_crs = []
-# for line_cr in voucher_vals['line_cr_ids']:
-# line_crs.append((0, 0, line_cr))
-# voucher_vals['line_cr_ids'] = line_crs
-# line['voucher_id'] = self.pool.get('account.voucher').create(cr, uid, voucher_vals, context=context)
+ structured_com = line['communication']
if 'counterpartyNumber' in line and line['counterpartyNumber']:
ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', str(line['counterpartyNumber']))])
- if ids and len(ids) > 0:
- partner = self.pool.get('res.partner.bank').browse(cr, uid, ids[0], context=context).partner_id
- partner_id = partner.id
+ if ids:
+ bank_account_id = ids[0]
+ partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
+ else:
+ #create the bank account, not linked to any partner. The reconciliation will link the partner manually
+ #chosen at the bank statement final confirmation time.
+ try:
+ type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
+ type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
+ bank_code = type_id.code
+ except ValueError:
+ bank_code = 'bank'
+ bank_account_id = self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': str(line['counterpartyNumber']), 'state': bank_code}, context=context)
if 'communication' in line and line['communication'] != '':
note.append(_('Communication') + ': ' + line['communication'])
data = {
'name': line['name'],
- 'note': "\n".join(note),
+ 'note': "\n".join(note),
'date': line['entryDate'],
'amount': line['amount'],
'partner_id': partner_id,
'statement_id': statement['id'],
- 'ref': line['ref'],
+ 'ref': structured_com,
'sequence': line['sequence'],
- 'coda_account_number': line['counterpartyNumber'],
+ 'bank_account_id': bank_account_id,
}
self.pool.get('account.bank.statement.line').create(cr, uid, data, context=context)
if statement['coda_note'] != '':
diff --git a/addons/l10n_be_invoice_bba/invoice.py b/addons/l10n_be_invoice_bba/invoice.py
index 854593a87cc..499e6e6c4ac 100644
--- a/addons/l10n_be_invoice_bba/invoice.py
+++ b/addons/l10n_be_invoice_bba/invoice.py
@@ -141,7 +141,7 @@ class account_invoice(osv.osv):
elif algorithm == 'random':
if not self.check_bbacomm(reference):
base = random.randint(1, 9999999999)
- bbacomm = str(base).rjust(7, '0')
+ bbacomm = str(base).rjust(10, '0')
base = int(bbacomm)
mod = base % 97 or 97
mod = str(mod).rjust(2, '0')
diff --git a/addons/l10n_hn/l10n_hn_base.xml b/addons/l10n_hn/l10n_hn_base.xml
index 8b9acf5c50a..2ea77baab1b 100644
--- a/addons/l10n_hn/l10n_hn_base.xml
+++ b/addons/l10n_hn/l10n_hn_base.xml
@@ -25,7 +25,7 @@
ISV por Cobrar
-
+ percent
@@ -42,7 +42,7 @@
ISV por Pagar
-
+ percent
diff --git a/addons/l10n_uk/__openerp__.py b/addons/l10n_uk/__openerp__.py
index 0590224c364..0556535296f 100644
--- a/addons/l10n_uk/__openerp__.py
+++ b/addons/l10n_uk/__openerp__.py
@@ -32,7 +32,7 @@ This is the latest UK OpenERP localisation necessary to run OpenERP accounting f
- a few other adaptations""",
'author': 'SmartMode LTD',
'website': 'http://www.smartmode.co.uk',
- 'depends': ['base_iban', 'base_vat', 'account_chart'],
+ 'depends': ['base_iban', 'base_vat', 'account_chart', 'account_anglo_saxon'],
'data': [
'data/account.account.type.csv',
'data/account.account.template.csv',
diff --git a/addons/l10n_us/__openerp__.py b/addons/l10n_us/__openerp__.py
index 90c0e6fd4ad..ea5792c1dca 100644
--- a/addons/l10n_us/__openerp__.py
+++ b/addons/l10n_us/__openerp__.py
@@ -28,7 +28,7 @@ United States - Chart of accounts.
==================================
""",
'website': 'http://www.openerp.com',
- 'depends': ['account_chart'],
+ 'depends': ['account_chart', 'account_anglo_saxon'],
'data': [
'l10n_us_account_type.xml',
'account_chart_template.xml',
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index ffc49414b2e..e4028111a73 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -176,7 +176,7 @@ class mail_notification(osv.Model):
references = message.parent_id.message_id if message.parent_id else False
# create email values
- max_recipients = 100
+ max_recipients = 50
chunks = [email_pids[x:x + max_recipients] for x in xrange(0, len(email_pids), max_recipients)]
email_ids = []
for chunk in chunks:
@@ -188,7 +188,7 @@ class mail_notification(osv.Model):
'references': references,
}
email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context))
- if force_send and len(chunks) < 6: # for more than 500 followers, use the queue system
+ if force_send and len(chunks) < 2: # for more than 50 followers, use the queue system
self.pool.get('mail.mail').send(cr, uid, email_ids, context=context)
return True
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 4ae47a96774..186787c121a 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -211,3 +211,16 @@ class mail_group(osv.Model):
return []
else:
return super(mail_group, self).get_suggested_thread(cr, uid, removed_suggested_threads, context)
+
+ def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
+ res = super(mail_group, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context)
+ group = self.browse(cr, uid, id, context=context)
+ res.update({
+ 'headers': {
+ 'Precedence': 'list',
+ }
+ })
+ if group.alias_domain:
+ res['headers']['List-Id'] = '%s.%s' % (group.alias_name, group.alias_domain)
+ res['headers']['List-Post'] = '' % (group.alias_name, group.alias_domain)
+ return res
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index a4814140479..cc12bde2436 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -204,12 +204,15 @@ class mail_mail(osv.Model):
"""
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body)
- return {
+ res = {
'body': body,
'body_alternative': body_alternative,
'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context),
'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context),
}
+ if mail.model and mail.res_id and self.pool.get(mail.model) and hasattr(self.pool[mail.model], 'message_get_email_values'):
+ res.update(self.pool[mail.model].message_get_email_values(cr, uid, mail.res_id, mail, context=context))
+ return res
def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
""" Sends the selected emails immediately, ignoring their current
@@ -268,6 +271,9 @@ class mail_mail(osv.Model):
# build an RFC2822 email.message.Message object and send it without queuing
res = None
for email in email_list:
+ email_headers = dict(headers)
+ if email.get('headers'):
+ email_headers.update(email['headers'])
msg = ir_mail_server.build_email(
email_from=mail.email_from,
email_to=email.get('email_to'),
@@ -282,7 +288,7 @@ class mail_mail(osv.Model):
object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype='html',
subtype_alternative='plain',
- headers=headers)
+ headers=email_headers)
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=mail.mail_server_id.id,
context=context)
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index dc2b98fdc93..76d3ab744d5 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -34,6 +34,7 @@ import pytz
import socket
import time
import xmlrpclib
+import re
from email.message import Message
from urllib import urlencode
@@ -48,6 +49,8 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
+mail_header_msgid_re = re.compile('<[^<>]+>')
+
def decode_header(message, header, separator=' '):
return separator.join(map(decode, filter(None, message.get_all(header, []))))
@@ -694,6 +697,16 @@ class mail_thread(osv.AbstractModel):
if record.alias_domain and record.alias_name else False
for record in self.browse(cr, SUPERUSER_ID, ids, context=context)]
+ def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
+ """ Temporary method to create custom notification email values for a given
+ model and document. This should be better to have a headers field on
+ the mail.mail model, computed when creating the notification email, but
+ this cannot be done in a stable version.
+
+ TDE FIXME: rethink this ulgy thing. """
+ res = dict()
+ return res
+
#------------------------------------------------------
# Mail gateway
#------------------------------------------------------
@@ -1301,13 +1314,13 @@ class mail_thread(osv.AbstractModel):
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
if message.get('In-Reply-To'):
- parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
+ parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To'].strip()))])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]
if message.get('References') and 'parent_id' not in msg_dict:
- parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
- [x.strip() for x in decode(message['References']).split()])])
+ msg_list = mail_header_msgid_re.findall(decode(message['References']))
+ parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', [x.strip() for x in msg_list])])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index cb068d566c1..e14a8f3dfe5 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -267,10 +267,7 @@ class mail_compose_message(osv.TransientModel):
# mass mailing: rendering override wizard static values
if mass_mail_mode and wizard.model:
# always keep a copy, reset record name (avoid browsing records)
- mail_values.update(notification=True, record_name=False)
- if hasattr(self.pool[wizard.model], 'message_new'):
- mail_values['model'] = wizard.model
- mail_values['res_id'] = res_id
+ mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False)
# auto deletion of mail_mail
if 'mail_auto_delete' in context:
mail_values['auto_delete'] = context.get('mail_auto_delete')
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index a67f88a2a28..0e44399a256 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -84,7 +84,8 @@ class MailMail(osv.Model):
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
res = super(MailMail, self).send_get_email_dict(cr, uid, mail, partner, context=context)
if mail.mailing_id and res.get('body') and res.get('email_to'):
- email_to = tools.email_split(res.get('email_to')[0])
+ emails = tools.email_split(res.get('email_to')[0])
+ email_to = emails and emails[0] or False
unsubscribe_url = self._get_unsubscribe_url(cr, uid, mail, email_to, context=context)
if unsubscribe_url:
res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p')
diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml
index 9806022498a..2939ebc3711 100644
--- a/addons/mass_mailing/views/mass_mailing.xml
+++ b/addons/mass_mailing/views/mass_mailing.xml
@@ -591,6 +591,7 @@
+
diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py
index f5b6cf466a5..ca4109c1b9a 100644
--- a/addons/mrp/mrp.py
+++ b/addons/mrp/mrp.py
@@ -1071,8 +1071,8 @@ class mrp_production(osv.osv):
return False
# Take routing location as a Source Location.
source_location_id = production.location_src_id.id
- if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
- source_location_id = production.bom_id.routing_id.location_id.id
+ if production.routing_id and production.routing_id.location_id:
+ source_location_id = production.routing_id.location_id.id
destination_location_id = production.product_id.property_stock_production.id
if not source_location_id:
diff --git a/addons/product/product.py b/addons/product/product.py
index eaed55c0b41..6fa0627db2c 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -678,6 +678,17 @@ class product_template(osv.osv):
if not context or "create_product_product" not in context:
self.create_variant_ids(cr, uid, [product_template_id], context=context)
self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
+
+ # TODO: this is needed to set given values to first variant after creation
+ # these fields should be moved to product as lead to confusion
+ related_vals = {}
+ if vals.get('ean13'):
+ related_vals['ean13'] = vals['ean13']
+ if vals.get('default_code'):
+ related_vals['default_code'] = vals['default_code']
+ if related_vals:
+ self.write(cr, uid, product_template_id, related_vals, context=context)
+
return product_template_id
def write(self, cr, uid, ids, vals, context=None):
diff --git a/addons/product_email_template/models/invoice.py b/addons/product_email_template/models/invoice.py
index d9e82e42012..d5876b510f1 100644
--- a/addons/product_email_template/models/invoice.py
+++ b/addons/product_email_template/models/invoice.py
@@ -27,7 +27,7 @@ class account_invoice(osv.Model):
template_values = Composer.onchange_template_id(
cr, uid, composer_id, line.product_id.email_template_id.id, 'comment', 'account.invoice', invoice.id
)['value']
- template_values['attachment_ids'] = [(4, id) for id in template_values.get('attachment_ids', '[]')]
+ template_values['attachment_ids'] = [(4, id) for id in template_values.get('attachment_ids', [])]
Composer.write(cr, uid, [composer_id], template_values, context=context)
Composer.send_mail(cr, uid, [composer_id], context=context)
return True
diff --git a/addons/project/project_demo.xml b/addons/project/project_demo.xml
index 54f78ba8c8b..695aa80af2a 100644
--- a/addons/project/project_demo.xml
+++ b/addons/project/project_demo.xml
@@ -50,8 +50,8 @@
project.task
+ ref('base.partner_root'),
+ ref('base.partner_demo')])]"/>
diff --git a/addons/project/res_config.py b/addons/project/res_config.py
index e7699a61ec8..187ad0327d8 100644
--- a/addons/project/res_config.py
+++ b/addons/project/res_config.py
@@ -27,11 +27,11 @@ class project_configuration(osv.osv_memory):
_inherit = 'res.config.settings'
_columns = {
- 'module_project_mrp': fields.boolean('Generate tasks from sale orders',
+ 'module_sale_service': fields.boolean('Generate tasks from sale orders',
help='This feature automatically creates project tasks from service products in sale orders. '
'More precisely, tasks are created for procurement lines with product of type \'Service\', '
'procurement method \'Make to Order\', and supply method \'Manufacture\'.\n'
- '-This installs the module project_mrp.'),
+ '-This installs the module sale_service.'),
'module_pad': fields.boolean("Use integrated collaborative note pads on task",
help='Lets the company customize which Pad installation should be used to link to new pads '
'(for example: http://ietherpad.com/).\n'
diff --git a/addons/project/res_config_view.xml b/addons/project/res_config_view.xml
index f190fba3b3a..16dce54d88b 100644
--- a/addons/project/res_config_view.xml
+++ b/addons/project/res_config_view.xml
@@ -33,8 +33,8 @@
-
-
+
+
diff --git a/addons/project_timesheet/project_timesheet.py b/addons/project_timesheet/project_timesheet.py
index 31a396369e2..f6c2321d1f9 100644
--- a/addons/project_timesheet/project_timesheet.py
+++ b/addons/project_timesheet/project_timesheet.py
@@ -137,7 +137,7 @@ class project_work(osv.osv):
amount = vals_line['unit_amount']
prod_id = vals_line['product_id']
unit = False
- timeline_id = timesheet_obj.create(cr, uid, vals=vals_line, context=context)
+ timeline_id = timesheet_obj.create(cr, uid, vals_line, context=context)
# Compute based on pricetype
amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, timeline_id,
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py
index bdd8af1afd8..09cc4c301a4 100644
--- a/addons/purchase/purchase.py
+++ b/addons/purchase/purchase.py
@@ -1310,6 +1310,7 @@ class procurement_order(osv.osv):
if available_draft_po_ids:
po_id = available_draft_po_ids[0]
po_rec = po_obj.browse(cr, uid, po_id, context=context)
+ #if the product has to be ordered earlier those in the existing PO, we replace the purchase date on the order to avoid ordering it too late
if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATE_FORMAT) > purchase_date:
po_obj.write(cr, uid, [po_id], {'date_order': purchase_date}, context=context)
#look for any other PO line in the selected PO with same product and UoM to sum quantities instead of creating a new po line
@@ -1369,8 +1370,11 @@ class product_template(osv.Model):
_inherit = 'product.template'
def _get_buy_route(self, cr, uid, context=None):
- buy_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'purchase', 'route_warehouse0_buy')[1]
- return [buy_route]
+
+ buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy')
+ if buy_route:
+ return [buy_route]
+ return []
def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, 0)
diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py
index 40689551195..26d2ff1002f 100644
--- a/addons/purchase/stock.py
+++ b/addons/purchase/stock.py
@@ -79,9 +79,7 @@ class stock_move(osv.osv):
if move.purchase_line_id:
purchase_line = move.purchase_line_id
res['invoice_line_tax_id'] = [(6, 0, [x.id for x in purchase_line.taxes_id])]
- #res['account_analytic_id'] = purc_line.order_id.project_id and sale_line.order_id.project_id.id or False
res['price_unit'] = purchase_line.price_unit
- #res['discount'] = sale_line.discount
return res
class stock_picking(osv.osv):
diff --git a/addons/report/models/report.py b/addons/report/models/report.py
index cbbee7fcebd..17635688063 100644
--- a/addons/report/models/report.py
+++ b/addons/report/models/report.py
@@ -134,14 +134,15 @@ class Report(osv.Model):
website = None
if request and hasattr(request, 'website'):
website = request.website
- values.update({
- 'time': time,
- 'translate_doc': translate_doc,
- 'editable': True, # Will active inherit_branding
- 'user': user,
- 'res_company': user.company_id,
- 'website': website,
- })
+ values.update(
+ time=time,
+ translate_doc=translate_doc,
+ editable=True, # Will active inherit_branding
+ user=user,
+ res_company=user.company_id,
+ website=website,
+ editable_no_editor=True,
+ )
return view_obj.render(cr, uid, template, values, context=context)
#--------------------------------------------------------------------------
diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py
index 94fd74e035e..f4892598332 100644
--- a/addons/sale/res_config.py
+++ b/addons/sale/res_config.py
@@ -122,7 +122,7 @@ Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
def onchange_task_work(self, cr, uid, ids, task_work, context=None):
return {'value': {
'module_project_timesheet': task_work,
- 'module_project_mrp': task_work,
+ 'module_sale_service': task_work,
}}
def onchange_timesheet(self, cr, uid, ids, timesheet, context=None):
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index 45c32a33b22..1f257376331 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -128,13 +128,12 @@ class sale_order(osv.osv):
sale_clause = ''
no_invoiced = False
for arg in args:
- if arg[1] == '=':
- if arg[2]:
- clause += 'AND inv.state = \'paid\''
- else:
- clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
- sale_clause = ', sale_order AS sale '
- no_invoiced = True
+ if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
+ clause += 'AND inv.state = \'paid\''
+ else:
+ clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
+ sale_clause = ', sale_order AS sale '
+ no_invoiced = True
cursor.execute('SELECT rel.order_id ' \
'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
@@ -686,7 +685,7 @@ class sale_order(osv.osv):
def procurement_needed(self, cr, uid, ids, context=None):
#when sale is installed only, there is no need to create procurements, that's only
- #further installed modules (project_mrp, sale_stock) that will change this.
+ #further installed modules (sale_service, sale_stock) that will change this.
sale_line_obj = self.pool.get('sale.order.line')
res = []
for order in self.browse(cr, uid, ids, context=context):
@@ -839,7 +838,7 @@ class sale_order_line(osv.osv):
def need_procurement(self, cr, uid, ids, context=None):
#when sale is installed only, there is no need to create procurements, that's only
- #further installed modules (project_mrp, sale_stock) that will change this.
+ #further installed modules (sale_service, sale_stock) that will change this.
return False
def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
diff --git a/addons/project_mrp/__init__.py b/addons/sale_service/__init__.py
similarity index 98%
rename from addons/project_mrp/__init__.py
rename to addons/sale_service/__init__.py
index c0c3172c29f..d6bf9f9551c 100644
--- a/addons/project_mrp/__init__.py
+++ b/addons/sale_service/__init__.py
@@ -19,6 +19,6 @@
#
##############################################################################
-import project_mrp
+import models
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/project_mrp/__openerp__.py b/addons/sale_service/__openerp__.py
similarity index 95%
rename from addons/project_mrp/__openerp__.py
rename to addons/sale_service/__openerp__.py
index 3730ec74113..85ea8df0735 100644
--- a/addons/project_mrp/__openerp__.py
+++ b/addons/sale_service/__openerp__.py
@@ -50,8 +50,8 @@ completed.
'website': 'http://www.openerp.com',
'images': ['images/product.jpeg', 'images/task_from_SO.jpeg'],
'depends': ['project', 'procurement', 'sale', 'procurement_jit'],
- 'data': ['project_mrp_view.xml'], #'process/project_mrp_process.xml'
- 'demo': ['project_mrp_demo.xml'],
+ 'data': ['views/sale_service_view.xml'],
+ 'demo': ['demo/sale_service_demo.xml'],
'test': ['test/project_task_procurement.yml'],
'installable': True,
'auto_install': False,
diff --git a/addons/project_mrp/project_mrp_demo.xml b/addons/sale_service/demo/sale_service_demo.xml
similarity index 100%
rename from addons/project_mrp/project_mrp_demo.xml
rename to addons/sale_service/demo/sale_service_demo.xml
diff --git a/addons/project_mrp/i18n/ar.po b/addons/sale_service/i18n/ar.po
similarity index 100%
rename from addons/project_mrp/i18n/ar.po
rename to addons/sale_service/i18n/ar.po
diff --git a/addons/project_mrp/i18n/bg.po b/addons/sale_service/i18n/bg.po
similarity index 100%
rename from addons/project_mrp/i18n/bg.po
rename to addons/sale_service/i18n/bg.po
diff --git a/addons/project_mrp/i18n/bs.po b/addons/sale_service/i18n/bs.po
similarity index 100%
rename from addons/project_mrp/i18n/bs.po
rename to addons/sale_service/i18n/bs.po
diff --git a/addons/project_mrp/i18n/ca.po b/addons/sale_service/i18n/ca.po
similarity index 100%
rename from addons/project_mrp/i18n/ca.po
rename to addons/sale_service/i18n/ca.po
diff --git a/addons/project_mrp/i18n/cs.po b/addons/sale_service/i18n/cs.po
similarity index 100%
rename from addons/project_mrp/i18n/cs.po
rename to addons/sale_service/i18n/cs.po
diff --git a/addons/project_mrp/i18n/da.po b/addons/sale_service/i18n/da.po
similarity index 100%
rename from addons/project_mrp/i18n/da.po
rename to addons/sale_service/i18n/da.po
diff --git a/addons/project_mrp/i18n/de.po b/addons/sale_service/i18n/de.po
similarity index 100%
rename from addons/project_mrp/i18n/de.po
rename to addons/sale_service/i18n/de.po
diff --git a/addons/project_mrp/i18n/el.po b/addons/sale_service/i18n/el.po
similarity index 100%
rename from addons/project_mrp/i18n/el.po
rename to addons/sale_service/i18n/el.po
diff --git a/addons/project_mrp/i18n/es.po b/addons/sale_service/i18n/es.po
similarity index 100%
rename from addons/project_mrp/i18n/es.po
rename to addons/sale_service/i18n/es.po
diff --git a/addons/project_mrp/i18n/es_AR.po b/addons/sale_service/i18n/es_AR.po
similarity index 100%
rename from addons/project_mrp/i18n/es_AR.po
rename to addons/sale_service/i18n/es_AR.po
diff --git a/addons/project_mrp/i18n/es_CR.po b/addons/sale_service/i18n/es_CR.po
similarity index 100%
rename from addons/project_mrp/i18n/es_CR.po
rename to addons/sale_service/i18n/es_CR.po
diff --git a/addons/project_mrp/i18n/es_EC.po b/addons/sale_service/i18n/es_EC.po
similarity index 100%
rename from addons/project_mrp/i18n/es_EC.po
rename to addons/sale_service/i18n/es_EC.po
diff --git a/addons/project_mrp/i18n/es_MX.po b/addons/sale_service/i18n/es_MX.po
similarity index 100%
rename from addons/project_mrp/i18n/es_MX.po
rename to addons/sale_service/i18n/es_MX.po
diff --git a/addons/project_mrp/i18n/es_VE.po b/addons/sale_service/i18n/es_VE.po
similarity index 100%
rename from addons/project_mrp/i18n/es_VE.po
rename to addons/sale_service/i18n/es_VE.po
diff --git a/addons/project_mrp/i18n/et.po b/addons/sale_service/i18n/et.po
similarity index 100%
rename from addons/project_mrp/i18n/et.po
rename to addons/sale_service/i18n/et.po
diff --git a/addons/project_mrp/i18n/fi.po b/addons/sale_service/i18n/fi.po
similarity index 100%
rename from addons/project_mrp/i18n/fi.po
rename to addons/sale_service/i18n/fi.po
diff --git a/addons/project_mrp/i18n/fr.po b/addons/sale_service/i18n/fr.po
similarity index 100%
rename from addons/project_mrp/i18n/fr.po
rename to addons/sale_service/i18n/fr.po
diff --git a/addons/project_mrp/i18n/gl.po b/addons/sale_service/i18n/gl.po
similarity index 100%
rename from addons/project_mrp/i18n/gl.po
rename to addons/sale_service/i18n/gl.po
diff --git a/addons/project_mrp/i18n/gu.po b/addons/sale_service/i18n/gu.po
similarity index 100%
rename from addons/project_mrp/i18n/gu.po
rename to addons/sale_service/i18n/gu.po
diff --git a/addons/project_mrp/i18n/hr.po b/addons/sale_service/i18n/hr.po
similarity index 100%
rename from addons/project_mrp/i18n/hr.po
rename to addons/sale_service/i18n/hr.po
diff --git a/addons/project_mrp/i18n/hu.po b/addons/sale_service/i18n/hu.po
similarity index 100%
rename from addons/project_mrp/i18n/hu.po
rename to addons/sale_service/i18n/hu.po
diff --git a/addons/project_mrp/i18n/id.po b/addons/sale_service/i18n/id.po
similarity index 100%
rename from addons/project_mrp/i18n/id.po
rename to addons/sale_service/i18n/id.po
diff --git a/addons/project_mrp/i18n/it.po b/addons/sale_service/i18n/it.po
similarity index 100%
rename from addons/project_mrp/i18n/it.po
rename to addons/sale_service/i18n/it.po
diff --git a/addons/project_mrp/i18n/ja.po b/addons/sale_service/i18n/ja.po
similarity index 100%
rename from addons/project_mrp/i18n/ja.po
rename to addons/sale_service/i18n/ja.po
diff --git a/addons/project_mrp/i18n/ko.po b/addons/sale_service/i18n/ko.po
similarity index 100%
rename from addons/project_mrp/i18n/ko.po
rename to addons/sale_service/i18n/ko.po
diff --git a/addons/project_mrp/i18n/lt.po b/addons/sale_service/i18n/lt.po
similarity index 100%
rename from addons/project_mrp/i18n/lt.po
rename to addons/sale_service/i18n/lt.po
diff --git a/addons/project_mrp/i18n/lv.po b/addons/sale_service/i18n/lv.po
similarity index 100%
rename from addons/project_mrp/i18n/lv.po
rename to addons/sale_service/i18n/lv.po
diff --git a/addons/project_mrp/i18n/mk.po b/addons/sale_service/i18n/mk.po
similarity index 100%
rename from addons/project_mrp/i18n/mk.po
rename to addons/sale_service/i18n/mk.po
diff --git a/addons/project_mrp/i18n/mn.po b/addons/sale_service/i18n/mn.po
similarity index 100%
rename from addons/project_mrp/i18n/mn.po
rename to addons/sale_service/i18n/mn.po
diff --git a/addons/project_mrp/i18n/nb.po b/addons/sale_service/i18n/nb.po
similarity index 100%
rename from addons/project_mrp/i18n/nb.po
rename to addons/sale_service/i18n/nb.po
diff --git a/addons/project_mrp/i18n/nl.po b/addons/sale_service/i18n/nl.po
similarity index 100%
rename from addons/project_mrp/i18n/nl.po
rename to addons/sale_service/i18n/nl.po
diff --git a/addons/project_mrp/i18n/nl_BE.po b/addons/sale_service/i18n/nl_BE.po
similarity index 100%
rename from addons/project_mrp/i18n/nl_BE.po
rename to addons/sale_service/i18n/nl_BE.po
diff --git a/addons/project_mrp/i18n/pl.po b/addons/sale_service/i18n/pl.po
similarity index 100%
rename from addons/project_mrp/i18n/pl.po
rename to addons/sale_service/i18n/pl.po
diff --git a/addons/project_mrp/i18n/pt.po b/addons/sale_service/i18n/pt.po
similarity index 100%
rename from addons/project_mrp/i18n/pt.po
rename to addons/sale_service/i18n/pt.po
diff --git a/addons/project_mrp/i18n/pt_BR.po b/addons/sale_service/i18n/pt_BR.po
similarity index 100%
rename from addons/project_mrp/i18n/pt_BR.po
rename to addons/sale_service/i18n/pt_BR.po
diff --git a/addons/project_mrp/i18n/ro.po b/addons/sale_service/i18n/ro.po
similarity index 100%
rename from addons/project_mrp/i18n/ro.po
rename to addons/sale_service/i18n/ro.po
diff --git a/addons/project_mrp/i18n/ru.po b/addons/sale_service/i18n/ru.po
similarity index 100%
rename from addons/project_mrp/i18n/ru.po
rename to addons/sale_service/i18n/ru.po
diff --git a/addons/project_mrp/i18n/project_mrp.pot b/addons/sale_service/i18n/sale_service.pot
similarity index 100%
rename from addons/project_mrp/i18n/project_mrp.pot
rename to addons/sale_service/i18n/sale_service.pot
diff --git a/addons/project_mrp/i18n/sl.po b/addons/sale_service/i18n/sl.po
similarity index 100%
rename from addons/project_mrp/i18n/sl.po
rename to addons/sale_service/i18n/sl.po
diff --git a/addons/project_mrp/i18n/sq.po b/addons/sale_service/i18n/sq.po
similarity index 100%
rename from addons/project_mrp/i18n/sq.po
rename to addons/sale_service/i18n/sq.po
diff --git a/addons/project_mrp/i18n/sv.po b/addons/sale_service/i18n/sv.po
similarity index 100%
rename from addons/project_mrp/i18n/sv.po
rename to addons/sale_service/i18n/sv.po
diff --git a/addons/project_mrp/i18n/tlh.po b/addons/sale_service/i18n/tlh.po
similarity index 100%
rename from addons/project_mrp/i18n/tlh.po
rename to addons/sale_service/i18n/tlh.po
diff --git a/addons/project_mrp/i18n/tr.po b/addons/sale_service/i18n/tr.po
similarity index 100%
rename from addons/project_mrp/i18n/tr.po
rename to addons/sale_service/i18n/tr.po
diff --git a/addons/project_mrp/i18n/uk.po b/addons/sale_service/i18n/uk.po
similarity index 100%
rename from addons/project_mrp/i18n/uk.po
rename to addons/sale_service/i18n/uk.po
diff --git a/addons/project_mrp/i18n/vi.po b/addons/sale_service/i18n/vi.po
similarity index 100%
rename from addons/project_mrp/i18n/vi.po
rename to addons/sale_service/i18n/vi.po
diff --git a/addons/project_mrp/i18n/zh_CN.po b/addons/sale_service/i18n/zh_CN.po
similarity index 100%
rename from addons/project_mrp/i18n/zh_CN.po
rename to addons/sale_service/i18n/zh_CN.po
diff --git a/addons/project_mrp/i18n/zh_TW.po b/addons/sale_service/i18n/zh_TW.po
similarity index 100%
rename from addons/project_mrp/i18n/zh_TW.po
rename to addons/sale_service/i18n/zh_TW.po
diff --git a/addons/sale_service/models/__init__.py b/addons/sale_service/models/__init__.py
new file mode 100644
index 00000000000..d9728749476
--- /dev/null
+++ b/addons/sale_service/models/__init__.py
@@ -0,0 +1 @@
+import sale_service
\ No newline at end of file
diff --git a/addons/project_mrp/project_mrp.py b/addons/sale_service/models/sale_service.py
similarity index 99%
rename from addons/project_mrp/project_mrp.py
rename to addons/sale_service/models/sale_service.py
index 651aa7dda48..bcce63cf66d 100644
--- a/addons/project_mrp/project_mrp.py
+++ b/addons/sale_service/models/sale_service.py
@@ -151,7 +151,7 @@ class sale_order_line(osv.osv):
_inherit = 'sale.order.line'
def need_procurement(self, cr, uid, ids, context=None):
- #when sale is installed alone, there is no need to create procurements, but with project_mrp
+ #when sale is installed alone, there is no need to create procurements, but with sale_service
#we must create a procurement for each service that has the auto_create_task boolean set to True.
for line in self.browse(cr, uid, ids, context=context):
if line.product_id and line.product_id.type == 'service' and line.product_id.auto_create_task:
diff --git a/addons/project_mrp/security/ir.model.access.csv b/addons/sale_service/security/ir.model.access.csv
similarity index 100%
rename from addons/project_mrp/security/ir.model.access.csv
rename to addons/sale_service/security/ir.model.access.csv
diff --git a/addons/project_mrp/test/project_task_procurement.yml b/addons/sale_service/test/project_task_procurement.yml
similarity index 100%
rename from addons/project_mrp/test/project_task_procurement.yml
rename to addons/sale_service/test/project_task_procurement.yml
diff --git a/addons/project_mrp/project_mrp_view.xml b/addons/sale_service/views/sale_service_view.xml
similarity index 90%
rename from addons/project_mrp/project_mrp_view.xml
rename to addons/sale_service/views/sale_service_view.xml
index 90ab6682cd5..0865d95fa0e 100644
--- a/addons/project_mrp/project_mrp_view.xml
+++ b/addons/sale_service/views/sale_service_view.xml
@@ -14,7 +14,7 @@
product.form.view.inheritproduct.template
-
+
@@ -34,8 +34,8 @@
-
- project.mrp.form.view.inherit
+
+ sale.service.form.view.inheritproject.task
diff --git a/addons/sale_stock/res_config.py b/addons/sale_stock/res_config.py
index 8c1d5c87f99..f702ca81929 100644
--- a/addons/sale_stock/res_config.py
+++ b/addons/sale_stock/res_config.py
@@ -35,7 +35,7 @@ class sale_configuration(osv.osv_memory):
help='Lets you transfer the entries under tasks defined for Project Management to '
'the Timesheet line entries for particular date and particular user with the effect of creating, editing and deleting either ways '
'and to automatically creates project tasks from procurement lines.\n'
- '-This installs the modules project_timesheet and project_mrp.'),
+ '-This installs the modules project_timesheet and sale_service.'),
'default_order_policy': fields.selection(
[('manual', 'Invoice based on sales orders'), ('picking', 'Invoice based on deliveries')],
'The default invoicing method is', default_model='sale.order',
@@ -50,7 +50,7 @@ class sale_configuration(osv.osv_memory):
implied_group='sale.group_mrp_properties',
help="Allows you to tag sales order lines with properties."),
'module_project_timesheet': fields.boolean("Project Timesheet"),
- 'module_project_mrp': fields.boolean("Project MRP"),
+ 'module_sale_service': fields.boolean("Sale Service"),
'group_route_so_lines': fields.boolean('Choose MTO, drop shipping,... on sales order lines',
implied_group='sale_stock.group_route_so_lines',
help="Allows you to choose a delivery route on sales order lines"),
@@ -63,7 +63,7 @@ class sale_configuration(osv.osv_memory):
def default_get(self, cr, uid, fields, context=None):
res = super(sale_configuration, self).default_get(cr, uid, fields, context)
# task_work, time_unit depend on other fields
- res['task_work'] = res.get('module_project_mrp') and res.get('module_project_timesheet')
+ res['task_work'] = res.get('module_sale_service') and res.get('module_project_timesheet')
return res
def get_default_sale_config(self, cr, uid, ids, context=None):
diff --git a/addons/sale_stock/res_config_view.xml b/addons/sale_stock/res_config_view.xml
index 89c2f191915..69824ffdd5a 100644
--- a/addons/sale_stock/res_config_view.xml
+++ b/addons/sale_stock/res_config_view.xml
@@ -38,7 +38,7 @@
-
+
diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py
index 04564bda56f..b8d7a03c9f2 100644
--- a/addons/stock/procurement.py
+++ b/addons/stock/procurement.py
@@ -78,7 +78,7 @@ class procurement_rule(osv.osv):
class procurement_order(osv.osv):
_inherit = "procurement.order"
_columns = {
- 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with project_mrp
+ 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with sale_service
'partner_dest_id': fields.many2one('res.partner', 'Customer Address', help="In case of dropshipping, we need to know the destination address more precisely"),
'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"),
'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"),
@@ -346,10 +346,7 @@ class procurement_order(osv.osv):
procurement_obj = self.pool.get('procurement.order')
offset = 0
ids = [1]
- if company_id:
- dom = [('company_id', '=', company_id)]
- else:
- dom = []
+ dom = company_id and [('company_id', '=', company_id)] or []
while ids:
ids = orderpoint_obj.search(cr, uid, dom, offset=offset, limit=100)
for op in orderpoint_obj.browse(cr, uid, ids, context=context):
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index 8f9cdff8165..1679cf88a67 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -3830,7 +3830,7 @@ class stock_pack_operation(osv.osv):
if pack_op.qty_done < pack_op.product_qty:
# we split the operation in two
op = self.copy(cr, uid, pack_op.id, {'product_qty': pack_op.qty_done, 'qty_done': pack_op.qty_done}, context=context)
- self.write(cr, uid, pack_op.id, {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0, 'lot_id': False}, context=context)
+ self.write(cr, uid, [pack_op.id], {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0, 'lot_id': False}, context=context)
processed_ids.append(op)
self.write(cr, uid, processed_ids, {'processed': 'true'}, context=context)
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index 5eb330b6a0d..cb2c2ab31d2 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -749,7 +749,7 @@
-->
-
+
@@ -1288,7 +1288,7 @@
ir.actions.act_windowformtree,form
-
+ {'product_receive': True, 'search_default_future': True}
diff --git a/addons/stock_account/stock_account_view.xml b/addons/stock_account/stock_account_view.xml
index ff16bf8e680..4f8c2122e55 100644
--- a/addons/stock_account/stock_account_view.xml
+++ b/addons/stock_account/stock_account_view.xml
@@ -33,7 +33,6 @@
-
diff --git a/addons/stock_account/wizard/stock_invoice_onshipping.py b/addons/stock_account/wizard/stock_invoice_onshipping.py
index 0f0c56cb5a3..1675275632a 100644
--- a/addons/stock_account/wizard/stock_invoice_onshipping.py
+++ b/addons/stock_account/wizard/stock_invoice_onshipping.py
@@ -99,16 +99,16 @@ class stock_invoice_onshipping(osv.osv_memory):
inv_type = journal2type.get(data.journal_type) or 'out_invoice'
data_pool = self.pool.get('ir.model.data')
if inv_type == "out_invoice":
- action_model,action_id = data_pool.get_object_reference(cr, uid, 'account', "action_invoice_tree1")
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree1')
elif inv_type == "in_invoice":
- action_model,action_id = data_pool.get_object_reference(cr, uid, 'account', "action_invoice_tree2")
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree2')
elif inv_type == "out_refund":
- action_model,action_id = data_pool.get_object_reference(cr, uid, 'account', "action_invoice_tree3")
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree3')
elif inv_type == "in_refund":
- action_model,action_id = data_pool.get_object_reference(cr, uid, 'account', "action_invoice_tree4")
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree4')
- if action_model:
- action_pool = self.pool[action_model]
+ if action_id:
+ action_pool = self.pool['ir.actions.act_window']
action = action_pool.read(cr, uid, action_id, context=context)
action['domain'] = "[('id','in', ["+','.join(map(str,invoice_ids))+"])]"
return action
@@ -117,7 +117,7 @@ class stock_invoice_onshipping(osv.osv_memory):
def create_invoice(self, cr, uid, ids, context=None):
context = context or {}
picking_pool = self.pool.get('stock.picking')
- data = self.browse(cr, uid, ids[0])
+ data = self.browse(cr, uid, ids[0], context=context)
journal2type = {'sale':'out_invoice', 'purchase':'in_invoice', 'sale_refund':'out_refund', 'purchase_refund':'in_refund'}
context['date_inv'] = data.invoice_date
acc_journal = self.pool.get("account.journal")
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index 638b2aefed4..c554558b34f 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -318,8 +318,9 @@ class WebsiteSurvey(http.Controller):
'filter_finish': filter_finish
})
- def prepare_result_dict(self,survey, current_filters=[]):
+ def prepare_result_dict(self,survey, current_filters=None):
"""Returns dictionary having values for rendering template"""
+ current_filters = current_filters if current_filters else []
survey_obj = request.registry['survey.survey']
result = {'survey':survey, 'page_ids': []}
for page in survey.page_ids:
@@ -347,8 +348,10 @@ class WebsiteSurvey(http.Controller):
total = ceil(total_record / float(limit))
return range(1, int(total + 1))
- def get_graph_data(self, question, current_filters=[]):
+ def get_graph_data(self, question, current_filters=None):
'''Returns formatted data required by graph library on basis of filter'''
+ # TODO refactor this terrible method and merge it with prepare_result_dict
+ current_filters = current_filters if current_filters else []
survey_obj = request.registry['survey.survey']
result = []
if question.type == 'multiple_choice':
@@ -360,9 +363,8 @@ class WebsiteSurvey(http.Controller):
data = survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)
for answer in data['answers']:
values = []
- for res in data['result']:
- if res[1] == answer:
- values.append({'text': data['rows'][res[0]], 'count': data['result'][res]})
+ for row in data['rows']:
+ values.append({'text': data['rows'].get(row), 'count': data['result'].get((row, answer))})
result.append({'key': data['answers'].get(answer), 'values': values})
return json.dumps(result)
diff --git a/addons/survey/survey.py b/addons/survey/survey.py
index 140e3e97d65..caecd16b7e1 100644
--- a/addons/survey/survey.py
+++ b/addons/survey/survey.py
@@ -26,6 +26,7 @@ from openerp.addons.website.models.website import slug
from urlparse import urljoin
from itertools import product
from collections import Counter
+from collections import OrderedDict
import datetime
import logging
@@ -289,8 +290,7 @@ class survey_survey(osv.Model):
:param finished: True for completely filled survey,Falser otherwise.
:returns list of filtered user_input_ids.
'''
- if context is None:
- context = {}
+ context = context if context else {}
if filters:
input_line_obj = self.pool.get('survey.user_input_line')
domain_filter, choice, filter_display_data = [], [], []
@@ -339,22 +339,27 @@ class survey_survey(osv.Model):
filter_display_data.append({'question_text': question.question, 'labels': [label.value for label in labels]})
return filter_display_data
- def prepare_result(self, cr, uid, question, current_filters=[], context=None):
+ def prepare_result(self, cr, uid, question, current_filters=None, context=None):
''' Compute statistical data for questions by counting number of vote per choice on basis of filter '''
- if context is None:
- context = {}
+ current_filters = current_filters if current_filters else []
+ context = context if context else {}
+
#Calculate and return statistics for choice
if question.type in ['simple_choice', 'multiple_choice']:
- result_summary = {}
- [result_summary.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
- for input_line in question.user_input_line_ids:
- if input_line.answer_type == 'suggestion' and result_summary.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
- result_summary[input_line.value_suggested.id]['count'] += 1
- result_summary = result_summary.values()
+ result_summary = []
+ for label in question.labels_ids:
+ count = 0
+ for input_line in question.user_input_line_ids:
+ if input_line.answer_type == 'suggestion' and input_line.value_suggested.id == label.id and (not current_filters or input_line.user_input_id.id in current_filters):
+ count = count + 1
+ label_summary = {'text': label.value, 'count': count, 'answer_id': label.id}
+ result_summary = result_summary + [label_summary]
#Calculate and return statistics for matrix
if question.type == 'matrix':
- rows, answers, res = {}, {}, {}
+ rows = OrderedDict()
+ answers = OrderedDict()
+ res = dict()
[rows.update({label.id: label.value}) for label in question.labels_ids_2]
[answers.update({label.id: label.value}) for label in question.labels_ids]
for cell in product(rows.keys(), answers.keys()):
@@ -386,10 +391,10 @@ class survey_survey(osv.Model):
'most_comman': Counter(all_inputs).most_common(5)})
return result_summary
- def get_input_summary(self, cr, uid, question, current_filters=[], context=None):
+ def get_input_summary(self, cr, uid, question, current_filters=None, context=None):
''' Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter '''
- if context is None:
- context = {}
+ current_filters = current_filters if current_filters else []
+ context = context if context else {}
result = {}
if question.survey_id.user_input_ids:
total_input_ids = current_filters or [input_id.id for input_id in question.survey_id.user_input_ids if input_id.state != 'new']
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css
index b234e2738df..91146789602 100644
--- a/addons/web/static/src/css/base.css
+++ b/addons/web/static/src/css/base.css
@@ -2325,7 +2325,7 @@
.openerp .oe_form .oe_form_field_image .oe_form_field_image_controls {
position: absolute;
top: 1px;
- padding: 4px 0;
+ padding: 6px 0;
width: 100%;
display: none;
text-align: center;
@@ -2373,7 +2373,7 @@
}
.openerp .oe_fileupload .oe_add button.oe_attach .oe_e {
position: relative;
- top: -1px;
+ top: -10px;
left: -9px;
}
.openerp .oe_fileupload .oe_add input.oe_form_binary_file {
@@ -2647,11 +2647,11 @@
.openerp .oe_list_editable .oe_list_content td.oe_list_field_cell {
padding: 4px 6px 3px;
}
-.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell:not(.oe_readonly) {
+.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell {
color: transparent;
text-shadow: none;
}
-.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell:not(.oe_readonly) * {
+.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell * {
visibility: hidden;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_drop_down_button {
@@ -2667,6 +2667,13 @@
min-width: 0;
max-width: none;
}
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_list_field_handle {
+ color: transparent;
+}
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_readonly {
+ padding: 4px 6px 3px;
+ text-align: left;
+}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea {
height: 27px;
-moz-border-radius: 0;
@@ -2678,9 +2685,14 @@
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field select {
min-width: 0;
}
-.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input {
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float.oe_readonly, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer.oe_readonly {
+ padding: 6px 0px 0px;
text-align: right;
+ max-width: 100px;
+}
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input {
width: 100% !important;
+ text-align: right;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master {
width: 100% !important;
@@ -3324,6 +3336,9 @@ body.oe_single_form .oe_single_form_container {
.openerp_ie ul.oe_form_status li.oe_active > .arrow span, .openerp_ie ul.oe_form_status_clickable li.oe_active > .arrow span {
background-color: #729fcf !important;
}
+.openerp_ie .oe_webclient {
+ height: auto !important;
+}
@media print {
.openerp {
@@ -3447,6 +3462,39 @@ input[type="radio"], input[type="checkbox"] {
opacity: 0.6;
}
+/* ---- EDITOR TOUR ---- {{{ */
+div.tour-backdrop {
+ z-index: 2009;
+}
+
+.popover.tour.orphan .arrow {
+ display: none;
+}
+.popover.tour .popover-navigation {
+ padding: 9px 14px;
+}
+.popover.tour .popover-navigation *[data-role="end"] {
+ float: right;
+}
+.popover.tour .popover-navigation *[data-role="next"], .popover.tour .popover-navigation *[data-role="end"] {
+ cursor: pointer;
+}
+
+.popover.fixed {
+ position: fixed;
+}
+
+.tour-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1100;
+ background-color: black;
+ opacity: 0.8;
+}
+
body {
overflow: auto;
}
diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass
index 093ebf6cb0b..07d683268f1 100644
--- a/addons/web/static/src/css/base.sass
+++ b/addons/web/static/src/css/base.sass
@@ -1902,7 +1902,7 @@ $sheet-padding: 16px
.oe_form_field_image_controls
position: absolute
top: 1px
- padding: 4px 0
+ padding: 6px 0
width: 100%
display: none
text-align: center
@@ -1939,7 +1939,7 @@ $sheet-padding: 16px
text-shadow: none
.oe_e
position: relative
- top: -1px
+ top: -10px
left: -9px
input.oe_form_binary_file
display: inline-block
@@ -2138,7 +2138,7 @@ $sheet-padding: 16px
.oe_list_editable .oe_list_content td.oe_list_field_cell
padding: 4px 6px 3px
.oe_list.oe_list_editable.oe_editing
- .oe_edition .oe_list_field_cell:not(.oe_readonly)
+ .oe_edition .oe_list_field_cell
*
visibility: hidden
color: transparent
@@ -2150,6 +2150,11 @@ $sheet-padding: 16px
.oe_input_icon
margin-top: 5px
.oe_form_field
+ &.oe_list_field_handle
+ color: transparent
+ &.oe_readonly
+ padding: 4px 6px 3px
+ text-align: left
min-width: 0
max-width: none
input, textarea
@@ -2160,9 +2165,13 @@ $sheet-padding: 16px
input, textarea, select
min-width: 0
&.oe_form_field_float,&.oe_form_view_integer
- input
+ &.oe_readonly
+ padding: 6px 0px 0px
text-align: right
+ max-width: 100px
+ input
width: 100% !important
+ text-align: right
&.oe_form_field_datetime,&.oe_form_field_date
input.oe_datepicker_master
width: 100% !important
@@ -2691,6 +2700,8 @@ body.oe_single_form
> .arrow span
background-color: #729fcf !important
+ .oe_webclient
+ height: auto !important
// }}}
// @media print {{{
@@ -2799,6 +2810,33 @@ input[type="radio"], input[type="checkbox"]
background-color: black
opacity: 0.6000000238418579
+/* ---- EDITOR TOUR ---- {{{ */
+
+div.tour-backdrop
+ z-index: 2009
+.popover.tour
+ &.orphan .arrow
+ display: none
+ .popover-navigation
+ padding: 9px 14px
+ *[data-role="end"]
+ float: right
+ *[data-role="next"],*[data-role="end"]
+ cursor: pointer
+.popover.fixed
+ position: fixed
+.tour-backdrop
+ position: fixed
+ top: 0
+ right: 0
+ bottom: 0
+ left: 0
+ z-index: 1100
+ background-color: #000
+ opacity: 0.8
+
+
+// }}}
body
overflow: auto
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
index 3c32dbe0426..f08dc31021a 100644
--- a/addons/web/static/src/js/formats.js
+++ b/addons/web/static/src/js/formats.js
@@ -233,7 +233,8 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
value = value.replace(instance.web._t.database.parameters.thousands_sep, "");
} while(tmp !== value);
tmp = Number(value);
- if (isNaN(tmp))
+ // do not accept not numbers or float values
+ if (isNaN(tmp) || tmp % 1)
throw new Error(_.str.sprintf(_t("'%s' is not a correct integer"), value));
return tmp;
case 'float':
@@ -268,6 +269,11 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
case 'datetime':
var datetime = Date.parseExact(
value, (date_pattern + ' ' + time_pattern));
+ if (datetime !== null)
+ return instance.web.datetime_to_str(datetime);
+ datetime = Date.parseExact(value.replace(/\d+/g, function(m){
+ return m.length === 1 ? "0" + m : m ;
+ }), (date_pattern + ' ' + time_pattern));
if (datetime !== null)
return instance.web.datetime_to_str(datetime);
datetime = Date.parse(value);
@@ -276,6 +282,11 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
case 'date':
var date = Date.parseExact(value, date_pattern);
+ if (date !== null)
+ return instance.web.date_to_str(date);
+ date = Date.parseExact(value.replace(/\d+/g, function(m){
+ return m.length === 1 ? "0" + m : m ;
+ }), date_pattern);
if (date !== null)
return instance.web.date_to_str(date);
date = Date.parse(value);
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
index 5653447566b..ac441d8eed6 100644
--- a/addons/web/static/src/js/search.js
+++ b/addons/web/static/src/js/search.js
@@ -346,11 +346,11 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
'keydown .oe_searchview_input, .oe_searchview_facet': function (e) {
switch(e.which) {
case $.ui.keyCode.LEFT:
- this.focusPreceding(this);
+ this.focusPreceding(e.target);
e.preventDefault();
break;
case $.ui.keyCode.RIGHT:
- this.focusFollowing(this);
+ this.focusFollowing(e.target);
e.preventDefault();
break;
}
diff --git a/addons/web/static/src/js/tour.js b/addons/web/static/src/js/tour.js
new file mode 100644
index 00000000000..961fd63f4cd
--- /dev/null
+++ b/addons/web/static/src/js/tour.js
@@ -0,0 +1,545 @@
+(function () {
+ 'use strict';
+
+// raise an error in test mode if openerp don't exist
+if (typeof openerp === "undefined") {
+ var error = "openerp is undefined"
+ + "\nhref: " + window.location.href
+ + "\nreferrer: " + document.referrer
+ + "\nlocalStorage: " + window.localStorage.getItem("tour");
+ if (typeof $ !== "undefined") {
+ error += '\n\n' + $("body").html();
+ }
+ throw new Error(error);
+}
+
+var website = openerp.website;
+
+// don't rewrite T in test mode
+if (typeof openerp.Tour !== "undefined") {
+ return;
+}
+
+/////////////////////////////////////////////////
+
+
+/* jQuery selector to match exact text inside an element
+ * :containsExact() - case insensitive
+ * :containsExactCase() - case sensitive
+ * :containsRegex() - set by user ( use: $(el).find(':containsRegex(/(red|blue|yellow)/gi)') )
+ */
+$.extend($.expr[':'],{
+ containsExact: function(a,i,m){
+ return $.trim(a.innerHTML.toLowerCase()) === m[3].toLowerCase();
+ },
+ containsExactCase: function(a,i,m){
+ return $.trim(a.innerHTML) === m[3];
+ },
+ // Note all escaped characters need to be double escaped
+ // inside of the containsRegex, so "\(" needs to be "\\("
+ containsRegex: function(a,i,m){
+ var regreg = /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})$/,
+ reg = regreg.exec(m[3]);
+ return reg ? new RegExp(reg[1], reg[2]).test($.trim(a.innerHTML)) : false;
+ }
+});
+$.ajaxSetup({
+ beforeSend:function(){
+ $.ajaxBusy = ($.ajaxBusy|0) + 1;
+ },
+ complete:function(){
+ $.ajaxBusy--;
+ }
+});
+
+/////////////////////////////////////////////////
+
+var localStorage = window.localStorage;
+
+var Tour = {
+ tours: {},
+ defaultDelay: 50,
+ retryRunningDelay: 1000,
+ errorDelay: 5000,
+ state: null,
+ $element: null,
+ timer: null,
+ testtimer: null,
+ currentTimer: null,
+ register: function (tour) {
+ if (tour.mode !== "test") tour.mode = "tutorial";
+ Tour.tours[tour.id] = tour;
+ },
+ run: function (tour_id, mode) {
+ var tour = Tour.tours[tour_id];
+ if (!tour) {
+ Tour.error(null, "Can't run '"+tour_id+"' (tour undefined)");
+ }
+ this.time = new Date().getTime();
+ if (tour.path && !window.location.href.match(new RegExp("("+Tour.getLang()+")?"+tour.path+"#?$", "i"))) {
+ var href = Tour.getLang()+tour.path;
+ console.log("Tour '"+tour_id+"' Begin from run method (redirection to "+href+")");
+ Tour.saveState(tour.id, mode || tour.mode, -1, 0);
+ $(document).one("ajaxStop", Tour.running);
+ window.location.href = href;
+ } else {
+ console.log("Tour '"+tour_id+"' Begin from run method");
+ Tour.saveState(tour.id, mode || tour.mode, 0, 0);
+ Tour.running();
+ }
+ },
+ registerSteps: function (tour, mode) {
+ if (tour.register) {
+ return;
+ }
+ tour.register = true;
+
+ for (var index=0, len=tour.steps.length; index 0 && tour.steps[index-1] &&
+ tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
+ step.waitNot = '.popover.tour.fade.in:visible';
+ }
+ if (!step.waitFor && index > 0 && tour.steps[index-1].snippet) {
+ step.waitFor = '.oe_overlay_options .oe_options:visible';
+ }
+
+
+ var snippet = step.element && step.element.match(/#oe_snippets (.*) \.oe_snippet_thumbnail/);
+ if (snippet) {
+ step.snippet = snippet[1];
+ } else if (step.snippet) {
+ step.element = '#oe_snippets '+step.snippet+' .oe_snippet_thumbnail';
+ }
+
+ if (!step.element) {
+ step.element = "body";
+ step.orphan = true;
+ step.backdrop = true;
+ } else {
+ step.popover = step.popover || {};
+ step.popover.arrow = true;
+ }
+ }
+ if (tour.steps[index-1] &&
+ tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
+ var step = {
+ _title: "close popover and finish",
+ id: index,
+ waitNot: '.popover.tour.fade.in:visible'
+ };
+ tour.steps.push(step);
+ }
+
+ // rendering bootstrap tour and popover
+ if (mode !== "test") {
+ for (var index=0, len=tour.steps.length; index
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index c41eae84ff8..3c84533e716 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -320,22 +320,24 @@ class ir_actions_act_window(osv.osv):
ids = [ids]
results = super(ir_actions_act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load)
- if not fields or 'help' in fields:
- context = dict(context or {})
- eval_dict = {
- 'active_model': context.get('active_model'),
- 'active_id': context.get('active_id'),
- 'active_ids': context.get('active_ids'),
- 'uid': uid,
- }
- for res in results:
- model = res.get('res_model')
- if model and self.pool.get(model):
- try:
- with tools.mute_logger("openerp.tools.safe_eval"):
- eval_context = eval(res['context'] or "{}", eval_dict) or {}
- except Exception:
- continue
+ context = dict(context or {})
+ eval_dict = {
+ 'active_model': context.get('active_model'),
+ 'active_id': context.get('active_id'),
+ 'active_ids': context.get('active_ids'),
+ 'uid': uid,
+ 'context': context,
+ }
+ for res in results:
+ model = res.get('res_model')
+ if model and self.pool.get(model):
+ try:
+ with tools.mute_logger("openerp.tools.safe_eval"):
+ eval_context = eval(res['context'] or "{}", eval_dict) or {}
+ res['context'] = str(eval_context)
+ except Exception:
+ continue
+ if not fields or 'help' in fields:
custom_context = dict(context, **eval_context)
res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
if ids_int:
@@ -489,6 +491,8 @@ class ir_actions_server(osv.osv):
"based on the sequence. Low number means high priority."),
'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
help="Base model on which the server action runs."),
+ 'model_name': fields.related('model_id', 'model', type='char',
+ string='Model Name', readonly=True),
'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
help='More menu entry.'),
# Client Action
@@ -648,6 +652,10 @@ class ir_actions_server(osv.osv):
'wkf_field_id': False,
'crud_model_id': model_id,
}
+
+ if model_id:
+ values['model_name'] = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
+
return {'value': values}
def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
@@ -751,6 +759,7 @@ class ir_actions_server(osv.osv):
crud_model_name = False
if crud_model_id:
crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model
+
values = {'link_field_id': False, 'crud_model_name': crud_model_name}
return {'value': values}
diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml
index 6cb498e9ebf..2aa520d7c55 100644
--- a/openerp/addons/base/ir/ir_actions.xml
+++ b/openerp/addons/base/ir/ir_actions.xml
@@ -349,8 +349,9 @@
Check to attach the newly created record to the record on which the server action runs.
+
diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py
index ac3091e33b9..21c8b7ef941 100644
--- a/openerp/addons/base/ir/ir_http.py
+++ b/openerp/addons/base/ir/ir_http.py
@@ -76,17 +76,23 @@ class ir_http(osv.AbstractModel):
request.uid = request.session.uid
def _authenticate(self, auth_method='user'):
- if request.session.uid:
- try:
- request.session.check_security()
- # what if error in security.check()
- # -> res_users.check()
- # -> res_users.check_credentials()
- except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
- # All other exceptions mean undetermined status (e.g. connection pool full),
- # let them bubble up
- request.session.logout()
- getattr(self, "_auth_method_%s" % auth_method)()
+ try:
+ if request.session.uid:
+ try:
+ request.session.check_security()
+ # what if error in security.check()
+ # -> res_users.check()
+ # -> res_users.check_credentials()
+ except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
+ # All other exceptions mean undetermined status (e.g. connection pool full),
+ # let them bubble up
+ request.session.logout()
+ getattr(self, "_auth_method_%s" % auth_method)()
+ except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
+ raise
+ except Exception:
+ _logger.exception("Exception during request Authentication.")
+ raise openerp.exceptions.AccessDenied()
return auth_method
def _handle_exception(self, exception):
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
index bb6c8cb2847..f0d15fad609 100644
--- a/openerp/addons/base/ir/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb.py
@@ -257,8 +257,13 @@ class QWeb(orm.AbstractModel):
uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
if not can_see:
+ if qwebcontext.get('editable') and not qwebcontext.get('editable_no_editor'):
+ errmsg = _("Editor disabled because some content can not be seen by a user who does not belong to the groups %s")
+ raise openerp.http.Retry(
+ _("User does not belong to groups %s") % attribute_value, {
+ 'editable_no_editor': errmsg % attribute_value
+ })
return ''
- continue
if isinstance(attribute_value, unicode):
attribute_value = attribute_value.encode("utf8")
@@ -302,7 +307,7 @@ class QWeb(orm.AbstractModel):
for current_node in element.childNodes:
try:
g_inner.append(self.render_node(current_node, qwebcontext))
- except QWebException:
+ except (QWebException, openerp.http.Retry):
raise
except Exception:
template = qwebcontext.get('__template__')
diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py
index eb154d4a05f..df673f95b32 100644
--- a/openerp/addons/base/module/module.py
+++ b/openerp/addons/base/module/module.py
@@ -496,6 +496,7 @@ class module(osv.osv):
'params': {'menu_id': menu_ids and menu_ids[0] or False}
}
+ #TODO remove me in master, not called anymore
def button_immediate_uninstall(self, cr, uid, ids, context=None):
"""
Uninstall the selected module(s) immediately and fully,
diff --git a/openerp/addons/base/module/module_view.xml b/openerp/addons/base/module/module_view.xml
index 3f92c79e2cf..7cc46539e8f 100644
--- a/openerp/addons/base/module/module_view.xml
+++ b/openerp/addons/base/module/module_view.xml
@@ -82,8 +82,7 @@
-
+
diff --git a/openerp/addons/base/module/wizard/base_module_upgrade.py b/openerp/addons/base/module/wizard/base_module_upgrade.py
index c75058ab835..365895be7ac 100644
--- a/openerp/addons/base/module/wizard/base_module_upgrade.py
+++ b/openerp/addons/base/module/wizard/base_module_upgrade.py
@@ -68,6 +68,20 @@ class base_module_upgrade(osv.osv_memory):
res = mod_obj.read(cr, uid, ids, ['name','state'], context)
return {'module_info': '\n'.join(map(lambda x: x['name']+' : '+x['state'], res))}
+ def upgrade_module_cancel(self, cr, uid, ids, context=None):
+ mod_obj = self.pool.get('ir.module.module')
+ to_installed_ids = mod_obj.search(cr, uid, [
+ ('state', 'in', ['to upgrade', 'to remove'])])
+ if to_installed_ids:
+ mod_obj.write(cr, uid, to_installed_ids, {'state': 'installed'}, context=context)
+
+ to_uninstalled_ids = mod_obj.search(cr, uid, [
+ ('state', '=', 'to install')])
+ if to_uninstalled_ids:
+ mod_obj.write(cr, uid, to_uninstalled_ids, {'state': 'uninstalled'}, context=context)
+
+ return {'type': 'ir.actions.act_window_close'}
+
def upgrade_module(self, cr, uid, ids, context=None):
ir_module = self.pool.get('ir.module.module')
diff --git a/openerp/addons/base/module/wizard/base_module_upgrade_view.xml b/openerp/addons/base/module/wizard/base_module_upgrade_view.xml
index 25e7ac0a3df..ae135b8fa90 100644
--- a/openerp/addons/base/module/wizard/base_module_upgrade_view.xml
+++ b/openerp/addons/base/module/wizard/base_module_upgrade_view.xml
@@ -7,14 +7,15 @@
base.module.upgrade
diff --git a/openerp/addons/base/res/res_bank.py b/openerp/addons/base/res/res_bank.py
index cc29c50928a..e8477ec18ff 100644
--- a/openerp/addons/base/res/res_bank.py
+++ b/openerp/addons/base/res/res_bank.py
@@ -128,8 +128,7 @@ class res_partner_bank(osv.osv):
change_default=True, domain="[('country_id','=',country_id)]"),
'company_id': fields.many2one('res.company', 'Company',
ondelete='cascade', help="Only if this bank account belong to your company"),
- 'partner_id': fields.many2one('res.partner', 'Account Owner', required=True,
- ondelete='cascade', select=True),
+ 'partner_id': fields.many2one('res.partner', 'Account Owner', ondelete='cascade', select=True),
'state': fields.selection(_bank_type_get, 'Bank Account Type', required=True,
change_default=True),
'sequence': fields.integer('Sequence'),
diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py
index 5bbd377f29d..56065465441 100644
--- a/openerp/addons/base/res/res_config.py
+++ b/openerp/addons/base/res/res_config.py
@@ -28,6 +28,7 @@ from openerp.osv import osv, fields
from openerp.tools import ustr
from openerp.tools.translate import _
from openerp import exceptions
+from lxml import etree
_logger = logging.getLogger(__name__)
@@ -221,10 +222,10 @@ class res_config_installer(osv.osv_memory, res_config_module_installation_mixin)
_install_if = {
('sale','crm'): ['sale_crm'],
- ('sale','project'): ['project_mrp'],
+ ('sale','project'): ['sale_service'],
}
- will install both ``sale_crm`` and ``project_mrp`` if all of
+ will install both ``sale_crm`` and ``sale_service`` if all of
``sale``, ``crm`` and ``project`` are selected for installation.
Hook methods
@@ -434,6 +435,43 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
def copy(self, cr, uid, id, values, context=None):
raise osv.except_osv(_("Cannot duplicate configuration!"), "")
+ def fields_view_get(self, cr, user, view_id=None, view_type='form',
+ context=None, toolbar=False, submenu=False):
+ ret_val = super(res_config_settings, self).fields_view_get(
+ cr, user, view_id=view_id, view_type=view_type, context=context,
+ toolbar=toolbar, submenu=submenu)
+
+ doc = etree.XML(ret_val['arch'])
+
+ for field in ret_val['fields']:
+ if not field.startswith("module_"):
+ continue
+ for node in doc.xpath("//field[@name='%s']" % field):
+ if 'on_change' not in node.attrib:
+ node.set("on_change",
+ "onchange_module(%s, '%s')" % (field, field))
+
+ ret_val['arch'] = etree.tostring(doc)
+ return ret_val
+
+ def onchange_module(self, cr, uid, ids, field_value, module_name, context={}):
+ module_pool = self.pool.get('ir.module.module')
+ module_ids = module_pool.search(
+ cr, uid, [('name', '=', module_name.replace("module_", '')),
+ ('state','in', ['to install', 'installed', 'to upgrade'])],
+ context=context)
+
+ if module_ids and not field_value:
+ dep_ids = module_pool.downstream_dependencies(cr, uid, module_ids, context=context)
+ dep_name = [x.shortdesc for x in module_pool.browse(
+ cr, uid, dep_ids + module_ids, context=context)]
+ message = '\n'.join(dep_name)
+ return {'warning': {'title': _('Warning!'),
+ 'message':
+ _('Disabling this option will also uninstall the following modules \n%s' % message)
+ }}
+ return {}
+
def _get_classified_fields(self, cr, uid, context=None):
""" return a dictionary with the fields classified by category::
diff --git a/openerp/http.py b/openerp/http.py
index 98a6556570a..fcbd2581350 100644
--- a/openerp/http.py
+++ b/openerp/http.py
@@ -270,8 +270,6 @@ class WebRequest(object):
to abitrary responses. Anything returned (except None) will
be used as response."""
self._failed = exception # prevent tx commit
- if isinstance(exception, werkzeug.exceptions.HTTPException):
- return exception
raise
def _call_function(self, *args, **kwargs):
@@ -538,6 +536,15 @@ class HttpRequest(WebRequest):
params.pop('session_id', None)
self.params = params
+ def _handle_exception(self, exception):
+ """Called within an except block to allow converting exceptions
+ to abitrary responses. Anything returned (except None) will
+ be used as response."""
+ try:
+ return super(HttpRequest, self)._handle_exception(exception)
+ except werkzeug.exceptions.HTTPException, e:
+ return e
+
def dispatch(self):
if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
headers = {
@@ -1037,6 +1044,15 @@ mimetypes.add_type('application/font-woff', '.woff')
mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
mimetypes.add_type('application/x-font-ttf', '.ttf')
+class Retry(RuntimeError):
+ """ Exception raised during QWeb rendering to signal that the rendering
+ should be retried with the provided ``render_updates`` dict merged into
+ the previous rendering context
+ """
+ def __init__(self, name, render_updates=None):
+ super(Retry, self).__init__(name)
+ self.updates = render_updates or {}
+
class Response(werkzeug.wrappers.Response):
""" Response object passed through controller route chain.
@@ -1077,7 +1093,13 @@ class Response(werkzeug.wrappers.Response):
def render(self):
view_obj = request.registry["ir.ui.view"]
uid = self.uid or request.uid or openerp.SUPERUSER_ID
- return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
+ while True:
+ try:
+ return view_obj.render(
+ request.cr, uid, self.template, self.qcontext,
+ context=request.context)
+ except Retry, e:
+ self.qcontext.update(e.updates)
def flatten(self):
self.response.append(self.render())
diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py
index 76a9f046dc6..b0f6141e78f 100644
--- a/openerp/modules/graph.py
+++ b/openerp/modules/graph.py
@@ -78,7 +78,7 @@ class Graph(dict):
)
## and we update the default values with values from the database
- additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
+ additional_data.update((x['name'], x) for x in cr.dictfetchall())
for package in self.values():
for k, v in additional_data[package.name].items():
diff --git a/openerp/modules/module.py b/openerp/modules/module.py
index 8f34a74f882..cd21e681a8b 100644
--- a/openerp/modules/module.py
+++ b/openerp/modules/module.py
@@ -384,11 +384,12 @@ class TestStream(object):
if self.r.match(s):
return
first = True
- for c in s.split('\n'):
+ level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
+ for c in s.splitlines():
if not first:
c = '` ' + c
first = False
- self.logger.info(c)
+ self.logger.log(level, c)
current_test = None
diff --git a/openerp/netsvc.py b/openerp/netsvc.py
index 391de55a32e..e6c05bf7298 100644
--- a/openerp/netsvc.py
+++ b/openerp/netsvc.py
@@ -212,9 +212,9 @@ PSEUDOCONFIG_MAPPER = {
'debug': ['openerp:DEBUG'],
'debug_sql': ['openerp.sql_db:DEBUG'],
'info': [],
- 'warn': ['openerp:WARNING'],
- 'error': ['openerp:ERROR'],
- 'critical': ['openerp:CRITICAL'],
+ 'warn': ['openerp:WARNING', 'werkzeug:WARNING'],
+ 'error': ['openerp:ERROR', 'werkzeug:ERROR'],
+ 'critical': ['openerp:CRITICAL', 'werkzeug:CRITICAL'],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index e24d74e0625..0eae4185aeb 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -422,7 +422,7 @@ class datetime(_column):
tz_name = registry.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
if tz_name:
try:
- utc = pytz.timezone('UTC')
+ utc = pytz.utc
context_tz = pytz.timezone(tz_name)
utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
return utc_timestamp.astimezone(context_tz)
@@ -561,7 +561,7 @@ class many2one(_column):
# we use uid=1 because the visibility of a many2one field value (just id and name)
# must be the access right of the parent form and not the linked object itself.
records = dict(obj.name_get(cr, SUPERUSER_ID,
- list(set([x for x in res.values() if isinstance(x, (int,long))])),
+ list(set([x for x in res.values() if x and isinstance(x, (int,long))])),
context=context))
for id in res:
if res[id] in records:
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index 1ada929482d..4289d6660f8 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -1704,8 +1704,8 @@ class BaseModel(object):
:return: True if the current user is a member of one of the
given groups
"""
- return any([self.pool.get('res.users').has_group(cr, uid, group_ext_id)
- for group_ext_id in groups.split(',')])
+ return any(self.pool['res.users'].has_group(cr, uid, group_ext_id)
+ for group_ext_id in groups.split(','))
def _get_default_form_view(self, cr, user, context=None):
""" Generates a default single-line form view using all fields
@@ -3348,6 +3348,8 @@ class BaseModel(object):
return []
if fields_to_read is None:
fields_to_read = self._columns.keys()
+ else:
+ fields_to_read = list(set(fields_to_read))
# all inherited fields + all non inherited fields for which the attribute whose name is in load is True
fields_pre = [f for f in fields_to_read if
diff --git a/openerp/service/server.py b/openerp/service/server.py
index 72e130edf8b..c0dc15caab7 100644
--- a/openerp/service/server.py
+++ b/openerp/service/server.py
@@ -46,8 +46,15 @@ SLEEP_INTERVAL = 60 # 1 min
#----------------------------------------------------------
# Werkzeug WSGI servers patched
#----------------------------------------------------------
+class LoggingBaseWSGIServerMixIn(object):
+ def handle_error(self, request, client_address):
+ t, e, _ = sys.exc_info()
+ if t == socket.error and e.errno == errno.EPIPE:
+ # broken pipe, ignore error
+ return
+ _logger.exception('Exception happened during processing of request from %s', client_address)
-class BaseWSGIServerNoBind(werkzeug.serving.BaseWSGIServer):
+class BaseWSGIServerNoBind(LoggingBaseWSGIServerMixIn, werkzeug.serving.BaseWSGIServer):
""" werkzeug Base WSGI Server patched to skip socket binding. PreforkServer
use this class, sets the socket and calls the process_request() manually
"""
@@ -74,7 +81,7 @@ class RequestHandler(werkzeug.serving.WSGIRequestHandler):
# should also work with systemd socket activation. This is currently untested
# and not yet used.
-class ThreadedWSGIServerReloadable(werkzeug.serving.ThreadedWSGIServer):
+class ThreadedWSGIServerReloadable(LoggingBaseWSGIServerMixIn, werkzeug.serving.ThreadedWSGIServer):
""" werkzeug Threaded WSGI Server patched to allow reusing a listen socket
given by the environement, this is used by autoreload to keep the listen
socket open when a reload happens.
diff --git a/openerp/sql_db.py b/openerp/sql_db.py
index 3e401347a18..add6cc9655a 100644
--- a/openerp/sql_db.py
+++ b/openerp/sql_db.py
@@ -30,12 +30,11 @@ the ORM does, in fact.
from contextlib import contextmanager
from functools import wraps
import logging
-import time
import uuid
+import psycopg2.extras
import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ
from psycopg2.pool import PoolError
-from psycopg2.psycopg1 import cursor as psycopg1cursor
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
@@ -76,7 +75,7 @@ sql_counter = 0
class Cursor(object):
"""Represents an open transaction to the PostgreSQL DB backend,
acting as a lightweight wrapper around psycopg2's
- ``psycopg1cursor`` objects.
+ ``cursor`` objects.
``Cursor`` is the object behind the ``cr`` variable used all
over the OpenERP code.
@@ -175,7 +174,7 @@ class Cursor(object):
self._serialized = serialized
self._cnx = pool.borrow(dsn(dbname))
- self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor)
+ self._obj = self._cnx.cursor()
if self.sql_log:
self.__caller = frame_codeinfo(currentframe(),2)
else:
@@ -188,6 +187,16 @@ class Cursor(object):
self.cache = {}
+ def __build_dict(self, row):
+ return { d.name: row[i] for i, d in enumerate(self._obj.description) }
+ def dictfetchone(self):
+ row = self._obj.fetchone()
+ return row and self.__build_dict(row)
+ def dictfetchmany(self, size):
+ return map(self.__build_dict, self._obj.fetchmany(size))
+ def dictfetchall(self):
+ return map(self.__build_dict, self._obj.fetchall())
+
def __del__(self):
if not self._closed and not self._cnx.closed:
# Oops. 'self' has not been closed explicitly.
diff --git a/openerp/tests/phantomtest.js b/openerp/tests/phantomtest.js
index 5a6a9696cc0..84f886c5dbb 100644
--- a/openerp/tests/phantomtest.js
+++ b/openerp/tests/phantomtest.js
@@ -1,14 +1,15 @@
-// Phantomjs openerp helper
+// Phantomjs odoo helper
+// jshint evil: true, loopfunc: true
-function waitFor (ready, callback, timeout, timeoutMessageCallback) {
+function waitFor (condition, callback, timeout, timeoutMessageCallback) {
timeout = timeout || 10000;
- var start = new Date;
+ var start = new Date();
(function waitLoop() {
- if(new Date - start > timeout) {
+ if(new Date() - start > timeout) {
console.log('error', timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms");
phantom.exit(1);
- } else if (ready()) {
+ } else if (condition()) {
callback();
} else {
setTimeout(waitLoop, 250);
@@ -44,7 +45,7 @@ function PhantomTest() {
}
return result.join('');
}));
- msg.push('(leaf frame on top)')
+ msg.push('(leaf frame on top)');
}
console.log('error', JSON.stringify(msg.join('\n')));
phantom.exit(1);
@@ -86,9 +87,9 @@ function PhantomTest() {
};
setTimeout(function () {
self.page.evaluate(function () {
- var message = ("Timeout\nhref: " + window.location.href
- + "\nreferrer: " + document.referrer
- + "\n\n" + (document.body && document.body.innerHTML)).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
+ var message = ("Timeout\nhref: " + window.location.href +
+ "\nreferrer: " + document.referrer +
+ "\n\n" + (document.body && document.body.innerHTML)).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
console.log('error', message);
phantom.exit(1);
});
@@ -107,6 +108,8 @@ function PhantomTest() {
url_path = "/login?" + qp.join('&');
}
var url = self.origin + url_path;
+ code = code || "true";
+ ready = ready || "true";
self.page.open(url, function(status) {
if (status !== 'success') {
console.log('error', "failed to load " + url);
@@ -115,7 +118,7 @@ function PhantomTest() {
console.log('loaded', url, status);
// process ready
waitFor(function() {
- console.log("PhantomTest.run: wait for condition: " + ready);
+ console.log("PhantomTest.run: wait for condition:", ready);
return self.page.evaluate(function (ready) {
var r = false;
try {
diff --git a/setup.py b/setup.py
index f26abc26e40..041a8df178b 100644
--- a/setup.py
+++ b/setup.py
@@ -95,6 +95,7 @@ def py2exe_options():
"markupsafe", # dependence of jinja2 and mako
"mock",
"openerp",
+ "passlib",
"poplib",
"psutil",
"pychart",
@@ -163,6 +164,7 @@ setuptools.setup(
'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'mako',
'mock',
+ 'passlib',
'pillow', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'psutil', # windows binary code.google.com/p/psutil/downloads/list
'psycopg2 >= 2.2',
diff --git a/setup/debian/control b/setup/debian/control
index ced6043a650..0d19361ad8b 100644
--- a/setup/debian/control
+++ b/setup/debian/control
@@ -28,6 +28,7 @@ Depends:
python-mako,
python-mock,
python-openid,
+ python-passlib,
python-psutil,
python-psycopg2,
python-pybabel,