merge upstream

This commit is contained in:
Christophe Matthieu 2014-06-20 10:09:16 +02:00
commit e75d9525bb
83 changed files with 1035 additions and 893 deletions

View File

@ -295,7 +295,8 @@ class account_invoice(osv.osv):
}, },
multi='all'), multi='all'),
'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}, track_visibility='always'), '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)]}), '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)]}), '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', 'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',

View File

@ -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): def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
if context is None: if context is None:
context = {} 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 context and context.get('next_partner_only', False):
if not context.get('partner_id', False): if not context.get('partner_id', False):
partner = self.list_partners_to_reconcile(cr, uid, context=context) 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) 'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
}, context=context) }, context=context)
move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, 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): 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') account_obj = self.pool.get('account.account')

View File

@ -351,7 +351,7 @@
<act_window <act_window
id="action_account_items" id="action_account_items"
name="Journal Items" name="Journal Items"
context="{'search_default_account_id': [active_id]}" context="{'search_default_account_id': [active_id], 'fiscalyear': context.get('fiscalyear')}"
res_model="account.move.line" res_model="account.move.line"
src_model="account.account" src_model="account.account"
key2="tree_but_open"/> key2="tree_but_open"/>

View File

@ -99,7 +99,7 @@
</tr> </tr>
<tr t-foreach="get_lines_with_out_partner(data['form'])" t-as="not_partner"> <tr t-foreach="get_lines_with_out_partner(data['form'])" t-as="not_partner">
<td> <td>
<span t-esc="partner['name']"/> <span t-esc="not_partner['name']"/>
</td> </td>
<td class="text-right"> <td class="text-right">
<span t-esc="formatLang(not_partner['direction'], currency_obj=res_company.currency_id)"/> <span t-esc="formatLang(not_partner['direction'], currency_obj=res_company.currency_id)"/>

View File

@ -62,9 +62,10 @@ class account_chart(osv.osv_memory):
ORDER BY p.date_stop DESC ORDER BY p.date_stop DESC
LIMIT 1) AS period_stop''', (fiscalyear_id, fiscalyear_id)) LIMIT 1) AS period_stop''', (fiscalyear_id, fiscalyear_id))
periods = [i[0] for i in cr.fetchall()] periods = [i[0] for i in cr.fetchall()]
if periods and len(periods) > 1: if periods:
start_period = periods[0] 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} res['value'] = {'period_from': start_period, 'period_to': end_period}
else: else:
res['value'] = {'period_from': False, 'period_to': False} res['value'] = {'period_from': False, 'period_to': False}

View File

@ -22,7 +22,6 @@ from dateutil.relativedelta import relativedelta
import datetime import datetime
import logging import logging
import time import time
import traceback
from openerp.osv import osv, fields from openerp.osv import osv, fields
from openerp.osv.orm import intersect, except_orm from openerp.osv.orm import intersect, except_orm
@ -73,6 +72,7 @@ class account_analytic_invoice_line(osv.osv):
result = {} result = {}
res = self.pool.get('product.product').browse(cr, uid, product, context=local_context) res = self.pool.get('product.product').browse(cr, uid, product, context=local_context)
price = False
if price_unit is not False: if price_unit is not False:
price = price_unit price = price_unit
elif pricelist_id: elif pricelist_id:
@ -746,29 +746,32 @@ class account_analytic_account(osv.osv):
contract_ids = ids contract_ids = ids
else: else:
contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True), ('type', '=', 'contract')]) 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): if contract_ids:
try: 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),))
invoice_values = self._prepare_invoice(cr, uid, contract, context=context) for company_id, ids in cr.fetchall():
invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context)) for contract in self.browse(cr, uid, ids, context=dict(context, company_id=company_id, force_company=company_id)):
next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d") try:
interval = contract.recurring_interval invoice_values = self._prepare_invoice(cr, uid, contract, context=context)
if contract.recurring_rule_type == 'daily': invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context))
new_date = next_date+relativedelta(days=+interval) next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
elif contract.recurring_rule_type == 'weekly': interval = contract.recurring_interval
new_date = next_date+relativedelta(weeks=+interval) if contract.recurring_rule_type == 'daily':
elif contract.recurring_rule_type == 'monthly': new_date = next_date+relativedelta(days=+interval)
new_date = next_date+relativedelta(months=+interval) elif contract.recurring_rule_type == 'weekly':
else: new_date = next_date+relativedelta(weeks=+interval)
new_date = next_date+relativedelta(years=+interval) elif contract.recurring_rule_type == 'monthly':
self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context) new_date = next_date+relativedelta(months=+interval)
if automatic: else:
cr.commit() new_date = next_date+relativedelta(years=+interval)
except Exception: self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
if automatic: if automatic:
cr.rollback() cr.commit()
_logger.error(traceback.format_exc()) except Exception:
else: if automatic:
raise cr.rollback()
_logger.exception('Fail to create recurring invoice for contract %s', contract.code)
else:
raise
return invoice_ids return invoice_ids
class account_analytic_account_summary_user(osv.osv): class account_analytic_account_summary_user(osv.osv):

View File

@ -248,7 +248,7 @@
<page string="Bill Information"> <page string="Bill Information">
<field name="line_dr_ids" on_change="onchange_price(line_dr_ids, tax_id, partner_id)" context="{'journal_id':journal_id,'partner_id':partner_id}"> <field name="line_dr_ids" on_change="onchange_price(line_dr_ids, tax_id, partner_id)" context="{'journal_id':journal_id,'partner_id':partner_id}">
<tree string="Expense Lines" editable="bottom"> <tree string="Expense Lines" editable="bottom">
<field name="account_id" widget="selection" domain="[('user_type.report_type','=','expense'), ('type','!=','view')]"/> <field name="account_id" domain="[('user_type.report_type','=','expense'), ('type','!=','view')]"/>
<field name="name"/> <field name="name"/>
<field name="amount"/> <field name="amount"/>
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"/> <field name="account_analytic_id" groups="analytic.group_analytic_accounting"/>

View File

