diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py
index eb43830f8f6..96d63c545c6 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -295,7 +295,8 @@ class account_invoice(osv.osv):
},
multi='all'),
'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}, track_visibility='always'),
- 'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
+ 'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]},
+ domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale_refund'], 'in_refund': ['purchase_refund'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]"),
'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), readonly=True, states={'draft':[('readonly',False)]}),
'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',
diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py
index 3d8d8302f32..de75c7079b3 100644
--- a/addons/account/account_move_line.py
+++ b/addons/account/account_move_line.py
@@ -741,6 +741,8 @@ class account_move_line(osv.osv):
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
if context is None:
context = {}
+ if context.get('fiscalyear'):
+ args.append(('period_id.fiscalyear_id', '=', context.get('fiscalyear', False)))
if context and context.get('next_partner_only', False):
if not context.get('partner_id', False):
partner = self.list_partners_to_reconcile(cr, uid, context=context)
@@ -823,7 +825,7 @@ class account_move_line(osv.osv):
'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
}, context=context)
move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
- return True
+ return r_id
def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
account_obj = self.pool.get('account.account')
diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml
index 8afa19a63b0..1d340001c55 100644
--- a/addons/account/account_view.xml
+++ b/addons/account/account_view.xml
@@ -351,7 +351,7 @@
diff --git a/addons/account/views/report_agedpartnerbalance.xml b/addons/account/views/report_agedpartnerbalance.xml
index 019f32a35de..c1f9047e330 100644
--- a/addons/account/views/report_agedpartnerbalance.xml
+++ b/addons/account/views/report_agedpartnerbalance.xml
@@ -99,7 +99,7 @@
-
+
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/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_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_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 e4da2a4c411..d90165ae022 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/stock/stock_view.xml b/addons/stock/stock_view.xml
index fc7a94fc51f..e2673b1ab1f 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -749,7 +749,7 @@
-->
-
+
@@ -1288,7 +1288,7 @@
ir.actions.act_window
form
tree,form
-
+
{'product_receive': True, 'search_default_future': True}
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css
index b234e2738df..35dfd21adef 100644
--- a/addons/web/static/src/css/base.css
+++ b/addons/web/static/src/css/base.css
@@ -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 {
@@ -3324,6 +3324,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 +3450,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..abeed36b038 100644
--- a/addons/web/static/src/css/base.sass
+++ b/addons/web/static/src/css/base.sass
@@ -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
@@ -2691,6 +2691,8 @@ body.oe_single_form
> .arrow span
background-color: #729fcf !important
+ .oe_webclient
+ height: auto !important
// }}}
// @media print {{{
@@ -2799,6 +2801,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..08cbd9d9460
--- /dev/null
+++ b/addons/web/static/src/js/tour.js
@@ -0,0 +1,543 @@
+(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 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 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');
+ }
+
+ if (step.backdrop || $element.parents("#website-top-navbar, .oe_navbar, .modal").size()) {
+ $tip.css("z-index", 2010);
+ }
+
+ // button click event
+ $tip.find("button")
+ .one("click", function () {
+ step.busy = true;
+ if (!$(this).is("[data-role='next']")) {
+ clearTimeout(Tour.timer);
+ Tour.endTour();
+ }
+ Tour.closePopover();
+ });
+
+ Tour.repositionPopover();
+ },
+ repositionPopover: function() {
+ var popover = Tour.$element.data("bs.popover");
+ var $tip = Tour.$element.data("bs.popover").tip();
+
+ if (popover.options.orphan) {
+ return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2);
+ }
+
+ var offsetBottom, offsetHeight, offsetRight, offsetWidth, originalLeft, originalTop, tipOffset;
+ offsetWidth = $tip[0].offsetWidth;
+ offsetHeight = $tip[0].offsetHeight;
+ tipOffset = $tip.offset();
+ originalLeft = tipOffset.left;
+ originalTop = tipOffset.top;
+ offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight();
+ if (offsetBottom < 0) {
+ tipOffset.top = tipOffset.top + offsetBottom;
+ }
+ offsetRight = $("html").outerWidth() - tipOffset.left - $tip.outerWidth();
+ if (offsetRight < 0) {
+ tipOffset.left = tipOffset.left + offsetRight;
+ }
+ if (tipOffset.top < 0) {
+ tipOffset.top = 0;
+ }
+ if (tipOffset.left < 0) {
+ tipOffset.left = 0;
+ }
+ $tip.offset(tipOffset);
+ if (popover.options.placement === "bottom" || popover.options.placement === "top") {
+ var left = Tour.$element.offset().left + Tour.$element.outerWidth()/2 - tipOffset.left;
+ $tip.find(".arrow").css("left", left ? left + "px" : "");
+ } else if (popover.options.placement !== "auto") {
+ var top = Tour.$element.offset().top + Tour.$element.outerHeight()/2 - tipOffset.top;
+ $tip.find(".arrow").css("top", top ? top + "px" : "");
+ }
+ },
+ _load_template: false,
+ load_template: function () {
+ // don't need template to use bootstrap Tour in automatic mode
+ Tour._load_template = true;
+ if (typeof QWeb2 === "undefined") return $.when();
+ var def = $.Deferred();
+ openerp.qweb.add_template('/web/static/src/xml/website.tour.xml', function(err) {
+ if (err) {
+ def.reject(err);
+ } else {
+ def.resolve();
+ }
+ });
+ return def;
+ },
+ popoverTitle: function (tour, options) {
+ return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover_title', options) : options.title;
+ },
+ popover: function (options) {
+ return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover', options) : options.title;
+ },
+ getLang: function () {
+ return $("html").attr("lang") ? "/" + $("html").attr("lang").replace(/-/, '_') : "";
+ },
+ getState: function () {
+ var state = JSON.parse(localStorage.getItem("tour") || 'false') || {};
+ if (state) { this.time = state.time; }
+ var tour_id,mode,step_id;
+ if (!state.id && window.location.href.indexOf("#tutorial.") > -1) {
+ state = {
+ "id": window.location.href.match(/#tutorial\.(.*)=true/)[1],
+ "mode": "tutorial",
+ "step_id": 0
+ };
+ window.location.hash = "";
+ console.log("Tour Begin from url hash");
+ Tour.saveState(state.id, state.mode, state.step_id, 0);
+ }
+ if (!state.id) {
+ return;
+ }
+ state.tour = Tour.tours[state.id];
+ state.step = state.tour && state.tour.steps[state.step_id === -1 ? 0 : state.step_id];
+ return state;
+ },
+ error: function (step, message) {
+ var state = Tour.getState();
+ message += '\n tour: ' + state.id
+ + (step ? '\n step: ' + step.id + ": '" + (step._title || step.title) + "'" : '' )
+ + '\n href: ' + window.location.href
+ + '\n referrer: ' + document.referrer
+ + (step ? '\n element: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) : '' )
+ + (step ? '\n waitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size()) : '' )
+ + (step ? '\n waitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size()) : '' )
+ + "\n localStorage: " + JSON.stringify(localStorage)
+ + '\n\n' + $("body").html();
+ Tour.reset();
+ if (state.mode === "test") {
+ throw new Error(message);
+ }
+ },
+ lists: function () {
+ var tour_ids = [];
+ for (var k in Tour.tours) {
+ tour_ids.push(k);
+ }
+ return tour_ids;
+ },
+ saveState: function (tour_id, mode, step_id, number, wait) {
+ localStorage.setItem("tour", JSON.stringify({
+ "id":tour_id,
+ "mode":mode,
+ "step_id":step_id || 0,
+ "time": this.time,
+ "number": number+1,
+ "wait": wait || 0
+ }));
+ },
+ reset: function () {
+ var state = Tour.getState();
+ if (state && state.tour) {
+ for (var k in state.tour.steps) {
+ state.tour.steps[k].busy = false;
+ }
+ }
+ localStorage.removeItem("tour");
+ clearTimeout(Tour.timer);
+ clearTimeout(Tour.testtimer);
+ Tour.closePopover();
+ },
+ running: function () {
+ var state = Tour.getState();
+ if (!state) return;
+ else if (state.tour) {
+ if (!Tour._load_template) {
+ Tour.load_template().then(Tour.running);
+ return;
+ }
+ console.log("Tour '"+state.id+"' is running");
+ Tour.registerSteps(state.tour, state.mode);
+ Tour.nextStep();
+ } else {
+ if (state.mode === "test" && state.wait >= 10) {
+ Tour.error(state.step, "Tour '"+state.id+"' undefined");
+ }
+ Tour.saveState(state.id, state.mode, state.step_id, state.number-1, state.wait+1);
+ console.log("Tour '"+state.id+"' wait for running (tour undefined)");
+ setTimeout(Tour.running, state.mode === "test" ? Tour.defaultDelay : Tour.retryRunningDelay);
+ }
+ },
+ check: function (step) {
+ return (step &&
+ (!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) &&
+ (!step.waitNot || !$(step.waitNot).size()) &&
+ (!step.waitFor || $(step.waitFor).size()));
+ },
+ waitNextStep: function () {
+ var state = Tour.getState();
+ var time = new Date().getTime();
+ var timer;
+ var next = state.tour.steps[state.step.id+1];
+ var overlaps = state.mode === "test" ? Tour.errorDelay : 0;
+
+ window.onbeforeunload = function () {
+ clearTimeout(Tour.timer);
+ clearTimeout(Tour.testtimer);
+ };
+
+ function checkNext () {
+ Tour.autoTogglePopover();
+
+ clearTimeout(Tour.timer);
+ if (Tour.check(next)) {
+ clearTimeout(Tour.currentTimer);
+ // use an other timeout for cke dom loading
+ Tour.saveState(state.id, state.mode, state.step.id, 0);
+ setTimeout(function () {
+ Tour.nextStep(next);
+ }, Tour.defaultDelay);
+ } else if (!overlaps || new Date().getTime() - time < overlaps) {
+ Tour.timer = setTimeout(checkNext, Tour.defaultDelay);
+ } else {
+ Tour.error(next, "Can't reach the next step");
+ }
+ }
+ checkNext();
+ },
+ nextStep: function (step) {
+ var state = Tour.getState();
+
+ if (!state) {
+ return;
+ }
+
+ step = step || state.step;
+ var next = state.tour.steps[step.id+1];
+
+ if (state.mode === "test" && state.number > 3) {
+ Tour.error(next, "Cycling. Can't reach the next step");
+ }
+
+ Tour.saveState(state.id, state.mode, step.id, state.number);
+
+ if (step.id !== state.step_id) {
+ console.log("Tour Step: '" + (step._title || step.title) + "' (" + (new Date().getTime() - this.time) + "ms)");
+ }
+
+ Tour.autoTogglePopover(true);
+
+ if (step.onload) {
+ step.onload();
+ }
+
+ if (next) {
+ setTimeout(function () {
+ if (Tour.getState()) {
+ Tour.waitNextStep();
+ }
+ if (state.mode === "test") {
+ setTimeout(function(){
+ Tour.autoNextStep(state.tour, step);
+ }, Tour.defaultDelay);
+ }
+ }, next.wait || 0);
+ } else {
+ setTimeout(function(){
+ Tour.autoNextStep(state.tour, step);
+ }, Tour.defaultDelay);
+ Tour.endTour();
+ }
+ },
+ endTour: function () {
+ var state = Tour.getState();
+ var test = state.step.id >= state.tour.steps.length-1;
+ Tour.reset();
+ if (test) {
+ console.log('ok');
+ } else {
+ console.log('error');
+ }
+ },
+ autoNextStep: function (tour, step) {
+ clearTimeout(Tour.testtimer);
+
+ function autoStep () {
+ if (!step) return;
+
+ if (step.autoComplete) {
+ step.autoComplete(tour);
+ }
+
+ $(".popover.tour [data-role='next']").click();
+
+ var $element = $(step.element);
+ if (!$element.size()) return;
+
+ if (step.snippet) {
+
+ Tour.autoDragAndDropSnippet($element);
+
+ } else if ($element.is(":visible")) {
+
+ $element.trigger($.Event("mouseenter", { srcElement: $element[0] }));
+ $element.trigger($.Event("mousedown", { srcElement: $element[0] }));
+
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ $element[0].dispatchEvent(evt);
+
+ // trigger after for step like: mouseenter, next step click on button display with mouseenter
+ setTimeout(function () {
+ $element.trigger($.Event("mouseup", { srcElement: $element[0] }));
+ $element.trigger($.Event("mouseleave", { srcElement: $element[0] }));
+ }, 1000);
+ }
+ if (step.sampleText) {
+
+ $element.trigger($.Event("keydown", { srcElement: $element }));
+ if ($element.is("input") ) {
+ $element.val(step.sampleText);
+ } if ($element.is("select")) {
+ $element.find("[value='"+step.sampleText+"'], option:contains('"+step.sampleText+"')").attr("selected", true);
+ $element.val(step.sampleText);
+ } else {
+ $element.html(step.sampleText);
+ }
+ setTimeout(function () {
+ $element.trigger($.Event("keyup", { srcElement: $element }));
+ $element.trigger($.Event("change", { srcElement: $element }));
+ }, self.defaultDelay<<1);
+
+ }
+ }
+ Tour.testtimer = setTimeout(autoStep, 100);
+ },
+ autoDragAndDropSnippet: function (selector) {
+ var $thumbnail = $(selector).first();
+ var thumbnailPosition = $thumbnail.position();
+ $thumbnail.trigger($.Event("mousedown", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top }));
+ $thumbnail.trigger($.Event("mousemove", { which: 1, pageX: document.body.scrollWidth/2, pageY: document.body.scrollHeight/2 }));
+ var $dropZone = $(".oe_drop_zone").first();
+ var dropPosition = $dropZone.position();
+ $dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top }));
+ }
+};
+openerp.Tour = Tour;
+
+/////////////////////////////////////////////////
+
+$(document).ready(Tour.running);
+
+}());
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index e276a5e3995..cb0b2e8ea80 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -2632,6 +2632,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
type_of_date: "datetime",
events: {
'change .oe_datepicker_master': 'change_datetime',
+ 'keypress .oe_datepicker_master': 'change_datetime',
},
init: function(parent) {
this._super(parent);
@@ -2750,8 +2751,8 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
format_client: function(v) {
return instance.web.format_value(v, {"widget": this.type_of_date});
},
- change_datetime: function() {
- if (this.is_valid_()) {
+ change_datetime: function(e) {
+ if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
this.set_value_from_ui_();
this.trigger("datetime_changed");
}
diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js
index e2c63eb191a..ea2c28d9c3f 100644
--- a/addons/web/static/src/js/view_list_editable.js
+++ b/addons/web/static/src/js/view_list_editable.js
@@ -130,15 +130,7 @@
if (this.editable()) {
this.$el.find('table:first').show();
this.$el.find('.oe_view_nocontent').remove();
- this.start_edition().then(function(){
- var fields = self.editor.form.fields;
- self.editor.form.fields_order.some(function(field){
- if (fields[field].$el.is(':visible')){
- fields[field].$el.find("input").select();
- return true;
- }
- });
- });
+ this.start_edition();
} else {
this._super();
}
@@ -243,6 +235,7 @@
return this.ensure_saved().then(function () {
var $recordRow = self.groups.get_row_for(record);
var cells = self.get_cells_for($recordRow);
+ var fields = {};
self.fields_for_resize.splice(0, self.fields_for_resize.length);
return self.with_event('edit', {
record: record.attributes,
@@ -256,10 +249,16 @@
// FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
field.$el.attr('data-fieldname', field_name);
+ fields[field_name] = field;
self.fields_for_resize.push({field: field, cell: cell});
}, options).then(function () {
$recordRow.addClass('oe_edition');
self.resize_fields();
+ var focus_field = options && options.focus_field ? options.focus_field : undefined;
+ if (!focus_field){
+ focus_field = _.find(self.editor.form.fields_order, function(field){ return fields[field] && fields[field].$el.is(':visible:has(input)'); });
+ }
+ if (focus_field) fields[focus_field].$el.find('input').select();
return record.attributes;
});
}).fail(function () {
@@ -749,31 +748,6 @@
throw new Error("is_editing's state filter must be either `new` or" +
" `edit` if provided");
},
- _focus_setup: function (focus_field) {
- var form = this.form;
-
- var field;
- // If a field to focus was specified
- if (focus_field
- // Is actually in the form
- && (field = form.fields[focus_field])
- // And is visible
- && field.$el.is(':visible')) {
- // focus it
- field.focus();
- return;
- }
-
- _(form.fields_order).detect(function (name) {
- // look for first visible field in fields_order, focus it
- var field = form.fields[name];
- if (!field.$el.is(':visible')) {
- return false;
- }
- // Stop as soon as a field got focused
- return field.focus() !== false;
- });
- },
edit: function (record, configureField, options) {
// TODO: specify sequence of edit calls
var self = this;
@@ -788,7 +762,6 @@
_(form.fields).each(function (field, name) {
configureField(name, field);
});
- self._focus_setup(options && options.focus_field);
return form;
});
},
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 913783a2396..fb2fcbf303e 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -2038,4 +2038,5 @@
+
diff --git a/addons/website/static/src/xml/website.tour.xml b/addons/web/static/src/xml/website.tour.xml
similarity index 90%
rename from addons/website/static/src/xml/website.tour.xml
rename to addons/web/static/src/xml/website.tour.xml
index dcecef021bb..cae823bd04e 100644
--- a/addons/website/static/src/xml/website.tour.xml
+++ b/addons/web/static/src/xml/website.tour.xml
@@ -1,8 +1,8 @@
-
+
-
+
×
diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html
index 5734f60bc62..19ae6a23bb2 100644
--- a/addons/web/views/database_manager.html
+++ b/addons/web/views/database_manager.html
@@ -51,6 +51,7 @@
+
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 2b022d86f30..08fc3be483d 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -22,6 +22,7 @@
+
@@ -246,6 +247,7 @@
diff --git a/addons/website_mail_group/__init__.py b/addons/website_mail_group/__init__.py
index ee5959455ad..9f86759e32b 100644
--- a/addons/website_mail_group/__init__.py
+++ b/addons/website_mail_group/__init__.py
@@ -1 +1,2 @@
import controllers
+import models
diff --git a/addons/website_mail_group/controllers/main.py b/addons/website_mail_group/controllers/main.py
index a28b808d9f2..59478b37fa3 100644
--- a/addons/website_mail_group/controllers/main.py
+++ b/addons/website_mail_group/controllers/main.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
import datetime
+from dateutil import relativedelta
-from openerp import tools
+from openerp import tools, SUPERUSER_ID
from openerp.addons.web import http
from openerp.addons.website.models.website import slug
from openerp.addons.web.http import request
@@ -28,12 +29,23 @@ class MailGroup(http.Controller):
def view(self, **post):
cr, uid, context = request.cr, request.uid, request.context
group_obj = request.registry.get('mail.group')
+ mail_message_obj = request.registry.get('mail.message')
group_ids = group_obj.search(cr, uid, [('alias_id', '!=', False), ('alias_id.alias_name', '!=', False)], context=context)
- values = {'groups': group_obj.browse(cr, uid, group_ids, context)}
+ groups = group_obj.browse(cr, uid, group_ids, context)
+ # compute statistics
+ month_date = datetime.datetime.today() - relativedelta.relativedelta(months=1)
+ group_data = dict.fromkeys(group_ids, dict())
+ for group in groups:
+ group_data[group.id]['monthly_message_nbr'] = mail_message_obj.search(
+ cr, SUPERUSER_ID,
+ [('model', '=', 'mail.group'), ('res_id', '=', group.id), ('date', '>=', month_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT))],
+ count=True, context=context)
+ values = {'groups': groups, 'group_data': group_data}
return request.website.render('website_mail_group.mail_groups', values)
@http.route(["/groups/subscription/"], type='json', auth="user")
def subscription(self, group_id=0, action=False, **post):
+ """ TDE FIXME: seems dead code """
cr, uid, context = request.cr, request.uid, request.context
group_obj = request.registry.get('mail.group')
if action:
diff --git a/addons/website_mail_group/models/__init__.py b/addons/website_mail_group/models/__init__.py
new file mode 100644
index 00000000000..ea8be51acde
--- /dev/null
+++ b/addons/website_mail_group/models/__init__.py
@@ -0,0 +1 @@
+import mail_group
diff --git a/addons/website_mail_group/models/mail_group.py b/addons/website_mail_group/models/mail_group.py
new file mode 100644
index 00000000000..804785b66f9
--- /dev/null
+++ b/addons/website_mail_group/models/mail_group.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import osv
+
+
+class MailGroup(osv.Model):
+ _inherit = 'mail.group'
+
+ def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
+ res = super(MailGroup, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context)
+ group = self.browse(cr, uid, id, context=context)
+ base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
+ res['headers'].update({
+ 'List-Archive': '<%s/groups/%s>' % (base_url, group.id),
+ 'List-Subscribe': '<%s/groups>' % (base_url),
+ 'List-Unsubscribe': '<%s/groups>' % (base_url),
+ })
+ return res
diff --git a/addons/website_mail_group/static/src/js/website_mail_group.snippet.js b/addons/website_mail_group/static/src/js/website_mail_group.snippet.js
index fd6a2d38cfa..8ad4f9ba796 100644
--- a/addons/website_mail_group/static/src/js/website_mail_group.snippet.js
+++ b/addons/website_mail_group/static/src/js/website_mail_group.snippet.js
@@ -3,8 +3,8 @@
var website = openerp.website;
- website.snippet.animationRegistry.follow = website.snippet.Animation.extend({
- selector: ".js_follow",
+ website.snippet.animationRegistry.follow_alias = website.snippet.Animation.extend({
+ selector: ".js_follow_alias",
start: function (editable_mode) {
var self = this;
this.is_user = false;
@@ -23,8 +23,8 @@
// not if editable mode to allow designer to edit alert field
if (!editable_mode) {
- $('.js_follow > .alert').addClass("hidden");
- $('.js_follow > .input-group-btn.hidden').removeClass("hidden");
+ $('.js_follow_alias > .alert').addClass("hidden");
+ $('.js_follow_alias > .input-group-btn.hidden').removeClass("hidden");
this.$target.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) {
event.preventDefault();
self.on_click();
diff --git a/addons/website_mail_group/views/snippets.xml b/addons/website_mail_group/views/snippets.xml
index 452f59f4902..95935cd4998 100644
--- a/addons/website_mail_group/views/snippets.xml
+++ b/addons/website_mail_group/views/snippets.xml
@@ -11,7 +11,7 @@
Discussion Group
-
@@ -38,7 +38,7 @@
diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml
index 8ecbd6c84ed..a1065027748 100644
--- a/addons/website_mail_group/views/website_mail_group.xml
+++ b/addons/website_mail_group/views/website_mail_group.xml
@@ -45,7 +45,7 @@
participants
- messages
+ messages / month
diff --git a/addons/website_report/views/layouts.xml b/addons/website_report/views/layouts.xml
index 9978d994821..03df41266be 100644
--- a/addons/website_report/views/layouts.xml
+++ b/addons/website_report/views/layouts.xml
@@ -62,8 +62,6 @@
-
-
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 7666a8bb5e0..5f1b54dd7a4 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -63,7 +63,7 @@ class table_compute(object):
self.table[(pos/PPR)+y2][(pos%PPR)+x2] = False
self.table[pos/PPR][pos%PPR] = {
'product': p, 'x':x, 'y': y,
- 'class': " ".join(map(lambda x: x.html_class, p.website_style_ids))
+ 'class': " ".join(map(lambda x: x.html_class or '', p.website_style_ids))
}
if index<=PPG:
maxy=max(maxy,y+(pos/PPR))
@@ -179,7 +179,7 @@ class website_sale(http.Controller):
values = {
'search': search,
- 'category': category and int(category),
+ 'category': category,
'attrib_values': attrib_values,
'attrib_set': attrib_set,
'pager': pager,
diff --git a/addons/website_sale/static/src/js/website.tour.sale.js b/addons/website_sale/static/src/js/website.tour.sale.js
index 03beae65a0f..65ed620e3a4 100644
--- a/addons/website_sale/static/src/js/website.tour.sale.js
+++ b/addons/website_sale/static/src/js/website.tour.sale.js
@@ -1,9 +1,6 @@
(function () {
'use strict';
-
- var website = openerp.website;
-
- website.Tour.register({
+ openerp.Tour.register({
id: 'shop_customize',
name: "Customize the page and search a product",
path: '/shop',
@@ -79,7 +76,7 @@
]
});
- website.Tour.register({
+ openerp.Tour.register({
id: 'shop_buy_product',
name: "Try to buy products",
path: '/shop',
diff --git a/addons/website_sale/static/src/js/website.tour.shop.js b/addons/website_sale/static/src/js/website.tour.shop.js
index 3dd5b54b357..c39a8b2f9e8 100644
--- a/addons/website_sale/static/src/js/website.tour.shop.js
+++ b/addons/website_sale/static/src/js/website.tour.shop.js
@@ -1,10 +1,9 @@
(function () {
'use strict';
- var website = openerp.website;
var _t = openerp._t;
- website.Tour.register({
+ openerp.Tour.register({
id: 'shop',
name: _t("Create a product"),
steps: [
diff --git a/addons/website_sale/tests/test_sale_process.py b/addons/website_sale/tests/test_sale_process.py
index 584a4e4bbfb..9e9f12a603b 100644
--- a/addons/website_sale/tests/test_sale_process.py
+++ b/addons/website_sale/tests/test_sale_process.py
@@ -3,22 +3,30 @@ import os
import openerp.tests
inject = [
- ("openerp.website.Tour", os.path.join(os.path.dirname(__file__), '../../website/static/src/js/website.tour.js')),
- ("openerp.website.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.sale.js")),
+ ("openerp.Tour", os.path.join(os.path.dirname(__file__), '../../web/static/src/js/tour.js')),
+ ("openerp.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.sale.js")),
]
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestUi(openerp.tests.HttpCase):
def test_01_admin_shop_tour(self):
+<<<<<<< HEAD
self.phantom_js("/", "openerp.website.Tour.run('shop', 'test')", "openerp.website.Tour.tours.shop", login="admin")
self.phantom_js("/", "openerp.website.Tour.run('shop_customize', 'test')", "openerp.website.Tour.tours.shop_customize", login="admin", inject=inject)
def test_02_admin_checkout(self):
self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="admin", inject=inject)
+=======
+ self.phantom_js("/", "openerp.Tour.run('shop', 'test')", "openerp.Tour.tours.shop", login="admin")
+
+ def test_02_admin_checkout(self):
+ self.phantom_js("/", "openerp.Tour.run('shop_customize', 'test')", "openerp.Tour.tours.shop_customize", login="admin", inject=inject)
+ self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", login="admin", inject=inject)
+>>>>>>> remotes/odoo/master
def test_03_demo_checkout(self):
- self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="demo", inject=inject)
+ self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", login="demo", inject=inject)
def test_04_public_checkout(self):
- self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", inject=inject)
+ self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", inject=inject)
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index c7e691dac5f..e22f66d6f98 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -244,7 +244,7 @@
-
+
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index d6d96737ca3..3c84533e716 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -491,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
@@ -650,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):
@@ -753,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/osv/fields.py b/openerp/osv/fields.py
index 57961a1d32c..0eae4185aeb 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -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 0a23dda61e3..4289d6660f8 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -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/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/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,