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 @@ - +
-
+

@@ -21,7 +21,7 @@
- +
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 @@