@ -1,137 +1,41 @@
#
# Implements encrypting functions.
#
# Copyright (c) 2008, F S 3 Consulting Inc.
#
# Maintainer:
# Alec Joseph Rivera (agi<at>fs3.ph)
# refactored by Antony Lesuisse <al<at>openerp.com>
#
import hashlib
import hmac
import logging import logging
from random import sample
from string import ascii_letters, digits from passlib.context import CryptContext
import openerp import openerp
from openerp.osv import fields, osv from openerp.osv import fields, osv
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
magic_md5 = '$1$' default_crypt_context = CryptContext(
magic_sha256 = '$5$' # kdf which can be verified by the context. The default encryption kdf is
# the first of the list
def gen_salt(length=8, symbols=None): ['pbkdf2_sha512', 'md5_crypt'],
if symbols is None: # deprecated algorithms are still verified as usual, but ``needs_update``
symbols = ascii_letters + digits # will indicate that the stored hash should be replaced by a more recent
return ''.join(sample(symbols, length)) # algorithm. Passlib 1.6 supports an `auto` value which deprecates any
# algorithm but the default, but Debian only provides 1.5 so...
def md5crypt( raw_pw, salt, magic=magic_md5 ): deprecated=['md5_crypt'],
""" 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):
*
* <phk@login.dknet.dk> 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)
class res_users(osv.osv): class res_users(osv.osv):
_inherit = "res.users" _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): def set_pw(self, cr, uid, id, name, value, args, context):
if value: if value:
encrypted = md5crypt(value, gen_salt()) self._set_password(cr, uid, id, value, context=context)
cr.execute("update res_users set password='', password_crypt=%s where id=%s", (encrypted, id))
del value
def get_pw( self, cr, uid, ids, name, args, 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)),)) cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
stored_pws = cr.fetchall() return dict(cr.fetchall())
res = {}
for id, stored_pw in stored_pws:
res[id] = stored_pw
return res
_columns = { _columns = {
'password': fields.function(get_pw, fnct_inv=set_pw, type='char', string='Password', invisible=True, store=True), '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): def check_credentials(self, cr, uid, password):
# convert to base_crypt if needed # convert to base_crypt if needed
cr.execute('SELECT password, password_crypt FROM res_users WHERE id=%s AND active', (uid,)) cr.execute('SELECT password, password_crypt FROM res_users WHERE id=%s AND active', (uid,))
encrypted = None
if cr.rowcount: if cr.rowcount:
stored_password, stored_password_crypt = cr.fetchone() stored, encrypted = cr.fetchone()
if stored_password and not stored_password_crypt: if stored and not encrypted:
salt = gen_salt() self._set_password(cr, uid, uid, stored)
stored_password_crypt = md5crypt(stored_password, salt)
cr.execute("UPDATE res_users SET password='', password_crypt=%s WHERE id=%s", (stored_password_crypt, uid))
try: try:
return super(res_users, self).check_credentials(cr, uid, password) return super(res_users, self).check_credentials(cr, uid, password)
except openerp.exceptions.AccessDenied: except openerp.exceptions.AccessDenied:
# check md5crypt if encrypted:
if stored_password_crypt: valid_pass, replacement = self._crypt_context(cr, uid, uid)\
if stored_password_crypt[:len(magic_md5)] == magic_md5: .verify_and_update(password, encrypted)
salt = stored_password_crypt[len(magic_md5):11] if replacement is not None:
if stored_password_crypt == md5crypt(password, salt): self._set_encrypted_password(cr, uid, uid, replacement)
return if valid_pass:
elif stored_password_crypt[:len(magic_md5)] == magic_sha256: return
salt = stored_password_crypt[len(magic_md5):11]
if stored_password_crypt == md5crypt(password, salt):
return
# Reraise password incorrect
raise 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: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -74,7 +74,7 @@ class OAuthLogin(Home):
state = dict( state = dict(
d=request.session.db, d=request.session.db,
p=provider['id'], p=provider['id'],
r=redirect, r=werkzeug.url_quote_plus(redirect),
) )
token = request.params.get('token') token = request.params.get('token')
if token: if token:
@ -143,7 +143,7 @@ class OAuthController(http.Controller):
cr.commit() cr.commit()
action = state.get('a') action = state.get('a')
menu = state.get('m') menu = state.get('m')
redirect = state.get('r') redirect = werkzeug.url_unquote_plus(state['r']) if state.get('r') else False
url = '/web' url = '/web'
if redirect: if redirect:
url = redirect url = redirect

View File

@ -102,7 +102,7 @@ openerp.calendar = function(instance) {
var self = this; var self = this;
var action_url = ''; 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(){ var reload_page = function(){
return location.replace(action_url); return location.replace(action_url);

View File

@ -32,7 +32,7 @@ class report_event_registration(osv.osv):
'draft_state': fields.integer(' # No of Draft Registrations', size=20), 'draft_state': fields.integer(' # No of Draft Registrations', size=20),
'confirm_state': fields.integer(' # No of Confirmed Registrations', size=20), 'confirm_state': fields.integer(' # No of Confirmed Registrations', size=20),
'seats_max': fields.integer('Max Seats'), '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'), '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), '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), '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, r.name AS name_registration,
e.company_id AS company_id, e.company_id AS company_id,
e.date_begin AS event_date, 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 ('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, CASE WHEN r.state IN ('open','done') THEN r.nb_register ELSE 0 END AS confirm_state,
e.type AS event_type, e.type AS event_type,

View File

@ -1,7 +1,7 @@
.oe_event_date{ .oe_event_date{
border-top-left-radius:3px; border-top-left-radius:3px;
border-top-right-radius:3px; border-top-right-radius:3px;
font-size: 48px; font-size: 36px;
height: auto; height: auto;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;

View File

@ -245,7 +245,8 @@ class event_ticket(osv.osv):
] ]
def onchange_product_id(self, cr, uid, ids, product_id=False, context=None): 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): class event_registration(osv.osv):

View File

@ -58,7 +58,7 @@ def start_end_date_for_period(period, default_start_date=False, default_end_date
end_date = default_end_date end_date = default_end_date
if start_date and 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: else:
return (start_date, end_date) return (start_date, end_date)

View File

@ -699,7 +699,7 @@ class google_calendar(osv.AbstractModel):
for att in att_obj.browse(cr, uid, my_att_ids, context=context): for att in att_obj.browse(cr, uid, my_att_ids, context=context):
event = att.event_id 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: if base_event_id not in event_to_synchronize:
event_to_synchronize[base_event_id] = {} event_to_synchronize[base_event_id] = {}
@ -721,7 +721,7 @@ class google_calendar(osv.AbstractModel):
for event in all_event_from_google.values(): for event in all_event_from_google.values():
event_id = event.get('id') 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: if base_event_id not in event_to_synchronize:
event_to_synchronize[base_event_id] = {} event_to_synchronize[base_event_id] = {}
@ -786,7 +786,7 @@ class google_calendar(osv.AbstractModel):
if actSrc == 'OE': if actSrc == 'OE':
self.delete_an_event(cr, uid, current_event[0], context=context) self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'GG': 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: if 'T' in new_google_event_id:
new_google_event_id = new_google_event_id.replace('T', '')[:-1] new_google_event_id = new_google_event_id.replace('T', '')[:-1]
else: else:
@ -795,7 +795,8 @@ class google_calendar(osv.AbstractModel):
if event.GG.status: if event.GG.status:
parent_event = {} parent_event = {}
if not event_to_synchronize[base_event][0][1].OE.event_id: 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) 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) res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)

View File

@ -225,7 +225,7 @@ class hr_employee(osv.osv):
"resized as a 128x128px image, with aspect ratio preserved. "\ "resized as a 128x128px image, with aspect ratio preserved. "\
"Use this field in form views or some kanban views."), "Use this field in form views or some kanban views."),
'image_small': fields.function(_get_image, fnct_inv=_set_image, '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 = { store = {
'hr.employee': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), 'hr.employee': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
}, },

View File

@ -40,7 +40,9 @@
<page string="Public Information"> <page string="Public Information">
<group> <group>
<group string="Contact Information"> <group string="Contact Information">
<field name="address_id" on_change="onchange_address_id(address_id)" context="{'show_address': 1}" options='{"always_reload": True, "highlight_first_line": True}'/> <field name="address_id" on_change="onchange_address_id(address_id)"
context="{'show_address': 1, 'default_customer': False}"
options='{"always_reload": True, "highlight_first_line": True}'/>
<field name="mobile_phone"/> <field name="mobile_phone"/>
<field name="work_location"/> <field name="work_location"/>
</group> </group>
@ -68,7 +70,9 @@
<field name="otherid" groups="base.group_hr_user"/> <field name="otherid" groups="base.group_hr_user"/>
</group> </group>
<group string="Contact Information"> <group string="Contact Information">
<field name="address_home_id" context="{'show_address': 1}" options='{"always_reload": True, "highlight_first_line": True}'/> <field name="address_home_id"
context="{'show_address': 1, 'default_customer': False}"
options='{"always_reload": True, "highlight_first_line": True}'/>
</group> </group>
<group string="Status"> <group string="Status">
<field name="gender"/> <field name="gender"/>

View File

@ -99,7 +99,7 @@ class hr_holidays_status(osv.osv):
for record in self.browse(cr, uid, ids, context=context): for record in self.browse(cr, uid, ids, context=context):
name = record.name name = record.name
if not record.limit: 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)) res.append((record.id, name))
return res return res

View File

@ -141,7 +141,7 @@ class account_invoice(osv.osv):
elif algorithm == 'random': elif algorithm == 'random':
if not self.check_bbacomm(reference): if not self.check_bbacomm(reference):
base = random.randint(1, 9999999999) base = random.randint(1, 9999999999)
bbacomm = str(base).rjust(7, '0') bbacomm = str(base).rjust(10, '0')
base = int(bbacomm) base = int(bbacomm)
mod = base % 97 or 97 mod = base % 97 or 97
mod = str(mod).rjust(2, '0') mod = str(mod).rjust(2, '0')

View File

@ -32,7 +32,7 @@ This is the latest UK OpenERP localisation necessary to run OpenERP accounting f
- a few other adaptations""", - a few other adaptations""",
'author': 'SmartMode LTD', 'author': 'SmartMode LTD',
'website': 'http://www.smartmode.co.uk', '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': [
'data/account.account.type.csv', 'data/account.account.type.csv',
'data/account.account.template.csv', 'data/account.account.template.csv',

View File

@ -28,7 +28,7 @@ United States - Chart of accounts.
================================== ==================================
""", """,
'website': 'http://www.openerp.com', 'website': 'http://www.openerp.com',
'depends': ['account_chart'], 'depends': ['account_chart', 'account_anglo_saxon'],
'data': [ 'data': [
'l10n_us_account_type.xml', 'l10n_us_account_type.xml',
'account_chart_template.xml', 'account_chart_template.xml',

View File

@ -176,7 +176,7 @@ class mail_notification(osv.Model):
references = message.parent_id.message_id if message.parent_id else False references = message.parent_id.message_id if message.parent_id else False
# create email values # 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)] chunks = [email_pids[x:x + max_recipients] for x in xrange(0, len(email_pids), max_recipients)]
email_ids = [] email_ids = []
for chunk in chunks: for chunk in chunks:
@ -188,7 +188,7 @@ class mail_notification(osv.Model):
'references': references, 'references': references,
} }
email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)) 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) self.pool.get('mail.mail').send(cr, uid, email_ids, context=context)
return True return True

View File

@ -211,3 +211,16 @@ class mail_group(osv.Model):
return [] return []
else: else:
return super(mail_group, self).get_suggested_thread(cr, uid, removed_suggested_threads, context) 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'] = '<mailto:%s@%s>' % (group.alias_name, group.alias_domain)
return res

View File

@ -204,12 +204,15 @@ class mail_mail(osv.Model):
""" """
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body) body_alternative = tools.html2plaintext(body)
return { res = {
'body': body, 'body': body,
'body_alternative': body_alternative, 'body_alternative': body_alternative,
'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context), '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), '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): def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
""" Sends the selected emails immediately, ignoring their current """ 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 # build an RFC2822 email.message.Message object and send it without queuing
res = None res = None
for email in email_list: for email in email_list:
email_headers = dict(headers)
if email.get('headers'):
email_headers.update(email['headers'])
msg = ir_mail_server.build_email( msg = ir_mail_server.build_email(
email_from=mail.email_from, email_from=mail.email_from,
email_to=email.get('email_to'), 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)), object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype='html', subtype='html',
subtype_alternative='plain', subtype_alternative='plain',
headers=headers) headers=email_headers)
res = ir_mail_server.send_email(cr, uid, msg, res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=mail.mail_server_id.id, mail_server_id=mail.mail_server_id.id,
context=context) context=context)

View File

@ -34,6 +34,7 @@ import pytz
import socket import socket
import time import time
import xmlrpclib import xmlrpclib
import re
from email.message import Message from email.message import Message
from urllib import urlencode from urllib import urlencode
@ -48,6 +49,8 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
mail_header_msgid_re = re.compile('<[^<>]+>')
def decode_header(message, header, separator=' '): def decode_header(message, header, separator=' '):
return separator.join(map(decode, filter(None, message.get_all(header, [])))) 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 if record.alias_domain and record.alias_name else False
for record in self.browse(cr, SUPERUSER_ID, ids, context=context)] 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 # Mail gateway
#------------------------------------------------------ #------------------------------------------------------
@ -1301,13 +1314,13 @@ class mail_thread(osv.AbstractModel):
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
if message.get('In-Reply-To'): 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: if parent_ids:
msg_dict['parent_id'] = parent_ids[0] msg_dict['parent_id'] = parent_ids[0]
if message.get('References') and 'parent_id' not in msg_dict: if message.get('References') and 'parent_id' not in msg_dict:
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', msg_list = mail_header_msgid_re.findall(decode(message['References']))
[x.strip() for x in decode(message['References']).split()])]) parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', [x.strip() for x in msg_list])])
if parent_ids: if parent_ids:
msg_dict['parent_id'] = parent_ids[0] msg_dict['parent_id'] = parent_ids[0]

View File

@ -267,10 +267,7 @@ class mail_compose_message(osv.TransientModel):
# mass mailing: rendering override wizard static values # mass mailing: rendering override wizard static values
if mass_mail_mode and wizard.model: if mass_mail_mode and wizard.model:
# always keep a copy, reset record name (avoid browsing records) # always keep a copy, reset record name (avoid browsing records)
mail_values.update(notification=True, record_name=False) mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False)
if hasattr(self.pool[wizard.model], 'message_new'):
mail_values['model'] = wizard.model
mail_values['res_id'] = res_id
# auto deletion of mail_mail # auto deletion of mail_mail
if 'mail_auto_delete' in context: if 'mail_auto_delete' in context:
mail_values['auto_delete'] = context.get('mail_auto_delete') mail_values['auto_delete'] = context.get('mail_auto_delete')

View File

@ -84,7 +84,8 @@ class MailMail(osv.Model):
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): 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) 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'): 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) unsubscribe_url = self._get_unsubscribe_url(cr, uid, mail, email_to, context=context)
if unsubscribe_url: if unsubscribe_url:
res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p') res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p')

View File

@ -591,6 +591,7 @@
<field name="mail_mail_id"/> <field name="mail_mail_id"/>
<field name="message_id"/> <field name="message_id"/>
<field name="sent"/> <field name="sent"/>
<field name="exception"/>
<field name="opened"/> <field name="opened"/>
<field name="replied"/> <field name="replied"/>
<field name="bounced"/> <field name="bounced"/>

View File

@ -1071,8 +1071,8 @@ class mrp_production(osv.osv):
return False return False
# Take routing location as a Source Location. # Take routing location as a Source Location.
source_location_id = production.location_src_id.id source_location_id = production.location_src_id.id
if production.bom_id.routing_id and production.bom_id.routing_id.location_id: if production.routing_id and production.routing_id.location_id:
source_location_id = production.bom_id.routing_id.location_id.id source_location_id = production.routing_id.location_id.id
destination_location_id = production.product_id.property_stock_production.id destination_location_id = production.product_id.property_stock_production.id
if not source_location_id: if not source_location_id:

View File

@ -678,6 +678,17 @@ class product_template(osv.osv):
if not context or "create_product_product" not in context: if not context or "create_product_product" not in context:
self.create_variant_ids(cr, uid, [product_template_id], context=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) 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 return product_template_id
def write(self, cr, uid, ids, vals, context=None): def write(self, cr, uid, ids, vals, context=None):

View File

@ -27,7 +27,7 @@ class account_invoice(osv.Model):
template_values = Composer.onchange_template_id( template_values = Composer.onchange_template_id(
cr, uid, composer_id, line.product_id.email_template_id.id, 'comment', 'account.invoice', invoice.id cr, uid, composer_id, line.product_id.email_template_id.id, 'comment', 'account.invoice', invoice.id
)['value'] )['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.write(cr, uid, [composer_id], template_values, context=context)
Composer.send_mail(cr, uid, [composer_id], context=context) Composer.send_mail(cr, uid, [composer_id], context=context)
return True return True

View File

@ -50,8 +50,8 @@
<field name="user_id" ref="base.user_demo"/> <field name="user_id" ref="base.user_demo"/>
<field name="alias_model">project.task</field> <field name="alias_model">project.task</field>
<field name="message_follower_ids" eval="[(6, 0, [ <field name="message_follower_ids" eval="[(6, 0, [
ref('base.user_root'), ref('base.partner_root'),
ref('base.user_demo')])]"/> ref('base.partner_demo')])]"/>
</record> </record>
<!-- We assign after so that default values applies --> <!-- We assign after so that default values applies -->

View File

@ -749,7 +749,7 @@
</tree> </tree>
</field> </field>
--> -->
<field name="move_lines" context="{'address_in_id': partner_id, 'form_view_ref':'stock.view_move_picking_form', 'tree_view_ref':'view_move_picking_tree', 'default_picking_type_id': picking_type_id,'default_picking_id': active_id}"/> <field name="move_lines" context="{'address_in_id': partner_id, 'form_view_ref':'stock.view_move_picking_form', 'tree_view_ref':'stock.view_move_picking_tree', 'default_picking_type_id': picking_type_id,'default_picking_id': active_id}"/>
<field name="pack_operation_exist" invisible="1"/> <field name="pack_operation_exist" invisible="1"/>
<field name="note" placeholder="Add an internal note..." class="oe_inline"/> <field name="note" placeholder="Add an internal note..." class="oe_inline"/>
</page> </page>
@ -1288,7 +1288,7 @@
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="domain" eval="['|','&amp;',('picking_id','=',False),('location_dest_id.usage', 'in', ['customer','supplier']),'&amp;',('picking_id','!=',False),('picking_id.picking_type_id.code','=','outgoing')]"/> <field name="domain" eval="[('picking_id.picking_type_id.code','=','incoming'), ('location_id.usage','!=','internal'), ('location_dest_id.usage', '=', 'internal')]"/>
<field name="view_id" ref="view_move_tree_reception_picking"/> <field name="view_id" ref="view_move_tree_reception_picking"/>
<field name="context">{'product_receive': True, 'search_default_future': True}</field> <field name="context">{'product_receive': True, 'search_default_future': True}</field>
<field name="help" type="html"> <field name="help" type="html">

View File

@ -2373,7 +2373,7 @@
} }
.openerp .oe_fileupload .oe_add button.oe_attach .oe_e { .openerp .oe_fileupload .oe_add button.oe_attach .oe_e {
position: relative; position: relative;
top: -1px; top: -10px;
left: -9px; left: -9px;
} }
.openerp .oe_fileupload .oe_add input.oe_form_binary_file { .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 { .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; background-color: #729fcf !important;
} }
}
.openerp_ie .oe_webclient {
height: auto !important;
@media print { @media print {
.openerp { .openerp {
@ -3447,6 +3450,39 @@ input[type="radio"], input[type="checkbox"] {
opacity: 0.6; 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 { body {
overflow: auto; overflow: auto;
} }

View File

@ -1939,7 +1939,7 @@ $sheet-padding: 16px
text-shadow: none text-shadow: none
.oe_e .oe_e
position: relative position: relative
top: -1px top: -10px
left: -9px left: -9px
input.oe_form_binary_file input.oe_form_binary_file
display: inline-block display: inline-block
@ -2691,6 +2691,8 @@ body.oe_single_form
> .arrow span > .arrow span
background-color: #729fcf !important background-color: #729fcf !important
.oe_webclient
height: auto !important
// }}} // }}}
// @media print {{{ // @media print {{{
@ -2799,6 +2801,33 @@ input[type="radio"], input[type="checkbox"]
background-color: black background-color: black
opacity: 0.6000000238418579 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 body
overflow: auto overflow: auto

View File

@ -233,7 +233,8 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
value = value.replace(instance.web._t.database.parameters.thousands_sep, ""); value = value.replace(instance.web._t.database.parameters.thousands_sep, "");
} while(tmp !== value); } while(tmp !== value);
tmp = Number(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)); throw new Error(_.str.sprintf(_t("'%s' is not a correct integer"), value));
return tmp; return tmp;
case 'float': case 'float':
@ -268,6 +269,11 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
case 'datetime': case 'datetime':
var datetime = Date.parseExact( var datetime = Date.parseExact(
value, (date_pattern + ' ' + time_pattern)); 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) if (datetime !== null)
return instance.web.datetime_to_str(datetime); return instance.web.datetime_to_str(datetime);
datetime = Date.parse(value); 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)); throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
case 'date': case 'date':
var date = Date.parseExact(value, date_pattern); 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) if (date !== null)
return instance.web.date_to_str(date); return instance.web.date_to_str(date);
date = Date.parse(value); date = Date.parse(value);

View File

@ -346,11 +346,11 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
'keydown .oe_searchview_input, .oe_searchview_facet': function (e) { 'keydown .oe_searchview_input, .oe_searchview_facet': function (e) {
switch(e.which) { switch(e.which) {
case $.ui.keyCode.LEFT: case $.ui.keyCode.LEFT:
this.focusPreceding(this); this.focusPreceding(e.target);
e.preventDefault(); e.preventDefault();
break; break;
case $.ui.keyCode.RIGHT: case $.ui.keyCode.RIGHT:
this.focusFollowing(this); this.focusFollowing(e.target);
e.preventDefault(); e.preventDefault();
break; break;
} }

View File

@ -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<len; index++) {
var step = tour.steps[index];
step.id = index;
if (!step.waitNot && 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<len; index++) {
var step = tour.steps[index];
step._title = step._title || step.title;
step.title = Tour.popoverTitle(tour, { title: step._title });
step.template = step.template || Tour.popover( step.popover );
}
}
},
closePopover: function () {
if (Tour.$element) {
Tour.$element.popover('destroy');
Tour.$element.removeData("tour");
Tour.$element.removeData("tour-step");
$(".tour-backdrop").remove();
$(".popover.tour").remove();
Tour.$element = null;
}
},
autoTogglePopover: function () {
var state = Tour.getState();
var step = state.step;
if (Tour.$element &&
Tour.$element.is(":visible") &&
Tour.$element.data("tour") === state.id &&
Tour.$element.data("tour-step") === step.id) {
Tour.repositionPopover();
return;
}
if (step.busy) {
return;
}
Tour.closePopover();
var $element = $(step.element).first();
if (!step.element || !$element.size() || !$element.is(":visible")) {
return;
}
Tour.$element = $element;
$element.data("tour", state.id);
$element.data("tour-step", step.id);
$element.popover({
placement: step.placement || "auto",
animation: true,
trigger: "manual",
title: step.title,
content: step.content,
html: true,
container: "body",
template: step.template,
orphan: step.orphan
}).popover("show");
var $tip = $element.data("bs.popover").tip();
// add popover style (orphan, static, backdrop)
if (step.orphan) {
$tip.addClass("orphan");
}
var node = $element[0];
var css;
do {
css = window.getComputedStyle(node);
if (!css || css.position == "fixed") {
$tip.addClass("fixed");
break;
}
} while ((node = node.parentNode) && node !== document);
if (step.backdrop) {
$("body").append('<div class="tour-backdrop"></div>');
}
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);
}());

View File

@ -2632,6 +2632,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
type_of_date: "datetime", type_of_date: "datetime",
events: { events: {
'change .oe_datepicker_master': 'change_datetime', 'change .oe_datepicker_master': 'change_datetime',
'keypress .oe_datepicker_master': 'change_datetime',
}, },
init: function(parent) { init: function(parent) {
this._super(parent); this._super(parent);
@ -2750,8 +2751,8 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
format_client: function(v) { format_client: function(v) {
return instance.web.format_value(v, {"widget": this.type_of_date}); return instance.web.format_value(v, {"widget": this.type_of_date});
}, },
change_datetime: function() { change_datetime: function(e) {
if (this.is_valid_()) { if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
this.set_value_from_ui_(); this.set_value_from_ui_();
this.trigger("datetime_changed"); this.trigger("datetime_changed");
} }

View File

@ -130,15 +130,7 @@
if (this.editable()) { if (this.editable()) {
this.$el.find('table:first').show(); this.$el.find('table:first').show();
this.$el.find('.oe_view_nocontent').remove(); this.$el.find('.oe_view_nocontent').remove();
this.start_edition().then(function(){ this.start_edition();
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;
}
});
});
} else { } else {
this._super(); this._super();
} }
@ -243,6 +235,7 @@
return this.ensure_saved().then(function () { return this.ensure_saved().then(function () {
var $recordRow = self.groups.get_row_for(record); var $recordRow = self.groups.get_row_for(record);
var cells = self.get_cells_for($recordRow); var cells = self.get_cells_for($recordRow);
var fields = {};
self.fields_for_resize.splice(0, self.fields_for_resize.length); self.fields_for_resize.splice(0, self.fields_for_resize.length);
return self.with_event('edit', { return self.with_event('edit', {
record: record.attributes, record: record.attributes,
@ -256,10 +249,16 @@
// FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
field.$el.attr('data-fieldname', field_name); field.$el.attr('data-fieldname', field_name);
fields[field_name] = field;
self.fields_for_resize.push({field: field, cell: cell}); self.fields_for_resize.push({field: field, cell: cell});
}, options).then(function () { }, options).then(function () {
$recordRow.addClass('oe_edition'); $recordRow.addClass('oe_edition');
self.resize_fields(); 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; return record.attributes;
}); });
}).fail(function () { }).fail(function () {
@ -749,31 +748,6 @@
throw new Error("is_editing's state filter must be either `new` or" + throw new Error("is_editing's state filter must be either `new` or" +
" `edit` if provided"); " `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) { edit: function (record, configureField, options) {
// TODO: specify sequence of edit calls // TODO: specify sequence of edit calls
var self = this; var self = this;
@ -788,7 +762,6 @@
_(form.fields).each(function (field, name) { _(form.fields).each(function (field, name) {
configureField(name, field); configureField(name, field);
}); });
self._focus_setup(options && options.focus_field);
return form; return form;
}); });
}, },

View File

@ -2038,4 +2038,5 @@
</t> </t>
<t t-name="StatInfo"> <t t-name="StatInfo">
<strong><t t-esc="value"/></strong><br/><t t-esc="text"/></t> <strong><t t-esc="value"/></strong><br/><t t-esc="text"/></t>
</templates> </templates>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve"> <templates id="template" xml:space="preserve">
<t t-name="website.tour_popover"> <t t-name="tour.popover">
<div t-attf-class="#{ fixed ? 'popover tour fixed' : 'popover tour' }"> <div t-attf-class="#{ fixed ? 'popover tour fixed' : 'popover tour' }">
<div class="arrow"></div> <div class="arrow" t-if="!next"></div>
<h3 class="popover-title"></h3> <h3 class="popover-title"></h3>
<div class="popover-content"></div> <div class="popover-content"></div>
<t t-if="next or end"> <t t-if="next or end">
@ -21,7 +21,7 @@
</t> </t>
</div> </div>
</t> </t>
<t t-name="website.tour_popover_title"> <t t-name="tour.popover_title">
<t t-esc="title"/><button title="End This Tutorial" type="button" class="close" data-role="end">×</button> <t t-esc="title"/><button title="End This Tutorial" type="button" class="close" data-role="end">×</button>
</t> </t>
</templates> </templates>

View File

@ -51,6 +51,7 @@
<script src="/web/static/src/js/view_list_editable.js" type="text/javascript"></script> <script src="/web/static/src/js/view_list_editable.js" type="text/javascript"></script>
<script src="/web/static/src/js/view_tree.js" type="text/javascript"></script> <script src="/web/static/src/js/view_tree.js" type="text/javascript"></script>
<script src="/base/static/src/js/apps.js" type="text/javascript"></script> <script src="/base/static/src/js/apps.js" type="text/javascript"></script>
<script src="/web/static/src/js/tour.js" type="text/javascript"></script>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/> <link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<link href="/web/static/lib/cleditor/jquery.cleditor.css" rel="stylesheet"/> <link href="/web/static/lib/cleditor/jquery.cleditor.css" rel="stylesheet"/>
<link href="/web/static/lib/jquery.textext/jquery.textext.css" rel="stylesheet"/> <link href="/web/static/lib/jquery.textext/jquery.textext.css" rel="stylesheet"/>

View File

@ -22,6 +22,7 @@
<script type="text/javascript" src="/web/static/lib/qweb/qweb2.js"></script> <script type="text/javascript" src="/web/static/lib/qweb/qweb2.js"></script>
<script type="text/javascript" src="/web/static/src/js/openerpframework.js"></script> <script type="text/javascript" src="/web/static/src/js/openerpframework.js"></script>
<script type="text/javascript" src="/web/static/src/js/tour.js"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
openerp._modules = <t t-raw="modules"/>; openerp._modules = <t t-raw="modules"/>;
</script> </script>
@ -246,6 +247,7 @@
<t t-call="web.assets_backend"/> <t t-call="web.assets_backend"/>
<script type="text/javascript" id="qunit_config"> <script type="text/javascript" id="qunit_config">
localStorage.clear();
QUnit.config.testTimeout = 5 * 60 * 1000; QUnit.config.testTimeout = 5 * 60 * 1000;
QUnit.moduleDone(function(result) { QUnit.moduleDone(function(result) {
console.log(result.name + " (" + result.passed + "/" + result.total + " passed tests)"); console.log(result.name + " (" + result.passed + "/" + result.total + " passed tests)");

View File

@ -218,7 +218,12 @@ openerp.web_calendar = function(instance) {
this.info_fields.push(fv.arch.children[fld].attrs.name); this.info_fields.push(fv.arch.children[fld].attrs.name);
} }
return (new instance.web.Model(this.dataset.model)) var edit_check = new instance.web.Model(this.dataset.model)
.call("check_access_rights", ["write", false])
.then(function (write_right) {
self.write_right = write_right;
});
var init = new instance.web.Model(this.dataset.model)
.call("check_access_rights", ["create", false]) .call("check_access_rights", ["create", false])
.then(function (create_right) { .then(function (create_right) {
self.create_right = create_right; self.create_right = create_right;
@ -228,6 +233,7 @@ openerp.web_calendar = function(instance) {
self.ready.resolve(); self.ready.resolve();
}); });
}); });
return $.when(edit_check, init);
}, },
get_fc_init_options: function () { get_fc_init_options: function () {
@ -841,7 +847,11 @@ openerp.web_calendar = function(instance) {
if (! this.open_popup_action) { if (! this.open_popup_action) {
var index = this.dataset.get_id_index(id); var index = this.dataset.get_id_index(id);
this.dataset.index = index; this.dataset.index = index;
this.do_switch_view('form', null, { mode: "edit" }); if (this.write_right) {
this.do_switch_view('form', null, { mode: "edit" });
} else {
this.do_switch_view('form', null, { mode: "view" });
}
} }
else { else {
var pop = new instance.web.form.FormOpenPopup(this); var pop = new instance.web.form.FormOpenPopup(this);

View File

@ -516,36 +516,3 @@ ul.oe_menu_editor .disclose {
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
opacity: 0; opacity: 0;
} }
/* ---- 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;
}

View File

@ -450,32 +450,4 @@ $infobar_height: 20px
// }}} // }}}
/* ---- 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
// }}}
// vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker: // vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker:

View File

@ -469,7 +469,7 @@
} }
); );
}); });
menu.on('click', 'a[data-action!=ace]', function (event) { menu.on('click', 'a[data-view-id]', function (event) {
var view_id = $(event.currentTarget).data('view-id'); var view_id = $(event.currentTarget).data('view-id');
return openerp.jsonRpc('/web/dataset/call_kw', 'call', { return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'ir.ui.view', model: 'ir.ui.view',
@ -1516,7 +1516,7 @@
url: this.link url: this.link
}); });
this.media.renameNode("img"); this.media.renameNode("img");
this.media.$.attributes.src = this.link; $(this.media).attr('src', this.link);
return this._super(); return this._super();
}, },
clear: function () { clear: function () {
@ -2000,6 +2000,11 @@
// a/@href, ...) // a/@href, ...)
_(mutations).chain() _(mutations).chain()
.filter(function (m) { .filter(function (m) {
// ignore any SVG target, these blokes are like weird mon
if (m.target && m.target instanceof SVGElement) {
return false;
}
// ignore any change related to mundane image-edit-button // ignore any change related to mundane image-edit-button
if (m.target && m.target.className if (m.target && m.target.className
&& m.target.className.indexOf('image-edit-button') !== -1) { && m.target.className.indexOf('image-edit-button') !== -1) {

View File

@ -20,7 +20,7 @@
if (!window.location.origin) { // fix for ie9 if (!window.location.origin) { // fix for ie9
window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: '');
} }
document.getElementById("mobile-viewport").src = window.location.origin + window.location.pathname + "#mobile-preview"; document.getElementById("mobile-viewport").src = window.location.origin + window.location.pathname + window.location.search + "#mobile-preview";
this.$el.modal(); this.$el.modal();
}, },
destroy: function () { destroy: function () {

View File

@ -1,10 +1,9 @@
(function () { (function () {
'use strict'; 'use strict';
var website = openerp.website;
var _t = openerp._t; var _t = openerp._t;
website.Tour.register({ openerp.Tour.register({
id: 'banner', id: 'banner',
name: _t("Build a page"), name: _t("Build a page"),
path: '/page/website.homepage', path: '/page/website.homepage',

View File

@ -1,535 +1,24 @@
(function () { (function () {
'use strict'; 'use strict';
// raise an error in test mode if openerp don't exist window.openerp.website.EditorBar.include({
if (typeof openerp === "undefined") { tours: [],
var error = "openerp is undefined" start: function () {
+ "\nhref: " + window.location.href var self = this;
+ "\nreferrer: " + document.referrer var menu = $('#help-menu');
+ "\nlocalStorage: " + window.localStorage.getItem("tour"); _.each(window.openerp.Tour.tours, function (tour) {
if (typeof $ !== "undefined") { if (tour.mode === "test") {
error += '\n\n' + $("body").html(); return;
} }
throw new Error(error); var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
} $menuItem.click(function () {
T.reset();
var website = window.openerp.website; T.run(tour.id);
// don't rewrite T in test mode
if (typeof website.Tour !== "undefined") {
return;
}
// don't need template to use bootstrap Tour in automatic mode
if (typeof QWeb2 !== "undefined") {
website.add_template_file('/website/static/src/xml/website.tour.xml');
}
if (website.EditorBar) {
website.EditorBar.include({
tours: [],
start: function () {
var self = this;
var menu = $('#help-menu');
_.each(T.tours, function (tour) {
if (tour.mode === "test") {
return;
}
var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
$menuItem.click(function () {
T.reset();
T.run(tour.id);
});
menu.append($menuItem);
}); });
return this._super(); menu.append($menuItem);
} });
}); return this._super();
}
/////////////////////////////////////////////////
/* 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 T = website.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";
T.tours[tour.id] = tour;
},
run: function (tour_id, mode) {
var tour = T.tours[tour_id];
this.time = new Date().getTime();
if (tour.path && !window.location.href.match(new RegExp("("+T.getLang()+")?"+tour.path+"#?$", "i"))) {
var href = "/"+T.getLang()+tour.path;
console.log("Tour Begin from run method (redirection to "+href+")");
T.saveState(tour.id, mode || tour.mode, -1, 0);
window.location.href = href;
} else {
console.log("Tour Begin from run method");
T.saveState(tour.id, mode || tour.mode, 0, 0);
T.running();
}
},
registerSteps: function (tour) {
if (tour.register) {
return;
}
tour.register = true;
for (var index=0, len=tour.steps.length; index<len; index++) {
var step = tour.steps[index];
step.id = index;
if (!step.waitNot && 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;
}
}
if (tour.steps[index-1] &&
tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
var step = {
_title: "",
id: index,
waitNot: '.popover.tour.fade.in:visible'
};
tour.steps.push(step);
}
// rendering bootstrap tour and popover
if (tour.mode !== "test") {
for (var index=0, len=tour.steps.length; index<len; index++) {
var step = tour.steps[index];
step._title = step._title || step.title;
step.title = T.popoverTitle(tour, { title: step._title });
step.template = step.template || T.popover( step.popover );
}
}
},
closePopover: function () {
if (T.$element) {
T.$element.popover('destroy');
T.$element.removeData("tour");
T.$element.removeData("tour-step");
$(".tour-backdrop").remove();
$(".popover.tour").remove();
T.$element = null;
}
},
autoTogglePopover: function () {
var state = T.getState();
var step = state.step;
if (T.$element &&
T.$element.is(":visible") &&
T.$element.data("tour") === state.id &&
T.$element.data("tour-step") === step.id) {
T.repositionPopover();
return;
}
if (step.busy) {
return;
}
T.closePopover();
var $element = $(step.element).first();
if (!step.element || !$element.size() || !$element.is(":visible")) {
return;
}
T.$element = $element;
$element.data("tour", state.id);
$element.data("tour-step", step.id);
$element.popover({
placement: step.placement || "auto",
animation: true,
trigger: "manual",
title: step.title,
content: step.content,
html: true,
container: "body",
template: step.template,
orphan: step.orphan
}).popover("show");
var $tip = $element.data("bs.popover").tip();
// add popover style (orphan, static, backdrop)
if (step.orphan) {
$tip.addClass("orphan");
}
var node = $element[0];
var css;
do {
css = window.getComputedStyle(node);
if (!css || css.position == "fixed") {
$tip.addClass("fixed");
break;
}
} while ((node = node.parentNode) && node !== document);
if (step.backdrop) {
$("body").append('<div class="tour-backdrop"></div>');
}
if (step.backdrop || $element.parents("#website-top-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(T.timer);
T.endTour();
}
T.closePopover();
});
T.repositionPopover();
},
repositionPopover: function() {
var popover = T.$element.data("bs.popover");
var $tip = T.$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 = T.$element.offset().left + T.$element.outerWidth()/2 - tipOffset.left;
$tip.find(".arrow").css("left", left ? left + "px" : "");
} else if (popover.options.placement !== "auto") {
var top = T.$element.offset().top + T.$element.outerHeight()/2 - tipOffset.top;
$tip.find(".arrow").css("top", top ? top + "px" : "");
}
},
popoverTitle: function (tour, options) {
return openerp.qweb ? openerp.qweb.render('website.tour_popover_title', options) : options.title;
},
popover: function (options) {
return openerp.qweb ? openerp.qweb.render('website.tour_popover', options) : options.title;
},
getLang: function () {
return $("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");
T.saveState(state.id, state.mode, state.step_id, 0);
}
if (!state.id) {
return;
}
state.tour = T.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 = T.getState();
message += '\n tour: ' + state.id
+ '\n step: ' + step.id + ": '" + (step._title || step.title) + "'"
+ '\n href: ' + window.location.href
+ '\n referrer: ' + document.referrer
+ '\n element: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden")))
+ '\n waitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size())
+ '\n waitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size())
+ "\n localStorage: " + JSON.stringify(localStorage)
+ '\n\n' + $("body").html();
T.reset();
throw new Error(message);
},
lists: function () {
var tour_ids = [];
for (var k in T.tours) {
tour_ids.push(k);
}
return tour_ids;
},
saveState: function (tour_id, mode, step_id, number) {
localStorage.setItem("tour", JSON.stringify({"id":tour_id, "mode":mode, "step_id":step_id || 0, "time": this.time, "number": number+1}));
},
reset: function () {
var state = T.getState();
if (state) {
for (var k in state.tour.steps) {
state.tour.steps[k].busy = false;
}
}
localStorage.removeItem("tour");
clearTimeout(T.timer);
clearTimeout(T.testtimer);
T.closePopover();
},
running: function () {
function run () {
var state = T.getState();
if (!state) return;
if (state.tour) {
console.log("Tour '"+state.id+"' is running");
T.registerSteps(state.tour);
T.nextStep();
} else {
console.log("Tour '"+state.id+"' wait for running (tour undefined)");
setTimeout(T.running, state.mode === "test" ? T.defaultDelay : T.retryRunningDelay);
}
}
setTimeout(function () {
if ($.ajaxBusy) {
$(document).ajaxStop(run);
} else {
run();
}
},0);
},
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 = T.getState();
var time = new Date().getTime();
var timer;
var next = state.tour.steps[state.step.id+1];
var overlaps = state.mode === "test" ? T.errorDelay : 0;
window.onbeforeunload = function () {
clearTimeout(T.timer);
clearTimeout(T.testtimer);
};
function checkNext () {
T.autoTogglePopover();
clearTimeout(T.timer);
if (T.check(next)) {
clearTimeout(T.currentTimer);
// use an other timeout for cke dom loading
T.saveState(state.id, state.mode, state.step.id, 0);
setTimeout(function () {
T.nextStep(next);
}, T.defaultDelay);
} else if (!overlaps || new Date().getTime() - time < overlaps) {
T.timer = setTimeout(checkNext, T.defaultDelay);
} else {
T.error(next, "Can't reach the next step");
}
}
checkNext();
},
nextStep: function (step) {
var state = T.getState();
if (!state) {
return;
}
step = step || state.step;
var next = state.tour.steps[step.id+1];
if (state.number > 3) {
T.error(next, "Cycling. Can't reach the next step");
}
T.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)");
}
T.autoTogglePopover(true);
if (step.onload) {
step.onload();
}
if (next) {
setTimeout(function () {
T.waitNextStep();
if (state.mode === "test") {
setTimeout(function(){
T.autoNextStep(state.tour, step);
}, T.defaultDelay);
}
}, next.wait || 0);
} else {
T.endTour();
}
},
endTour: function () {
var state = T.getState();
var test = state.step.id >= state.tour.steps.length-1;
T.reset();
if (test) {
console.log('ok');
} else {
console.log('error');
}
},
autoNextStep: function (tour, step) {
clearTimeout(T.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) {
T.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);
}
}
T.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 }));
}
};
//$(document).ready(T.running);
website.ready().then(T.running);
}()); }());

View File

@ -8,6 +8,6 @@ class TestUi(openerp.tests.HttpCase):
self.phantom_js("/", "console.log('ok')", "openerp.website.editor", login='admin') self.phantom_js("/", "console.log('ok')", "openerp.website.editor", login='admin')
def test_04_admin_tour_banner(self): def test_04_admin_tour_banner(self):
self.phantom_js("/", "openerp.website.Tour.run('banner', 'test')", "openerp.website.Tour.tours.banner", login='admin') self.phantom_js("/", "openerp.Tour.run('banner', 'test')", "openerp.Tour.tours.banner", login='admin')
# vim:et: # vim:et:

View File

@ -1,10 +1,9 @@
(function () { (function () {
'use strict'; 'use strict';
var website = openerp.website;
var _t = openerp._t; var _t = openerp._t;
website.Tour.register({ openerp.Tour.register({
id: 'blog', id: 'blog',
name: _t("Create a blog post"), name: _t("Create a blog post"),
steps: [ steps: [

View File

@ -2,5 +2,5 @@ import openerp.tests
class TestUi(openerp.tests.HttpCase): class TestUi(openerp.tests.HttpCase):
def test_admin(self): def test_admin(self):
self.phantom_js("/", "openerp.website.Tour.run('blog', 'test')", "openerp.website.Tour.tours.blog") self.phantom_js("/", "openerp.Tour.run('blog', 'test')", "openerp.Tour.tours.blog")

View File

@ -1,10 +1,9 @@
(function () { (function () {
'use strict'; 'use strict';
var website = openerp.website;
var _t = openerp._t; var _t = openerp._t;
website.Tour.register({ openerp.Tour.register({
id: 'event', id: 'event',
name: _t("Create an event"), name: _t("Create an event"),
steps: [ steps: [

View File

@ -2,5 +2,5 @@ import openerp.tests
class TestUi(openerp.tests.HttpCase): class TestUi(openerp.tests.HttpCase):
def test_admin(self): def test_admin(self):
self.phantom_js("/", "openerp.website.Tour.run('event', 'test')", "openerp.website.Tour.tours.event") self.phantom_js("/", "openerp.Tour.run('event', 'test')", "openerp.Tour.tours.event")

View File

@ -1,9 +1,7 @@
(function () { (function () {
'use strict'; 'use strict';
var website = openerp.website; openerp.Tour.register({
website.Tour.register({
id: 'event_buy_tickets', id: 'event_buy_tickets',
name: "Try to buy tickets for event", name: "Try to buy tickets for event",
path: '/event', path: '/event',

View File

@ -3,19 +3,19 @@ import os
import openerp.tests import openerp.tests
inject = [ inject = [
("openerp.website.Tour", os.path.join(os.path.dirname(__file__), '../../website/static/src/js/website.tour.js')), ("openerp.Tour", os.path.join(os.path.dirname(__file__), '../../web/static/src/js/tour.js')),
("openerp.website.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.event_sale.js")), ("openerp.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.event_sale.js")),
] ]
@openerp.tests.common.at_install(False) @openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True) @openerp.tests.common.post_install(True)
class TestUi(openerp.tests.HttpCase): class TestUi(openerp.tests.HttpCase):
def test_admin(self): def test_admin(self):
self.phantom_js("/", "openerp.website.Tour.run('event_buy_tickets', 'test')", "openerp.website.Tour.tours.event_buy_tickets", inject=inject) self.phantom_js("/", "openerp.Tour.run('event_buy_tickets', 'test')", "openerp.Tour.tours.event_buy_tickets", inject=inject)
def test_demo(self): def test_demo(self):
self.phantom_js("/", "openerp.website.Tour.run('event_buy_tickets', 'test')", "openerp.website.Tour.tours.event_buy_tickets", login="demo", password="demo", inject=inject); self.phantom_js("/", "openerp.Tour.run('event_buy_tickets', 'test')", "openerp.Tour.tours.event_buy_tickets", login="demo", password="demo", inject=inject);
def test_public(self): def test_public(self):
self.phantom_js("/", "openerp.website.Tour.run('event_buy_tickets', 'test')", "openerp.website.Tour.tours.event_buy_tickets", login=None, inject=inject); self.phantom_js("/", "openerp.Tour.run('event_buy_tickets', 'test')", "openerp.Tour.tours.event_buy_tickets", login=None, inject=inject);

View File

@ -120,7 +120,7 @@ class WebsiteForum(http.Controller):
question_count = Post.search(cr, uid, domain, count=True, context=context) question_count = Post.search(cr, uid, domain, count=True, context=context)
if tag: if tag:
url = "/forum/%s/%s/questions" % (slug(forum), slug(tag)) url = "/forum/%s/tag/%s/questions" % (slug(forum), slug(tag))
else: else:
url = "/forum/%s" % slug(forum) url = "/forum/%s" % slug(forum)

View File

@ -148,7 +148,7 @@ function IsKarmaValid(eventNumber,minKarma){
CKEDITOR.tools.callFunction(eventNumber,this); CKEDITOR.tools.callFunction(eventNumber,this);
return false; return false;
} else { } else {
alert("Sorry you need more than 30 Karma."); alert("Sorry you need more than " + minKarma + " Karma.");
} }
} }

View File

@ -92,9 +92,8 @@ class WebsiteMail(http.Controller):
values['is_follower'] = len( values['is_follower'] = len(
request.registry['mail.followers'].search( request.registry['mail.followers'].search(
cr, SUPERUSER_ID, [ cr, SUPERUSER_ID, [
('res_model', '=', 'mail.group'), ('res_model', '=', model),
('res_id', '=', obj_ids[0]), ('res_id', '=', obj_ids[0]),
('partner_id', '=', partner_id.id) ('partner_id', '=', partner_id.id)
], context=context)) == 1 ], context=context)) == 1
return values return values

View File

@ -0,0 +1,68 @@
(function () {
'use strict';
var website = openerp.website;
website.snippet.animationRegistry.follow = website.snippet.Animation.extend({
selector: ".js_follow",
start: function (editable_mode) {
var self = this;
this.is_user = false;
openerp.jsonRpc('/website_mail/is_follower', 'call', {
model: this.$target.data('object'),
id: this.$target.data('id'),
}).always(function (data) {
self.is_user = data.is_user;
self.email = data.email;
self.toggle_subscription(data.is_follower, data.email);
self.$target.removeClass("hidden");
});
// 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");
this.$target.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) {
event.preventDefault();
self.on_click();
});
}
return;
},
on_click: function () {
var self = this;
var $email = this.$target.find(".js_follow_email");
if ($email.length && !$email.val().match(/.+@.+/)) {
this.$target.addClass('has-error');
return false;
}
this.$target.removeClass('has-error');
openerp.jsonRpc('/website_mail/follow', 'call', {
'id': +this.$target.data('id'),
'object': this.$target.data('object'),
'message_is_follower': this.$target.attr("data-follow") || "off",
'email': $email.length ? $email.val() : false,
}).then(function (follow) {
self.toggle_subscription(follow, self.email);
});
},
toggle_subscription: function(follow, email) {
console.log(follow, email);
if (follow) {
this.$target.find(".js_follow_btn").addClass("hidden");
this.$target.find(".js_unfollow_btn").removeClass("hidden");
}
else {
this.$target.find(".js_follow_btn").removeClass("hidden");
this.$target.find(".js_unfollow_btn").addClass("hidden");
}
this.$target.find('input.js_follow_email')
.val(email ? email : "")
.attr("disabled", follow || (email.length && this.is_user) ? "disabled" : false);
this.$target.attr("data-follow", follow ? 'on' : 'off');
},
});
})();

View File

@ -21,6 +21,7 @@
<template id="head" inherit_id="website.assets_frontend" name="Mail customization"> <template id="head" inherit_id="website.assets_frontend" name="Mail customization">
<xpath expr="/t" position="inside"> <xpath expr="/t" position="inside">
<script type="text/javascript" src="/website_mail/static/src/js/follow.js"></script>
<link rel='stylesheet' href='/website_mail/static/src/css/website_mail.css'/> <link rel='stylesheet' href='/website_mail/static/src/css/website_mail.css'/>
</xpath> </xpath>
</template> </template>

View File

@ -1 +1,2 @@
import controllers import controllers
import models

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime 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.web import http
from openerp.addons.website.models.website import slug from openerp.addons.website.models.website import slug
from openerp.addons.web.http import request from openerp.addons.web.http import request
@ -28,12 +29,23 @@ class MailGroup(http.Controller):
def view(self, **post): def view(self, **post):
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
group_obj = request.registry.get('mail.group') 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) 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) return request.website.render('website_mail_group.mail_groups', values)
@http.route(["/groups/subscription/"], type='json', auth="user") @http.route(["/groups/subscription/"], type='json', auth="user")
def subscription(self, group_id=0, action=False, **post): def subscription(self, group_id=0, action=False, **post):
""" TDE FIXME: seems dead code """
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
group_obj = request.registry.get('mail.group') group_obj = request.registry.get('mail.group')
if action: if action:

View File

@ -0,0 +1 @@
import mail_group

View File

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

View File

@ -3,8 +3,8 @@
var website = openerp.website; var website = openerp.website;
website.snippet.animationRegistry.follow = website.snippet.Animation.extend({ website.snippet.animationRegistry.follow_alias = website.snippet.Animation.extend({
selector: ".js_follow", selector: ".js_follow_alias",
start: function (editable_mode) { start: function (editable_mode) {
var self = this; var self = this;
this.is_user = false; this.is_user = false;
@ -23,8 +23,8 @@
// not if editable mode to allow designer to edit alert field // not if editable mode to allow designer to edit alert field
if (!editable_mode) { if (!editable_mode) {
$('.js_follow > .alert').addClass("hidden"); $('.js_follow_alias > .alert').addClass("hidden");
$('.js_follow > .input-group-btn.hidden').removeClass("hidden"); $('.js_follow_alias > .input-group-btn.hidden').removeClass("hidden");
this.$target.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) { this.$target.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) {
event.preventDefault(); event.preventDefault();
self.on_click(); self.on_click();

View File

@ -11,7 +11,7 @@
<span class="oe_snippet_thumbnail_title">Discussion Group</span> <span class="oe_snippet_thumbnail_title">Discussion Group</span>
</div> </div>
<div class="oe_snippet_body js_follow" <div class="oe_snippet_body js_follow_alias"
data-id="0" data-id="0"
data-object="mail.group" data-object="mail.group"
data-follow="off"> data-follow="off">
@ -38,7 +38,7 @@
<xpath expr="//div[@id='snippet_options']" position="inside"> <xpath expr="//div[@id='snippet_options']" position="inside">
<div data-snippet-option-id='subscribe' <div data-snippet-option-id='subscribe'
data-selector=".js_follow" data-selector=".js_follow_alias"
data-selector-siblings="p, h1, h2, h3, blockquote, .well, .panel" data-selector-siblings="p, h1, h2, h3, blockquote, .well, .panel"
> >
<li> <li>

View File

@ -45,7 +45,7 @@
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<i class='fa fa-user'/> <t t-esc="len(group.message_follower_ids)"/> participants<br /> <i class='fa fa-user'/> <t t-esc="len(group.message_follower_ids)"/> participants<br />
<i class='fa fa-envelope-o'/> <t t-esc="len(group.message_ids)"/> messages <i class='fa fa-envelope-o'/> <t t-raw="group_data[group.id]['monthly_message_nbr']"/> messages / month
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<t t-call="website_mail.follow"><t t-set="object" t-value="group"/></t> <t t-call="website_mail.follow"><t t-set="object" t-value="group"/></t>

View File

@ -62,8 +62,6 @@
<script type="text/javascript" src="/website/static/src/js/website.menu.js"></script> <!-- groups="base.group_website_designer" --> <script type="text/javascript" src="/website/static/src/js/website.menu.js"></script> <!-- groups="base.group_website_designer" -->
<script type="text/javascript" src="/website/static/src/js/website.mobile.js"></script> <script type="text/javascript" src="/website/static/src/js/website.mobile.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.seo.js"></script> <script type="text/javascript" src="/website/static/src/js/website.seo.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.tour.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.tour.banner.js"></script> <!-- groups="base.group_website_designer" -->
<script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script> <script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.ace.js"></script> <script type="text/javascript" src="/website/static/src/js/website.ace.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.translator.js"></script> <script type="text/javascript" src="/website/static/src/js/website.translator.js"></script>

View File

@ -63,7 +63,7 @@ class table_compute(object):
self.table[(pos/PPR)+y2][(pos%PPR)+x2] = False self.table[(pos/PPR)+y2][(pos%PPR)+x2] = False
self.table[pos/PPR][pos%PPR] = { self.table[pos/PPR][pos%PPR] = {
'product': p, 'x':x, 'y': y, '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: if index<=PPG:
maxy=max(maxy,y+(pos/PPR)) maxy=max(maxy,y+(pos/PPR))
@ -179,7 +179,7 @@ class website_sale(http.Controller):
values = { values = {
'search': search, 'search': search,
'category': category and int(category), 'category': category,
'attrib_values': attrib_values, 'attrib_values': attrib_values,
'attrib_set': attrib_set, 'attrib_set': attrib_set,
'pager': pager, 'pager': pager,

View File

@ -1,9 +1,6 @@
(function () { (function () {
'use strict'; 'use strict';
openerp.Tour.register({
var website = openerp.website;
website.Tour.register({
id: 'shop_customize', id: 'shop_customize',
name: "Customize the page and search a product", name: "Customize the page and search a product",
path: '/shop', path: '/shop',
@ -79,7 +76,7 @@
] ]
}); });
website.Tour.register({ openerp.Tour.register({
id: 'shop_buy_product', id: 'shop_buy_product',
name: "Try to buy products", name: "Try to buy products",
path: '/shop', path: '/shop',

View File

@ -1,10 +1,9 @@
(function () { (function () {
'use strict'; 'use strict';
var website = openerp.website;
var _t = openerp._t; var _t = openerp._t;
website.Tour.register({ openerp.Tour.register({
id: 'shop', id: 'shop',
name: _t("Create a product"), name: _t("Create a product"),
steps: [ steps: [

View File

@ -3,22 +3,30 @@ import os
import openerp.tests import openerp.tests
inject = [ inject = [
("openerp.website.Tour", os.path.join(os.path.dirname(__file__), '../../website/static/src/js/website.tour.js')), ("openerp.Tour", os.path.join(os.path.dirname(__file__), '../../web/static/src/js/tour.js')),
("openerp.website.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.sale.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.at_install(False)
@openerp.tests.common.post_install(True) @openerp.tests.common.post_install(True)
class TestUi(openerp.tests.HttpCase): class TestUi(openerp.tests.HttpCase):
def test_01_admin_shop_tour(self): 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', '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) 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): 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.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): 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): 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)

View File

@ -244,7 +244,7 @@
<!-- Add to cart button--> <!-- Add to cart button-->
<template id="categories_recursive" name="Category list"> <template id="categories_recursive" name="Category list">
<li t-att-class="'active' if c.id == category else ''"> <li t-att-class="'active' if c.id == int(category or 0) else ''">
<a t-att-href="keep('/shop/category/' + slug(c), category=0)" t-field="c.name"></a> <a t-att-href="keep('/shop/category/' + slug(c), category=0)" t-field="c.name"></a>
<ul t-if="c.child_id" class="nav nav-pills nav-stacked nav-hierarchy"> <ul t-if="c.child_id" class="nav nav-pills nav-stacked nav-hierarchy">
<t t-foreach="c.child_id" t-as="c"> <t t-foreach="c.child_id" t-as="c">

View File

@ -491,6 +491,8 @@ class ir_actions_server(osv.osv):
"based on the sequence. Low number means high priority."), "based on the sequence. Low number means high priority."),
'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade', 'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
help="Base model on which the server action runs."), 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, 'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
help='More menu entry.'), help='More menu entry.'),
# Client Action # Client Action
@ -650,6 +652,10 @@ class ir_actions_server(osv.osv):
'wkf_field_id': False, 'wkf_field_id': False,
'crud_model_id': model_id, '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} 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): 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 crud_model_name = False
if crud_model_id: if crud_model_id:
crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model 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} values = {'link_field_id': False, 'crud_model_name': crud_model_name}
return {'value': values} return {'value': values}

View File

@ -349,8 +349,9 @@
Check to attach the newly created record to the record on which the server action runs. Check to attach the newly created record to the record on which the server action runs.
</p> </p>
<group> <group>
<field name="model_name" invisible="1"/>
<field name="link_field_id" <field name="link_field_id"
domain="[('model_id', '=', model_id), ('relation', '=', crud_model_name), ('ttype', 'in', ['many2one'])]" domain="[('model_id', '=', crud_model_id), ('relation', '=', model_name), ('ttype', 'in', ['many2one'])]"
attrs="{'required': [('state', '=', 'object_create'), ('link_new_record', '=', True)], attrs="{'required': [('state', '=', 'object_create'), ('link_new_record', '=', True)],
'invisible': ['|', ('state', '!=', 'object_create'), ('link_new_record', '=', False)]}"/> 'invisible': ['|', ('state', '!=', 'object_create'), ('link_new_record', '=', False)]}"/>
</group> </group>

View File

@ -561,7 +561,7 @@ class many2one(_column):
# we use uid=1 because the visibility of a many2one field value (just id and name) # 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. # must be the access right of the parent form and not the linked object itself.
records = dict(obj.name_get(cr, SUPERUSER_ID, 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)) context=context))
for id in res: for id in res:
if res[id] in records: if res[id] in records:

View File

@ -3348,6 +3348,8 @@ class BaseModel(object):
return [] return []
if fields_to_read is None: if fields_to_read is None:
fields_to_read = self._columns.keys() 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 # 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 fields_pre = [f for f in fields_to_read if

View File

@ -30,12 +30,11 @@ the ORM does, in fact.
from contextlib import contextmanager from contextlib import contextmanager
from functools import wraps from functools import wraps
import logging import logging
import time
import uuid import uuid
import psycopg2.extras
import psycopg2.extensions import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ
from psycopg2.pool import PoolError from psycopg2.pool import PoolError
from psycopg2.psycopg1 import cursor as psycopg1cursor
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
@ -76,7 +75,7 @@ sql_counter = 0
class Cursor(object): class Cursor(object):
"""Represents an open transaction to the PostgreSQL DB backend, """Represents an open transaction to the PostgreSQL DB backend,
acting as a lightweight wrapper around psycopg2's acting as a lightweight wrapper around psycopg2's
``psycopg1cursor`` objects. ``cursor`` objects.
``Cursor`` is the object behind the ``cr`` variable used all ``Cursor`` is the object behind the ``cr`` variable used all
over the OpenERP code. over the OpenERP code.
@ -175,7 +174,7 @@ class Cursor(object):
self._serialized = serialized self._serialized = serialized
self._cnx = pool.borrow(dsn(dbname)) self._cnx = pool.borrow(dsn(dbname))
self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor) self._obj = self._cnx.cursor()
if self.sql_log: if self.sql_log:
self.__caller = frame_codeinfo(currentframe(),2) self.__caller = frame_codeinfo(currentframe(),2)
else: else:
@ -188,6 +187,16 @@ class Cursor(object):
self.cache = {} 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): def __del__(self):
if not self._closed and not self._cnx.closed: if not self._closed and not self._cnx.closed:
# Oops. 'self' has not been closed explicitly. # Oops. 'self' has not been closed explicitly.

View File

@ -95,6 +95,7 @@ def py2exe_options():
"markupsafe", # dependence of jinja2 and mako "markupsafe", # dependence of jinja2 and mako
"mock", "mock",
"openerp", "openerp",
"passlib",
"poplib", "poplib",
"psutil", "psutil",
"pychart", "pychart",
@ -163,6 +164,7 @@ setuptools.setup(
'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/ 'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'mako', 'mako',
'mock', 'mock',
'passlib',
'pillow', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/ 'pillow', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'psutil', # windows binary code.google.com/p/psutil/downloads/list 'psutil', # windows binary code.google.com/p/psutil/downloads/list
'psycopg2 >= 2.2', 'psycopg2 >= 2.2',

View File

@ -28,6 +28,7 @@ Depends:
python-mako, python-mako,
python-mock, python-mock,
python-openid, python-openid,
python-passlib,
python-psutil, python-psutil,
python-psycopg2, python-psycopg2,
python-pybabel, python-pybabel,