diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index c543b493831..57f16473b82 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -1360,6 +1360,7 @@ class account_invoice_line(osv.osv): _columns = { 'name': fields.text('Description', required=True), 'origin': fields.char('Source', size=256, help="Reference of the document that produced this invoice."), + 'sequence': fields.integer('Sequence', help="Gives the sequence of this line when displaying the invoice."), 'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True), 'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'), 'product_id': fields.many2one('product.product', 'Product', ondelete='set null'), diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index 26e18271dc2..3c2922ff141 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -458,7 +458,6 @@ - diff --git a/addons/account_followup/i18n/ar.po b/addons/account_followup/i18n/ar.po index 567253c4c02..bf1fde2db25 100644 --- a/addons/account_followup/i18n/ar.po +++ b/addons/account_followup/i18n/ar.po @@ -7,14 +7,14 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2012-02-08 00:35+0000\n" -"PO-Revision-Date: 2009-02-03 06:24+0000\n" -"Last-Translator: <>\n" +"PO-Revision-Date: 2012-10-08 15:59+0000\n" +"Last-Translator: waleed bazaza \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2012-08-28 06:07+0000\n" -"X-Generator: Launchpad (build 15864)\n" +"X-Launchpad-Export-Date: 2012-10-09 04:51+0000\n" +"X-Generator: Launchpad (build 16112)\n" #. module: account_followup #: view:account_followup.followup:0 @@ -42,7 +42,7 @@ msgstr "متابعة" #: help:account.followup.print.all,test_print:0 msgid "" "Check if you want to print followups without changing followups level." -msgstr "" +msgstr "اختر هذه الخانة إذا أردت طباعة المتابعات بدون تغيير متوى المتابعات." #. module: account_followup #: model:account_followup.followup.line,description:account_followup.demo_followup_line2 @@ -101,7 +101,7 @@ msgstr "الدليل" #. module: account_followup #: view:account_followup.stat:0 msgid "Follow up Entries with period in current year" -msgstr "" +msgstr "مدخلات المتابعات بفترات في السنة الحالية" #. module: account_followup #: view:account.followup.print.all:0 @@ -314,6 +314,9 @@ msgid "" "\n" "%s" msgstr "" +"جميع رسائل البريد الالكتروني قد أرسلت بنجاح للشركاء:.\n" +"\n" +"%s" #. module: account_followup #: constraint:account_followup.followup.line:0 @@ -362,6 +365,11 @@ msgid "" "\n" "%s" msgstr "" +"\n" +"\n" +"رسالة البريد الالكتروني قد أرسلت للشركاء التاليين.!\n" +"\n" +"%s" #. module: account_followup #: help:account.followup.print,date:0 @@ -503,7 +511,7 @@ msgstr "تقرير المتابعة" #. module: account_followup #: view:account_followup.followup.line:0 msgid "Follow-Up Steps" -msgstr "" +msgstr "خطوات المتابعة" #. module: account_followup #: field:account_followup.stat,period_id:0 @@ -514,7 +522,7 @@ msgstr "فترة" #: code:addons/account_followup/wizard/account_followup_print.py:307 #, python-format msgid "Followup Summary" -msgstr "" +msgstr "خلاصة المتابعة" #. module: account_followup #: view:account.followup.print:0 @@ -535,7 +543,7 @@ msgstr "مستوى اعلى متابعة" #. module: account_followup #: model:ir.actions.act_window,name:account_followup.action_view_account_followup_followup_form msgid "Review Invoicing Follow-Ups" -msgstr "" +msgstr "استعراض متابعة الفوترة" #. module: account_followup #: constraint:account.move.line:0 @@ -595,7 +603,7 @@ msgstr "الوصف" #. module: account_followup #: constraint:account_followup.followup:0 msgid "Only One Followup by Company." -msgstr "" +msgstr "متابعة واحدة غقط من الشركة." #. module: account_followup #: view:account_followup.stat:0 diff --git a/addons/auth_oauth/auth_oauth_data.xml b/addons/auth_oauth/auth_oauth_data.xml index 83e9df2e44d..c07359a062c 100644 --- a/addons/auth_oauth/auth_oauth_data.xml +++ b/addons/auth_oauth/auth_oauth_data.xml @@ -1,6 +1,6 @@ - + OpenERP Accounts diff --git a/addons/auth_reset_password/__init__.py b/addons/auth_reset_password/__init__.py index 28bbdd18161..8341ccd60c7 100644 --- a/addons/auth_reset_password/__init__.py +++ b/addons/auth_reset_password/__init__.py @@ -1 +1,23 @@ -import auth_reset_password +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-today OpenERP SA () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see +# +############################################################################## + +import controllers +import res_users diff --git a/addons/auth_reset_password/__openerp__.py b/addons/auth_reset_password/__openerp__.py index ccf5ca53a21..e620d515fa1 100644 --- a/addons/auth_reset_password/__openerp__.py +++ b/addons/auth_reset_password/__openerp__.py @@ -1,3 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-today OpenERP SA () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see +# +############################################################################## + { 'name': 'Reset Password', 'description': """ @@ -9,9 +30,11 @@ Allow users to reset their password from the login page. 'category': 'Authentication', 'website': 'http://www.openerp.com', 'installable': True, - 'depends': ['auth_anonymous', 'email_template'], - 'data': ['auth_reset_password.xml'], + 'depends': ['auth_signup', 'email_template'], + 'data': [ + 'auth_reset_password.xml', + 'res_users_view.xml', + ], 'js': ['static/src/js/reset_password.js'], - 'css': ['static/src/css/reset_password.css'], 'qweb': ['static/src/xml/reset_password.xml'], } diff --git a/addons/auth_reset_password/auth_reset_password.py b/addons/auth_reset_password/auth_reset_password.py deleted file mode 100644 index cef7bc9c212..00000000000 --- a/addons/auth_reset_password/auth_reset_password.py +++ /dev/null @@ -1,130 +0,0 @@ -import base64 -import hashlib -import simplejson -import time -import urlparse - -from openerp.tools import config -from openerp.osv import osv, fields -from openerp import SUPERUSER_ID - -TWENTY_FOUR_HOURS = 24 * 60 * 60 - -def message_sign(data, secret): - src = simplejson.dumps([data, secret], indent=None, separators=(',', ':'), sort_keys=True) - sign = hashlib.sha1(src).hexdigest() - msg = simplejson.dumps([data, sign], indent=None, separators=(',', ':'), sort_keys=True) - # pad message to avoid '=' - pad = (3 - len(msg) % 3) % 3 - msg = msg + " " * pad - msg = base64.urlsafe_b64encode(msg) - return msg, sign - -def message_check(msg, secret): - msg = base64.urlsafe_b64decode(msg) - l = simplejson.loads(msg) - msg_data = l[0] - msg_sign = l[1] - tmp, sign = message_sign(msg_data, secret) - if msg_sign == sign: - return msg_data - -class res_users(osv.osv): - _inherit = 'res.users' - - def _auth_reset_password_secret(self, cr, uid, context=None): - uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid') - res = { - 'dbname': cr.dbname, - 'uuid': uuid, - 'admin_passwd': config['admin_passwd'] - } - return res - - def _auth_reset_password_host(self, cr, uid, context=None): - return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '') - - def _auth_reset_password_link(self, cr, uid, ids, context=None): - assert len(ids) == 1 - host = self._auth_reset_password_host(cr, uid, context) - secret = self._auth_reset_password_secret(cr, uid, context) - msg_src = { - 'time': time.time(), - 'uid': ids[0], - } - msg, sign = message_sign(msg_src, secret) - link = urlparse.urljoin(host, '/login?db=%s&login=anonymous&key=anonymous#action=reset_password&token=%s' % (cr.dbname, msg)) - return link - - def _auth_reset_password_check_token(self, cr, uid, token, context=None): - secret = self._auth_reset_password_secret(cr, uid, context) - data = message_check(token, secret) - if data and (time.time() - data['time'] < TWENTY_FOUR_HOURS): - return data - return None - - def _auth_reset_password_send_email(self, cr, uid, email_to, tpl_name, res_id, context=None): - model, tpl_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'auth_reset_password', tpl_name) - assert model == 'email.template' - - msg_id = self.pool.get(model).send_mail(cr, uid, tpl_id, res_id, force_send=False, context=context) - MailMessage = self.pool.get('mail.message') - MailMessage.write(cr, uid, [msg_id], {'email_to': email_to}, context=context) - MailMessage.send(cr, uid, [msg_id], context=context) - - def send_reset_password_request(self, cr, uid, email, context=None): - # TODO reseting a password knowing only an email is not good enough (email can be shared between multiple logins). - ids = self.pool.get('res.users').search(cr, SUPERUSER_ID, [('user_email', '=', email)], context=context) - if ids: - self._auth_reset_password_send_email(cr, SUPERUSER_ID, email, 'reset_password_email', ids[0], context=context) - return True - #else: - # _m, company_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company') - # self._auth_reset_password_send_email(cr, uid, email, 'email_no_user', company_id, context=context) - return False - -class auth_reset_password(osv.TransientModel): - _name = 'auth.reset_password' - _rec_name = 'password' - _columns = { - 'password': fields.char('Password', size=64), - 'password_confirmation': fields.char('Confirm Password', size=64), - 'token': fields.char('Token', size=128), - 'state': fields.selection([(x, x) for x in 'draft done missmatch error'.split()], required=True), - } - _defaults = { - 'state': 'draft', - } - - def create(self, cr, uid, values, context=None): - # NOTE here, invalid values raises exceptions to avoid storing - # sensitive data into the database (which then are available to anyone) - - pw = values.get('password') - if not pw or pw != values.get('password_confirmation'): - raise osv.except_osv('Error', 'Passwords missmatch') - - Users = self.pool.get('res.users') - data = Users._auth_reset_password_check_token(cr, uid, values.get('token', '')) - if data: - Users.write(cr, SUPERUSER_ID, data['uid'], {'password': pw}, context=context) - else: - raise osv.except_osv('Error', 'Invalid token') - - # Dont store password - values = {'state': 'done'} - return super(auth_reset_password, self).create(cr, uid, values, context) - - def change(self, cr, uid, ids, context=None): - return True - - def onchange_pw(self, cr, uid, ids, password, password_confirmation, context=None): - if password != password_confirmation: - return {'value': {'state': 'missmatch'}} - return {'value': {'state': 'draft'}} - - def onchange_token(self, cr, uid, ids, token, context=None): - Users = self.pool.get('res.users') - if not Users._auth_reset_password_check_token(cr, uid, token, context=context): - return {'value': {'state': 'error'}} - return {} diff --git a/addons/auth_reset_password/auth_reset_password.xml b/addons/auth_reset_password/auth_reset_password.xml index bb75da34ce8..90f08a25899 100644 --- a/addons/auth_reset_password/auth_reset_password.xml +++ b/addons/auth_reset_password/auth_reset_password.xml @@ -7,56 +7,15 @@ Reset Password ]]> - + ${object.email} Password reset A password reset was requested the OpenERP account linked to this email on ${object._auth_reset_password_host()}

+

A password reset was requested for the OpenERP account linked to this email.

-

You may change your password following this link, -or by copy-pasting the following URL in your browser: ${object._auth_reset_password_link()}

+

You may change your password following this link.

Note: If you did not ask for a password reset, you can safely ignore this email.

]]>
- - - auth.reset_password.form - auth.reset_password - -
- - - - - - -
Passwords missmatch
-
- - -
  • - +
  • Reset password
  • + diff --git a/addons/auth_signup/__openerp__.py b/addons/auth_signup/__openerp__.py index 9cfe89cdc1c..9cbeb60638f 100644 --- a/addons/auth_signup/__openerp__.py +++ b/addons/auth_signup/__openerp__.py @@ -34,7 +34,9 @@ Allow users to sign up. 'data': [ 'auth_signup_data.xml', 'res_config.xml', + 'res_users_view.xml', ], 'js': ['static/src/js/auth_signup.js'], + 'css' : ['static/src/css/base.css'], 'qweb': ['static/src/xml/auth_signup.xml'], } diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 821ef44f459..56e5e910212 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -1,35 +1,63 @@ -import logging - -import werkzeug.urls +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-today OpenERP SA () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see +# +############################################################################## +from openerp import SUPERUSER_ID from openerp.modules.registry import RegistryManager from openerp.addons.web.controllers.main import login_and_redirect import openerp.addons.web.common.http as openerpweb -from openerp import SUPERUSER_ID +import werkzeug + +import logging _logger = logging.getLogger(__name__) -class OpenIDController(openerpweb.Controller): +class Controller(openerpweb.Controller): _cp_path = '/auth_signup' + @openerpweb.jsonrequest + def retrieve(self, req, dbname, token): + """ retrieve the user info (name, login or email) corresponding to a signup token """ + registry = RegistryManager.get(dbname) + user_info = None + with registry.cursor() as cr: + res_partner = registry.get('res.partner') + user_info = res_partner.signup_retrieve_info(cr, SUPERUSER_ID, token) + return user_info + @openerpweb.httprequest - def signup(self, req, dbname, name, login, password): + def signup(self, req, dbname, token, name, login, password): + """ sign up a user (new or existing), and log it in """ url = '/' registry = RegistryManager.get(dbname) with registry.cursor() as cr: try: - Users = registry.get('res.users') - credentials = Users.auth_signup(cr, SUPERUSER_ID, name, login, password) + res_users = registry.get('res.users') + values = {'name': name, 'login': login, 'password': password} + credentials = res_users.signup(cr, SUPERUSER_ID, values, token) cr.commit() return login_and_redirect(req, *credentials) - except AttributeError: - # auth_signup is not installed - _logger.exception('attribute error when signup') - url = "/#action=auth_signup&error=NA" # Not Available - except Exception: + except Exception as e: # signup error _logger.exception('error when signup') - url = "/#action=auth_signup&error=UE" # Unexcpected Error + url = "/#action=login&error_message=%s" % werkzeug.urls.url_quote(e.message) return werkzeug.utils.redirect(url) # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/auth_signup/res_config.py b/addons/auth_signup/res_config.py index 8bfe1fc3ddb..21fce19745c 100644 --- a/addons/auth_signup/res_config.py +++ b/addons/auth_signup/res_config.py @@ -20,22 +20,27 @@ ############################################################################## from openerp.osv import osv, fields +from openerp.tools.safe_eval import safe_eval class base_config_settings(osv.TransientModel): _inherit = 'base.config.settings' _columns = { - 'auth_signup_uninvited': fields.boolean('Allow public users to sign up', help="If unchecked only invited users may sign up"), + 'auth_signup_uninvited': fields.boolean('Allow external users to sign up', help="If unchecked, only invited users may sign up"), 'auth_signup_template_user_id': fields.many2one('res.users', 'Template user for new users created through signup'), } def get_default_auth_signup_template_user_id(self, cr, uid, fields, context=None): icp = self.pool.get('ir.config_parameter') + # we use safe_eval on the result, since the value of the parameter is a nonempty string return { - 'auth_signup_template_user_id': icp.get_param(cr, uid, 'auth_signup.template_user_id', 0) or False + 'auth_signup_uninvited': safe_eval(icp.get_param(cr, uid, 'auth_signup.allow_uninvited', 'False')), + 'auth_signup_template_user_id': safe_eval(icp.get_param(cr, uid, 'auth_signup.template_user_id', 'False')), } def set_auth_signup_template_user_id(self, cr, uid, ids, context=None): config = self.browse(cr, uid, ids[0], context=context) icp = self.pool.get('ir.config_parameter') - icp.set_param(cr, uid, 'auth_signup.template_user_id', config.auth_signup_template_user_id.id) + # we store the repr of the values, since the value of the parameter is a required string + icp.set_param(cr, uid, 'auth_signup.allow_uninvited', repr(config.auth_signup_uninvited)) + icp.set_param(cr, uid, 'auth_signup.template_user_id', repr(config.auth_signup_template_user_id.id)) diff --git a/addons/auth_signup/res_config.xml b/addons/auth_signup/res_config.xml index c17634ececb..9d3eb4eaa5a 100644 --- a/addons/auth_signup/res_config.xml +++ b/addons/auth_signup/res_config.xml @@ -14,7 +14,9 @@
    diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py index 5f6463508b6..7b663bd0e33 100644 --- a/addons/auth_signup/res_users.py +++ b/addons/auth_signup/res_users.py @@ -1,47 +1,205 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-today OpenERP SA () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see +# +############################################################################## + import openerp -from openerp.osv import osv +from openerp.osv import osv, fields from openerp import SUPERUSER_ID +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools.safe_eval import safe_eval + +import time +import random +import urllib +import urlparse + +def random_token(): + # the token has an entropy of about 120 bits (6 bits/char * 20 chars) + chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + return ''.join(random.choice(chars) for i in xrange(20)) + +def now(): + return time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + + +class res_partner(osv.Model): + _inherit = 'res.partner' + + def _get_signup_valid(self, cr, uid, ids, name, arg, context=None): + dt = now() + res = {} + for partner in self.browse(cr, uid, ids, context): + res[partner.id] = bool(partner.signup_token) and \ + (not partner.signup_expiration or dt <= partner.signup_expiration) + return res + + def _get_signup_url(self, cr, uid, ids, name, arg, context=None): + """ determine a signup url for a given partner """ + base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') + + # if required, make sure that every partner without user has a valid signup token + if context and context.get('signup_valid'): + unsigned_ids = [p.id for p in self.browse(cr, uid, ids, context) if not p.user_ids] + self.signup_prepare(cr, uid, unsigned_ids, context=context) + + res = dict.fromkeys(ids, False) + for partner in self.browse(cr, uid, ids, context): + if partner.signup_token: + params = (urllib.quote(cr.dbname), urllib.quote(partner.signup_token)) + res[partner.id] = urlparse.urljoin(base_url, "#action=login&db=%s&token=%s" % params) + elif partner.user_ids: + user = partner.user_ids[0] + params = (urllib.quote(cr.dbname), urllib.quote(user.login)) + res[partner.id] = urlparse.urljoin(base_url, "#action=login&db=%s&login=%s" % params) + return res + + _columns = { + 'signup_token': fields.char(size=24, string='Signup Token'), + 'signup_expiration': fields.datetime(string='Signup Expiration'), + 'signup_valid': fields.function(_get_signup_valid, type='boolean', string='Signup Token is Valid'), + 'signup_url': fields.function(_get_signup_url, type='char', string='Signup URL'), + } + + def action_signup_prepare(self, cr, uid, ids, context=None): + return self.signup_prepare(cr, uid, ids, context=context) + + def signup_prepare(self, cr, uid, ids, expiration=False, context=None): + """ generate a new token for the partners with the given validity, if necessary + :param expiration: the expiration datetime of the token (string, optional) + """ + for partner in self.browse(cr, uid, ids, context): + if expiration or not partner.signup_valid: + token = random_token() + while self._signup_retrieve_partner(cr, uid, token, context=context): + token = random_token() + partner.write({'signup_token': token, 'signup_expiration': expiration}) + return True + + def _signup_retrieve_partner(self, cr, uid, token, + check_validity=False, raise_exception=False, context=None): + """ find the partner corresponding to a token, and possibly check its validity + :param token: the token to resolve + :param check_validity: if True, also check validity + :param raise_exception: if True, raise exception instead of returning False + :return: partner (browse record) or False (if raise_exception is False) + """ + partner_ids = self.search(cr, uid, [('signup_token', '=', token)], context=context) + if not partner_ids: + if raise_exception: + raise Exception("Signup token '%s' is not valid" % token) + return False + partner = self.browse(cr, uid, partner_ids[0], context) + if check_validity and not partner.signup_valid: + if raise_exception: + raise Exception("Signup token '%s' is no longer valid" % token) + return False + return partner + + def signup_retrieve_info(self, cr, uid, token, context=None): + """ retrieve the user info about the token + :return: a dictionary with the user information: + - 'db': the name of the database + - 'token': the token, if token is valid + - 'name': the name of the partner, if token is valid + - 'login': the user login, if the user already exists + - 'email': the partner email, if the user does not exist + """ + partner = self._signup_retrieve_partner(cr, uid, token, raise_exception=True, context=None) + res = {'db': cr.dbname} + if partner.signup_valid: + res['token'] = token + res['name'] = partner.name + if partner.user_ids: + res['login'] = partner.user_ids[0].login + else: + res['email'] = partner.email or '' + return res + + class res_users(osv.Model): _inherit = 'res.users' - def auth_signup_create(self, cr, uid, new_user, context=None): - # new_user: - # login - # email - # name (optional) - # partner_id (optional) - # groups (optional) - # sign (for partner_id and groups) - # - user_template_id = self.pool.get('ir.config_parameter').get_param(cr, uid, 'auth.signup_template_user_id', 0) - if user_template_id: - self.pool.get('res.users').copy(cr, SUPERUSER_ID, user_template_id, new_user, context=context) - else: - self.pool.get('res.users').create(cr, SUPERUSER_ID, new_user, context=context) + def _get_state(self, cr, uid, ids, name, arg, context=None): + return dict((user.id, 'new' if not user.login_date else 'reset' if user.signup_token else 'active') + for user in self.browse(cr, uid, ids, context)) - def auth_signup(self, cr, uid, name, login, password, context=None): - r = (cr.dbname, login, password) - res = self.search(cr, uid, [("login", "=", login)]) - if res: - # Existing user - user_id = res[0] - try: - self.check(cr.dbname, user_id, password) - # Same password - except openerp.exceptions.AccessDenied: - # Different password - raise - else: - # New user - new_user = { - 'name': name, - 'login': login, - 'user_email': login, - 'password': password, - 'active': True, - } - self.auth_signup_create(cr, uid, new_user) - return r + _columns = { + 'state': fields.function(_get_state, string='State', type='selection', + selection=[('new', 'New'), ('active', 'Active'), ('reset', 'Resetting Password')]), + } -# + def signup(self, cr, uid, values, token=None, context=None): + """ signup a user, to either: + - create a new user (no token), or + - create a user for a partner (with token, but no user for partner), or + - change the password of a user (with token, and existing user). + :param values: a dictionary with field values + :param token: signup token (optional) + :return: (dbname, login, password) for the signed up user + """ + assert values.get('login') and values.get('password') + result = (cr.dbname, values['login'], values['password']) + + if token: + # signup with a token: find the corresponding partner id + res_partner = self.pool.get('res.partner') + partner = res_partner._signup_retrieve_partner(cr, uid, token, + check_validity=True, raise_exception=True, context=None) + # invalidate signup token + partner.write({'signup_token': False, 'signup_expiration': False}) + if partner.user_ids: + # user exists, modify its password + partner.user_ids[0].write({'password': values['password']}) + else: + # user does not exist: sign up invited user + self._signup_create_user(cr, uid, { + 'name': partner.name, + 'login': values['login'], + 'password': values['password'], + 'email': values['login'], + 'partner_id': partner.id, + }, context=context) + return result + + # sign up an external user + assert values.get('name'), 'Signup: no name given for new user' + self._signup_create_user(cr, uid, { + 'name': values['name'], + 'login': values['login'], + 'password': values['password'], + 'email': values['login'], + }, context=context) + return result + + def _signup_create_user(self, cr, uid, values, context=None): + """ create a new user from the template user """ + ir_config_parameter = self.pool.get('ir.config_parameter') + template_user_id = safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.template_user_id', 'False')) + assert template_user_id and self.exists(cr, uid, template_user_id, context=context), 'Signup: invalid template user' + + # check that uninvited users may sign up + if 'partner_id' not in values: + if not safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.allow_uninvited', 'False')): + raise Exception('Signup is not allowed for uninvited users') + + # create a copy of the template user (attached to a specific partner_id if given) + values['active'] = True + return self.copy(cr, uid, template_user_id, values, context=context) diff --git a/addons/auth_signup/res_users_view.xml b/addons/auth_signup/res_users_view.xml new file mode 100644 index 00000000000..c42e1ab5f41 --- /dev/null +++ b/addons/auth_signup/res_users_view.xml @@ -0,0 +1,19 @@ + + + + + + user.form.state + res.users + + + +
    + +
    +
    +
    +
    + +
    +
    diff --git a/addons/auth_signup/static/src/css/Makefile b/addons/auth_signup/static/src/css/Makefile new file mode 100644 index 00000000000..d6b4f4b2fc8 --- /dev/null +++ b/addons/auth_signup/static/src/css/Makefile @@ -0,0 +1,3 @@ +base.css: base.sass + sass --trace -t expanded base.sass base.css + diff --git a/addons/auth_signup/static/src/css/base.css b/addons/auth_signup/static/src/css/base.css new file mode 100644 index 00000000000..3f97e7a9301 --- /dev/null +++ b/addons/auth_signup/static/src/css/base.css @@ -0,0 +1,10 @@ +@charset "utf-8"; +.openerp .oe_login .oe_signup_show { + display: none; +} +.openerp .oe_login_signup .oe_signup_show { + display: block !important; +} +.openerp .oe_login_signup .oe_signup_hide { + display: none; +} diff --git a/addons/auth_signup/static/src/css/base.sass b/addons/auth_signup/static/src/css/base.sass new file mode 100644 index 00000000000..f97665d192c --- /dev/null +++ b/addons/auth_signup/static/src/css/base.sass @@ -0,0 +1,13 @@ +@charset "utf-8" + +.openerp + // Regular login form + .oe_login + .oe_signup_show + display: none + // Signup form + .oe_login_signup + .oe_signup_show + display: block !important + .oe_signup_hide + display: none diff --git a/addons/auth_signup/static/src/js/auth_signup.js b/addons/auth_signup/static/src/js/auth_signup.js index 2ab13aa7598..509fafa35cc 100644 --- a/addons/auth_signup/static/src/js/auth_signup.js +++ b/addons/auth_signup/static/src/js/auth_signup.js @@ -5,56 +5,99 @@ openerp.auth_signup = function(instance) { instance.web.Login.include({ start: function() { var self = this; - this.$('a.oe_signup').click(function() { - var dbname = self.$("form [name=db]").val(); - self.do_action({ - type: 'ir.actions.client', - tag: 'auth_signup.signup', - params: {'dbname': dbname}, - target: 'new', - name: 'Sign up' - }); - return true; + var d = this._super(); + + // to switch between the signup and regular login form + this.$('a.oe_signup_signup').click(function() { + self.$el.addClass("oe_login_signup"); + }); + this.$('a.oe_signup_back').click(function() { + self.$el.removeClass("oe_login_signup"); + delete self.params.token; }); - return this._super(); - }, - }); + // if there is an error message in params, show it then forget it + if (self.params.error_message) { + this.show_error(self.params.error_message); + delete self.params.error_message; + } - instance.auth_signup.Signup = instance.web.Widget.extend({ - template: 'auth_signup.signup', - init: function(parent, params) { - this.params = params; - return this._super(); + // in case of a signup, retrieve the user information from the token + if (self.params.db && self.params.token) { + d = self.rpc("/auth_signup/retrieve", {dbname: self.params.db, token: self.params.token}) + .done(self.on_token_loaded) + .fail(self.on_token_failed); + } + return d; }, - start: function() { - var self = this; - this.$('input[name=password_confirmation]').keyup(function() { - var v = $(this).val(); - var $b = self.$('button'); - if (_.isEmpty(v) || self.$('input[name=password]').val() === v) { - $b.removeAttr('disabled'); + on_token_loaded: function(result) { + // select the right the database + this.selected_db = result.db; + this.on_db_loaded({db_list: [result.db]}); + if (result.token) { + // switch to signup mode, set user name and login + this.$el.addClass("oe_login_signup"); + this.$("form input[name=name]").val(result.name).attr("readonly", "readonly"); + if (result.login) { + this.$("form input[name=login]").val(result.login).attr("readonly", "readonly"); } else { - $b.attr('disabled', 'disabled'); + this.$("form input[name=login]").val(result.email); } - }); - - this.$('form').submit(function(ev) { - if(ev) { - ev.preventDefault(); + } else { + // remain in login mode, set login if present + delete this.params.token; + this.$("form input[name=login]").val(result.login || ""); + } + }, + on_token_failed: function(result, ev) { + if (ev) { + ev.preventDefault(); + } + this.show_error("Invalid signup token"); + delete this.params.db; + delete this.params.token; + }, + on_submit: function(ev) { + if (ev) { + ev.preventDefault(); + } + if (this.$el.hasClass("oe_login_signup")) { + // signup user (or reset password) + var db = this.$("form [name=db]").val(); + var name = this.$("form input[name=name]").val(); + var login = this.$("form input[name=login]").val(); + var password = this.$("form input[name=password]").val(); + var confirm_password = this.$("form input[name=confirm_password]").val(); + if (!db) { + this.do_warn("Login", "No database selected !"); + return false; + } else if (!name) { + this.do_warn("Login", "Please enter a name.") + return false; + } else if (!login) { + this.do_warn("Login", "Please enter a username.") + return false; + } else if (!password || !confirm_password) { + this.do_warn("Login", "Please enter a password and confirm it.") + return false; + } else if (password !== confirm_password) { + this.do_warn("Login", "Passwords do not match; please retype them.") + return false; } var params = { - dbname : self.params.dbname, - name: self.$('input[name=name]').val(), - login: self.$('input[name=email]').val(), - password: self.$('input[name=password]').val(), + dbname : db, + token: this.params.token || "", + name: name, + login: login, + password: password, }; var url = "/auth_signup/signup?" + $.param(params); window.location = url; - }); - return this._super(); - } + } else { + // regular login + this._super(ev); + } + }, }); - instance.web.client_actions.add("auth_signup.signup", "instance.auth_signup.Signup"); }; diff --git a/addons/auth_signup/static/src/xml/auth_signup.xml b/addons/auth_signup/static/src/xml/auth_signup.xml index c2eec846d15..e63bd5863b0 100644 --- a/addons/auth_signup/static/src/xml/auth_signup.xml +++ b/addons/auth_signup/static/src/xml/auth_signup.xml @@ -1,28 +1,28 @@ - + - - -
  • - -
  • + + + + + + + + + + + + + + + + + + +
  • +
  • +
    -
    - - -
    -
    - Name =
    - Email =
    - Password =
    - Confirmation =
    - -
    -
    -
    - -
    diff --git a/addons/base_calendar/crm_meeting_view.xml b/addons/base_calendar/crm_meeting_view.xml index 37797a012aa..4df0a8a22b2 100644 --- a/addons/base_calendar/crm_meeting_view.xml +++ b/addons/base_calendar/crm_meeting_view.xml @@ -280,7 +280,7 @@ - + diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index f91a8756598..6a152f44b67 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -356,10 +356,10 @@ - + - + @@ -569,10 +569,10 @@ - + - + @@ -580,9 +580,8 @@ domain="['|', ('section_id.user_id','=',uid), ('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}" help="Opportunities that are assigned to either me or one of the sale teams I manage" /> - - + diff --git a/addons/crm/res_partner_view.xml b/addons/crm/res_partner_view.xml index 84a0bd4cf49..b7aa334a9c3 100644 --- a/addons/crm/res_partner_view.xml +++ b/addons/crm/res_partner_view.xml @@ -15,31 +15,12 @@
    - - view.res.partner.tree.crm.inherited2 - res.partner - - - - - - - - - view.res.partner.search.crm.inherited3 res.partner - - - - - - - diff --git a/addons/crm_partner_assign/crm_lead_view.xml b/addons/crm_partner_assign/crm_lead_view.xml index cd2e3f08c9a..2cffb6fb899 100644 --- a/addons/crm_partner_assign/crm_lead_view.xml +++ b/addons/crm_partner_assign/crm_lead_view.xml @@ -56,7 +56,7 @@ - + diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml index b46fafe270f..0155846fec3 100644 --- a/addons/event/event_view.xml +++ b/addons/event/event_view.xml @@ -339,7 +339,7 @@ - + @@ -529,7 +529,7 @@ - + diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index 69567902763..89c327f4d13 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -112,8 +112,6 @@ - - diff --git a/addons/hr_expense/hr_expense_view.xml b/addons/hr_expense/hr_expense_view.xml index 388e5262f6e..67d629eee6a 100644 --- a/addons/hr_expense/hr_expense_view.xml +++ b/addons/hr_expense/hr_expense_view.xml @@ -155,11 +155,12 @@ - + + + - diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml index 08e5f31a2d2..d48086f5663 100644 --- a/addons/hr_recruitment/hr_recruitment_view.xml +++ b/addons/hr_recruitment/hr_recruitment_view.xml @@ -208,7 +208,7 @@ - + diff --git a/addons/hr_timesheet_sheet/__openerp__.py b/addons/hr_timesheet_sheet/__openerp__.py index 9aab412d425..fd025491588 100644 --- a/addons/hr_timesheet_sheet/__openerp__.py +++ b/addons/hr_timesheet_sheet/__openerp__.py @@ -65,5 +65,8 @@ The validation can be configured in the company: 'installable': True, 'auto_install': False, 'application': True, + 'js': ['static/src/js/timesheet.js',], + 'css': ['static/src/css/timesheet.css',], + 'qweb': ['static/src/xml/timesheet.xml',], } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py index cd58f52f959..96cb15ddce1 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py @@ -27,80 +27,6 @@ from osv import fields, osv from tools.translate import _ import netsvc -class one2many_mod2(fields.one2many): - def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - if context is None: - context = {} - - if values is None: - values = {} - - # res6 = {id: date_current, ...} - res6 = dict([(rec['id'], rec['date_current']) - for rec in obj.read(cr, user, ids, ['date_current'], context=context)]) - - dom = [] - for c, id in enumerate(ids): - if id in res6: - if c: # skip first - dom.insert(0 ,'|') - dom.append('&') - dom.append('&') - dom.append(('name', '>=', res6[id])) - dom.append(('name', '<=', res6[id])) - dom.append(('sheet_id', '=', id)) - - ids2 = obj.pool.get(self._obj).search(cr, user, dom, limit=self._limit) - - res = {} - for i in ids: - res[i] = [] - - for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_read'): - if r[self._fields_id]: - res[r[self._fields_id][0]].append(r['id']) - return res - - def set(self, cr, obj, id, field, values, user=None, context=None): - if context is None: - context = {} - - context = context.copy() - context['sheet_id'] = id - return super(one2many_mod2, self).set(cr, obj, id, field, values, user=user, context=context) - - -class one2many_mod(fields.one2many): - def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - if context is None: - context = {} - - if values is None: - values = {} - - - res5 = obj.read(cr, user, ids, ['date_current'], context=context) - res6 = {} - for r in res5: - res6[r['id']] = r['date_current'] - - ids2 = [] - for id in ids: - dom = [] - if id in res6: - dom = [('date', '=', res6[id]), ('sheet_id', '=', id)] - ids2.extend(obj.pool.get(self._obj).search(cr, user, - dom, limit=self._limit)) - res = {} - for i in ids: - res[i] = [] - for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, - [self._fields_id], context=context, load='_classic_read'): - if r[self._fields_id]: - res[r[self._fields_id][0]].append(r['id']) - - return res - class hr_timesheet_sheet(osv.osv): _name = "hr_timesheet_sheet.sheet" _inherit = "mail.thread" @@ -111,8 +37,7 @@ class hr_timesheet_sheet(osv.osv): def _total_attendances(self, cr, uid, ids, name, args, context=None): """ Get the total attendance for the timesheets Returns a dict like : - {id: {'date_current': '2011-06-17', - 'total_per_day': {day: timedelta, ...}, + {id: {'total_per_day': {day: timedelta, ...}, }, ... } @@ -122,7 +47,6 @@ class hr_timesheet_sheet(osv.osv): res = {} for sheet_id in ids: sheet = self.browse(cr, uid, sheet_id, context=context) - date_current = sheet.date_current # field attendances_ids of hr_timesheet_sheet.sheet only # returns attendances of timesheet's current date attendance_ids = attendance_obj.search(cr, uid, [('sheet_id', '=', sheet_id)], context=context) @@ -143,20 +67,7 @@ class hr_timesheet_sheet(osv.osv): else: total_attendance[day] += attendance_interval - # if the delta is negative, it means that a sign out is missing - # in a such case, we want to have the time to the end of the day - # for a past date, and the time to now for the current date - if total_attendance[day] < timedelta(0): - if day == date_current: - now = datetime.now() - total_attendance[day] += timedelta(hours=now.hour, - minutes=now.minute, - seconds=now.second) - else: - total_attendance[day] += timedelta(days=1) - - res[sheet_id] = {'date_current': date_current, - 'total_per_day': total_attendance} + res[sheet_id] = {'total_per_day': total_attendance} return res def _total_timesheet(self, cr, uid, ids, name, args, context=None): @@ -210,24 +121,16 @@ class hr_timesheet_sheet(osv.osv): all_attendances_sheet = all_timesheet_attendances[id] - date_current = all_attendances_sheet['date_current'] total_attendances_sheet = all_attendances_sheet['total_per_day'] total_attendances_all_days = sum_all_days(total_attendances_sheet) - total_attendances_day = total_attendances_sheet.get(date_current, timedelta(seconds=0)) total_timesheets_sheet = all_timesheet_lines[id] total_timesheets_all_days = sum_all_days(total_timesheets_sheet) - total_timesheets_day = total_timesheets_sheet.get(date_current, timedelta(seconds=0)) total_difference_all_days = total_attendances_all_days - total_timesheets_all_days - total_difference_day = total_attendances_day - total_timesheets_day res[id]['total_attendance'] = timedelta_to_hours(total_attendances_all_days) res[id]['total_timesheet'] = timedelta_to_hours(total_timesheets_all_days) res[id]['total_difference'] = timedelta_to_hours(total_difference_all_days) - - res[id]['total_attendance_day'] = timedelta_to_hours(total_attendances_day) - res[id]['total_timesheet_day'] = timedelta_to_hours(total_timesheets_day) - res[id]['total_difference_day'] = timedelta_to_hours(total_difference_day) return res def check_employee_attendance_state(self, cr, uid, sheet_id, context=None): @@ -277,44 +180,6 @@ class hr_timesheet_sheet(osv.osv): raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,)) return True - def date_today(self, cr, uid, ids, context=None): - for sheet in self.browse(cr, uid, ids, context=context): - if datetime.today() <= datetime.strptime(sheet.date_from, '%Y-%m-%d'): - self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context) - elif datetime.now() >= datetime.strptime(sheet.date_to, '%Y-%m-%d'): - self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context) - else: - self.write(cr, uid, [sheet.id], {'date_current': time.strftime('%Y-%m-%d')}, context=context) - return True - - def date_previous(self, cr, uid, ids, context=None): - for sheet in self.browse(cr, uid, ids, context=context): - if datetime.strptime(sheet.date_current, '%Y-%m-%d') <= datetime.strptime(sheet.date_from, '%Y-%m-%d'): - self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context) - else: - self.write(cr, uid, [sheet.id], { - 'date_current': (datetime.strptime(sheet.date_current, '%Y-%m-%d') + relativedelta(days=-1)).strftime('%Y-%m-%d'), - }, context=context) - return True - - def date_next(self, cr, uid, ids, context=None): - for sheet in self.browse(cr, uid, ids, context=context): - if datetime.strptime(sheet.date_current, '%Y-%m-%d') >= datetime.strptime(sheet.date_to, '%Y-%m-%d'): - self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context) - else: - self.write(cr, uid, [sheet.id], { - 'date_current': (datetime.strptime(sheet.date_current, '%Y-%m-%d') + relativedelta(days=1)).strftime('%Y-%m-%d'), - }, context=context) - return True - - def button_dummy(self, cr, uid, ids, context=None): - for sheet in self.browse(cr, uid, ids, context=context): - if datetime.strptime(sheet.date_current, '%Y-%m-%d') <= datetime.strptime(sheet.date_from, '%Y-%m-%d'): - self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context) - elif datetime.strptime(sheet.date_current, '%Y-%m-%d') >= datetime.strptime(sheet.date_to, '%Y-%m-%d'): - self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context) - return True - def attendance_action_change(self, cr, uid, ids, context=None): hr_employee = self.pool.get('hr.employee') employee_ids = [] @@ -329,14 +194,13 @@ class hr_timesheet_sheet(osv.osv): 'user_id': fields.related('employee_id', 'user_id', type="many2one", relation="res.users", store=True, string="User", required=False, readonly=True),#fields.many2one('res.users', 'User', required=True, select=1, states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}), 'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}), 'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}), - 'date_current': fields.date('Current date', required=True, select=1), - 'timesheet_ids' : one2many_mod('hr.analytic.timesheet', 'sheet_id', - 'Timesheet lines', domain=[('date', '=', time.strftime('%Y-%m-%d'))], + 'timesheet_ids' : fields.one2many('hr.analytic.timesheet', 'sheet_id', + 'Timesheet lines', readonly=True, states={ 'draft': [('readonly', False)], 'new': [('readonly', False)]} ), - 'attendances_ids' : one2many_mod2('hr.attendance', 'sheet_id', 'Attendances'), + 'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'), 'state' : fields.selection([ ('new', 'New'), ('draft','Open'), @@ -346,9 +210,6 @@ class hr_timesheet_sheet(osv.osv): \n* The \'Confirmed\' state is used for to confirm the timesheet by user. \ \n* The \'Done\' state is used when users timesheet is accepted by his/her senior.'), 'state_attendance' : fields.related('employee_id', 'state', type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Current Status', readonly=True), - 'total_attendance_day': fields.function(_total, method=True, string='Total Attendance', multi="_total"), - 'total_timesheet_day': fields.function(_total, method=True, string='Total Timesheet', multi="_total"), - 'total_difference_day': fields.function(_total, method=True, string='Difference', multi="_total"), 'total_attendance': fields.function(_total, method=True, string='Total Attendance', multi="_total"), 'total_timesheet': fields.function(_total, method=True, string='Total Timesheet', multi="_total"), 'total_difference': fields.function(_total, method=True, string='Difference', multi="_total"), @@ -386,7 +247,6 @@ class hr_timesheet_sheet(osv.osv): _defaults = { 'date_from' : _default_date_from, - 'date_current' : lambda *a: time.strftime('%Y-%m-%d'), 'date_to' : _default_date_to, 'state': 'new', 'employee_id': _default_employee, @@ -406,16 +266,9 @@ class hr_timesheet_sheet(osv.osv): return False return True - def _date_current_check(self, cr, uid, ids, context=None): - for sheet in self.browse(cr, uid, ids, context=context): - if sheet.date_current < sheet.date_from or sheet.date_current > sheet.date_to: - return False - return True - _constraints = [ (_sheet_date, 'You cannot have 2 timesheets that overlaps !\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']), - (_date_current_check, 'You must select a Current date which is in the timesheet dates !', ['date_current']), ] def action_set_to_draft(self, cr, uid, ids, *args): @@ -498,7 +351,7 @@ class hr_timesheet_line(osv.osv): _columns = { 'sheet_id': fields.function(_sheet, string='Sheet', - type='many2one', relation='hr_timesheet_sheet.sheet', + type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade", store={ 'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10), 'account.analytic.line': (_get_account_analytic_line, ['user_id', 'date'], 10), @@ -534,6 +387,10 @@ class hr_timesheet_line(osv.osv): raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet.')) return True + def multi_on_change_account_id(self, cr, uid, ids, account_ids, context=None): + return dict([(el, self.on_change_account_id(cr, uid, ids, el)) for el in account_ids]) + + hr_timesheet_line() class hr_attendance(osv.osv): diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml index 42497eb537e..4e0e1becbe9 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml @@ -6,7 +6,6 @@ Sheet 1 - -->
    diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml index 9a7416464af..7dd2b9a1b88 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml @@ -71,20 +71,13 @@
    + + + + - -
    -
    -
    -
    -
    - + @@ -101,9 +94,9 @@ - + - + diff --git a/addons/hr_timesheet_sheet/report/timesheet_report.py b/addons/hr_timesheet_sheet/report/timesheet_report.py index 3db7ff8feb2..497fe8da117 100644 --- a/addons/hr_timesheet_sheet/report/timesheet_report.py +++ b/addons/hr_timesheet_sheet/report/timesheet_report.py @@ -46,7 +46,6 @@ class timesheet_report(osv.osv): 'department_id':fields.many2one('hr.department','Department',readonly=True), 'date_from': fields.date('Date from',readonly=True,), 'date_to': fields.date('Date to',readonly=True), - 'date_current': fields.date('Current date', required=True), 'state' : fields.selection([ ('new', 'New'), ('draft','Draft'), @@ -62,13 +61,9 @@ class timesheet_report(osv.osv): create or replace view timesheet_report as ( select min(aal.id) as id, - htss.date_current, htss.name, htss.date_from, htss.date_to, - to_char(htss.date_current,'YYYY') as year, - to_char(htss.date_current,'MM') as month, - to_char(htss.date_current, 'YYYY-MM-DD') as day, count(*) as nbr, aal.unit_amount as quantity, aal.amount as cost, @@ -77,18 +72,15 @@ class timesheet_report(osv.osv): (SELECT sum(day.total_difference) FROM hr_timesheet_sheet_sheet AS sheet LEFT JOIN hr_timesheet_sheet_sheet_day AS day - ON (sheet.id = day.sheet_id - AND day.name = sheet.date_current) where sheet.id=htss.id) as total_diff, + ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_diff, (SELECT sum(day.total_timesheet) FROM hr_timesheet_sheet_sheet AS sheet LEFT JOIN hr_timesheet_sheet_sheet_day AS day - ON (sheet.id = day.sheet_id - AND day.name = sheet.date_current) where sheet.id=htss.id) as total_timesheet, + ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_timesheet, (SELECT sum(day.total_attendance) FROM hr_timesheet_sheet_sheet AS sheet LEFT JOIN hr_timesheet_sheet_sheet_day AS day - ON (sheet.id = day.sheet_id - AND day.name = sheet.date_current) where sheet.id=htss.id) as total_attendance, + ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_attendance, aal.to_invoice, aal.general_account_id, htss.user_id, @@ -99,15 +91,11 @@ class timesheet_report(osv.osv): left join hr_analytic_timesheet as hat ON (hat.line_id=aal.id) left join hr_timesheet_sheet_sheet as htss ON (hat.line_id=htss.id) group by - to_char(htss.date_current,'YYYY'), - to_char(htss.date_current,'MM'), - to_char(htss.date_current, 'YYYY-MM-DD'), aal.account_id, htss.date_from, htss.date_to, aal.unit_amount, aal.amount, - htss.date_current, aal.to_invoice, aal.product_id, aal.general_account_id, diff --git a/addons/hr_timesheet_sheet/report/timesheet_report_view.xml b/addons/hr_timesheet_sheet/report/timesheet_report_view.xml index 7b31f72235b..711bbaf2a35 100644 --- a/addons/hr_timesheet_sheet/report/timesheet_report_view.xml +++ b/addons/hr_timesheet_sheet/report/timesheet_report_view.xml @@ -17,7 +17,6 @@ timesheet.report - @@ -56,7 +55,6 @@ - diff --git a/addons/hr_timesheet_sheet/static/src/css/timesheet.css b/addons/hr_timesheet_sheet/static/src/css/timesheet.css new file mode 100644 index 00000000000..b486f76694d --- /dev/null +++ b/addons/hr_timesheet_sheet/static/src/css/timesheet.css @@ -0,0 +1,79 @@ + +.openerp .oe_timesheet_weekly { + overflow-x: auto; +} + +.openerp .oe_timesheet_weekly table { + width: 100%; +} + +.openerp .oe_timesheet_weekly td { + padding-top: 15px; +} + +.openerp .oe_timesheet_weekly th { + text-align: right; + color: #069; + font-family: 'Helvetica Neue', Arial, Verdana, 'Nimbus Sans L', sans-serif; + font-size: 10px; +} + +.openerp .oe_timesheet_weekly th.oe_timesheet_weekly_date_head { + width: 60px; +} + +.openerp .oe_timesheet_weekly td { + text-align: right; + vertical-align: middle; +} + +.openerp .oe_timesheet_weekly .oe_timesheet_weekly_account { + text-align: left; + padding-right: 30px; +} + +.openerp .oe_timesheet_weekly td input.oe_timesheet_weekly_input { + border: 1px solid #CCC; + padding: 5px 2px !important; + color: #666 !important; + font-size: 14px; + font-weight: bold; + width: 38px; + text-align: right; + min-width: 0 !important; +} + +.openerp .oe_timesheet_weekly td .oe_timesheet_weekly_box { + padding: 5px 2px !important; + color: #666 !important; + font-size: 14px; + font-weight: bold; + width: 38px; + display: inline-block; +} + +.openerp .oe_timesheet_weekly .oe_timesheet_weekly_adding_tot { + display: table; + width: 100%; +} + +.openerp .oe_timesheet_weekly .oe_timesheet_weekly_adding { + display: table-cell; + text-align: left; +} + +.openerp .oe_timesheet_weekly .oe_timesheet_weekly_tottot { + display: table-cell; +} + +.openerp .oe_timesheet_weekly .oe_timesheet_weekly_add_row td { + text-align: left; +} + +.openerp .oe_timesheet_weekly .oe_timesheet_weekly_add_row .oe_form_field_many2one { + display: inline-block; + width: 200px; +} + +.openerp .oe_timesheet_weekly_today { +} diff --git a/addons/hr_timesheet_sheet/static/src/js/timesheet.js b/addons/hr_timesheet_sheet/static/src/js/timesheet.js new file mode 100644 index 00000000000..1be965fd63d --- /dev/null +++ b/addons/hr_timesheet_sheet/static/src/js/timesheet.js @@ -0,0 +1,299 @@ + +openerp.hr_timesheet_sheet = function(instance) { + var QWeb = instance.web.qweb; + var _t = instance.web._t; + + instance.hr_timesheet_sheet.WeeklyTimesheet = instance.web.form.FormWidget.extend(instance.web.form.ReinitializeWidgetMixin, { + init: function() { + this._super.apply(this, arguments); + this.set({ + sheets: [], + date_to: false, + date_from: false, + }); + this.field_manager.on("field_changed:timesheet_ids", this, this.query_sheets); + this.field_manager.on("field_changed:date_from", this, function() { + this.set({"date_from": instance.web.str_to_date(this.field_manager.get_field_value("date_from"))}); + }); + this.field_manager.on("field_changed:date_to", this, function() { + this.set({"date_to": instance.web.str_to_date(this.field_manager.get_field_value("date_to"))}); + }); + this.field_manager.on("field_changed:user_id", this, function() { + this.set({"user_id": this.field_manager.get_field_value("user_id")}); + }); + this.on("change:sheets", this, this.update_sheets); + this.res_o2m_drop = new instance.web.DropMisordered(); + this.render_drop = new instance.web.DropMisordered(); + this.description_line = _t("/"); + }, + query_sheets: function() { + var self = this; + if (self.updating) + return; + var commands = this.field_manager.get_field_value("timesheet_ids"); + this.res_o2m_drop.add(new instance.web.Model(this.view.model).call("resolve_2many_commands", ["timesheet_ids", commands, [], + new instance.web.CompoundContext()])) + .then(function(result) { + self.querying = true; + self.set({sheets: result}); + self.querying = false; + }); + }, + update_sheets: function() { + var self = this; + if (self.querying) + return; + self.updating = true; + self.field_manager.set_values({timesheet_ids: self.get("sheets")}).then(function() { + self.updating = false; + }); + }, + initialize_field: function() { + instance.web.form.ReinitializeWidgetMixin.initialize_field.call(this); + var self = this; + self.on("change:sheets", self, self.initialize_content); + self.on("change:date_to", self, self.initialize_content); + self.on("change:date_from", self, self.initialize_content); + self.on("change:user_id", self, self.initialize_content); + }, + initialize_content: function() { + var self = this; + if (self.setting) + return; + // don't render anything until we have date_to and date_from + if (!self.get("date_to") || !self.get("date_from")) + return; + this.destroy_content(); + + // it's important to use those vars to avoid race conditions + var dates; + var accounts; + var account_names; + var default_get; + return this.render_drop.add(new instance.web.Model("hr.analytic.timesheet").call("default_get", [ + ['account_id','general_account_id', 'journal_id','date','name','user_id','product_id','product_uom_id','to_invoice','amount','unit_amount'], + new instance.web.CompoundContext({'user_id': self.get('user_id')})]).pipe(function(result) { + default_get = result; + // calculating dates + dates = []; + var start = self.get("date_from"); + var end = self.get("date_to"); + while (start <= end) { + dates.push(start); + start = start.clone().addDays(1); + } + // group by account + accounts = _(self.get("sheets")).chain() + .map(function(el) { + // much simpler to use only the id in all cases + if (typeof(el.account_id) === "object") + el.account_id = el.account_id[0]; + return el; + }) + .groupBy("account_id").value(); + + var account_ids = _.map(_.keys(accounts), function(el) { return el === "false" ? false : Number(el) }); + + return new instance.web.Model("hr.analytic.timesheet").call("multi_on_change_account_id", [[], account_ids, + new instance.web.CompoundContext({'user_id': self.get('user_id')})]).pipe(function(accounts_defaults) { + accounts = _(accounts).chain().map(function(lines, account_id) { + account_defaults = _.extend({}, default_get, accounts_defaults[account_id]); + // group by days + account_id = account_id === "false" ? false : Number(account_id); + var index = _.groupBy(lines, "date"); + var days = _.map(dates, function(date) { + var day = {day: date, lines: index[instance.web.date_to_str(date)] || []}; + // add line where we will insert/remove hours + var to_add = _.find(day.lines, function(line) { return line.name === self.description_line }); + if (to_add) { + day.lines = _.without(day.lines, to_add); + day.lines.unshift(to_add); + } else { + day.lines.unshift(_.extend(_.clone(account_defaults), { + name: self.description_line, + unit_amount: 0, + date: instance.web.date_to_str(date), + account_id: account_id, + })); + } + return day; + }); + return {account: account_id, days: days, account_defaults: account_defaults}; + }).value(); + + // we need the name_get of the analytic accounts + return new instance.web.Model("account.analytic.account").call("name_get", [_.pluck(accounts, "account"), + new instance.web.CompoundContext()]).pipe(function(result) { + account_names = {}; + _.each(result, function(el) { + account_names[el[0]] = el[1]; + }); + accounts = _.sortBy(accounts, function(el) { + return account_names[el.account]; + }); + });; + }); + })).pipe(function(result) { + // we put all the gathered data in self, then we render + self.dates = dates; + self.accounts = accounts; + self.account_names = account_names; + self.default_get = default_get; + //real rendering + self.display_data(); + }); + }, + destroy_content: function() { + if (this.dfm) { + this.dfm.destroy(); + this.dfm = undefined; + } + }, + display_data: function() { + var self = this; + self.$el.html(QWeb.render("hr_timesheet_sheet.WeeklyTimesheet", {widget: self})); + _.each(self.accounts, function(account) { + _.each(_.range(account.days.length), function(day_count) { + if (!self.get('effective_readonly')) { + self.get_box(account, day_count).val(self.sum_box(account, day_count)).change(function() { + var num = Number($(this).val()); + if (isNaN(num)) { + $(this).val(self.sum_box(account, day_count)); + } else { + account.days[day_count].lines[0].unit_amount += num - self.sum_box(account, day_count); + self.display_totals(); + self.sync(); + } + }); + } else { + self.get_box(account, day_count).html(self.sum_box(account, day_count)); + } + }); + }); + self.display_totals(); + self.$(".oe_timesheet_weekly_adding button").click(_.bind(this.init_add_account, this)); + }, + init_add_account: function() { + var self = this; + if (self.dfm) + return; + self.$(".oe_timesheet_weekly_add_row").show(); + self.dfm = new instance.web.form.DefaultFieldManager(self); + self.dfm.extend_field_desc({ + account: { + relation: "account.analytic.account", + }, + }); + self.account_m2o = new instance.web.form.FieldMany2One(self.dfm, { + attrs: { + name: "account", + type: "many2one", + domain: [ + ['type','in',['normal', 'contract']], + ['state', '<>', 'close'], + ['use_timesheets','=',1], + ['id', 'not in', _.pluck(self.accounts, "account")], + ], + modifiers: '{"required": true}', + }, + }); + self.account_m2o.prependTo(self.$(".oe_timesheet_weekly_add_row td")); + self.$(".oe_timesheet_weekly_add_row button").click(function() { + var id = self.account_m2o.get_value(); + if (id === false) { + self.dfm.set({display_invalid_fields: true}); + return; + } + var ops = self.generate_o2m_value(); + new instance.web.Model("hr.analytic.timesheet").call("on_change_account_id", [[], id]).pipe(function(res) { + var def = _.extend({}, self.default_get, res.value, { + name: self.description_line, + unit_amount: 0, + date: instance.web.date_to_str(self.dates[0]), + account_id: id, + }); + ops.push(def); + self.set({"sheets": ops}); + }); + }); + }, + get_box: function(account, day_count) { + return this.$('[data-account="' + account.account + '"][data-day-count="' + day_count + '"]'); + }, + get_total: function(account) { + return this.$('[data-account-total="' + account.account + '"]'); + }, + get_day_total: function(day_count) { + return this.$('[data-day-total="' + day_count + '"]'); + }, + get_super_total: function() { + return this.$('.oe_timesheet_weekly_supertotal'); + }, + sum_box: function(account, day_count) { + var line_total = 0; + _.each(account.days[day_count].lines, function(line) { + line_total += line.unit_amount; + }); + return line_total; + }, + display_totals: function() { + var self = this; + var day_tots = _.map(_.range(self.dates.length), function() { return 0 }); + var super_tot = 0; + _.each(self.accounts, function(account) { + var acc_tot = 0; + _.each(_.range(self.dates.length), function(day_count) { + var sum = self.sum_box(account, day_count); + acc_tot += sum; + day_tots[day_count] += sum; + super_tot += sum; + }); + self.get_total(account).html(acc_tot); + }); + _.each(_.range(self.dates.length), function(day_count) { + self.get_day_total(day_count).html(day_tots[day_count]); + }); + self.get_super_total().html(super_tot); + }, + sync: function() { + var self = this; + self.setting = true; + self.set({sheets: this.generate_o2m_value()}); + self.setting = false; + }, + generate_o2m_value: function() { + var self = this; + var ops = []; + + _.each(self.accounts, function(account) { + var auth_keys = _.extend(_.clone(account.account_defaults), { + name: true, unit_amount: true, date: true, account_id:true, + }); + _.each(account.days, function(day) { + _.each(day.lines, function(line) { + if (line.unit_amount !== 0) { + var tmp = _.clone(line); + tmp.id = undefined; + _.each(line, function(v, k) { + if (v instanceof Array) { + tmp[k] = v[0]; + } + }); + // we have to remove some keys, because analytic lines are shitty + _.each(_.keys(tmp), function(key) { + if (auth_keys[key] === undefined) { + tmp[key] = undefined; + } + }); + ops.push(tmp); + } + }); + }); + }); + return ops; + }, + }); + + instance.web.form.custom_widgets.add('weekly_timesheet', 'instance.hr_timesheet_sheet.WeeklyTimesheet'); + +}; diff --git a/addons/hr_timesheet_sheet/static/src/xml/timesheet.xml b/addons/hr_timesheet_sheet/static/src/xml/timesheet.xml new file mode 100644 index 00000000000..0c4ad857d3b --- /dev/null +++ b/addons/hr_timesheet_sheet/static/src/xml/timesheet.xml @@ -0,0 +1,56 @@ + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    TOTAL
    + + + +
    +
    +
    +
    TOTAL
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml b/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml index 070079a4b80..71776af61ba 100644 --- a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml +++ b/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml @@ -16,7 +16,6 @@ I create a timesheet for employee "Quentin Paolinon". - !record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}: - date_current: !eval time.strftime('%Y-%m-%d') date_from: !eval time.strftime('%Y-%m-01') name: Quentin Paolinon state: new @@ -34,30 +33,6 @@ - !assert {model: hr.employee, id: hr.employee_qdp}: - state == 'present' -- - I want to check attendance and work of yesterday. I click on <- button. -- - !python {model: hr_timesheet_sheet.sheet}: | - date_prev = self.date_previous(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None) - assert date_prev == True, "I See Previous Date Timesheet" -- - Then I click on "Today" button to fill today's timesheet. -- - !python {model: hr_timesheet_sheet.sheet}: | - date_to = self.date_today(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None) - assert date_to == True, "I See Today Date Timesheet" -- - I can also move to next day by clicking on -> button. -- - !python {model: hr_timesheet_sheet.sheet}: | - date_next = self.date_next(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None) - assert date_next == True, "I See Next Date Timesheet" -- - I want to go to a particular date and see attendance then I select the date and click on "Go to:" button. -- - !python {model: hr_timesheet_sheet.sheet}: | - button_dumy = self.button_dummy(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None) - assert button_dumy == True, "I See Particular Date Attendance" - At the time of logout, I create attendance and perform "Sign Out". - diff --git a/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py b/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py index a6974fc2ccf..3bd495036d5 100644 --- a/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py +++ b/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py @@ -42,7 +42,6 @@ class hr_timesheet_current_open(osv.osv_memory): view_type = 'tree,form' domain = "[('id','in',["+','.join(map(str, ids))+"]),('user_id', '=', uid)]" elif len(ids)==1: - ts.write(cr, uid, ids, {'date_current': time.strftime('%Y-%m-%d')}, context=context) domain = "[('user_id', '=', uid)]" else: domain = "[('user_id', '=', uid)]" diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 2261b940944..529ee8c65cc 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -39,63 +39,6 @@ _logger = logging.getLogger(__name__) def decode_header(message, header, separator=' '): return separator.join(map(decode, message.get_all(header, []))) -class many2many_reference(fields.many2many): - """ many2many_reference manages many2many fields where one id is found - by a reference-like key (a char column in addition to the foreign id). - The reference_column attribute on the many2many fields is used; - if not defined, ``res_model`` is used. """ - - def _get_query_and_where_params(self, cr, model, ids, values, where_params): - """ Add in where condition like mail_followers.res_model = 'crm.lead' """ - reference_column = self.reference_column if self.reference_column else 'res_model' - values.update(reference_column=reference_column, reference_value=model._name) - query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \ - FROM %(rel)s, %(from_c)s \ - WHERE %(rel)s.%(id1)s IN %%s \ - AND %(rel)s.%(id2)s = %(tbl)s.id \ - AND %(rel)s.%(reference_column)s = \'%(reference_value)s\' \ - %(where_c)s \ - %(order_by)s \ - %(limit)s \ - OFFSET %(offset)d' \ - % values - return query, where_params - - def set(self, cr, model, id, name, values, user=None, context=None): - """ Override to add the reference field in queries. """ - if not values: return - rel, id1, id2 = self._sql_names(model) - obj = model.pool.get(self._obj) - # reference column name: given by attribute or res_model - reference_column = self.reference_column if self.reference_column else 'res_model' - for act in values: - if not (isinstance(act, list) or isinstance(act, tuple)) or not act: - continue - if act[0] == 0: - idnew = obj.create(cr, user, act[2], context=context) - cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, idnew, model._name)) - elif act[0] == 3: - cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name)) - elif act[0] == 4: - # following queries are in the same transaction - so should be relatively safe - cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name)) - if not cr.fetchone(): - cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act[1], model._name)) - elif act[0] == 5: - cr.execute('delete from '+rel+' where '+id1+' = %s AND '+reference_column+'=%s', (id, model._name)) - elif act[0] == 6: - d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context) - if d1: - d1 = ' and ' + ' and '.join(d1) - else: - d1 = '' - cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+reference_column+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, model._name, id]+d2) - for act_nbr in act[2]: - cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act_nbr, model._name)) - # cases 1, 2: performs write and unlink -> default implementation is ok - else: - return super(many2many_reference, self).set(cr, model, id, name, values, user, context) - class mail_thread(osv.AbstractModel): ''' mail_thread model is meant to be inherited by any model that needs to @@ -186,6 +129,62 @@ class mail_thread(osv.AbstractModel): res[notif.message_id.res_id] = True return [('id', 'in', res.keys())] + def _get_followers(self, cr, uid, ids, name, arg, context=None): + fol_obj = self.pool.get('mail.followers') + fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids)]) + res = dict((res_id, []) for res_id in ids) + for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids): + res[fol.res_id].append(fol.partner_id.id) + return res + + def _set_followers(self, cr, uid, id, name, value, arg, context=None): + partner_obj = self.pool.get('res.partner') + fol_obj = self.pool.get('mail.followers') + + # read the old set of followers, and determine the new set of followers + fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', '=', id)]) + old = set(fol.partner_id.id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)) + new = set(old) + + for command in value: + if isinstance(command, (int, long)): + new.add(command) + elif command[0] == 0: + new.add(partner_obj.create(cr, uid, command[2], context=context)) + elif command[0] == 1: + partner_obj.write(cr, uid, [command[1]], command[2], context=context) + new.add(command[1]) + elif command[0] == 2: + partner_obj.unlink(cr, uid, [command[1]], context=context) + new.discard(command[1]) + elif command[0] == 3: + new.discard(command[1]) + elif command[0] == 4: + new.add(command[1]) + elif command[0] == 5: + new.clear() + elif command[0] == 6: + new = set(command[2]) + + # remove partners that are no longer followers + fol_ids = fol_obj.search(cr, SUPERUSER_ID, + [('res_model', '=', self._name), ('res_id', '=', id), ('partner_id', 'not in', list(new))]) + fol_obj.unlink(cr, SUPERUSER_ID, fol_ids) + + # add new followers + for partner_id in new - old: + fol_obj.create(cr, SUPERUSER_ID, {'res_model': self._name, 'res_id': id, 'partner_id': partner_id}) + + def _search_followers(self, cr, uid, obj, name, args, context): + fol_obj = self.pool.get('mail.followers') + res = [] + for field, operator, value in args: + assert field == name + fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('partner_id', operator, value)]) + res_ids = [fol.res_id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)] + res.append(('id', 'in', res_ids)) + return res + _columns = { 'message_is_follower': fields.function(_get_subscription_data, type='boolean', string='Is a Follower', multi='_get_subscription_data,'), @@ -194,9 +193,8 @@ class mail_thread(osv.AbstractModel): help="Holds data about the subtypes. The content of this field "\ "is a structure holding the current model subtypes, and the "\ "current document followed subtypes."), - 'message_follower_ids': many2many_reference('res.partner', - 'mail_followers', 'res_id', 'partner_id', - reference_column='res_model', string='Followers'), + 'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers, + fnct_search=_search_followers, type='many2many', obj='res.partner', string='Followers'), 'message_comment_ids': fields.one2many('mail.message', 'res_id', domain=lambda self: [('model', '=', self._name), ('type', 'in', ('comment', 'email'))], string='Comments and emails', diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml index b503565bd11..be3bb667628 100644 --- a/addons/mail/security/mail_security.xml +++ b/addons/mail/security/mail_security.xml @@ -7,7 +7,7 @@ Mail.group: access only public and joined groups - ['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.id]), '&', ('public','=','groups'), ('group_public_id','in', [x.id for x in user.groups_id])] + ['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.partner_id.id]), '&', ('public','=','groups'), ('group_public_id','in', [g.id for g in user.groups_id])] diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py index 10392be53ff..83b10b39c28 100644 --- a/addons/portal/tests/test_portal.py +++ b/addons/portal/tests/test_portal.py @@ -33,6 +33,7 @@ class test_portal(test_mail.TestMailMockups): self.mail_group = self.registry('mail.group') self.mail_mail = self.registry('mail.mail') self.mail_message = self.registry('mail.message') + self.mail_invite = self.registry('mail.wizard.invite') self.res_users = self.registry('res.users') self.res_partner = self.registry('res.partner') @@ -41,13 +42,13 @@ class test_portal(test_mail.TestMailMockups): {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}) # Find Portal group - group_portal_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_portal') - self.group_portal_id = group_portal_ref and group_portal_ref[1] or False + group_portal = self.registry('ir.model.data').get_object(cr, uid, 'portal', 'group_portal') + self.group_portal_id = group_portal.id # Create Chell (portal user) self.user_chell_id = self.res_users.create(cr, uid, {'name': 'Chell Gladys', 'login': 'chell', 'groups_id': [(6, 0, [self.group_portal_id])]}) - self.user_chell = self.res_users.browse(cr, uid, self.user_chell_id) - self.partner_chell_id = self.user_chell.partner_id.id + user_chell = self.res_users.browse(cr, uid, self.user_chell_id) + self.partner_chell_id = user_chell.partner_id.id # Set an email address for the user running the tests, used as Sender for outgoing mails self.res_users.write(cr, uid, uid, {'email': 'test@localhost'}) @@ -55,8 +56,6 @@ class test_portal(test_mail.TestMailMockups): def test_00_access_rights(self): """ Test basic mail_message and mail_group access rights for portal users. """ cr, uid = self.cr, self.uid - partner_chell_id = self.partner_chell_id - user_chell_id = self.user_chell_id # Prepare group: Pigs (portal) self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message') @@ -67,56 +66,56 @@ class test_portal(test_mail.TestMailMockups): # ---------------------------------------- # Do: Chell reads Pigs messages, ok because restricted to portal group - message_ids = self.mail_group.read(cr, user_chell_id, self.group_pigs_id, ['message_ids'])['message_ids'] - self.mail_message.read(cr, user_chell_id, message_ids) + message_ids = self.mail_group.read(cr, self.user_chell_id, self.group_pigs_id, ['message_ids'])['message_ids'] + self.mail_message.read(cr, self.user_chell_id, message_ids) + # Do: Chell posts a message on Pigs, crash because can not write on group or is not in the followers - self.assertRaises(except_orm, - self.mail_group.message_post, - cr, user_chell_id, self.group_pigs_id, body='Message') + with self.assertRaises(except_orm): + self.mail_group.message_post(cr, self.user_chell_id, self.group_pigs_id, body='Message') + # Do: Chell is added to Pigs followers - self.mail_group.message_subscribe(cr, uid, [self.group_pigs_id], [partner_chell_id]) + self.mail_group.message_subscribe(cr, uid, [self.group_pigs_id], [self.partner_chell_id]) + # Test: Chell posts a message on Pigs, ok because in the followers - self.mail_group.message_post(cr, user_chell_id, self.group_pigs_id, body='Message') + self.mail_group.message_post(cr, self.user_chell_id, self.group_pigs_id, body='Message') def test_50_mail_invite(self): cr, uid = self.cr, self.uid user_admin = self.res_users.browse(cr, uid, uid) - self.mail_invite = self.registry('mail.wizard.invite') base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='') - portal_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_portal') - portal_id = portal_ref and portal_ref[1] or False - # 0 - Admin - p_a_id = user_admin.partner_id.id + partner_admin_id = user_admin.partner_id.id # 1 - Bert Tartopoils, with email, should receive emails for comments and emails - p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'}) + partner_bert_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'}) # ---------------------------------------- - # CASE1: generated URL + # CASE: invite Bert to follow Pigs # ---------------------------------------- - url = self.mail_mail._generate_signin_url(cr, uid, p_b_id, portal_id, 1234) - self.assertEqual(url, base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (p_b_id, portal_id, 1234), - 'generated signin URL incorrect') - - # ---------------------------------------- - # CASE2: invite Bert - # ---------------------------------------- - - _sent_email_subject = 'Invitation to follow Pigs' - _sent_email_body = append_content_to_html('
    You have been invited to follow Pigs.
    ', url) - # Do: create a mail_wizard_invite, validate it self._init_mock_build_email() - mail_invite_id = self.mail_invite.create(cr, uid, {'partner_ids': [(4, p_b_id)]}, {'default_res_model': 'mail.group', 'default_res_id': self.group_pigs_id}) + context = {'default_res_model': 'mail.group', 'default_res_id': self.group_pigs_id} + mail_invite_id = self.mail_invite.create(cr, uid, {'partner_ids': [(4, partner_bert_id)]}, context) self.mail_invite.add_followers(cr, uid, [mail_invite_id]) - group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) # Test: Pigs followers should contain Admin and Bert + group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) follower_ids = [follower.id for follower in group_pigs.message_follower_ids] - self.assertEqual(set(follower_ids), set([p_a_id, p_b_id]), 'Pigs followers after invite is incorrect') - # Test: sent email subject, body + self.assertEqual(set(follower_ids), set([partner_admin_id, partner_bert_id]), 'Pigs followers after invite is incorrect') + + # Test: partner must have been prepared for signup + partner_bert = self.res_partner.browse(cr, uid, partner_bert_id) + self.assertTrue(partner_bert.signup_valid, 'partner has not been prepared for signup') + self.assertTrue(base_url in partner_bert.signup_url, 'signup url is incorrect') + self.assertTrue(cr.dbname in partner_bert.signup_url, 'signup url is incorrect') + self.assertTrue(partner_bert.signup_token in partner_bert.signup_url, 'signup url is incorrect') + + # Test: (pretend to) send email and check subject, body self.assertEqual(len(self._build_email_kwargs_list), 1, 'sent email number incorrect, should be only for Bert') for sent_email in self._build_email_kwargs_list: - self.assertEqual(sent_email.get('subject'), _sent_email_subject, 'sent email subject incorrect') - self.assertEqual(sent_email.get('body'), _sent_email_body, 'sent email body incorrect') + self.assertEqual(sent_email.get('subject'), 'Invitation to follow Pigs', + 'subject of invitation email is incorrect') + self.assertTrue('You have been invited to follow Pigs' in sent_email.get('body'), + 'body of invitation email is incorrect') + self.assertTrue(partner_bert.signup_url in sent_email.get('body'), + 'body of invitation email does not contain signup url') diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index 3cc706c2129..c94d6509f63 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -30,17 +30,19 @@ from openerp import SUPERUSER_ID from base.res.res_partner import _lang_get _logger = logging.getLogger(__name__) -# welcome/goodbye email sent to portal users +# welcome email sent to portal users # (note that calling '_' has no effect except exporting those strings for translation) WELCOME_EMAIL_SUBJECT = _("Your OpenERP account at %(company)s") WELCOME_EMAIL_BODY = _("""Dear %(name)s, -You have been given access to %(portal)s at %(url)s. +You have been given access to %(portal)s. Your login account data is: Database: %(db)s -User: %(login)s -Password: %(password)s +Username: %(login)s + +In order to complete the signin process, click on the following url: +%(url)s %(welcome_message)s @@ -49,28 +51,10 @@ OpenERP - Open Source Business Applications http://www.openerp.com """) -GOODBYE_EMAIL_SUBJECT = _("Your OpenERP account at %(company)s") -GOODBYE_EMAIL_BODY = _("""Dear %(name)s, - -Your access to %(portal)s has been withdrawn. - -%(goodbye_message)s - --- -OpenERP - Open Source Business Applications -http://www.openerp.com -""") - -# character sets for passwords, excluding 0, O, o, 1, I, l -_PASSU = 'ABCDEFGHIJKLMNPQRSTUVWXYZ' -_PASSL = 'abcdefghijkmnpqrstuvwxyz' -_PASSD = '23456789' - def random_password(): - # get 3 uppercase letters, 3 lowercase letters, 2 digits, and shuffle them - chars = map(random.choice, [_PASSU] * 3 + [_PASSL] * 3 + [_PASSD] * 2) - random.shuffle(chars) - return ''.join(chars) + # temporary random stuff; user password is reset by signup process + chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + return ''.join(random.choice(chars) for i in xrange(12)) def extract_email(email): """ extract the email address from a user-friendly email address """ @@ -92,8 +76,6 @@ class wizard(osv.osv_memory): 'user_ids': fields.one2many('portal.wizard.user', 'wizard_id', string='Users'), 'welcome_message': fields.text(string='Invitation Message', help="This text is included in the email sent to new users of the portal."), - 'goodbye_message': fields.text(string='Withdrawal Message', - help="This text is included in the email sent to users withdrawn from the portal."), } def _default_portal(self, cr, uid, context): @@ -164,6 +146,8 @@ class wizard_user(osv.osv_memory): user = self._create_user(cr, SUPERUSER_ID, wizard_user, context) if (not user.active) or (portal not in user.groups_id): user.write({'active': True, 'groups_id': [(4, portal.id)]}) + # prepare for the signup process + user.partner_id.signup_prepare() wizard_user = self.browse(cr, SUPERUSER_ID, wizard_user.id, context) self._send_email(cr, uid, wizard_user, context) else: @@ -174,8 +158,6 @@ class wizard_user(osv.osv_memory): user.write({'groups_id': [(3, portal.id)], 'active': False}) else: user.write({'groups_id': [(3, portal.id)]}) - wizard_user = self.browse(cr, SUPERUSER_ID, wizard_user.id, context) - self._send_email(cr, uid, wizard_user, context) def _retrieve_user(self, cr, uid, wizard_user, context=None): """ retrieve the (possibly inactive) user corresponding to wizard_user.partner_id @@ -208,7 +190,7 @@ class wizard_user(osv.osv_memory): return res_users.browse(cr, uid, user_id, context) def _send_email(self, cr, uid, wizard_user, context=None): - """ send notification email to a new/former portal user + """ send notification email to a new portal user @param wizard_user: browse record of model portal.wizard.user @return: the id of the created mail.mail record """ @@ -219,33 +201,23 @@ class wizard_user(osv.osv_memory): _('You must have an email address in your User Preferences to send emails.')) # determine subject and body in the portal user's language - url = self.pool.get('ir.config_parameter').get_param(cr, SUPERUSER_ID, 'web.base.url', context=this_context) user = self._retrieve_user(cr, SUPERUSER_ID, wizard_user, context) context = dict(this_context or {}, lang=user.lang) data = { 'company': this_user.company_id.name, 'portal': wizard_user.wizard_id.portal_id.name, 'welcome_message': wizard_user.wizard_id.welcome_message or "", - 'goodbye_message': wizard_user.wizard_id.goodbye_message or "", - 'url': url or _("(missing url)"), 'db': cr.dbname, + 'name': user.name, 'login': user.login, - 'password': user.password, - 'name': user.name + 'url': user.signup_url, } - if wizard_user.in_portal: - subject = _(WELCOME_EMAIL_SUBJECT) % data - body = _(WELCOME_EMAIL_BODY) % data - else: - subject = _(GOODBYE_EMAIL_SUBJECT) % data - body = _(GOODBYE_EMAIL_BODY) % data - mail_mail = self.pool.get('mail.mail') mail_values = { 'email_from': this_user.email, 'email_to': user.email, - 'subject': subject, - 'body_html': '
    %s
    ' % body, + 'subject': _(WELCOME_EMAIL_SUBJECT) % data, + 'body_html': '
    %s
    ' % (_(WELCOME_EMAIL_BODY) % data), 'state': 'outgoing', } return mail_mail.create(cr, uid, mail_values, context=this_context) diff --git a/addons/portal/wizard/portal_wizard_view.xml b/addons/portal/wizard/portal_wizard_view.xml index c8e3f30b128..373b4ad8e7a 100644 --- a/addons/portal/wizard/portal_wizard_view.xml +++ b/addons/portal/wizard/portal_wizard_view.xml @@ -27,8 +27,6 @@ -