[MERGE] Sync with trunk
bzr revid: odo@openerp.com-20121009155456-03uqk5jwd9qayuj4
This commit is contained in:
commit
af24485be3
|
@ -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'),
|
||||
|
|
|
@ -458,7 +458,6 @@
|
|||
<filter domain="[('user_id','=',uid)]" help="My Invoices" icon="terp-personal"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="user_id" string="Salesperson"/>
|
||||
<field name="journal_id"/>
|
||||
<field name="period_id" string="Period"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}"/>
|
||||
|
|
|
@ -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 <waleed_bazaza@yahoo.com>\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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="provider_openerp" model="auth.oauth.provider">
|
||||
<field name="name">OpenERP Accounts</field>
|
||||
|
|
|
@ -1 +1,23 @@
|
|||
import auth_reset_password
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import controllers
|
||||
import res_users
|
||||
|
|
|
@ -1,3 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'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'],
|
||||
}
|
||||
|
|
|
@ -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 {}
|
|
@ -7,56 +7,15 @@
|
|||
<field name="name">Reset Password</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="email_from"><![CDATA[${object.company_id.name} <${object.company_id.email}>]]></field>
|
||||
<field name="email_to" eval="False"><!--(set by reset_password module)--></field>
|
||||
<field name="email_to">${object.email}</field>
|
||||
<field name="subject">Password reset</field>
|
||||
<field name="body_html"><![CDATA[
|
||||
<p>A password reset was requested the OpenERP account linked to this email on ${object._auth_reset_password_host()}</p>
|
||||
<p>A password reset was requested for the OpenERP account linked to this email.</p>
|
||||
|
||||
<p>You may change your password following this <a href="${object._auth_reset_password_link()}">link</a>,
|
||||
or by copy-pasting the following URL in your browser: ${object._auth_reset_password_link()}</p>
|
||||
<p>You may change your password following <a href="${object.signup_url}">this link</a>.</p>
|
||||
|
||||
<p>Note: If you did not ask for a password reset, you can safely ignore this email.</p>]]></field>
|
||||
</record>
|
||||
|
||||
<!-- TODO get own css -->
|
||||
<record id="reset_password_wizard_form_view" model="ir.ui.view">
|
||||
<field name="name">auth.reset_password.form</field>
|
||||
<field name="model">auth.reset_password</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reset Password" version="7.0">
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="token" on_change="onchange_token(token)" invisible="1"/>
|
||||
<group colspan="4" states="draft,missmatch">
|
||||
<field name="password" required='1' on_change="onchange_pw(password,password_confirmation)"/>
|
||||
<field name="password_confirmation" required='1' on_change="onchange_pw(password,password_confirmation)"/>
|
||||
<group colspan="4" states="missmatch">
|
||||
<div>Passwords missmatch</div>
|
||||
</group>
|
||||
<group colspan="2" col="1">
|
||||
<button string="Change Password" name="change" icon="gtk-dialog-authentication" attrs="{'readonly': [('state', '=', 'missmatch')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group colspan="4" states="error" col="1">
|
||||
<div>Invalid or expired token</div>
|
||||
<button special="cancel" string="Close"/>
|
||||
</group>
|
||||
<group colspan="4" states="done" col="1">
|
||||
<div>Password changed. We sent you an email confirming the password change.</div>
|
||||
<button special="cancel" string="Close"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_reset" model="ir.actions.act_window">
|
||||
<field name="name">Reset Password</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">auth.reset_password</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import main
|
||||
|
||||
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp.modules.registry import RegistryManager
|
||||
import openerp.addons.web.common.http as openerpweb
|
||||
|
||||
import werkzeug
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Controller(openerpweb.Controller):
|
||||
_cp_path = '/auth_reset_password'
|
||||
|
||||
@openerpweb.httprequest
|
||||
def reset_password(self, req, dbname, login):
|
||||
""" retrieve user, and perform reset password """
|
||||
url = '/'
|
||||
registry = RegistryManager.get(dbname)
|
||||
with registry.cursor() as cr:
|
||||
try:
|
||||
res_users = registry.get('res.users')
|
||||
res_users.reset_password(cr, SUPERUSER_ID, login)
|
||||
cr.commit()
|
||||
message = 'An email has been sent with credentials to reset your password'
|
||||
except Exception as e:
|
||||
# signup error
|
||||
_logger.exception('error when resetting password')
|
||||
message = e.message
|
||||
url = "/#action=login&error_message=%s" % werkzeug.urls.url_quote(message)
|
||||
return werkzeug.utils.redirect(url)
|
||||
|
||||
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import osv, fields
|
||||
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def now(**kwargs):
|
||||
dt = datetime.now() + timedelta(**kwargs)
|
||||
return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
|
||||
class res_users(osv.osv):
|
||||
_inherit = 'res.users'
|
||||
|
||||
def reset_password(self, cr, uid, login, context=None):
|
||||
""" retrieve the user corresponding to login (login or email),
|
||||
and reset their password
|
||||
"""
|
||||
user_ids = self.search(cr, uid, [('login', '=', login)], context=context)
|
||||
if not user_ids:
|
||||
user_ids = self.search(cr, uid, [('email', '=', login)], context=context)
|
||||
if len(user_ids) != 1:
|
||||
raise Exception('Reset password: invalid username or email')
|
||||
return self.action_reset_password(cr, uid, user_ids, context=context)
|
||||
|
||||
def action_reset_password(self, cr, uid, ids, context=None):
|
||||
""" create signup token for each user, and send their signup url by email """
|
||||
# prepare reset password signup
|
||||
res_partner = self.pool.get('res.partner')
|
||||
partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context)]
|
||||
res_partner.signup_prepare(cr, uid, partner_ids, expiration=now(days=+1), context=context)
|
||||
|
||||
# send email to users with their signup url
|
||||
template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_reset_password', 'reset_password_email')
|
||||
assert template._name == 'email.template'
|
||||
for user in self.browse(cr, uid, ids, context):
|
||||
self.pool.get('email.template').send_mail(cr, uid, template.id, user.id, context=context)
|
||||
|
||||
return True
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="res_users_form_view" model="ir.ui.view">
|
||||
<field name="name">user.form.reset_password</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet/*[1]" position="before">
|
||||
<div class="oe_right oe_button_box">
|
||||
<button string="Reset Password" type="object" name="action_reset_password"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,12 +0,0 @@
|
|||
.openerp .oe_login .oe_login_pane {
|
||||
height: 152px;
|
||||
}
|
||||
.openerp .oe_login .oe_login_pane ul.oe_login_switch a {
|
||||
color: #eeeeee;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.openerp .oe_login .oe_login_pane ul.oe_login_switch a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
|
@ -1,72 +1,30 @@
|
|||
openerp.auth_reset_password = function(instance) {
|
||||
var _t = instance.web._t;
|
||||
|
||||
instance.web.Login.include({
|
||||
start: function() {
|
||||
var $e = this.$el;
|
||||
$e.find('a.oe_login_switch').click(function() {
|
||||
$e.find('ul.oe_login_switch').toggle();
|
||||
var $m = $e.find('form input[name=is_reset_pw]');
|
||||
$m.attr('checked', !$m.is(':checked'));
|
||||
});
|
||||
this.$('a.oe_reset_password').click(this.do_reset_password);
|
||||
return this._super();
|
||||
},
|
||||
on_submit: function(ev) {
|
||||
if(ev) {
|
||||
do_reset_password: function(ev) {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
var $e = this.$el;
|
||||
var db = $e.find("form [name=db]").val();
|
||||
var db = this.$("form [name=db]").val();
|
||||
var login = this.$("form input[name=login]").val();
|
||||
if (!db) {
|
||||
this.do_warn(_t("Login"), _t("No database selected !"));
|
||||
this.do_warn("Login", "No database selected !");
|
||||
return false;
|
||||
} else if (!login) {
|
||||
this.do_warn("Login", "Please enter a username or email address.")
|
||||
return false;
|
||||
}
|
||||
var $m = $e.find('form input[name=is_reset_pw]');
|
||||
if ($m.is(':checked')) {
|
||||
var email = $e.find('form input[name=email]').val();
|
||||
return this.do_reset_password(db, email);
|
||||
} else {
|
||||
return this._super(ev);
|
||||
}
|
||||
},
|
||||
do_reset_password: function(db, email) {
|
||||
var self = this;
|
||||
instance.session.session_authenticate(db, 'anonymous', 'anonymous', true).pipe(function () {
|
||||
var func = new instance.web.Model("res.users").get_func("send_reset_password_request");
|
||||
return func(email).then(function(res) {
|
||||
// show message
|
||||
self.do_notify(_t('Reset Password'), _.str.sprintf(_t('We have sent an email to %s with further instructions'), email), true);
|
||||
}, function(error, event) {
|
||||
// no traceback please
|
||||
event.preventDefault();
|
||||
});
|
||||
}).fail(function(error, event) {
|
||||
// cannot log as anonymous or reset_password not installed
|
||||
self.do_warn(_t('Reset Password'), _.str.sprintf(_t('Reset Password functionnality is not available for database %s'), db), true);
|
||||
});
|
||||
var params = {
|
||||
dbname : db,
|
||||
login: login,
|
||||
};
|
||||
var url = "/auth_reset_password/reset_password?" + $.param(params);
|
||||
window.location = url;
|
||||
}
|
||||
});
|
||||
|
||||
instance.reset_password = {};
|
||||
instance.reset_password.ResetPassword = instance.web.Widget.extend({
|
||||
init: function(parent, params) {
|
||||
this._super(parent);
|
||||
this.token = (params && params.token) || false;
|
||||
},
|
||||
start: function() {
|
||||
this.do_action({
|
||||
name: 'Reset Password',
|
||||
type: 'ir.actions.act_window',
|
||||
context: {default_token: this.token},
|
||||
res_model: 'auth.reset_password',
|
||||
target: 'new',
|
||||
views: [[false, 'form']]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
instance.web.client_actions.add("reset_password", "instance.reset_password.ResetPassword");
|
||||
|
||||
|
||||
};
|
||||
|
|
|
@ -1,26 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- vim:fdl=1:
|
||||
-->
|
||||
<!-- vim:fdl=1: -->
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="Login">
|
||||
<t t-jquery="form ul:first">
|
||||
// addClass does not work :(
|
||||
this.attr('class', (this.attr('class') || '') + ' oe_login_switch');
|
||||
</t>
|
||||
<t t-jquery="form ul:first li:last" t-operation="after">
|
||||
<li>
|
||||
<a class="oe_login_switch" href="#">Forgot your password?</a>
|
||||
</li>
|
||||
</t>
|
||||
<t t-jquery="form ul:first" t-operation="after">
|
||||
<ul class="oe_login_switch" style="display:none;">
|
||||
<li style="display:none;"><input type="checkbox" name="is_reset_pw"/></li>
|
||||
<li>Email</li>
|
||||
<li><input type="email" name="email"/></li>
|
||||
<li><button name="submit">Reset Password</button></li>
|
||||
<li><a class="oe_login_switch" href="#">< Back</a></li>
|
||||
</ul>
|
||||
<li><a class="oe_reset_password" href="#">Reset password</a></li>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
|
@ -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'],
|
||||
}
|
||||
|
|
|
@ -1,35 +1,63 @@
|
|||
import logging
|
||||
|
||||
import werkzeug.urls
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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:
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
</div>
|
||||
<div attrs="{'invisible':[('auth_signup_uninvited','=',False)]}">
|
||||
<label for="auth_signup_template_user_id"/>
|
||||
<field name="auth_signup_template_user_id" class="oe_inline" domain="['|',('active','=',0),('active','=',1)]"/>
|
||||
<field name="auth_signup_template_user_id" class="oe_inline"
|
||||
attrs="{'required': [('auth_signup_uninvited', '=', True)]}"
|
||||
domain="['|',('active','=',0),('active','=',1)]"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
|
|
|
@ -1,47 +1,205 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="res_users_form_view" model="ir.ui.view">
|
||||
<field name="name">user.form.state</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet" position="before">
|
||||
<header>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -0,0 +1,3 @@
|
|||
base.css: base.sass
|
||||
sass --trace -t expanded base.sass 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;
|
||||
}
|
|
@ -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
|
|
@ -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");
|
||||
|
||||
};
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- vim:fdl=1:
|
||||
-->
|
||||
<!-- vim:fdl=1: -->
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="Login">
|
||||
<t t-jquery="form ul:first li:last" t-operation="after">
|
||||
<li>
|
||||
<a class="oe_signup" href="#">Sign Up</a>
|
||||
</li>
|
||||
<t t-extend="Login">
|
||||
<t t-jquery="form ul:first li:contains('Username')" t-operation="before">
|
||||
<li class="oe_signup_show">Name</li>
|
||||
<li class="oe_signup_show"><input name="name" type="text"/></li>
|
||||
</t>
|
||||
<t t-jquery="form ul:first li:contains('Username')" t-operation="replace">
|
||||
<li class="oe_signup_hide">Username</li>
|
||||
<li class="oe_signup_show">Username (Email)</li>
|
||||
</t>
|
||||
<t t-jquery="form ul:first li:has(input[name=password])" t-operation="after">
|
||||
<li class="oe_signup_show">Confirm Password</li>
|
||||
<li class="oe_signup_show"><input name="confirm_password" type="password"/></li>
|
||||
</t>
|
||||
<t t-jquery="form ul:first li:has(button[name=submit])" t-operation="replace">
|
||||
<li class="oe_signup_hide"><button name="submit">Log in</button></li>
|
||||
<li class="oe_signup_show"><button name="submit">Sign in</button></li>
|
||||
</t>
|
||||
<t t-jquery="form ul:first li:last" t-operation="after">
|
||||
<li><a class="oe_signup_hide oe_signup_signup" href="#">Sign Up</a></li>
|
||||
<li><a class="oe_signup_show oe_signup_back" href="#">Back to Login</a></li>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="auth_signup.signup">
|
||||
<div>
|
||||
<form>
|
||||
Name = <input type="text" name="name"/><br/>
|
||||
Email = <input type="email" name="email"/><br/>
|
||||
Password = <input type="password" name="password"/><br/>
|
||||
Confirmation = <input type="password" name="password_confirmation"/><br/>
|
||||
<button type="submit" disabled="disabled">Signup</button>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
|
||||
</templates>
|
||||
|
|
|
@ -280,7 +280,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Search Meetings">
|
||||
<field name="name" string="Meeting" filter_domain="[('name','ilike',self)]"/>
|
||||
<filter string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter string="My Meetings" help="My Meetings" domain="[('user_id','=',uid)]"/>
|
||||
<field name="user_id"/>
|
||||
|
|
|
@ -356,10 +356,10 @@
|
|||
<field name="name" string="Lead / Customer" filter_domain="['|','|',('partner_name','ilike',self),('email_from','ilike',self),('name','ilike',self)]"/>
|
||||
<field name="categ_ids" string="Category" filter_domain="[('categ_ids','ilike',self)]" />
|
||||
<field name="create_date"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter icon="terp-check" string="New" name="new" help="New Leads" domain="[('state','=','draft')]"/>
|
||||
<filter icon="terp-camera_test" string="Open" name="open" domain="[('state','=','open')]"/>
|
||||
<filter icon="terp-camera_test" string="In Progress" name="open" domain="[('state','=','open')]"/>
|
||||
<separator/>
|
||||
<filter string="Unassigned Leads" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Leads" />
|
||||
<separator/>
|
||||
|
@ -569,10 +569,10 @@
|
|||
<field name="name" string="Opportunity / Customer"
|
||||
filter_domain="['|','|','|',('partner_id','ilike',self),('partner_name','ilike',self),('email_from','ilike',self),('name', 'ilike', self)]"/>
|
||||
<field name="categ_ids" string="Category" filter_domain="[('categ_ids','ilike', self)]" />
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter icon="terp-check" string="New" help="New Opportunities" name="new" domain="[('state','=','draft')]"/>
|
||||
<filter icon="terp-camera_test" string="Open" help="Open Opportunities" name="open" domain="[('state','=','open')]"/>
|
||||
<filter icon="terp-camera_test" string="In Progress" help="Open Opportunities" name="open" domain="[('state','=','open')]"/>
|
||||
<separator/>
|
||||
<filter string="Unassigned Opportunities" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Opportunities" />
|
||||
<separator/>
|
||||
|
@ -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" />
|
||||
<field name="user_id"/>
|
||||
<field name="country_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="section_id" context="{'invisible_section': False, 'default_section_id': self}"/>
|
||||
<field name="partner_id"/>
|
||||
<group expand="0" string="Group By..." colspan="16">
|
||||
<filter string="Salesperson" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}" />
|
||||
<filter string="Team" help="Sales Team" icon="terp-personal+" domain="[]" context="{'group_by':'section_id'}"/>
|
||||
|
|
|
@ -15,31 +15,12 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partners_tree_crm2" model="ir.ui.view">
|
||||
<field name="name">view.res.partner.tree.crm.inherited2</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="phone" position="after">
|
||||
<field name="section_id" completion="1" invisible="context.get('invisible_section', True)"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partners_form_crm3" model="ir.ui.view">
|
||||
<field name="name">view.res.partner.search.crm.inherited3</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_res_partner_filter"/>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="category_id" position="after">
|
||||
<field name="section_id" completion="1"/>
|
||||
<field name="section_id" completion="1" widget="selection" context="{'invisible_section': False}"/>
|
||||
</field>
|
||||
<xpath expr="//field[@name='user_id']" position="after">
|
||||
<field name="country_id"/>
|
||||
</xpath>
|
||||
<xpath expr="//group[@string='Group By...']" position="after">
|
||||
<group string="Display">
|
||||
<filter string="Show Sales Team" context="{'invisible_section': False}"/>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<filter string="Assigned Partner" icon="terp-personal" domain="[]" context="{'group_by':'partner_assigned_id'}"/>
|
||||
</filter>
|
||||
|
||||
<field name="user_id" position="after">
|
||||
<field name="partner_id" position="after">
|
||||
<field name="partner_assigned_id"/>
|
||||
</field>
|
||||
</field>
|
||||
|
|
|
@ -339,7 +339,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Events">
|
||||
<field name="name" string="Events"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter icon="terp-check" string="Unconfirmed" name="draft" domain="[('state','=','draft')]" help="Events in New state"/>
|
||||
<filter icon="terp-camera_test" string="Confirmed" domain="[('state','=','confirm')]" help="Confirmed events"/>
|
||||
|
@ -529,7 +529,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Event Registration">
|
||||
<field name="name" string="Participant" filter_domain="['|','|','|',('name','ilike',self),('partner_id','ilike',self),('email','ilike',self),('origin','ilike',self)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter icon="terp-check" string="New" name="draft" domain="[('state','=','draft')]" help="Registrations in unconfirmed state"/>
|
||||
<filter icon="terp-camera_test" string="Confirmed" domain="[('state','=','open')]" help="Confirmed registrations"/>
|
||||
|
|
|
@ -112,8 +112,6 @@
|
|||
<search string="Employees">
|
||||
<field name="name" string="Employees"/>
|
||||
<field name="department_id" />
|
||||
<field name="job_id"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="category_ids"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Manager" icon="terp-personal" domain="[]" context="{'group_by':'parent_id'}"/>
|
||||
|
|
|
@ -155,11 +155,12 @@
|
|||
<field name="name" string="Expenses"/>
|
||||
<field name="date"/>
|
||||
<filter icon="terp-document-new" domain="[('state','=','draft')]" string="New" help="New Expense"/>
|
||||
<filter icon="terp-camera_test" domain="[('state','=','confirm')]" string="To Approve" help="Confirmed Expense"/>
|
||||
<filter icon="terp-camera_test" domain="[('state','=','confirm')]" string="To Approve" help="Confirmed Expenses"/>
|
||||
<filter icon="terp-dolar" domain="[('state','=','accepted')]" string="To Pay" help="Expenses to Invoice"/>
|
||||
<separator/>
|
||||
<filter domain="[('user_id', '=', uid)]" string="My Expenses"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id" string="Department" context="{'invisible_department': False}"/>
|
||||
<field name="user_id" string="User"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Employee" icon="terp-personal" domain="[]" context="{'group_by':'employee_id'}"/>
|
||||
<filter string="Department" icon="terp-personal+" domain="[]" context="{'group_by':'department_id'}"/>
|
||||
|
|
|
@ -208,7 +208,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Search Jobs">
|
||||
<field name="partner_name" filter_domain="['|','|',('name','ilike',self),('partner_name','ilike',self),('email_from','ilike',self)]" string="Subject / Applicant"/>
|
||||
<filter string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter string="New" domain="[('state','=','draft')]" help="All Initial Jobs"/>
|
||||
<filter string="In Progress" domain="[('state','=','open')]" help="Open Jobs"/>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<field name="name">Sheet 1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="employee_id" ref="hr.employee_fp" />
|
||||
<field eval="time.strftime('%Y-%m-%d')" name="date_current"/>
|
||||
</record>
|
||||
-->
|
||||
</data>
|
||||
|
|
|
@ -71,20 +71,13 @@
|
|||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Weekly">
|
||||
<widget type="weekly_timesheet">
|
||||
</widget>
|
||||
</page>
|
||||
<page string="Daily">
|
||||
<group>
|
||||
<div>
|
||||
<button name="button_dummy" class="oe_inline" string="Go to" type="object" icon="terp-gtk-jump-to-ltr"/> :
|
||||
<field name="date_current" class="oe_inline"/>
|
||||
</div>
|
||||
<div align="right">
|
||||
<button class="oe_inline" icon="terp-gtk-go-back-ltr" name="date_previous" string="" type="object"/>
|
||||
<button class="oe_inline" name="date_today" string="Today" type="object" icon="terp-go-today"/>
|
||||
<button class="oe_inline" icon="terp-gtk-go-back-rtl" name="date_next" string="" type="object"/>
|
||||
</div>
|
||||
</group>
|
||||
<group colspan="4" col="3">
|
||||
<field context="{'name':date_current,'user_id':user_id}" name="attendances_ids" nolabel="1" groups="base.group_hr_attendance">
|
||||
<field context="{'user_id':user_id}" name="attendances_ids" nolabel="1" groups="base.group_hr_attendance">
|
||||
<tree string="Attendances" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="action"/>
|
||||
|
@ -101,9 +94,9 @@
|
|||
<group col="4">
|
||||
<field name="state_attendance" groups="base.group_hr_attendance"/>
|
||||
</group>
|
||||
<field colspan="4" context="{'date':date_current,'user_id':user_id}" domain="[('name','=',date_current)]" name="timesheet_ids" nolabel="1">
|
||||
<field colspan="4" context="{'user_id':user_id}" name="timesheet_ids" nolabel="1">
|
||||
<tree editable="top" string="Timesheet Lines">
|
||||
<field invisible="1" name="date"/>
|
||||
<field name="date"/>
|
||||
<field domain="[('type','in',['normal', 'contract']), ('state', '<>', 'close'),('use_timesheets','=',1)]" name="account_id" on_change="on_change_account_id(account_id)" context="{'default_use_timesheets': 1}"/>
|
||||
<field name="name"/>
|
||||
<field name="unit_amount" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" widget="float_time"/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<field name="model">timesheet.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree colors="blue:state == 'draft';black:state in ('confirm','new');gray:state == 'cancel'" string="Timesheet">
|
||||
<field name="date_current" invisible="1"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="date_from" invisible="1"/>
|
||||
|
@ -56,7 +55,6 @@
|
|||
<field name="product_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="date_current"/>
|
||||
<field name="date_to"/>
|
||||
<field name="date_from"/>
|
||||
</group>
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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');
|
||||
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="hr_timesheet_sheet.WeeklyTimesheet">
|
||||
<div class="oe_timesheet_weekly">
|
||||
<table>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<t t-foreach="widget.dates" t-as="date">
|
||||
<th t-att-class="'oe_timesheet_weekly_date_head' + (Date.compare(date, Date.today()) === 0 ? ' oe_timesheet_weekly_today' : '')">
|
||||
<t t-esc="date.toString('ddd')"/><br />
|
||||
<t t-esc="date.toString('MMM d')"/>
|
||||
</th>
|
||||
</t>
|
||||
<th class="oe_timesheet_weekly_date_head">TOTAL</th>
|
||||
</tr>
|
||||
<tr t-foreach="widget.accounts" t-as="account">
|
||||
<td class="oe_timesheet_weekly_account"><t t-esc="widget.account_names[account.account]"/></td>
|
||||
<t t-set="day_count" t-value="0"/>
|
||||
<t t-foreach="account.days" t-as="day">
|
||||
<td t-att-class="(Date.compare(day.day, Date.today()) === 0 ? 'oe_timesheet_weekly_today' : '')">
|
||||
<input t-if="!widget.get('effective_readonly')" class="oe_timesheet_weekly_input" t-att-data-account="account.account"
|
||||
t-att-data-day-count="day_count" type="text"/>
|
||||
<span t-if="widget.get('effective_readonly')" t-att-data-account="account.account"
|
||||
t-att-data-day-count="day_count" class="oe_timesheet_weekly_box"/>
|
||||
<t t-set="day_count" t-value="day_count + 1"/>
|
||||
</td>
|
||||
</t>
|
||||
<td t-att-data-account-total="account.account"> </td>
|
||||
</tr>
|
||||
<tr class="oe_timesheet_weekly_add_row" style="display: none">
|
||||
<td t-att-colspan="widget.dates.length + 2">
|
||||
<button>Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="oe_timesheet_weekly_adding_tot">
|
||||
<div class="oe_timesheet_weekly_adding"><button>Add Row</button></div>
|
||||
<div class="oe_timesheet_weekly_tottot"><span>TOTAL</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<t t-set="day_count" t-value="0"/>
|
||||
<t t-foreach="widget.dates" t-as="date">
|
||||
<td t-att-class="(Date.compare(date, Date.today()) === 0 ? 'oe_timesheet_weekly_today' : '')">
|
||||
<span class="oe_timesheet_weekly_box" t-att-data-day-total="day_count">
|
||||
</span>
|
||||
<t t-set="day_count" t-value="day_count + 1"/>
|
||||
</td>
|
||||
</t>
|
||||
<td class="oe_timesheet_weekly_supertotal"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
|
@ -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".
|
||||
-
|
||||
|
|
|
@ -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)]"
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<field name="name">Mail.group: access only public and joined groups</field>
|
||||
<field name="model_id" ref="model_mail_group"/>
|
||||
<!-- This rule has to be improved for employee only groups -->
|
||||
<field name="domain_force">['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.id]), '&', ('public','=','groups'), ('group_public_id','in', [x.id for x in user.groups_id])]</field>
|
||||
<field name="domain_force">['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.partner_id.id]), '&', ('public','=','groups'), ('group_public_id','in', [g.id for g in user.groups_id])]</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
|
|
|
@ -149,7 +149,6 @@ openerp_mail_followers = function(session, mail) {
|
|||
var self = this;
|
||||
var node_user_list = this.$('ul.oe_mail_followers_display').empty();
|
||||
this.$('div.oe_mail_recthread_followers h4').html(this.options.title + (records.length>=5 ? ' (' + records.length + ')' : '') );
|
||||
console.log(records);
|
||||
for(var i=0; i<records.length&&i<5; i++) {
|
||||
var record=records[i];
|
||||
record.avatar_url = mail.ChatterUtils.get_image(self.session, 'res.partner', 'image_small', record.id);
|
||||
|
|
|
@ -194,7 +194,7 @@ class test_mail(TestMailMockups):
|
|||
'plaintext mail incorrectly parsed')
|
||||
|
||||
def test_10_many2many_reference_field(self):
|
||||
""" Tests designed for the many2many_reference field (follower_ids).
|
||||
""" Tests designed for the many2many function field 'follower_ids'.
|
||||
We will test to perform writes using the many2many commands 0, 3, 4,
|
||||
5 and 6. """
|
||||
cr, uid = self.cr, self.uid
|
||||
|
|
|
@ -8,19 +8,19 @@ msgstr ""
|
|||
"Project-Id-Version: openobject-addons\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2012-02-08 00:36+0000\n"
|
||||
"PO-Revision-Date: 2012-01-12 05:49+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2012-10-08 15:06+0000\n"
|
||||
"Last-Translator: kifcaliph <Unknown>\n"
|
||||
"Language-Team: Arabic <ar@li.org>\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:38+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: marketing_campaign
|
||||
#: view:marketing.campaign:0
|
||||
msgid "Manual Mode"
|
||||
msgstr ""
|
||||
msgstr "النمط اليدوي"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign.transition,activity_from_id:0
|
||||
|
@ -31,12 +31,12 @@ msgstr "النشاط السابق"
|
|||
#: code:addons/marketing_campaign/marketing_campaign.py:818
|
||||
#, python-format
|
||||
msgid "The current step for this item has no email or report to preview."
|
||||
msgstr ""
|
||||
msgstr "الخطوة الحالية لهذا الصنف لا تملك بريد إلكتروني أو تقرير للمعاينة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: constraint:marketing.campaign.transition:0
|
||||
msgid "The To/From Activity of transition must be of the same Campaign "
|
||||
msgstr ""
|
||||
msgstr "الإنتقال من / إلى النشاط يجب أن يكون من نفس الحملة "
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: selection:marketing.campaign.transition,trigger:0
|
||||
|
@ -46,7 +46,7 @@ msgstr "الوقت"
|
|||
#. module: marketing_campaign
|
||||
#: selection:marketing.campaign.activity,type:0
|
||||
msgid "Custom Action"
|
||||
msgstr ""
|
||||
msgstr "تخصيص إجراء"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:campaign.analysis:0
|
||||
|
@ -63,11 +63,13 @@ msgid ""
|
|||
"reached this point has generated a certain revenue. You can get revenue "
|
||||
"statistics in the Reporting section"
|
||||
msgstr ""
|
||||
"تعيين الإيرادات المتوقعة إذا كنت ترى أن كل بند من بنود الحملة وصلت الى تلك "
|
||||
"النقطة وولدت عائد معين. يمكنك الحصول على إحصاءات الإيرادات في قسم التقارير"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign.transition,trigger:0
|
||||
msgid "Trigger"
|
||||
msgstr ""
|
||||
msgstr "زر"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: field:campaign.analysis,count:0
|
||||
|
@ -77,7 +79,7 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign:0
|
||||
msgid "Campaign Editor"
|
||||
msgstr ""
|
||||
msgstr "محرر الحملة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:campaign.analysis:0
|
||||
|
@ -106,13 +108,13 @@ msgstr "كائن"
|
|||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.segment:0
|
||||
msgid "Sync mode: only records created after last sync"
|
||||
msgstr ""
|
||||
msgstr "نمط المزامنة: السجلات فقط التي تم إنشاؤها بعد آخر المزامنة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: model:email.template,body_text:marketing_campaign.email_template_2
|
||||
msgid ""
|
||||
"Hello, We are happy to announce that you now become our Silver Partner."
|
||||
msgstr ""
|
||||
msgstr "مرحبا، ونحن سعداء أن نعلن أنك أصبحت الآن شريكاً فضياً لدينا."
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign:0
|
||||
|
@ -123,7 +125,7 @@ msgstr "حفظ كمسودة"
|
|||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign.activity,to_ids:0
|
||||
msgid "Next Activities"
|
||||
msgstr ""
|
||||
msgstr "النشاطات التالية"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.segment:0
|
||||
|
@ -133,7 +135,7 @@ msgstr "مزامنة"
|
|||
#. module: marketing_campaign
|
||||
#: sql_constraint:marketing.campaign.transition:0
|
||||
msgid "The interval must be positive or zero"
|
||||
msgstr ""
|
||||
msgstr "الفترة يجب أن تكون إيجابية أو صفر"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: code:addons/marketing_campaign/marketing_campaign.py:818
|
||||
|
@ -145,7 +147,7 @@ msgstr "لا معاينة"
|
|||
#: view:marketing.campaign.segment:0
|
||||
#: field:marketing.campaign.segment,date_run:0
|
||||
msgid "Launch Date"
|
||||
msgstr ""
|
||||
msgstr "تاريخ الإنشاء"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:campaign.analysis:0
|
||||
|
@ -156,7 +158,7 @@ msgstr "يوم"
|
|||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.activity:0
|
||||
msgid "Outgoing Transitions"
|
||||
msgstr ""
|
||||
msgstr "التنقلات الصادرة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.workitem:0
|
||||
|
@ -166,18 +168,18 @@ msgstr "إستعادة"
|
|||
#. module: marketing_campaign
|
||||
#: help:marketing.campaign,object_id:0
|
||||
msgid "Choose the resource on which you want this campaign to be run"
|
||||
msgstr ""
|
||||
msgstr "اختيار المورد الذي تريده ليتم تشغيله لهذه الحملة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign.segment,sync_last_date:0
|
||||
msgid "Last Synchronization"
|
||||
msgstr ""
|
||||
msgstr "التزامن الأخير"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: code:addons/marketing_campaign/marketing_campaign.py:214
|
||||
#, python-format
|
||||
msgid "You can not duplicate a campaign, it's not supported yet."
|
||||
msgstr ""
|
||||
msgstr "لا يمكنك تكرار الحملة، انها غير مدعومة حتى الآن."
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: selection:marketing.campaign.transition,interval_type:0
|
||||
|
@ -189,7 +191,7 @@ msgstr "سنة/سنين"
|
|||
msgid ""
|
||||
"Date on which this segment was synchronized last time (automatically or "
|
||||
"manually)"
|
||||
msgstr ""
|
||||
msgstr "التاريخ الذي تزامن هذا القطاع آخر مرة (آليا أو يدويا)"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: selection:campaign.analysis,state:0
|
||||
|
@ -216,11 +218,19 @@ msgid ""
|
|||
"Normal - the campaign runs normally and automatically sends all emails and "
|
||||
"reports (be very careful with this mode, you're live!)"
|
||||
msgstr ""
|
||||
"اختبار - وهو يخلق ويعالج كافة الأنشطة مباشرة (دون انتظار تأخير على التحولات) "
|
||||
"ولكن لا يرسل رسائل البريد الإلكتروني أو يعد التقارير.\n"
|
||||
" اختبار في الوقت الحقيقي - وهو يخلق ويعالج جميع الأنشطة بشكل مباشر ولكن لا "
|
||||
"يرسل رسائل البريد الإلكتروني أو يعد التقارير.\n"
|
||||
" مع التأكيد اليدوي - يعمل الحملات عادة، ولكن المستخدم لديه للتحقق من صحة كل "
|
||||
"بنود العمل يدويا.\n"
|
||||
" طبيعي - يعمل الحملة بشكل طبيعي يرسل تلقائيا جميع رسائل البريد الإلكتروني "
|
||||
"والتقارير (نكون حذرين للغاية مع هذا الوضع، أنت على الهواء!)"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: help:marketing.campaign.segment,date_run:0
|
||||
msgid "Initial start date of this segment."
|
||||
msgstr ""
|
||||
msgstr "تاريخ بدء الأولي لهذا القطاع."
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:campaign.analysis:0
|
||||
|
@ -237,7 +247,7 @@ msgstr "حملة"
|
|||
#. module: marketing_campaign
|
||||
#: model:email.template,subject:marketing_campaign.email_template_3
|
||||
msgid "Congratulation! You become our Gold Partner."
|
||||
msgstr ""
|
||||
msgstr "تهنئة! لقد أصبحت شريكاً ذهبياً الآن."
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:campaign.analysis:0
|
||||
|
@ -251,7 +261,7 @@ msgstr "قطعة"
|
|||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.activity:0
|
||||
msgid "Cost / Revenue"
|
||||
msgstr ""
|
||||
msgstr "التكلفة/العائد"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: help:marketing.campaign.activity,type:0
|
||||
|
@ -313,7 +323,7 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: view:campaign.analysis:0
|
||||
msgid "Marketing Reports"
|
||||
msgstr ""
|
||||
msgstr "تقارير التسويق"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: selection:marketing.campaign,state:0
|
||||
|
@ -342,7 +352,7 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign.segment,sync_mode:0
|
||||
msgid "Synchronization mode"
|
||||
msgstr ""
|
||||
msgstr "نمط التزامن"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign:0
|
||||
|
@ -353,7 +363,7 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign.activity,from_ids:0
|
||||
msgid "Previous Activities"
|
||||
msgstr ""
|
||||
msgstr "النشاطات السابقة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: help:marketing.campaign.segment,date_done:0
|
||||
|
@ -363,7 +373,7 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.workitem:0
|
||||
msgid "Marketing Campaign Activities"
|
||||
msgstr ""
|
||||
msgstr "نشاطات حملة التسويق"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.workitem:0
|
||||
|
@ -427,7 +437,7 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: model:ir.model,name:marketing_campaign.model_marketing_campaign_segment
|
||||
msgid "Campaign Segment"
|
||||
msgstr ""
|
||||
msgstr "حملة القطاع"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign:0
|
||||
|
@ -456,7 +466,7 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign,fixed_cost:0
|
||||
msgid "Fixed Cost"
|
||||
msgstr ""
|
||||
msgstr "التكلفة الثابتة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: model:email.template,subject:marketing_campaign.email_template_2
|
||||
|
@ -466,12 +476,12 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign.segment:0
|
||||
msgid "Newly Modified"
|
||||
msgstr ""
|
||||
msgstr "تم التعديل حديثاً"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: field:marketing.campaign.transition,interval_nbr:0
|
||||
msgid "Interval Value"
|
||||
msgstr ""
|
||||
msgstr "قيمة الفترة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: field:campaign.analysis,revenue:0
|
||||
|
@ -512,22 +522,22 @@ msgstr ""
|
|||
#. module: marketing_campaign
|
||||
#: model:ir.actions.act_window,name:marketing_campaign.act_marketing_campaing_followup
|
||||
msgid "Campaign Follow-up"
|
||||
msgstr ""
|
||||
msgstr "متابعة الحملة"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: help:marketing.campaign.activity,email_template_id:0
|
||||
msgid "The e-mail to send when this activity is activated"
|
||||
msgstr ""
|
||||
msgstr "بريد إلكتروني للإرسال عندما يتم تنشيط المنشط"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: view:marketing.campaign:0
|
||||
msgid "Test Mode"
|
||||
msgstr ""
|
||||
msgstr "نمط الإختبار"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: selection:marketing.campaign.segment,sync_mode:0
|
||||
msgid "Only records modified after last sync (no duplicates)"
|
||||
msgstr ""
|
||||
msgstr "فقط السجلات المعدلة بعد آخر مزامنة (بدون تكرار)"
|
||||
|
||||
#. module: marketing_campaign
|
||||
#: model:ir.model,name:marketing_campaign.model_ir_actions_report_xml
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
'depends': [
|
||||
'base',
|
||||
'share',
|
||||
'auth_anonymous',
|
||||
'auth_signup',
|
||||
],
|
||||
'author': 'OpenERP SA',
|
||||
|
|
|
@ -19,34 +19,22 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import tools
|
||||
from osv import osv
|
||||
import tools
|
||||
|
||||
|
||||
class mail_mail_portal(osv.Model):
|
||||
""" Update of mail_mail class, to add the signin URL to notifications.
|
||||
"""
|
||||
_name = 'mail.mail'
|
||||
_inherit = ['mail.mail']
|
||||
|
||||
def _generate_signin_url(self, cr, uid, partner_id, portal_group_id, key, context=None):
|
||||
""" Generate the signin url """
|
||||
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='', context=context)
|
||||
return base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (partner_id, portal_group_id, key)
|
||||
class mail_mail(osv.Model):
|
||||
""" Update of mail_mail class, to add the signin URL to notifications. """
|
||||
_inherit = 'mail.mail'
|
||||
|
||||
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
|
||||
""" Return a specific ir_email body. The main purpose of this method
|
||||
is to be inherited by Portal, to add a link for signing in, in
|
||||
each notification email a partner receives.
|
||||
|
||||
""" add a signin link inside the body of a mail.mail
|
||||
:param mail: mail.mail browse_record
|
||||
:param partner: browse_record of the specific recipient partner
|
||||
:return: the resulting body_html
|
||||
"""
|
||||
body = super(mail_mail, self).send_get_mail_body(cr, uid, mail, partner, context=context)
|
||||
if partner:
|
||||
portal_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_portal')
|
||||
portal_id = portal_ref and portal_ref[1] or False
|
||||
url = self._generate_signin_url(cr, uid, partner.id, portal_id, 1234, context=context)
|
||||
body = tools.append_content_to_html(mail.body_html, url)
|
||||
return body
|
||||
else:
|
||||
return super(mail_mail_portal, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context)
|
||||
context = dict(context or {}, signup_valid=True)
|
||||
partner = self.pool.get('res.partner').browse(cr, uid, partner.id, context)
|
||||
body = tools.append_content_to_html(body, "Log in our portal at: %s" % partner.signup_url)
|
||||
return body
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<!-- Top menu item -->
|
||||
<menuitem name="Portal"
|
||||
id="portal_menu"
|
||||
groups="base.group_no_one,portal.group_portal,auth_anonymous.group_anonymous"
|
||||
groups="base.group_no_one,portal.group_portal"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem name="Our company" id="portal_company" parent="portal_menu" sequence="10"/>
|
||||
|
|
|
@ -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('<div>You have been invited to follow Pigs.</div>', 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')
|
||||
|
|
|
@ -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': '<pre>%s</pre>' % body,
|
||||
'subject': _(WELCOME_EMAIL_SUBJECT) % data,
|
||||
'body_html': '<pre>%s</pre>' % (_(WELCOME_EMAIL_BODY) % data),
|
||||
'state': 'outgoing',
|
||||
}
|
||||
return mail_mail.create(cr, uid, mail_values, context=this_context)
|
||||
|
|
|
@ -27,8 +27,6 @@
|
|||
<field name="user_ids"/>
|
||||
<field name="welcome_message"
|
||||
placeholder="This text is included in the email sent to new portal users."/>
|
||||
<field name="goodbye_message"
|
||||
placeholder="This text is included in the email sent to users withdrawn from the portal."/>
|
||||
<footer>
|
||||
<button string="Apply" name="action_apply" type="object" class="oe_highlight"/>
|
||||
or
|
||||
|
|
|
@ -239,9 +239,7 @@ instance.web.ViewManager.include({
|
|||
title: _t('Process')
|
||||
});
|
||||
var form_controller = pop.view_form;
|
||||
pop.on_write_completed.add_last(function() {
|
||||
self.initialize_process_view();
|
||||
});
|
||||
pop.on('on_write_complete', self, self.initialize_process_view);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -163,7 +163,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Search Project">
|
||||
<field name="complete_name" string="Project Name"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter icon="terp-check" string="Open" name="Current" domain="[('state', '=','open')]" help="Open Projects"/>
|
||||
<filter icon="gtk-media-pause" string="Pending" name="Pending" domain="[('state', '=','pending')]" help="Pending Projects"/>
|
||||
|
@ -619,7 +619,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Tasks">
|
||||
<field name="name" string="Tasks"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter name="draft" string="New" domain="[('state','=','draft')]" help="New Tasks" icon="terp-check"/>
|
||||
<filter name="open" string="In Progress" domain="[('state','=','open')]" help="In Progress Tasks" icon="terp-camera_test"/>
|
||||
|
|
|
@ -198,7 +198,7 @@
|
|||
<search string="Issue Tracker Search">
|
||||
<field name="name" string="Issue" filter_domain="['|', '|',('partner_id','ilike',self),('email_from','ilike',self),('name','ilike',self)]"/>
|
||||
<field name="id"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter string="New" icon="terp-document-new" domain="[('state','=','draft')]" help="New Issues"/>
|
||||
<filter string="To Do" domain="[('state','=','open')]" help="To Do Issues" icon="terp-check"/>
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
Create a timesheet sheet for HR manager
|
||||
-
|
||||
!record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_sheetforhrmanager0}:
|
||||
date_current: !eval time.strftime('%Y-05-%d')
|
||||
date_from: !eval "'%s-05-01' %(datetime.now().year)"
|
||||
date_to: !eval "'%s-05-31' %(datetime.now().year)"
|
||||
name: Sheet for hr manager
|
||||
|
|
|
@ -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 01:37+0100\n"
|
||||
"PO-Revision-Date: 2012-02-08 02:55+0000\n"
|
||||
"PO-Revision-Date: 2012-10-08 16:00+0000\n"
|
||||
"Last-Translator: kifcaliph <Unknown>\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-09-07 04:56+0000\n"
|
||||
"X-Generator: Launchpad (build 15914)\n"
|
||||
"X-Launchpad-Export-Date: 2012-10-09 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build 16112)\n"
|
||||
|
||||
#. module: purchase
|
||||
#: model:process.transition,note:purchase.process_transition_confirmingpurchaseorder0
|
||||
|
@ -45,7 +45,7 @@ msgstr "المقصد"
|
|||
#: code:addons/purchase/purchase.py:236
|
||||
#, python-format
|
||||
msgid "In order to delete a purchase order, it must be cancelled first!"
|
||||
msgstr ""
|
||||
msgstr "من أجل حذف أمر الشراء، يجب الإلغاء أولاً!"
|
||||
|
||||
#. module: purchase
|
||||
#: help:purchase.report,date:0
|
||||
|
@ -87,7 +87,7 @@ msgstr ""
|
|||
#. module: purchase
|
||||
#: view:purchase.order:0
|
||||
msgid "Approved purchase order"
|
||||
msgstr ""
|
||||
msgstr "الموافقة على أمر الشراء"
|
||||
|
||||
#. module: purchase
|
||||
#: view:purchase.order:0 field:purchase.order,partner_id:0
|
||||
|
@ -149,7 +149,7 @@ msgstr "لا يوجد قائمة اسعار !"
|
|||
#. module: purchase
|
||||
#: model:ir.model,name:purchase.model_purchase_config_wizard
|
||||
msgid "purchase.config.wizard"
|
||||
msgstr ""
|
||||
msgstr "شراء.ضبط.صندوق حوار"
|
||||
|
||||
#. module: purchase
|
||||
#: view:board.board:0 model:ir.actions.act_window,name:purchase.purchase_draft
|
||||
|
@ -249,7 +249,7 @@ msgstr "يوم"
|
|||
#. module: purchase
|
||||
#: selection:purchase.order,invoice_method:0
|
||||
msgid "Based on generated draft invoice"
|
||||
msgstr ""
|
||||
msgstr "على أساس توليد مسودة فاتورة"
|
||||
|
||||
#. module: purchase
|
||||
#: view:purchase.report:0
|
||||
|
@ -259,7 +259,7 @@ msgstr "ترتيب اليوم"
|
|||
#. module: purchase
|
||||
#: view:board.board:0
|
||||
msgid "Monthly Purchases by Category"
|
||||
msgstr ""
|
||||
msgstr "مشتريات شهرية حسب الفئة"
|
||||
|
||||
#. module: purchase
|
||||
#: model:ir.actions.act_window,name:purchase.action_purchase_line_product_tree
|
||||
|
@ -269,7 +269,7 @@ msgstr "المشتريات"
|
|||
#. module: purchase
|
||||
#: view:purchase.order:0
|
||||
msgid "Purchase order which are in draft state"
|
||||
msgstr ""
|
||||
msgstr "أمر الشراء التي هي في حالة مسودة"
|
||||
|
||||
#. module: purchase
|
||||
#: view:purchase.order:0
|
||||
|
@ -392,7 +392,7 @@ msgstr "حركة مخزن"
|
|||
#: code:addons/purchase/purchase.py:419
|
||||
#, python-format
|
||||
msgid "You must first cancel all invoices related to this purchase order."
|
||||
msgstr ""
|
||||
msgstr "يجب عليك أولا إلغاء جميع الفواتير المتعلقة بهذا أمر الشراء."
|
||||
|
||||
#. module: purchase
|
||||
#: field:purchase.report,dest_address_id:0
|
||||
|
@ -436,13 +436,13 @@ msgstr "تم التحقق من الصلاحية عن طريق"
|
|||
#. module: purchase
|
||||
#: view:purchase.report:0
|
||||
msgid "Order in last month"
|
||||
msgstr ""
|
||||
msgstr "طلب في الشهر الماضي"
|
||||
|
||||
#. module: purchase
|
||||
#: code:addons/purchase/purchase.py:412
|
||||
#, python-format
|
||||
msgid "You must first cancel all receptions related to this purchase order."
|
||||
msgstr ""
|
||||
msgstr "يجب عليك أولا إلغاء جميع حفلات الاستقبال المتعلقة بهذا أمر الشراء."
|
||||
|
||||
#. module: purchase
|
||||
#: selection:purchase.order.line,state:0
|
||||
|
@ -467,7 +467,7 @@ msgstr "توضح انه قد تم عمل الاختيار"
|
|||
#. module: purchase
|
||||
#: view:purchase.order:0
|
||||
msgid "Purchase orders which are in exception state"
|
||||
msgstr ""
|
||||
msgstr "أوامر الشراء التي هي في حالة الاستثناء"
|
||||
|
||||
#. module: purchase
|
||||
#: report:purchase.order:0 field:purchase.report,validator:0
|
||||
|
@ -506,7 +506,7 @@ msgstr "تأكيد"
|
|||
#: model:ir.ui.menu,name:purchase.menu_action_picking_tree4_picking_to_invoice
|
||||
#: selection:purchase.order,invoice_method:0
|
||||
msgid "Based on receptions"
|
||||
msgstr ""
|
||||
msgstr "استناداً على ما تم استقباله"
|
||||
|
||||
#. module: purchase
|
||||
#: constraint:res.company:0
|
||||
|
@ -541,7 +541,7 @@ msgstr ""
|
|||
#. module: purchase
|
||||
#: view:purchase.order:0
|
||||
msgid "Purchase order which are in the exception state"
|
||||
msgstr ""
|
||||
msgstr "أمر الشراء التي هي في حالة استثناء"
|
||||
|
||||
#. module: purchase
|
||||
#: model:ir.actions.act_window,help:purchase.action_stock_move_report_po
|
||||
|
@ -597,7 +597,7 @@ msgstr "السعر الإجمالي"
|
|||
#. module: purchase
|
||||
#: model:ir.actions.act_window,name:purchase.action_import_create_supplier_installer
|
||||
msgid "Create or Import Suppliers"
|
||||
msgstr ""
|
||||
msgstr "إنشاء أو استيراد الموردون"
|
||||
|
||||
#. module: purchase
|
||||
#: view:stock.picking:0
|
||||
|
@ -662,7 +662,7 @@ msgstr ""
|
|||
#. module: purchase
|
||||
#: report:purchase.order:0
|
||||
msgid "Purchase Order Confirmation N°"
|
||||
msgstr ""
|
||||
msgstr "تأكيد أمر الشراء ن°"
|
||||
|
||||
#. module: purchase
|
||||
#: model:ir.actions.act_window,help:purchase.action_purchase_order_report_all
|
||||
|
@ -748,7 +748,7 @@ msgstr "الاستقبالات"
|
|||
#: code:addons/purchase/purchase.py:285
|
||||
#, python-format
|
||||
msgid "You cannot confirm a purchase order without any lines."
|
||||
msgstr ""
|
||||
msgstr "لا يمكنك تأكيد أمر الشراء دون أية أسطر."
|
||||
|
||||
#. module: purchase
|
||||
#: model:ir.actions.act_window,help:purchase.action_invoice_pending
|
||||
|
@ -784,7 +784,7 @@ msgstr "يناير"
|
|||
#. module: purchase
|
||||
#: model:ir.actions.server,name:purchase.ir_actions_server_edi_purchase
|
||||
msgid "Auto-email confirmed purchase orders"
|
||||
msgstr ""
|
||||
msgstr "البريد التلقائي لتأكيد طلبات الشراء"
|
||||
|
||||
#. module: purchase
|
||||
#: model:process.transition,name:purchase.process_transition_approvingpurchaseorder0
|
||||
|
@ -840,7 +840,7 @@ msgstr "دمج امر الشراء"
|
|||
#. module: purchase
|
||||
#: view:purchase.report:0
|
||||
msgid "Order in current month"
|
||||
msgstr ""
|
||||
msgstr "طلب في الشهر الحالي"
|
||||
|
||||
#. module: purchase
|
||||
#: view:purchase.report:0 field:purchase.report,delay_pass:0
|
||||
|
@ -881,7 +881,7 @@ msgstr "خطوط الاوامر الكلية للمستخدم لكل شهر"
|
|||
#. module: purchase
|
||||
#: view:purchase.order:0
|
||||
msgid "Approved purchase orders"
|
||||
msgstr ""
|
||||
msgstr "تأكيد أوامر الشراء"
|
||||
|
||||
#. module: purchase
|
||||
#: view:purchase.report:0 field:purchase.report,month:0
|
||||
|
@ -891,7 +891,7 @@ msgstr "شهر"
|
|||
#. module: purchase
|
||||
#: model:email.template,subject:purchase.email_template_edi_purchase
|
||||
msgid "${object.company_id.name} Order (Ref ${object.name or 'n/a' })"
|
||||
msgstr ""
|
||||
msgstr "${object.company_id.name} أمر (Ref ${object.name or 'n/a' })"
|
||||
|
||||
#. module: purchase
|
||||
#: report:purchase.quotation:0
|
||||
|
@ -932,7 +932,7 @@ msgstr "ـكون هذه القائمة المختارة التي تم جمعها
|
|||
#. module: purchase
|
||||
#: view:stock.picking:0
|
||||
msgid "Is a Back Order"
|
||||
msgstr ""
|
||||
msgstr "طلب عودة"
|
||||
|
||||
#. module: purchase
|
||||
#: model:process.node,note:purchase.process_node_invoiceafterpacking0
|
||||
|
@ -984,7 +984,7 @@ msgstr ""
|
|||
#. module: purchase
|
||||
#: selection:purchase.config.wizard,default_method:0
|
||||
msgid "Pre-Generate Draft Invoices based on Purchase Orders"
|
||||
msgstr ""
|
||||
msgstr "قبل انشاء مسودة فاتورة على طلبات الشراء"
|
||||
|
||||
#. module: purchase
|
||||
#: model:ir.actions.act_window,name:purchase.action_view_purchase_line_invoice
|
||||
|
@ -1010,7 +1010,7 @@ msgstr "عرض النتيجة"
|
|||
#. module: purchase
|
||||
#: selection:purchase.config.wizard,default_method:0
|
||||
msgid "Based on Purchase Order Lines"
|
||||
msgstr ""
|
||||
msgstr "اعتماداً على سطور طلب الشراء"
|
||||
|
||||
#. module: purchase
|
||||
#: help:purchase.order,amount_untaxed:0
|
||||
|
@ -1130,7 +1130,7 @@ msgstr "مرشحات مفصلة..."
|
|||
#. module: purchase
|
||||
#: view:purchase.config.wizard:0
|
||||
msgid "Invoicing Control on Purchases"
|
||||
msgstr ""
|
||||
msgstr "التحكم في الفاتورة عند الشراء"
|
||||
|
||||
#. module: purchase
|
||||
#: code:addons/purchase/wizard/purchase_order_group.py:48
|
||||
|
|
|
@ -323,7 +323,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Search Purchase Order">
|
||||
<field name="name" string="Reference"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter icon="terp-document-new" name="draft" string="Quotations" domain="[('state','=','draft')]" help="Purchase orders which are in draft state"/>
|
||||
<filter icon="terp-check" name="approved" string="Purchase Orders" domain="[('state','not in',('draft','cancel'))]" help="Approved purchase orders"/>
|
||||
|
|
|
@ -793,6 +793,7 @@ class sale_order_line(osv.osv):
|
|||
_('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
|
||||
res = {
|
||||
'name': line.name,
|
||||
'sequence': line.sequence,
|
||||
'origin': line.order_id.name,
|
||||
'account_id': account_id,
|
||||
'price_unit': pu,
|
||||
|
|
|
@ -337,7 +337,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Search Sales Order">
|
||||
<field name="name" string="Sales Order" filter_domain="['|',('name','ilike',self),('client_order_ref','ilike',self)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Inbox" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<filter icon="terp-mail-message-new" string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter icon="terp-document-new" string="Quotations" name="draft" domain="[('state','in',('draft','sent'))]" help="Sales Order that haven't yet been confirmed"/>
|
||||
<filter icon="terp-check" string="Sales" name="sales" domain="[('state','in',('manual','progress'))]"/>
|
||||
|
|
|
@ -8,14 +8,14 @@ msgstr ""
|
|||
"Project-Id-Version: openobject-addons\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2012-02-08 01:37+0100\n"
|
||||
"PO-Revision-Date: 2012-01-13 21:14+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2012-10-08 15:06+0000\n"
|
||||
"Last-Translator: almodhesh <Unknown>\n"
|
||||
"Language-Team: Arabic <ar@li.org>\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:34+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: share
|
||||
#: field:share.wizard,embed_option_title:0
|
||||
|
@ -25,7 +25,7 @@ msgstr "عرض العنوان"
|
|||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
msgid "Access granted!"
|
||||
msgstr ""
|
||||
msgstr "منح الوصول!"
|
||||
|
||||
#. module: share
|
||||
#: field:share.wizard,user_type:0
|
||||
|
@ -50,13 +50,15 @@ msgstr "مشاركة"
|
|||
#. module: share
|
||||
#: field:share.wizard,share_root_url:0
|
||||
msgid "Share Access URL"
|
||||
msgstr ""
|
||||
msgstr "مشاركة الوصول URL"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:782
|
||||
#, python-format
|
||||
msgid "You may use your current login (%s) and password to view them.\n"
|
||||
msgstr ""
|
||||
"يمكنك استخدام معلومات تسجيل الدخول الخاصة بك الحالية(%s) وكلمة المرور "
|
||||
"لعرضها.\n"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:601
|
||||
|
@ -83,24 +85,25 @@ msgstr ""
|
|||
#. module: share
|
||||
#: field:share.wizard,embed_url:0 field:share.wizard.result.line,share_url:0
|
||||
msgid "Share URL"
|
||||
msgstr ""
|
||||
msgstr "مشاركة URL"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:776
|
||||
#, python-format
|
||||
msgid "These are your credentials to access this protected area:\n"
|
||||
msgstr ""
|
||||
msgstr "هذا هو اعتمادك للوصول إلى هذه المناطق المحمية\n"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:643
|
||||
#, python-format
|
||||
msgid "You must be a member of the Share/User group to use the share wizard"
|
||||
msgstr ""
|
||||
"يجب أن تكون عضوا في المجموعة مشاركة / المستخدم لاستخدام مشاركة صندوق الحوار"
|
||||
|
||||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
msgid "Access info"
|
||||
msgstr ""
|
||||
msgstr "معلومات الولوج"
|
||||
|
||||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
|
@ -111,12 +114,12 @@ msgstr "حصة"
|
|||
#: code:addons/share/wizard/share_wizard.py:551
|
||||
#, python-format
|
||||
msgid "(Duplicated for modified sharing permissions)"
|
||||
msgstr ""
|
||||
msgstr "(تكرار للحصول على أذونات مشاركة تعديل)"
|
||||
|
||||
#. module: share
|
||||
#: help:share.wizard,domain:0
|
||||
msgid "Optional domain for further data filtering"
|
||||
msgstr ""
|
||||
msgstr "المجال اختياري لمزيد من بيانات التصفية"
|
||||
|
||||
#. module: share
|
||||
#: sql_constraint:res.users:0
|
||||
|
@ -159,7 +162,7 @@ msgstr "إغلاق"
|
|||
#: code:addons/share/wizard/share_wizard.py:640
|
||||
#, python-format
|
||||
msgid "Action and Access Mode are required to create a shared access"
|
||||
msgstr ""
|
||||
msgstr "نمط الإجراء والوصول لإنشاء مشاركة الوصول"
|
||||
|
||||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
|
@ -167,6 +170,7 @@ msgid ""
|
|||
"Please select the action that opens the screen containing the data you want "
|
||||
"to share."
|
||||
msgstr ""
|
||||
"الرجاء اختيار الإجراء الذي يفتح شاشة تحتوي على البيانات التي تريد مشاركتها."
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:781
|
||||
|
@ -174,7 +178,7 @@ msgstr ""
|
|||
msgid ""
|
||||
"The documents have been automatically added to your current OpenERP "
|
||||
"documents.\n"
|
||||
msgstr ""
|
||||
msgstr "المستندات تمت إضافتها بشكل تلقائي لمستندات اوبن اي ار بي الخاصة بك\n"
|
||||
|
||||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
|
@ -184,7 +188,7 @@ msgstr "إلغاء"
|
|||
#. module: share
|
||||
#: field:res.groups,share:0
|
||||
msgid "Share Group"
|
||||
msgstr ""
|
||||
msgstr "مشاركة المجموعة"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:763
|
||||
|
@ -197,12 +201,12 @@ msgstr "لا بدّ من ذكر عنوان بريد إلكتروني"
|
|||
msgid ""
|
||||
"Optionally, you may specify an additional domain restriction that will be "
|
||||
"applied to the shared data."
|
||||
msgstr ""
|
||||
msgstr "اختيارياً، اختيار محدودية مجال لتطبيقها على المعلومات المشاركة."
|
||||
|
||||
#. module: share
|
||||
#: help:share.wizard,name:0
|
||||
msgid "Title for the share (displayed to users as menu and shortcut name)"
|
||||
msgstr ""
|
||||
msgstr "عنوان المشاركة (تعرض للمستخدمين كقائمة واسم مختصر)"
|
||||
|
||||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
|
@ -212,7 +216,7 @@ msgstr "خيارات"
|
|||
#. module: share
|
||||
#: view:res.groups:0
|
||||
msgid "Regular groups only (no share groups"
|
||||
msgstr ""
|
||||
msgstr "المجموعات المنتظمة فقط( لا مجموعات مشاركة)"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:787
|
||||
|
@ -222,27 +226,32 @@ msgid ""
|
|||
"Sales, HR, etc.)\n"
|
||||
"It is open source and can be found on http://www.openerp.com."
|
||||
msgstr ""
|
||||
"OpenERP هي قوية ومجموعة من تطبيقات الأعمال سهل الاستعمال(إدارة علاقات "
|
||||
"العملاء,المبيعات, موارد بشرية, الخ.)\n"
|
||||
"هي مفتوحة المصدر ويمكنك الوصول إليها عن طريق هذا الرابط "
|
||||
"http://www.openerp.com."
|
||||
|
||||
#. module: share
|
||||
#: field:share.wizard,action_id:0
|
||||
msgid "Action to share"
|
||||
msgstr ""
|
||||
msgstr "إجراء للمشاركة"
|
||||
|
||||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
msgid "Optional: include a personal message"
|
||||
msgstr ""
|
||||
msgstr "اختياري: تضمين رسالة خاصة"
|
||||
|
||||
#. module: share
|
||||
#: field:res.users,share:0
|
||||
msgid "Share User"
|
||||
msgstr ""
|
||||
msgstr "مشاركة المستخدم"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:647
|
||||
#, python-format
|
||||
msgid "Please indicate the emails of the persons to share with, one per line"
|
||||
msgstr ""
|
||||
"يرجى الإشارة إلى رسائل البريد الإلكتروني للأشخاص للمشاركة معه، واحد في كل سطر"
|
||||
|
||||
#. module: share
|
||||
#: field:share.wizard,embed_code:0 field:share.wizard.result.line,user_id:0
|
||||
|
@ -252,13 +261,13 @@ msgstr "مجهول"
|
|||
#. module: share
|
||||
#: help:res.groups,share:0
|
||||
msgid "Group created to set access rights for sharing data with some users."
|
||||
msgstr ""
|
||||
msgstr "إنشاء مجموعة لتعيين حقوق الوصول لتبادل البيانات مع بعض المستخدمين."
|
||||
|
||||
#. module: share
|
||||
#: help:share.wizard,action_id:0
|
||||
msgid ""
|
||||
"The action that opens the screen containing the data you wish to share."
|
||||
msgstr ""
|
||||
msgstr "الإجراء الذي يفتح شاشة تحتوي على البيانات التي ترغب في مشاركتها."
|
||||
|
||||
#. module: share
|
||||
#: constraint:res.users:0
|
||||
|
@ -270,12 +279,12 @@ msgstr ""
|
|||
#: code:addons/share/wizard/share_wizard.py:526
|
||||
#, python-format
|
||||
msgid "(Copy for sharing)"
|
||||
msgstr ""
|
||||
msgstr "(نسخة من المشاركة)"
|
||||
|
||||
#. module: share
|
||||
#: field:share.wizard.result.line,newly_created:0
|
||||
msgid "Newly created"
|
||||
msgstr ""
|
||||
msgstr "منشأة حديثاً"
|
||||
|
||||
#. module: share
|
||||
#: code:addons/share/wizard/share_wizard.py:616
|
||||
|
@ -292,7 +301,7 @@ msgstr ""
|
|||
#. module: share
|
||||
#: help:share.wizard,share_root_url:0
|
||||
msgid "Main access page for users that are granted shared access"
|
||||
msgstr ""
|
||||
msgstr "صفحة الوصول الرئيسية للمستخدمين لضمان مشاركة الوصول"
|
||||
|
||||
#. module: share
|
||||
#: sql_constraint:res.groups:0
|
||||
|
@ -343,12 +352,12 @@ msgstr "ملخّص"
|
|||
#: code:addons/share/wizard/share_wizard.py:493
|
||||
#, python-format
|
||||
msgid "Copied access for sharing"
|
||||
msgstr ""
|
||||
msgstr "نسخ الوصول للمشاركة"
|
||||
|
||||
#. module: share
|
||||
#: model:ir.actions.act_window,name:share.action_share_wizard_step1
|
||||
msgid "Share your documents"
|
||||
msgstr ""
|
||||
msgstr "مشاركة المستندات الخاصة بك"
|
||||
|
||||
#. module: share
|
||||
#: view:share.wizard:0
|
||||
|
@ -370,22 +379,22 @@ msgstr "share.wizard.result.line"
|
|||
#. module: share
|
||||
#: help:share.wizard,user_type:0
|
||||
msgid "Select the type of user(s) you would like to share data with."
|
||||
msgstr ""
|
||||
msgstr "استخدم نوع المستخدم الذي تود مشاركة المعلومات معه"
|
||||
|
||||
#. module: share
|
||||
#: field:share.wizard,view_type:0
|
||||
msgid "Current View Type"
|
||||
msgstr ""
|
||||
msgstr "نوع عرض الحالي"
|
||||
|
||||
#. module: share
|
||||
#: selection:share.wizard,access_mode:0
|
||||
msgid "Can view"
|
||||
msgstr ""
|
||||
msgstr "امكانية المشاهدة"
|
||||
|
||||
#. module: share
|
||||
#: selection:share.wizard,access_mode:0
|
||||
msgid "Can edit"
|
||||
msgstr ""
|
||||
msgstr "امكانية التعديل"
|
||||
|
||||
#. module: share
|
||||
#: help:share.wizard,message:0
|
||||
|
@ -528,3 +537,10 @@ msgstr "مشاركة مع..."
|
|||
|
||||
#~ msgid "Share with these people (one e-mail per line)"
|
||||
#~ msgstr "المشاركة مع هؤلاء الناس (بريد إلكتروني واحد في كل سطر)"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "An optional personal message, to be included in the e-mail notification."
|
||||
#~ msgstr "رسالة اختيارية شخصية، ليتم تضمينها في اخطار البريد الإلكتروني."
|
||||
|
||||
#~ msgid "Use this link"
|
||||
#~ msgstr "استخدم هذا الرابط"
|
||||
|
|
Loading…
Reference in New Issue