[Merge] Merge with main addons.

bzr revid: mdi@tinyerp.com-20121010043229-bqz4gq67ix25jww2
This commit is contained in:
Divyesh Makwana (Open ERP) 2012-10-10 10:02:29 +05:30
commit 1c31287162
73 changed files with 1426 additions and 942 deletions

View File

@ -458,7 +458,6 @@
<filter domain="[('user_id','=',uid)]" help="My Invoices" icon="terp-personal"/> <filter domain="[('user_id','=',uid)]" help="My Invoices" icon="terp-personal"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="user_id" string="Salesperson"/> <field name="user_id" string="Salesperson"/>
<field name="journal_id"/>
<field name="period_id" string="Period"/> <field name="period_id" string="Period"/>
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter string="Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}"/> <filter string="Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}"/>

View File

@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.4\n" "Project-Id-Version: OpenERP Server 5.0.4\n"
"Report-Msgid-Bugs-To: support@openerp.com\n" "Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-02-08 00:35+0000\n" "POT-Creation-Date: 2012-02-08 00:35+0000\n"
"PO-Revision-Date: 2009-02-03 06:24+0000\n" "PO-Revision-Date: 2012-10-08 15:59+0000\n"
"Last-Translator: <>\n" "Last-Translator: waleed bazaza <waleed_bazaza@yahoo.com>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-08-28 06:07+0000\n" "X-Launchpad-Export-Date: 2012-10-09 04:51+0000\n"
"X-Generator: Launchpad (build 15864)\n" "X-Generator: Launchpad (build 16112)\n"
#. module: account_followup #. module: account_followup
#: view:account_followup.followup:0 #: view:account_followup.followup:0
@ -42,7 +42,7 @@ msgstr "متابعة"
#: help:account.followup.print.all,test_print:0 #: help:account.followup.print.all,test_print:0
msgid "" msgid ""
"Check if you want to print followups without changing followups level." "Check if you want to print followups without changing followups level."
msgstr "" msgstr "اختر هذه الخانة إذا أردت طباعة المتابعات بدون تغيير متوى المتابعات."
#. module: account_followup #. module: account_followup
#: model:account_followup.followup.line,description:account_followup.demo_followup_line2 #: model:account_followup.followup.line,description:account_followup.demo_followup_line2
@ -101,7 +101,7 @@ msgstr "الدليل"
#. module: account_followup #. module: account_followup
#: view:account_followup.stat:0 #: view:account_followup.stat:0
msgid "Follow up Entries with period in current year" msgid "Follow up Entries with period in current year"
msgstr "" msgstr "مدخلات المتابعات بفترات في السنة الحالية"
#. module: account_followup #. module: account_followup
#: view:account.followup.print.all:0 #: view:account.followup.print.all:0
@ -314,6 +314,9 @@ msgid ""
"\n" "\n"
"%s" "%s"
msgstr "" msgstr ""
"جميع رسائل البريد الالكتروني قد أرسلت بنجاح للشركاء:.\n"
"\n"
"%s"
#. module: account_followup #. module: account_followup
#: constraint:account_followup.followup.line:0 #: constraint:account_followup.followup.line:0
@ -362,6 +365,11 @@ msgid ""
"\n" "\n"
"%s" "%s"
msgstr "" msgstr ""
"\n"
"\n"
"رسالة البريد الالكتروني قد أرسلت للشركاء التاليين.!\n"
"\n"
"%s"
#. module: account_followup #. module: account_followup
#: help:account.followup.print,date:0 #: help:account.followup.print,date:0
@ -503,7 +511,7 @@ msgstr "تقرير المتابعة"
#. module: account_followup #. module: account_followup
#: view:account_followup.followup.line:0 #: view:account_followup.followup.line:0
msgid "Follow-Up Steps" msgid "Follow-Up Steps"
msgstr "" msgstr "خطوات المتابعة"
#. module: account_followup #. module: account_followup
#: field:account_followup.stat,period_id:0 #: field:account_followup.stat,period_id:0
@ -514,7 +522,7 @@ msgstr "فترة"
#: code:addons/account_followup/wizard/account_followup_print.py:307 #: code:addons/account_followup/wizard/account_followup_print.py:307
#, python-format #, python-format
msgid "Followup Summary" msgid "Followup Summary"
msgstr "" msgstr "خلاصة المتابعة"
#. module: account_followup #. module: account_followup
#: view:account.followup.print:0 #: view:account.followup.print:0
@ -535,7 +543,7 @@ msgstr "مستوى اعلى متابعة"
#. module: account_followup #. module: account_followup
#: model:ir.actions.act_window,name:account_followup.action_view_account_followup_followup_form #: model:ir.actions.act_window,name:account_followup.action_view_account_followup_followup_form
msgid "Review Invoicing Follow-Ups" msgid "Review Invoicing Follow-Ups"
msgstr "" msgstr "استعراض متابعة الفوترة"
#. module: account_followup #. module: account_followup
#: constraint:account.move.line:0 #: constraint:account.move.line:0
@ -595,7 +603,7 @@ msgstr "الوصف"
#. module: account_followup #. module: account_followup
#: constraint:account_followup.followup:0 #: constraint:account_followup.followup:0
msgid "Only One Followup by Company." msgid "Only One Followup by Company."
msgstr "" msgstr "متابعة واحدة غقط من الشركة."
#. module: account_followup #. module: account_followup
#: view:account_followup.stat:0 #: view:account_followup.stat:0

View File

@ -1,6 +1,6 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<openerp> <openerp>
<data> <data noupdate="1">
<record id="provider_openerp" model="auth.oauth.provider"> <record id="provider_openerp" model="auth.oauth.provider">
<field name="name">OpenERP Accounts</field> <field name="name">OpenERP Accounts</field>

View File

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

View File

@ -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', 'name': 'Reset Password',
'description': """ 'description': """
@ -9,9 +30,11 @@ Allow users to reset their password from the login page.
'category': 'Authentication', 'category': 'Authentication',
'website': 'http://www.openerp.com', 'website': 'http://www.openerp.com',
'installable': True, 'installable': True,
'depends': ['auth_anonymous', 'email_template'], 'depends': ['auth_signup', 'email_template'],
'data': ['auth_reset_password.xml'], 'data': [
'auth_reset_password.xml',
'res_users_view.xml',
],
'js': ['static/src/js/reset_password.js'], 'js': ['static/src/js/reset_password.js'],
'css': ['static/src/css/reset_password.css'],
'qweb': ['static/src/xml/reset_password.xml'], 'qweb': ['static/src/xml/reset_password.xml'],
} }

View File

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

View File

@ -7,56 +7,15 @@
<field name="name">Reset Password</field> <field name="name">Reset Password</field>
<field name="model_id" ref="base.model_res_users"/> <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_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="subject">Password reset</field>
<field name="body_html"><![CDATA[ <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>, <p>You may change your password following <a href="${object.signup_url}">this link</a>.</p>
or by copy-pasting the following URL in your browser: ${object._auth_reset_password_link()}</p>
<p>Note: If you did not ask for a password reset, you can safely ignore this email.</p>]]></field> <p>Note: If you did not ask for a password reset, you can safely ignore this email.</p>]]></field>
</record> </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> </data>
</openerp> </openerp>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,72 +1,30 @@
openerp.auth_reset_password = function(instance) { openerp.auth_reset_password = function(instance) {
var _t = instance.web._t; var _t = instance.web._t;
instance.web.Login.include({ instance.web.Login.include({
start: function() { start: function() {
var $e = this.$el; this.$('a.oe_reset_password').click(this.do_reset_password);
$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'));
});
return this._super(); return this._super();
}, },
on_submit: function(ev) { do_reset_password: function(ev) {
if(ev) { if (ev) {
ev.preventDefault(); ev.preventDefault();
} }
var db = this.$("form [name=db]").val();
var $e = this.$el; var login = this.$("form input[name=login]").val();
var db = $e.find("form [name=db]").val();
if (!db) { 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; return false;
} }
var $m = $e.find('form input[name=is_reset_pw]'); var params = {
if ($m.is(':checked')) { dbname : db,
var email = $e.find('form input[name=email]').val(); login: login,
return this.do_reset_password(db, email); };
} else { var url = "/auth_reset_password/reset_password?" + $.param(params);
return this._super(ev); window.location = url;
}
},
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);
});
} }
}); });
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");
}; };

View File

@ -1,26 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1: <!-- vim:fdl=1: -->
-->
<templates id="template" xml:space="preserve"> <templates id="template" xml:space="preserve">
<t t-extend="Login"> <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"> <t t-jquery="form ul:first li:last" t-operation="after">
<li> <li><a class="oe_reset_password" href="#">Reset password</a></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="#">&lt; Back</a></li>
</ul>
</t> </t>
</t> </t>
</templates> </templates>

View File

@ -34,7 +34,9 @@ Allow users to sign up.
'data': [ 'data': [
'auth_signup_data.xml', 'auth_signup_data.xml',
'res_config.xml', 'res_config.xml',
'res_users_view.xml',
], ],
'js': ['static/src/js/auth_signup.js'], 'js': ['static/src/js/auth_signup.js'],
'css' : ['static/src/css/base.css'],
'qweb': ['static/src/xml/auth_signup.xml'], 'qweb': ['static/src/xml/auth_signup.xml'],
} }

View File

@ -1,35 +1,63 @@
import logging # -*- coding: utf-8 -*-
##############################################################################
import werkzeug.urls #
# 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.modules.registry import RegistryManager
from openerp.addons.web.controllers.main import login_and_redirect from openerp.addons.web.controllers.main import login_and_redirect
import openerp.addons.web.common.http as openerpweb import openerp.addons.web.common.http as openerpweb
from openerp import SUPERUSER_ID
import werkzeug
import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class OpenIDController(openerpweb.Controller): class Controller(openerpweb.Controller):
_cp_path = '/auth_signup' _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 @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 = '/' url = '/'
registry = RegistryManager.get(dbname) registry = RegistryManager.get(dbname)
with registry.cursor() as cr: with registry.cursor() as cr:
try: try:
Users = registry.get('res.users') res_users = registry.get('res.users')
credentials = Users.auth_signup(cr, SUPERUSER_ID, name, login, password) values = {'name': name, 'login': login, 'password': password}
credentials = res_users.signup(cr, SUPERUSER_ID, values, token)
cr.commit() cr.commit()
return login_and_redirect(req, *credentials) return login_and_redirect(req, *credentials)
except AttributeError: except Exception as e:
# auth_signup is not installed
_logger.exception('attribute error when signup')
url = "/#action=auth_signup&error=NA" # Not Available
except Exception:
# signup error # signup error
_logger.exception('error when signup') _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) return werkzeug.utils.redirect(url)
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -20,22 +20,27 @@
############################################################################## ##############################################################################
from openerp.osv import osv, fields from openerp.osv import osv, fields
from openerp.tools.safe_eval import safe_eval
class base_config_settings(osv.TransientModel): class base_config_settings(osv.TransientModel):
_inherit = 'base.config.settings' _inherit = 'base.config.settings'
_columns = { _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'), '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): def get_default_auth_signup_template_user_id(self, cr, uid, fields, context=None):
icp = self.pool.get('ir.config_parameter') 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 { 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): def set_auth_signup_template_user_id(self, cr, uid, ids, context=None):
config = self.browse(cr, uid, ids[0], context=context) config = self.browse(cr, uid, ids[0], context=context)
icp = self.pool.get('ir.config_parameter') 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))

View File

@ -14,7 +14,9 @@
</div> </div>
<div attrs="{'invisible':[('auth_signup_uninvited','=',False)]}"> <div attrs="{'invisible':[('auth_signup_uninvited','=',False)]}">
<label for="auth_signup_template_user_id"/> <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> </div>
</xpath> </xpath>
</field> </field>

View File

@ -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 import openerp
from openerp.osv import osv from openerp.osv import osv, fields
from openerp import SUPERUSER_ID 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): class res_users(osv.Model):
_inherit = 'res.users' _inherit = 'res.users'
def auth_signup_create(self, cr, uid, new_user, context=None): def _get_state(self, cr, uid, ids, name, arg, context=None):
# new_user: return dict((user.id, 'new' if not user.login_date else 'reset' if user.signup_token else 'active')
# login for user in self.browse(cr, uid, ids, context))
# 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 auth_signup(self, cr, uid, name, login, password, context=None): _columns = {
r = (cr.dbname, login, password) 'state': fields.function(_get_state, string='State', type='selection',
res = self.search(cr, uid, [("login", "=", login)]) selection=[('new', 'New'), ('active', 'Active'), ('reset', 'Resetting Password')]),
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
# 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)

View File

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

View File

@ -0,0 +1,3 @@
base.css: base.sass
sass --trace -t expanded base.sass base.css

View File

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

View File

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

View File

@ -5,56 +5,99 @@ openerp.auth_signup = function(instance) {
instance.web.Login.include({ instance.web.Login.include({
start: function() { start: function() {
var self = this; var self = this;
this.$('a.oe_signup').click(function() { var d = this._super();
var dbname = self.$("form [name=db]").val();
self.do_action({ // to switch between the signup and regular login form
type: 'ir.actions.client', this.$('a.oe_signup_signup').click(function() {
tag: 'auth_signup.signup', self.$el.addClass("oe_login_signup");
params: {'dbname': dbname}, });
target: 'new', this.$('a.oe_signup_back').click(function() {
name: 'Sign up' self.$el.removeClass("oe_login_signup");
}); delete self.params.token;
return true;
}); });
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({ // in case of a signup, retrieve the user information from the token
template: 'auth_signup.signup', if (self.params.db && self.params.token) {
init: function(parent, params) { d = self.rpc("/auth_signup/retrieve", {dbname: self.params.db, token: self.params.token})
this.params = params; .done(self.on_token_loaded)
return this._super(); .fail(self.on_token_failed);
}
return d;
}, },
start: function() { on_token_loaded: function(result) {
var self = this; // select the right the database
this.$('input[name=password_confirmation]').keyup(function() { this.selected_db = result.db;
var v = $(this).val(); this.on_db_loaded({db_list: [result.db]});
var $b = self.$('button'); if (result.token) {
if (_.isEmpty(v) || self.$('input[name=password]').val() === v) { // switch to signup mode, set user name and login
$b.removeAttr('disabled'); 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 { } else {
$b.attr('disabled', 'disabled'); this.$("form input[name=login]").val(result.email);
} }
}); } else {
// remain in login mode, set login if present
this.$('form').submit(function(ev) { delete this.params.token;
if(ev) { this.$("form input[name=login]").val(result.login || "");
ev.preventDefault(); }
},
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 = { var params = {
dbname : self.params.dbname, dbname : db,
name: self.$('input[name=name]').val(), token: this.params.token || "",
login: self.$('input[name=email]').val(), name: name,
password: self.$('input[name=password]').val(), login: login,
password: password,
}; };
var url = "/auth_signup/signup?" + $.param(params); var url = "/auth_signup/signup?" + $.param(params);
window.location = url; window.location = url;
}); } else {
return this._super(); // regular login
} this._super(ev);
}
},
}); });
instance.web.client_actions.add("auth_signup.signup", "instance.auth_signup.Signup");
}; };

View File

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1: <!-- vim:fdl=1: -->
-->
<templates id="template" xml:space="preserve"> <templates id="template" xml:space="preserve">
<t t-extend="Login"> <t t-extend="Login">
<t t-jquery="form ul:first li:last" t-operation="after"> <t t-jquery="form ul:first li:contains('Username')" t-operation="before">
<li> <li class="oe_signup_show">Name</li>
<a class="oe_signup" href="#">Sign Up</a> <li class="oe_signup_show"><input name="name" type="text"/></li>
</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 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> </templates>

View File

@ -280,7 +280,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Meetings"> <search string="Search Meetings">
<field name="name" string="Meeting" filter_domain="[('name','ilike',self)]"/> <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/> <separator/>
<filter string="My Meetings" help="My Meetings" domain="[('user_id','=',uid)]"/> <filter string="My Meetings" help="My Meetings" domain="[('user_id','=',uid)]"/>
<field name="user_id"/> <field name="user_id"/>

View File

@ -26,6 +26,7 @@ from osv import fields, osv
import time import time
import tools import tools
from tools.translate import _ from tools.translate import _
from tools import html2plaintext
from base.res.res_partner import format_address from base.res.res_partner import format_address
@ -812,9 +813,11 @@ class crm_lead(base_stage, format_address, osv.osv):
This override updates the document according to the email. This override updates the document according to the email.
""" """
if custom_values is None: custom_values = {} if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
custom_values.update({ custom_values.update({
'name': msg.get('subject') or _("No Subject"), 'name': msg.get('subject') or _("No Subject"),
'description': msg.get('body'), 'description': desc,
'email_from': msg.get('from'), 'email_from': msg.get('from'),
'email_cc': msg.get('cc'), 'email_cc': msg.get('cc'),
'user_id': False, 'user_id': False,

View File

@ -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="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="categ_ids" string="Category" filter_domain="[('categ_ids','ilike',self)]" />
<field name="create_date"/> <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/> <separator/>
<filter icon="terp-check" string="New" name="new" help="New Leads" domain="[('state','=','draft')]"/> <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/> <separator/>
<filter string="Unassigned Leads" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Leads" /> <filter string="Unassigned Leads" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Leads" />
<separator/> <separator/>
@ -569,10 +569,10 @@
<field name="name" string="Opportunity / Customer" <field name="name" string="Opportunity / Customer"
filter_domain="['|','|','|',('partner_id','ilike',self),('partner_name','ilike',self),('email_from','ilike',self),('name', 'ilike', self)]"/> 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)]" /> <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/> <separator/>
<filter icon="terp-check" string="New" help="New Opportunities" name="new" domain="[('state','=','draft')]"/> <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/> <separator/>
<filter string="Unassigned Opportunities" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Opportunities" /> <filter string="Unassigned Opportunities" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Opportunities" />
<separator/> <separator/>
@ -580,9 +580,8 @@
domain="['|', ('section_id.user_id','=',uid), ('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}" 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" /> help="Opportunities that are assigned to either me or one of the sale teams I manage" />
<field name="user_id"/> <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="section_id" context="{'invisible_section': False, 'default_section_id': self}"/>
<field name="partner_id"/>
<group expand="0" string="Group By..." colspan="16"> <group expand="0" string="Group By..." colspan="16">
<filter string="Salesperson" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}" /> <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'}"/> <filter string="Team" help="Sales Team" icon="terp-personal+" domain="[]" context="{'group_by':'section_id'}"/>

View File

@ -15,31 +15,12 @@
</field> </field>
</record> </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"> <record id="view_partners_form_crm3" model="ir.ui.view">
<field name="name">view.res.partner.search.crm.inherited3</field> <field name="name">view.res.partner.search.crm.inherited3</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/> <field name="inherit_id" ref="base.view_res_partner_filter"/>
<field eval="18" name="priority"/> <field eval="18" name="priority"/>
<field name="arch" type="xml"> <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"> <xpath expr="//group[@string='Group By...']" position="after">
<group string="Display"> <group string="Display">
<filter string="Show Sales Team" context="{'invisible_section': False}"/> <filter string="Show Sales Team" context="{'invisible_section': False}"/>

View File

@ -26,6 +26,7 @@ from osv import fields, osv
import time import time
import tools import tools
from tools.translate import _ from tools.translate import _
from tools import html2plaintext
CRM_CLAIM_PENDING_STATES = ( CRM_CLAIM_PENDING_STATES = (
crm.AVAILABLE_STATES[2][0], # Cancelled crm.AVAILABLE_STATES[2][0], # Cancelled
@ -192,9 +193,10 @@ class crm_claim(base_stage, osv.osv):
This override updates the document according to the email. This override updates the document according to the email.
""" """
if custom_values is None: custom_values = {} if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
custom_values.update({ custom_values.update({
'name': msg.get('subject') or _("No Subject"), 'name': msg.get('subject') or _("No Subject"),
'description': msg.get('body'), 'description': desc,
'email_from': msg.get('from'), 'email_from': msg.get('from'),
'email_cc': msg.get('cc'), 'email_cc': msg.get('cc'),
}) })

View File

@ -25,6 +25,7 @@ from crm import crm
from osv import fields, osv from osv import fields, osv
import tools import tools
from tools.translate import _ from tools.translate import _
from tools import html2plaintext
CRM_HELPDESK_STATES = ( CRM_HELPDESK_STATES = (
crm.AVAILABLE_STATES[2][0], # Cancelled crm.AVAILABLE_STATES[2][0], # Cancelled
@ -104,9 +105,10 @@ class crm_helpdesk(base_state, base_stage, osv.osv):
This override updates the document according to the email. This override updates the document according to the email.
""" """
if custom_values is None: custom_values = {} if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
custom_values.update({ custom_values.update({
'name': msg.get('subject') or _("No Subject"), 'name': msg.get('subject') or _("No Subject"),
'description': msg.get('body'), 'description': desc,
'email_from': msg.get('from'), 'email_from': msg.get('from'),
'email_cc': msg.get('cc'), 'email_cc': msg.get('cc'),
'user_id': False, 'user_id': False,

View File

@ -56,7 +56,7 @@
<filter string="Assigned Partner" icon="terp-personal" domain="[]" context="{'group_by':'partner_assigned_id'}"/> <filter string="Assigned Partner" icon="terp-personal" domain="[]" context="{'group_by':'partner_assigned_id'}"/>
</filter> </filter>
<field name="user_id" position="after"> <field name="partner_id" position="after">
<field name="partner_assigned_id"/> <field name="partner_assigned_id"/>
</field> </field>
</field> </field>

View File

@ -339,7 +339,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Events"> <search string="Events">
<field name="name" 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/> <separator/>
<filter icon="terp-check" string="Unconfirmed" name="draft" domain="[('state','=','draft')]" help="Events in New state"/> <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"/> <filter icon="terp-camera_test" string="Confirmed" domain="[('state','=','confirm')]" help="Confirmed events"/>
@ -529,7 +529,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Event Registration"> <search string="Event Registration">
<field name="name" string="Participant" filter_domain="['|','|','|',('name','ilike',self),('partner_id','ilike',self),('email','ilike',self),('origin','ilike',self)]"/> <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/> <separator/>
<filter icon="terp-check" string="New" name="draft" domain="[('state','=','draft')]" help="Registrations in unconfirmed state"/> <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"/> <filter icon="terp-camera_test" string="Confirmed" domain="[('state','=','open')]" help="Confirmed registrations"/>

View File

@ -112,8 +112,6 @@
<search string="Employees"> <search string="Employees">
<field name="name" string="Employees"/> <field name="name" string="Employees"/>
<field name="department_id" /> <field name="department_id" />
<field name="job_id"/>
<field name="parent_id"/>
<field name="category_ids"/> <field name="category_ids"/>
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter string="Manager" icon="terp-personal" domain="[]" context="{'group_by':'parent_id'}"/> <filter string="Manager" icon="terp-personal" domain="[]" context="{'group_by':'parent_id'}"/>

View File

@ -155,11 +155,12 @@
<field name="name" string="Expenses"/> <field name="name" string="Expenses"/>
<field name="date"/> <field name="date"/>
<filter icon="terp-document-new" domain="[('state','=','draft')]" string="New" help="New Expense"/> <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"/> <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="employee_id"/>
<field name="department_id" string="Department" context="{'invisible_department': False}"/> <field name="department_id" string="Department" context="{'invisible_department': False}"/>
<field name="user_id" string="User"/>
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter string="Employee" icon="terp-personal" domain="[]" context="{'group_by':'employee_id'}"/> <filter string="Employee" icon="terp-personal" domain="[]" context="{'group_by':'employee_id'}"/>
<filter string="Department" icon="terp-personal+" domain="[]" context="{'group_by':'department_id'}"/> <filter string="Department" icon="terp-personal+" domain="[]" context="{'group_by':'department_id'}"/>

View File

@ -26,6 +26,7 @@ from base_status.base_stage import base_stage
from datetime import datetime from datetime import datetime
from osv import fields, osv from osv import fields, osv
from tools.translate import _ from tools.translate import _
from tools import html2plaintext
AVAILABLE_STATES = [ AVAILABLE_STATES = [
('draft', 'New'), ('draft', 'New'),
@ -327,9 +328,10 @@ class hr_applicant(base_stage, osv.Model):
This override updates the document according to the email. This override updates the document according to the email.
""" """
if custom_values is None: custom_values = {} if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
custom_values.update({ custom_values.update({
'name': msg.get('subject') or _("No Subject"), 'name': msg.get('subject') or _("No Subject"),
'description': msg.get('body'), 'description': desc,
'email_from': msg.get('from'), 'email_from': msg.get('from'),
'email_cc': msg.get('cc'), 'email_cc': msg.get('cc'),
'user_id': False, 'user_id': False,

View File

@ -208,7 +208,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Jobs"> <search string="Search Jobs">
<field name="partner_name" filter_domain="['|','|',('name','ilike',self),('partner_name','ilike',self),('email_from','ilike',self)]" string="Subject / Applicant"/> <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/> <separator/>
<filter string="New" domain="[('state','=','draft')]" help="All Initial Jobs"/> <filter string="New" domain="[('state','=','draft')]" help="All Initial Jobs"/>
<filter string="In Progress" domain="[('state','=','open')]" help="Open Jobs"/> <filter string="In Progress" domain="[('state','=','open')]" help="Open Jobs"/>

View File

@ -65,5 +65,8 @@ The validation can be configured in the company:
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,
'application': True, '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: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -27,80 +27,6 @@ from osv import fields, osv
from tools.translate import _ from tools.translate import _
import netsvc 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): class hr_timesheet_sheet(osv.osv):
_name = "hr_timesheet_sheet.sheet" _name = "hr_timesheet_sheet.sheet"
_inherit = "mail.thread" _inherit = "mail.thread"
@ -111,8 +37,7 @@ class hr_timesheet_sheet(osv.osv):
def _total_attendances(self, cr, uid, ids, name, args, context=None): def _total_attendances(self, cr, uid, ids, name, args, context=None):
""" Get the total attendance for the timesheets """ Get the total attendance for the timesheets
Returns a dict like : Returns a dict like :
{id: {'date_current': '2011-06-17', {id: {'total_per_day': {day: timedelta, ...},
'total_per_day': {day: timedelta, ...},
}, },
... ...
} }
@ -122,7 +47,6 @@ class hr_timesheet_sheet(osv.osv):
res = {} res = {}
for sheet_id in ids: for sheet_id in ids:
sheet = self.browse(cr, uid, sheet_id, context=context) sheet = self.browse(cr, uid, sheet_id, context=context)
date_current = sheet.date_current
# field attendances_ids of hr_timesheet_sheet.sheet only # field attendances_ids of hr_timesheet_sheet.sheet only
# returns attendances of timesheet's current date # returns attendances of timesheet's current date
attendance_ids = attendance_obj.search(cr, uid, [('sheet_id', '=', sheet_id)], context=context) attendance_ids = attendance_obj.search(cr, uid, [('sheet_id', '=', sheet_id)], context=context)
@ -143,20 +67,7 @@ class hr_timesheet_sheet(osv.osv):
else: else:
total_attendance[day] += attendance_interval total_attendance[day] += attendance_interval
# if the delta is negative, it means that a sign out is missing res[sheet_id] = {'total_per_day': total_attendance}
# 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}
return res return res
def _total_timesheet(self, cr, uid, ids, name, args, context=None): 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] 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_sheet = all_attendances_sheet['total_per_day']
total_attendances_all_days = sum_all_days(total_attendances_sheet) 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_sheet = all_timesheet_lines[id]
total_timesheets_all_days = sum_all_days(total_timesheets_sheet) 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_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_attendance'] = timedelta_to_hours(total_attendances_all_days)
res[id]['total_timesheet'] = timedelta_to_hours(total_timesheets_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_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 return res
def check_employee_attendance_state(self, cr, uid, sheet_id, context=None): 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,)) raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
return True 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): def attendance_action_change(self, cr, uid, ids, context=None):
hr_employee = self.pool.get('hr.employee') hr_employee = self.pool.get('hr.employee')
employee_ids = [] 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)]}), '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_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_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' : fields.one2many('hr.analytic.timesheet', 'sheet_id',
'timesheet_ids' : one2many_mod('hr.analytic.timesheet', 'sheet_id', 'Timesheet lines',
'Timesheet lines', domain=[('date', '=', time.strftime('%Y-%m-%d'))],
readonly=True, states={ readonly=True, states={
'draft': [('readonly', False)], 'draft': [('readonly', False)],
'new': [('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([ 'state' : fields.selection([
('new', 'New'), ('new', 'New'),
('draft','Open'), ('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 \'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.'), \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), '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_attendance': fields.function(_total, method=True, string='Total Attendance', multi="_total"),
'total_timesheet': fields.function(_total, method=True, string='Total Timesheet', 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"), 'total_difference': fields.function(_total, method=True, string='Difference', multi="_total"),
@ -386,7 +247,6 @@ class hr_timesheet_sheet(osv.osv):
_defaults = { _defaults = {
'date_from' : _default_date_from, 'date_from' : _default_date_from,
'date_current' : lambda *a: time.strftime('%Y-%m-%d'),
'date_to' : _default_date_to, 'date_to' : _default_date_to,
'state': 'new', 'state': 'new',
'employee_id': _default_employee, 'employee_id': _default_employee,
@ -406,16 +266,9 @@ class hr_timesheet_sheet(osv.osv):
return False return False
return True 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 = [ _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']), (_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): def action_set_to_draft(self, cr, uid, ids, *args):
@ -498,7 +351,7 @@ class hr_timesheet_line(osv.osv):
_columns = { _columns = {
'sheet_id': fields.function(_sheet, string='Sheet', 'sheet_id': fields.function(_sheet, string='Sheet',
type='many2one', relation='hr_timesheet_sheet.sheet', type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
store={ store={
'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10), '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), '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.')) raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet.'))
return True 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() hr_timesheet_line()
class hr_attendance(osv.osv): class hr_attendance(osv.osv):

View File

@ -6,7 +6,6 @@
<field name="name">Sheet 1</field> <field name="name">Sheet 1</field>
<field name="user_id" ref="base.user_root"/> <field name="user_id" ref="base.user_root"/>
<field name="employee_id" ref="hr.employee_fp" /> <field name="employee_id" ref="hr.employee_fp" />
<field eval="time.strftime('%Y-%m-%d')" name="date_current"/>
</record> </record>
--> -->
</data> </data>

View File

@ -71,20 +71,13 @@
</group> </group>
</group> </group>
<notebook> <notebook>
<page string="Weekly">
<widget type="weekly_timesheet">
</widget>
</page>
<page string="Daily"> <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"> <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"> <tree string="Attendances" editable="bottom">
<field name="name"/> <field name="name"/>
<field name="action"/> <field name="action"/>
@ -101,9 +94,9 @@
<group col="4"> <group col="4">
<field name="state_attendance" groups="base.group_hr_attendance"/> <field name="state_attendance" groups="base.group_hr_attendance"/>
</group> </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"> <tree editable="top" string="Timesheet Lines">
<field invisible="1" name="date"/> <field name="date"/>
<field domain="[('type','in',['normal', 'contract']), ('state', '&lt;&gt;', 'close'),('use_timesheets','=',1)]" name="account_id" on_change="on_change_account_id(account_id)" context="{'default_use_timesheets': 1}"/> <field domain="[('type','in',['normal', 'contract']), ('state', '&lt;&gt;', '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="name"/>
<field name="unit_amount" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" widget="float_time"/> <field name="unit_amount" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" widget="float_time"/>

View File

@ -46,7 +46,6 @@ class timesheet_report(osv.osv):
'department_id':fields.many2one('hr.department','Department',readonly=True), 'department_id':fields.many2one('hr.department','Department',readonly=True),
'date_from': fields.date('Date from',readonly=True,), 'date_from': fields.date('Date from',readonly=True,),
'date_to': fields.date('Date to',readonly=True), 'date_to': fields.date('Date to',readonly=True),
'date_current': fields.date('Current date', required=True),
'state' : fields.selection([ 'state' : fields.selection([
('new', 'New'), ('new', 'New'),
('draft','Draft'), ('draft','Draft'),
@ -62,13 +61,9 @@ class timesheet_report(osv.osv):
create or replace view timesheet_report as ( create or replace view timesheet_report as (
select select
min(aal.id) as id, min(aal.id) as id,
htss.date_current,
htss.name, htss.name,
htss.date_from, htss.date_from,
htss.date_to, 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, count(*) as nbr,
aal.unit_amount as quantity, aal.unit_amount as quantity,
aal.amount as cost, aal.amount as cost,
@ -77,18 +72,15 @@ class timesheet_report(osv.osv):
(SELECT sum(day.total_difference) (SELECT sum(day.total_difference)
FROM hr_timesheet_sheet_sheet AS sheet FROM hr_timesheet_sheet_sheet AS sheet
LEFT JOIN hr_timesheet_sheet_sheet_day AS day LEFT JOIN hr_timesheet_sheet_sheet_day AS day
ON (sheet.id = day.sheet_id ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_diff,
AND day.name = sheet.date_current) where sheet.id=htss.id) as total_diff,
(SELECT sum(day.total_timesheet) (SELECT sum(day.total_timesheet)
FROM hr_timesheet_sheet_sheet AS sheet FROM hr_timesheet_sheet_sheet AS sheet
LEFT JOIN hr_timesheet_sheet_sheet_day AS day LEFT JOIN hr_timesheet_sheet_sheet_day AS day
ON (sheet.id = day.sheet_id ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_timesheet,
AND day.name = sheet.date_current) where sheet.id=htss.id) as total_timesheet,
(SELECT sum(day.total_attendance) (SELECT sum(day.total_attendance)
FROM hr_timesheet_sheet_sheet AS sheet FROM hr_timesheet_sheet_sheet AS sheet
LEFT JOIN hr_timesheet_sheet_sheet_day AS day LEFT JOIN hr_timesheet_sheet_sheet_day AS day
ON (sheet.id = day.sheet_id ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_attendance,
AND day.name = sheet.date_current) where sheet.id=htss.id) as total_attendance,
aal.to_invoice, aal.to_invoice,
aal.general_account_id, aal.general_account_id,
htss.user_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_analytic_timesheet as hat ON (hat.line_id=aal.id)
left join hr_timesheet_sheet_sheet as htss ON (hat.line_id=htss.id) left join hr_timesheet_sheet_sheet as htss ON (hat.line_id=htss.id)
group by 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, aal.account_id,
htss.date_from, htss.date_from,
htss.date_to, htss.date_to,
aal.unit_amount, aal.unit_amount,
aal.amount, aal.amount,
htss.date_current,
aal.to_invoice, aal.to_invoice,
aal.product_id, aal.product_id,
aal.general_account_id, aal.general_account_id,

View File

@ -17,7 +17,6 @@
<field name="model">timesheet.report</field> <field name="model">timesheet.report</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree colors="blue:state == 'draft';black:state in ('confirm','new');gray:state == 'cancel'" string="Timesheet"> <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="name" invisible="1"/>
<field name="user_id" invisible="1"/> <field name="user_id" invisible="1"/>
<field name="date_from" invisible="1"/> <field name="date_from" invisible="1"/>
@ -56,7 +55,6 @@
<field name="product_id"/> <field name="product_id"/>
<field name="department_id"/> <field name="department_id"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="date_current"/>
<field name="date_to"/> <field name="date_to"/>
<field name="date_from"/> <field name="date_from"/>
</group> </group>

View File

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

View File

@ -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');
};

View File

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

View File

@ -16,7 +16,6 @@
I create a timesheet for employee "Quentin Paolinon". I create a timesheet for employee "Quentin Paolinon".
- -
!record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}: !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') date_from: !eval time.strftime('%Y-%m-01')
name: Quentin Paolinon name: Quentin Paolinon
state: new state: new
@ -34,30 +33,6 @@
- -
!assert {model: hr.employee, id: hr.employee_qdp}: !assert {model: hr.employee, id: hr.employee_qdp}:
- state == 'present' - 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". At the time of logout, I create attendance and perform "Sign Out".
- -

View File

@ -42,7 +42,6 @@ class hr_timesheet_current_open(osv.osv_memory):
view_type = 'tree,form' view_type = 'tree,form'
domain = "[('id','in',["+','.join(map(str, ids))+"]),('user_id', '=', uid)]" domain = "[('id','in',["+','.join(map(str, ids))+"]),('user_id', '=', uid)]"
elif len(ids)==1: elif len(ids)==1:
ts.write(cr, uid, ids, {'date_current': time.strftime('%Y-%m-%d')}, context=context)
domain = "[('user_id', '=', uid)]" domain = "[('user_id', '=', uid)]"
else: else:
domain = "[('user_id', '=', uid)]" domain = "[('user_id', '=', uid)]"

View File

@ -42,10 +42,5 @@ This month you also get 250 EUR of eco-vouchers if you have been in the company
<field name="type">comment</field> <field name="type">comment</field>
</record> </record>
<record model="ir.config_parameter" id="user_mail_alias">
<field name="key">mail.catchall.domain</field>
<field name="value">demo.openerp.com</field>
</record>
</data> </data>
</openerp> </openerp>

View File

@ -63,10 +63,12 @@ class mail_mail(osv.Model):
} }
def _get_default_from(self, cr, uid, context=None): def _get_default_from(self, cr, uid, context=None):
cur = self.pool.get('res.users').browse(cr, uid, uid, context=context) this = self.pool.get('res.users').browse(cr, uid, uid, context=context)
if not cur.alias_domain: if this.alias_domain:
raise osv.except_osv(_('Invalid Action!'), _('Unable to send email, set an alias domain in your server settings.')) return '%s@%s' % (this.alias_name, this.alias_domain)
return cur.alias_name + '@' + cur.alias_domain elif this.email:
return this.email
raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
_defaults = { _defaults = {
'state': 'outgoing', 'state': 'outgoing',

View File

@ -180,7 +180,6 @@ class mail_message(osv.Model):
fields allow to have the foreign record name without having fields allow to have the foreign record name without having
to check external access rights). to check external access rights).
""" """
child_nbr = len(msg.child_ids)
has_voted = False has_voted = False
vote_ids = self.pool.get('res.users').name_get(cr, SUPERUSER_ID, [user.id for user in msg.vote_user_ids], context=context) vote_ids = self.pool.get('res.users').name_get(cr, SUPERUSER_ID, [user.id for user in msg.vote_user_ids], context=context)
for vote in vote_ids: for vote in vote_ids:
@ -194,7 +193,7 @@ class mail_message(osv.Model):
try: try:
author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0] author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
is_author = uid == msg.author_id.user_ids[0].id is_author = uid == msg.author_id.user_ids[0].id
except (orm.except_orm, osv.except_osv): except Exception:
author_id = False author_id = False
is_author = False is_author = False
try: try:
@ -502,7 +501,7 @@ class mail_message(osv.Model):
def create(self, cr, uid, values, context=None): def create(self, cr, uid, values, context=None):
if not values.get('message_id') and values.get('res_id') and values.get('model'): if not values.get('message_id') and values.get('res_id') and values.get('model'):
values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s' % values) values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
newid = super(mail_message, self).create(cr, uid, values, context) newid = super(mail_message, self).create(cr, uid, values, context)
self._notify(cr, SUPERUSER_ID, newid, context=context) self._notify(cr, SUPERUSER_ID, newid, context=context)
return newid return newid

View File

@ -39,63 +39,6 @@ _logger = logging.getLogger(__name__)
def decode_header(message, header, separator=' '): def decode_header(message, header, separator=' '):
return separator.join(map(decode, message.get_all(header, []))) 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): class mail_thread(osv.AbstractModel):
''' mail_thread model is meant to be inherited by any model that needs to ''' 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 res[notif.message_id.res_id] = True
return [('id', 'in', res.keys())] 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 = { _columns = {
'message_is_follower': fields.function(_get_subscription_data, 'message_is_follower': fields.function(_get_subscription_data,
type='boolean', string='Is a Follower', multi='_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 "\ help="Holds data about the subtypes. The content of this field "\
"is a structure holding the current model subtypes, and the "\ "is a structure holding the current model subtypes, and the "\
"current document followed subtypes."), "current document followed subtypes."),
'message_follower_ids': many2many_reference('res.partner', 'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers,
'mail_followers', 'res_id', 'partner_id', fnct_search=_search_followers, type='many2many', obj='res.partner', string='Followers'),
reference_column='res_model', string='Followers'),
'message_comment_ids': fields.one2many('mail.message', 'res_id', 'message_comment_ids': fields.one2many('mail.message', 'res_id',
domain=lambda self: [('model', '=', self._name), ('type', 'in', ('comment', 'email'))], domain=lambda self: [('model', '=', self._name), ('type', 'in', ('comment', 'email'))],
string='Comments and emails', string='Comments and emails',
@ -422,12 +420,12 @@ class mail_thread(osv.AbstractModel):
model_pool = self.pool.get(model) model_pool = self.pool.get(model)
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
"Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \ "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
(msg['message-id'], model) (msg['message_id'], model)
if thread_id and hasattr(model_pool, 'message_update'): if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=context) model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
else: else:
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context) thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
self.message_post(cr, uid, [thread_id], context=context, **msg) model_pool.message_post(cr, uid, [thread_id], context=context, **msg)
return thread_id return thread_id
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
@ -538,7 +536,7 @@ class mail_thread(osv.AbstractModel):
field may not be present if missing in original field may not be present if missing in original
message:: message::
{ 'message-id': msg_id, { 'message_id': msg_id,
'subject': subject, 'subject': subject,
'from': from, 'from': from,
'to': to, 'to': to,

View File

@ -19,6 +19,8 @@
# #
############################################################################## ##############################################################################
import urlparse
from openerp.osv import osv, fields from openerp.osv import osv, fields
class project_configuration(osv.TransientModel): class project_configuration(osv.TransientModel):
@ -31,9 +33,16 @@ class project_configuration(osv.TransientModel):
} }
def get_default_alias_domain(self, cr, uid, ids, context=None): def get_default_alias_domain(self, cr, uid, ids, context=None):
return {'alias_domain': self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.domain", context=context)} alias_domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.domain", context=context)
if not alias_domain:
domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "web.base.url", context=context)
try:
alias_domain = urlparse.urlsplit(domain).netloc.split(':')[0]
except Exception:
pass
return {'alias_domain': alias_domain}
def set_alias_domain(self, cr, uid, ids, context=None): def set_alias_domain(self, cr, uid, ids, context=None):
config_parameters = self.pool.get("ir.config_parameter") config_parameters = self.pool.get("ir.config_parameter")
for record in self.browse(cr, uid, ids, context=context): for record in self.browse(cr, uid, ids, context=context):
config_parameters.set_param(cr, uid, "mail.catchall.domain", record.alias_domain or '', context=context) config_parameters.set_param(cr, uid, "mail.catchall.domain", record.alias_domain or '', context=context)

View File

@ -110,9 +110,19 @@ class res_users(osv.Model):
alias_pool.unlink(cr, uid, alias_ids, context=context) alias_pool.unlink(cr, uid, alias_ids, context=context)
return res return res
def message_post(self, cr, uid, thread_id, **kwargs): def message_post(self, cr, uid, thread_id, context=None, **kwargs):
partner_id = self.pool.get('res.users').browse(cr, uid, thread_id)[0].partner_id.id assert thread_id, "res.users does not support posting global messages"
return self.pool.get('res.partner').message_post(cr, uid, partner_id, **kwargs) if context and 'thread_model' in context:
context['thread_model'] = 'res.partner'
if isinstance(thread_id, (list, tuple)):
thread_id = thread_id[0]
partner_id = self.pool.get('res.users').browse(cr, uid, thread_id).partner_id.id
return self.pool.get('res.partner').message_post(cr, uid, partner_id, context=context, **kwargs)
def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
partner_id = self.pool.get('res.users').browse(cr, uid, ids)[0].partner_id.id
return self.pool.get('res.partner').message_update(cr, uid, [partner_id], msg_dict,
update_vals=update_vals, context=context)
class res_users_mail_group(osv.Model): class res_users_mail_group(osv.Model):
""" Update of res.users class """ Update of res.users class

View File

@ -7,7 +7,7 @@
<field name="name">Mail.group: access only public and joined groups</field> <field name="name">Mail.group: access only public and joined groups</field>
<field name="model_id" ref="model_mail_group"/> <field name="model_id" ref="model_mail_group"/>
<!-- This rule has to be improved for employee only groups --> <!-- This rule has to be improved for employee only groups -->
<field name="domain_force">['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.id]), '&amp;', ('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]), '&amp;', ('public','=','groups'), ('group_public_id','in', [g.id for g in user.groups_id])]</field>
</record> </record>
<!-- <!--

View File

@ -194,7 +194,7 @@ class test_mail(TestMailMockups):
'plaintext mail incorrectly parsed') 'plaintext mail incorrectly parsed')
def test_10_many2many_reference_field(self): 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, We will test to perform writes using the many2many commands 0, 3, 4,
5 and 6. """ 5 and 6. """
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid

View File

@ -8,19 +8,19 @@ msgstr ""
"Project-Id-Version: openobject-addons\n" "Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-02-08 00:36+0000\n" "POT-Creation-Date: 2012-02-08 00:36+0000\n"
"PO-Revision-Date: 2012-01-12 05:49+0000\n" "PO-Revision-Date: 2012-10-08 15:06+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: kifcaliph <Unknown>\n"
"Language-Team: Arabic <ar@li.org>\n" "Language-Team: Arabic <ar@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-08-28 06:38+0000\n" "X-Launchpad-Export-Date: 2012-10-09 04:51+0000\n"
"X-Generator: Launchpad (build 15864)\n" "X-Generator: Launchpad (build 16112)\n"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign:0 #: view:marketing.campaign:0
msgid "Manual Mode" msgid "Manual Mode"
msgstr "" msgstr "النمط اليدوي"
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign.transition,activity_from_id:0 #: field:marketing.campaign.transition,activity_from_id:0
@ -31,12 +31,12 @@ msgstr "النشاط السابق"
#: code:addons/marketing_campaign/marketing_campaign.py:818 #: code:addons/marketing_campaign/marketing_campaign.py:818
#, python-format #, python-format
msgid "The current step for this item has no email or report to preview." msgid "The current step for this item has no email or report to preview."
msgstr "" msgstr "الخطوة الحالية لهذا الصنف لا تملك بريد إلكتروني أو تقرير للمعاينة"
#. module: marketing_campaign #. module: marketing_campaign
#: constraint:marketing.campaign.transition:0 #: constraint:marketing.campaign.transition:0
msgid "The To/From Activity of transition must be of the same Campaign " msgid "The To/From Activity of transition must be of the same Campaign "
msgstr "" msgstr "الإنتقال من / إلى النشاط يجب أن يكون من نفس الحملة "
#. module: marketing_campaign #. module: marketing_campaign
#: selection:marketing.campaign.transition,trigger:0 #: selection:marketing.campaign.transition,trigger:0
@ -46,7 +46,7 @@ msgstr "الوقت"
#. module: marketing_campaign #. module: marketing_campaign
#: selection:marketing.campaign.activity,type:0 #: selection:marketing.campaign.activity,type:0
msgid "Custom Action" msgid "Custom Action"
msgstr "" msgstr "تخصيص إجراء"
#. module: marketing_campaign #. module: marketing_campaign
#: view:campaign.analysis:0 #: view:campaign.analysis:0
@ -63,11 +63,13 @@ msgid ""
"reached this point has generated a certain revenue. You can get revenue " "reached this point has generated a certain revenue. You can get revenue "
"statistics in the Reporting section" "statistics in the Reporting section"
msgstr "" msgstr ""
"تعيين الإيرادات المتوقعة إذا كنت ترى أن كل بند من بنود الحملة وصلت الى تلك "
"النقطة وولدت عائد معين. يمكنك الحصول على إحصاءات الإيرادات في قسم التقارير"
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign.transition,trigger:0 #: field:marketing.campaign.transition,trigger:0
msgid "Trigger" msgid "Trigger"
msgstr "" msgstr "زر"
#. module: marketing_campaign #. module: marketing_campaign
#: field:campaign.analysis,count:0 #: field:campaign.analysis,count:0
@ -77,7 +79,7 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign:0 #: view:marketing.campaign:0
msgid "Campaign Editor" msgid "Campaign Editor"
msgstr "" msgstr "محرر الحملة"
#. module: marketing_campaign #. module: marketing_campaign
#: view:campaign.analysis:0 #: view:campaign.analysis:0
@ -106,13 +108,13 @@ msgstr "كائن"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.segment:0 #: view:marketing.campaign.segment:0
msgid "Sync mode: only records created after last sync" msgid "Sync mode: only records created after last sync"
msgstr "" msgstr "نمط المزامنة: السجلات فقط التي تم إنشاؤها بعد آخر المزامنة"
#. module: marketing_campaign #. module: marketing_campaign
#: model:email.template,body_text:marketing_campaign.email_template_2 #: model:email.template,body_text:marketing_campaign.email_template_2
msgid "" msgid ""
"Hello, We are happy to announce that you now become our Silver Partner." "Hello, We are happy to announce that you now become our Silver Partner."
msgstr "" msgstr "مرحبا، ونحن سعداء أن نعلن أنك أصبحت الآن شريكاً فضياً لدينا."
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign:0 #: view:marketing.campaign:0
@ -123,7 +125,7 @@ msgstr "حفظ كمسودة"
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign.activity,to_ids:0 #: field:marketing.campaign.activity,to_ids:0
msgid "Next Activities" msgid "Next Activities"
msgstr "" msgstr "النشاطات التالية"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.segment:0 #: view:marketing.campaign.segment:0
@ -133,7 +135,7 @@ msgstr "مزامنة"
#. module: marketing_campaign #. module: marketing_campaign
#: sql_constraint:marketing.campaign.transition:0 #: sql_constraint:marketing.campaign.transition:0
msgid "The interval must be positive or zero" msgid "The interval must be positive or zero"
msgstr "" msgstr "الفترة يجب أن تكون إيجابية أو صفر"
#. module: marketing_campaign #. module: marketing_campaign
#: code:addons/marketing_campaign/marketing_campaign.py:818 #: code:addons/marketing_campaign/marketing_campaign.py:818
@ -145,7 +147,7 @@ msgstr "لا معاينة"
#: view:marketing.campaign.segment:0 #: view:marketing.campaign.segment:0
#: field:marketing.campaign.segment,date_run:0 #: field:marketing.campaign.segment,date_run:0
msgid "Launch Date" msgid "Launch Date"
msgstr "" msgstr "تاريخ الإنشاء"
#. module: marketing_campaign #. module: marketing_campaign
#: view:campaign.analysis:0 #: view:campaign.analysis:0
@ -156,7 +158,7 @@ msgstr "يوم"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.activity:0 #: view:marketing.campaign.activity:0
msgid "Outgoing Transitions" msgid "Outgoing Transitions"
msgstr "" msgstr "التنقلات الصادرة"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.workitem:0 #: view:marketing.campaign.workitem:0
@ -166,18 +168,18 @@ msgstr "إستعادة"
#. module: marketing_campaign #. module: marketing_campaign
#: help:marketing.campaign,object_id:0 #: help:marketing.campaign,object_id:0
msgid "Choose the resource on which you want this campaign to be run" msgid "Choose the resource on which you want this campaign to be run"
msgstr "" msgstr "اختيار المورد الذي تريده ليتم تشغيله لهذه الحملة"
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign.segment,sync_last_date:0 #: field:marketing.campaign.segment,sync_last_date:0
msgid "Last Synchronization" msgid "Last Synchronization"
msgstr "" msgstr "التزامن الأخير"
#. module: marketing_campaign #. module: marketing_campaign
#: code:addons/marketing_campaign/marketing_campaign.py:214 #: code:addons/marketing_campaign/marketing_campaign.py:214
#, python-format #, python-format
msgid "You can not duplicate a campaign, it's not supported yet." msgid "You can not duplicate a campaign, it's not supported yet."
msgstr "" msgstr "لا يمكنك تكرار الحملة، انها غير مدعومة حتى الآن."
#. module: marketing_campaign #. module: marketing_campaign
#: selection:marketing.campaign.transition,interval_type:0 #: selection:marketing.campaign.transition,interval_type:0
@ -189,7 +191,7 @@ msgstr "سنة/سنين"
msgid "" msgid ""
"Date on which this segment was synchronized last time (automatically or " "Date on which this segment was synchronized last time (automatically or "
"manually)" "manually)"
msgstr "" msgstr "التاريخ الذي تزامن هذا القطاع آخر مرة (آليا أو يدويا)"
#. module: marketing_campaign #. module: marketing_campaign
#: selection:campaign.analysis,state:0 #: selection:campaign.analysis,state:0
@ -216,11 +218,19 @@ msgid ""
"Normal - the campaign runs normally and automatically sends all emails and " "Normal - the campaign runs normally and automatically sends all emails and "
"reports (be very careful with this mode, you're live!)" "reports (be very careful with this mode, you're live!)"
msgstr "" msgstr ""
"اختبار - وهو يخلق ويعالج كافة الأنشطة مباشرة (دون انتظار تأخير على التحولات) "
"ولكن لا يرسل رسائل البريد الإلكتروني أو يعد التقارير.\n"
" اختبار في الوقت الحقيقي - وهو يخلق ويعالج جميع الأنشطة بشكل مباشر ولكن لا "
"يرسل رسائل البريد الإلكتروني أو يعد التقارير.\n"
" مع التأكيد اليدوي - يعمل الحملات عادة، ولكن المستخدم لديه للتحقق من صحة كل "
"بنود العمل يدويا.\n"
" طبيعي - يعمل الحملة بشكل طبيعي يرسل تلقائيا جميع رسائل البريد الإلكتروني "
"والتقارير (نكون حذرين للغاية مع هذا الوضع، أنت على الهواء!)"
#. module: marketing_campaign #. module: marketing_campaign
#: help:marketing.campaign.segment,date_run:0 #: help:marketing.campaign.segment,date_run:0
msgid "Initial start date of this segment." msgid "Initial start date of this segment."
msgstr "" msgstr "تاريخ بدء الأولي لهذا القطاع."
#. module: marketing_campaign #. module: marketing_campaign
#: view:campaign.analysis:0 #: view:campaign.analysis:0
@ -237,7 +247,7 @@ msgstr "حملة"
#. module: marketing_campaign #. module: marketing_campaign
#: model:email.template,subject:marketing_campaign.email_template_3 #: model:email.template,subject:marketing_campaign.email_template_3
msgid "Congratulation! You become our Gold Partner." msgid "Congratulation! You become our Gold Partner."
msgstr "" msgstr "تهنئة! لقد أصبحت شريكاً ذهبياً الآن."
#. module: marketing_campaign #. module: marketing_campaign
#: view:campaign.analysis:0 #: view:campaign.analysis:0
@ -251,7 +261,7 @@ msgstr "قطعة"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.activity:0 #: view:marketing.campaign.activity:0
msgid "Cost / Revenue" msgid "Cost / Revenue"
msgstr "" msgstr "التكلفة/العائد"
#. module: marketing_campaign #. module: marketing_campaign
#: help:marketing.campaign.activity,type:0 #: help:marketing.campaign.activity,type:0
@ -313,7 +323,7 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: view:campaign.analysis:0 #: view:campaign.analysis:0
msgid "Marketing Reports" msgid "Marketing Reports"
msgstr "" msgstr "تقارير التسويق"
#. module: marketing_campaign #. module: marketing_campaign
#: selection:marketing.campaign,state:0 #: selection:marketing.campaign,state:0
@ -342,7 +352,7 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign.segment,sync_mode:0 #: field:marketing.campaign.segment,sync_mode:0
msgid "Synchronization mode" msgid "Synchronization mode"
msgstr "" msgstr "نمط التزامن"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign:0 #: view:marketing.campaign:0
@ -353,7 +363,7 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign.activity,from_ids:0 #: field:marketing.campaign.activity,from_ids:0
msgid "Previous Activities" msgid "Previous Activities"
msgstr "" msgstr "النشاطات السابقة"
#. module: marketing_campaign #. module: marketing_campaign
#: help:marketing.campaign.segment,date_done:0 #: help:marketing.campaign.segment,date_done:0
@ -363,7 +373,7 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.workitem:0 #: view:marketing.campaign.workitem:0
msgid "Marketing Campaign Activities" msgid "Marketing Campaign Activities"
msgstr "" msgstr "نشاطات حملة التسويق"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.workitem:0 #: view:marketing.campaign.workitem:0
@ -427,7 +437,7 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: model:ir.model,name:marketing_campaign.model_marketing_campaign_segment #: model:ir.model,name:marketing_campaign.model_marketing_campaign_segment
msgid "Campaign Segment" msgid "Campaign Segment"
msgstr "" msgstr "حملة القطاع"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign:0 #: view:marketing.campaign:0
@ -456,7 +466,7 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign,fixed_cost:0 #: field:marketing.campaign,fixed_cost:0
msgid "Fixed Cost" msgid "Fixed Cost"
msgstr "" msgstr "التكلفة الثابتة"
#. module: marketing_campaign #. module: marketing_campaign
#: model:email.template,subject:marketing_campaign.email_template_2 #: model:email.template,subject:marketing_campaign.email_template_2
@ -466,12 +476,12 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign.segment:0 #: view:marketing.campaign.segment:0
msgid "Newly Modified" msgid "Newly Modified"
msgstr "" msgstr "تم التعديل حديثاً"
#. module: marketing_campaign #. module: marketing_campaign
#: field:marketing.campaign.transition,interval_nbr:0 #: field:marketing.campaign.transition,interval_nbr:0
msgid "Interval Value" msgid "Interval Value"
msgstr "" msgstr "قيمة الفترة"
#. module: marketing_campaign #. module: marketing_campaign
#: field:campaign.analysis,revenue:0 #: field:campaign.analysis,revenue:0
@ -512,22 +522,22 @@ msgstr ""
#. module: marketing_campaign #. module: marketing_campaign
#: model:ir.actions.act_window,name:marketing_campaign.act_marketing_campaing_followup #: model:ir.actions.act_window,name:marketing_campaign.act_marketing_campaing_followup
msgid "Campaign Follow-up" msgid "Campaign Follow-up"
msgstr "" msgstr "متابعة الحملة"
#. module: marketing_campaign #. module: marketing_campaign
#: help:marketing.campaign.activity,email_template_id:0 #: help:marketing.campaign.activity,email_template_id:0
msgid "The e-mail to send when this activity is activated" msgid "The e-mail to send when this activity is activated"
msgstr "" msgstr "بريد إلكتروني للإرسال عندما يتم تنشيط المنشط"
#. module: marketing_campaign #. module: marketing_campaign
#: view:marketing.campaign:0 #: view:marketing.campaign:0
msgid "Test Mode" msgid "Test Mode"
msgstr "" msgstr "نمط الإختبار"
#. module: marketing_campaign #. module: marketing_campaign
#: selection:marketing.campaign.segment,sync_mode:0 #: selection:marketing.campaign.segment,sync_mode:0
msgid "Only records modified after last sync (no duplicates)" msgid "Only records modified after last sync (no duplicates)"
msgstr "" msgstr "فقط السجلات المعدلة بعد آخر مزامنة (بدون تكرار)"
#. module: marketing_campaign #. module: marketing_campaign
#: model:ir.model,name:marketing_campaign.model_ir_actions_report_xml #: model:ir.model,name:marketing_campaign.model_ir_actions_report_xml

View File

@ -25,7 +25,6 @@
'depends': [ 'depends': [
'base', 'base',
'share', 'share',
'auth_anonymous',
'auth_signup', 'auth_signup',
], ],
'author': 'OpenERP SA', 'author': 'OpenERP SA',

View File

@ -19,34 +19,22 @@
# #
############################################################################## ##############################################################################
import tools
from osv import osv from osv import osv
import tools
class mail_mail(osv.Model):
class mail_mail_portal(osv.Model): """ Update of mail_mail class, to add the signin URL to notifications. """
""" Update of mail_mail class, to add the signin URL to notifications. _inherit = 'mail.mail'
"""
_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)
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None): 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 """ add a signin link inside the body of a mail.mail
is to be inherited by Portal, to add a link for signing in, in
each notification email a partner receives.
:param mail: mail.mail browse_record :param mail: mail.mail browse_record
:param partner: browse_record of the specific recipient partner :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: if partner:
portal_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_portal') context = dict(context or {}, signup_valid=True)
portal_id = portal_ref and portal_ref[1] or False partner = self.pool.get('res.partner').browse(cr, uid, partner.id, context)
url = self._generate_signin_url(cr, uid, partner.id, portal_id, 1234, context=context) body = tools.append_content_to_html(body, "Log in our portal at: %s" % partner.signup_url)
body = tools.append_content_to_html(mail.body_html, url) return body
return body
else:
return super(mail_mail_portal, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context)

View File

@ -5,7 +5,7 @@
<!-- Top menu item --> <!-- Top menu item -->
<menuitem name="Portal" <menuitem name="Portal"
id="portal_menu" 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"/> sequence="20"/>
<menuitem name="Our company" id="portal_company" parent="portal_menu" sequence="10"/> <menuitem name="Our company" id="portal_company" parent="portal_menu" sequence="10"/>

View File

@ -33,6 +33,7 @@ class test_portal(test_mail.TestMailMockups):
self.mail_group = self.registry('mail.group') self.mail_group = self.registry('mail.group')
self.mail_mail = self.registry('mail.mail') self.mail_mail = self.registry('mail.mail')
self.mail_message = self.registry('mail.message') self.mail_message = self.registry('mail.message')
self.mail_invite = self.registry('mail.wizard.invite')
self.res_users = self.registry('res.users') self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner') self.res_partner = self.registry('res.partner')
@ -41,19 +42,20 @@ class test_portal(test_mail.TestMailMockups):
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}) {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
# Find Portal group # Find Portal group
group_portal_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_portal') group_portal = self.registry('ir.model.data').get_object(cr, uid, 'portal', 'group_portal')
self.group_portal_id = group_portal_ref and group_portal_ref[1] or False self.group_portal_id = group_portal.id
# Create Chell (portal user) # 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_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) user_chell = self.res_users.browse(cr, uid, self.user_chell_id)
self.partner_chell_id = self.user_chell.partner_id.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'})
def test_00_access_rights(self): def test_00_access_rights(self):
""" Test basic mail_message and mail_group access rights for portal users. """ """ Test basic mail_message and mail_group access rights for portal users. """
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
partner_chell_id = self.partner_chell_id
user_chell_id = self.user_chell_id
# Prepare group: Pigs (portal) # Prepare group: Pigs (portal)
self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message') self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message')
@ -64,56 +66,56 @@ class test_portal(test_mail.TestMailMockups):
# ---------------------------------------- # ----------------------------------------
# Do: Chell reads Pigs messages, ok because restricted to portal group # 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'] message_ids = self.mail_group.read(cr, self.user_chell_id, self.group_pigs_id, ['message_ids'])['message_ids']
self.mail_message.read(cr, user_chell_id, 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 # Do: Chell posts a message on Pigs, crash because can not write on group or is not in the followers
self.assertRaises(except_orm, with self.assertRaises(except_orm):
self.mail_group.message_post, self.mail_group.message_post(cr, self.user_chell_id, self.group_pigs_id, body='Message')
cr, user_chell_id, self.group_pigs_id, body='Message')
# Do: Chell is added to Pigs followers # 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 # 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): def test_50_mail_invite(self):
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
user_admin = self.res_users.browse(cr, uid, 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='') 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 # 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 # 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 # Do: create a mail_wizard_invite, validate it
self._init_mock_build_email() 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]) 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 # 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] 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') self.assertEqual(set(follower_ids), set([partner_admin_id, partner_bert_id]), 'Pigs followers after invite is incorrect')
# Test: sent email subject, body
# 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') 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: 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('subject'), 'Invitation to follow Pigs',
self.assertEqual(sent_email.get('body'), _sent_email_body, 'sent email body incorrect') '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')

View File

@ -30,17 +30,19 @@ from openerp import SUPERUSER_ID
from base.res.res_partner import _lang_get from base.res.res_partner import _lang_get
_logger = logging.getLogger(__name__) _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) # (note that calling '_' has no effect except exporting those strings for translation)
WELCOME_EMAIL_SUBJECT = _("Your OpenERP account at %(company)s") WELCOME_EMAIL_SUBJECT = _("Your OpenERP account at %(company)s")
WELCOME_EMAIL_BODY = _("""Dear %(name)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: Your login account data is:
Database: %(db)s Database: %(db)s
User: %(login)s Username: %(login)s
Password: %(password)s
In order to complete the signin process, click on the following url:
%(url)s
%(welcome_message)s %(welcome_message)s
@ -49,28 +51,10 @@ OpenERP - Open Source Business Applications
http://www.openerp.com 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(): def random_password():
# get 3 uppercase letters, 3 lowercase letters, 2 digits, and shuffle them # temporary random stuff; user password is reset by signup process
chars = map(random.choice, [_PASSU] * 3 + [_PASSL] * 3 + [_PASSD] * 2) chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
random.shuffle(chars) return ''.join(random.choice(chars) for i in xrange(12))
return ''.join(chars)
def extract_email(email): def extract_email(email):
""" extract the email address from a user-friendly email address """ """ 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'), 'user_ids': fields.one2many('portal.wizard.user', 'wizard_id', string='Users'),
'welcome_message': fields.text(string='Invitation Message', 'welcome_message': fields.text(string='Invitation Message',
help="This text is included in the email sent to new users of the portal."), 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): 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) user = self._create_user(cr, SUPERUSER_ID, wizard_user, context)
if (not user.active) or (portal not in user.groups_id): if (not user.active) or (portal not in user.groups_id):
user.write({'active': True, 'groups_id': [(4, portal.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) wizard_user = self.browse(cr, SUPERUSER_ID, wizard_user.id, context)
self._send_email(cr, uid, wizard_user, context) self._send_email(cr, uid, wizard_user, context)
else: else:
@ -174,8 +158,6 @@ class wizard_user(osv.osv_memory):
user.write({'groups_id': [(3, portal.id)], 'active': False}) user.write({'groups_id': [(3, portal.id)], 'active': False})
else: else:
user.write({'groups_id': [(3, portal.id)]}) 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): def _retrieve_user(self, cr, uid, wizard_user, context=None):
""" retrieve the (possibly inactive) user corresponding to wizard_user.partner_id """ 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) return res_users.browse(cr, uid, user_id, context)
def _send_email(self, cr, uid, wizard_user, context=None): 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 @param wizard_user: browse record of model portal.wizard.user
@return: the id of the created mail.mail record @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.')) _('You must have an email address in your User Preferences to send emails.'))
# determine subject and body in the portal user's language # 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) user = self._retrieve_user(cr, SUPERUSER_ID, wizard_user, context)
context = dict(this_context or {}, lang=user.lang) context = dict(this_context or {}, lang=user.lang)
data = { data = {
'company': this_user.company_id.name, 'company': this_user.company_id.name,
'portal': wizard_user.wizard_id.portal_id.name, 'portal': wizard_user.wizard_id.portal_id.name,
'welcome_message': wizard_user.wizard_id.welcome_message or "", '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, 'db': cr.dbname,
'name': user.name,
'login': user.login, 'login': user.login,
'password': user.password, 'url': user.signup_url,
'name': user.name
} }
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_mail = self.pool.get('mail.mail')
mail_values = { mail_values = {
'email_from': this_user.email, 'email_from': this_user.email,
'email_to': user.email, 'email_to': user.email,
'subject': subject, 'subject': _(WELCOME_EMAIL_SUBJECT) % data,
'body_html': '<pre>%s</pre>' % body, 'body_html': '<pre>%s</pre>' % (_(WELCOME_EMAIL_BODY) % data),
'state': 'outgoing', 'state': 'outgoing',
} }
return mail_mail.create(cr, uid, mail_values, context=this_context) return mail_mail.create(cr, uid, mail_values, context=this_context)

View File

@ -27,8 +27,6 @@
<field name="user_ids"/> <field name="user_ids"/>
<field name="welcome_message" <field name="welcome_message"
placeholder="This text is included in the email sent to new portal users."/> 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> <footer>
<button string="Apply" name="action_apply" type="object" class="oe_highlight"/> <button string="Apply" name="action_apply" type="object" class="oe_highlight"/>
or or

View File

@ -19,12 +19,14 @@
# #
############################################################################## ##############################################################################
from base_status.base_stage import base_stage import time
from datetime import datetime, date
from lxml import etree from lxml import etree
from datetime import datetime, date
import tools
from base_status.base_stage import base_stage
from osv import fields, osv from osv import fields, osv
from openerp.addons.resource.faces import task as Task from openerp.addons.resource.faces import task as Task
import time
from tools.translate import _ from tools.translate import _
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
@ -1173,11 +1175,10 @@ class task(base_stage, osv.osv):
""" Override to updates the document according to the email. """ """ Override to updates the document according to the email. """
if custom_values is None: custom_values = {} if custom_values is None: custom_values = {}
custom_values.update({ custom_values.update({
'name': subject, 'name': msg.get('subject'),
'planned_hours': 0.0, 'planned_hours': 0.0,
'subject': msg.get('subject'),
}) })
return super(project_tasks,self).message_new(cr, uid, msg, custom_values=custom_values, context=context) return super(task,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
""" Override to update the task according to the email. """ """ Override to update the task according to the email. """
@ -1202,7 +1203,7 @@ class task(base_stage, osv.osv):
act = 'do_%s' % res.group(2).lower() act = 'do_%s' % res.group(2).lower()
if act: if act:
getattr(self,act)(cr, uid, ids, context=context) getattr(self,act)(cr, uid, ids, context=context)
return super(project_tasks,self).message_update(cr, uid, msg, update_vals=update_vals, context=context) return super(task,self).message_update(cr, uid, msg, update_vals=update_vals, context=context)
# --------------------------------------------------- # ---------------------------------------------------
# OpenChatter methods and notifications # OpenChatter methods and notifications

View File

@ -163,7 +163,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Project"> <search string="Search Project">
<field name="complete_name" string="Project Name"/> <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/> <separator/>
<filter icon="terp-check" string="Open" name="Current" domain="[('state', '=','open')]" help="Open Projects"/> <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"/> <filter icon="gtk-media-pause" string="Pending" name="Pending" domain="[('state', '=','pending')]" help="Pending Projects"/>
@ -619,7 +619,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Tasks"> <search string="Tasks">
<field name="name" 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/> <separator/>
<filter name="draft" string="New" domain="[('state','=','draft')]" help="New Tasks" icon="terp-check"/> <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"/> <filter name="open" string="In Progress" domain="[('state','=','open')]" help="In Progress Tasks" icon="terp-camera_test"/>

View File

@ -27,6 +27,7 @@ from tools.translate import _
import binascii import binascii
import time import time
import tools import tools
from tools import html2plaintext
class project_issue_version(osv.osv): class project_issue_version(osv.osv):
_name = "project.issue.version" _name = "project.issue.version"
@ -462,9 +463,11 @@ class project_issue(base_stage, osv.osv):
if context is None: context = {} if context is None: context = {}
context['state_to'] = 'draft' context['state_to'] = 'draft'
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
custom_values.update({ custom_values.update({
'name': msg.get('subject') or _("No Subject"), 'name': msg.get('subject') or _("No Subject"),
'description': msg.get('body'), 'description': desc,
'email_from': msg.get('from'), 'email_from': msg.get('from'),
'email_cc': msg.get('cc'), 'email_cc': msg.get('cc'),
'user_id': False, 'user_id': False,
@ -486,7 +489,6 @@ class project_issue(base_stage, osv.osv):
if update_vals is None: update_vals = {} if update_vals is None: update_vals = {}
# Update doc values according to the message # Update doc values according to the message
update_vals['description'] = msg.get('body', '')
if msg.get('priority'): if msg.get('priority'):
update_vals['priority'] = msg.get('priority') update_vals['priority'] = msg.get('priority')
# Parse 'body' to find values to update # Parse 'body' to find values to update

View File

@ -198,7 +198,7 @@
<search string="Issue Tracker Search"> <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="name" string="Issue" filter_domain="['|', '|',('partner_id','ilike',self),('email_from','ilike',self),('name','ilike',self)]"/>
<field name="id"/> <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/> <separator/>
<filter string="New" icon="terp-document-new" domain="[('state','=','draft')]" help="New Issues"/> <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"/> <filter string="To Do" domain="[('state','=','open')]" help="To Do Issues" icon="terp-check"/>

View File

@ -7,6 +7,7 @@
<record id="project.project_project_1" model="project.project"> <record id="project.project_project_1" model="project.project">
<field name="resource_calendar_id" ref="resource.timesheet_group1"/> <field name="resource_calendar_id" ref="resource.timesheet_group1"/>
<field name="use_phases" eval="True"/>
</record> </record>
<!-- Project Phases --> <!-- Project Phases -->

View File

@ -45,7 +45,6 @@
Create a timesheet sheet for HR manager Create a timesheet sheet for HR manager
- -
!record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_sheetforhrmanager0}: !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_from: !eval "'%s-05-01' %(datetime.now().year)"
date_to: !eval "'%s-05-31' %(datetime.now().year)" date_to: !eval "'%s-05-31' %(datetime.now().year)"
name: Sheet for hr manager name: Sheet for hr manager

View File

@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.4\n" "Project-Id-Version: OpenERP Server 5.0.4\n"
"Report-Msgid-Bugs-To: support@openerp.com\n" "Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-02-08 01:37+0100\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" "Last-Translator: kifcaliph <Unknown>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-09-07 04:56+0000\n" "X-Launchpad-Export-Date: 2012-10-09 04:51+0000\n"
"X-Generator: Launchpad (build 15914)\n" "X-Generator: Launchpad (build 16112)\n"
#. module: purchase #. module: purchase
#: model:process.transition,note:purchase.process_transition_confirmingpurchaseorder0 #: model:process.transition,note:purchase.process_transition_confirmingpurchaseorder0
@ -45,7 +45,7 @@ msgstr "المقصد"
#: code:addons/purchase/purchase.py:236 #: code:addons/purchase/purchase.py:236
#, python-format #, python-format
msgid "In order to delete a purchase order, it must be cancelled first!" msgid "In order to delete a purchase order, it must be cancelled first!"
msgstr "" msgstr "من أجل حذف أمر الشراء، يجب الإلغاء أولاً!"
#. module: purchase #. module: purchase
#: help:purchase.report,date:0 #: help:purchase.report,date:0
@ -87,7 +87,7 @@ msgstr ""
#. module: purchase #. module: purchase
#: view:purchase.order:0 #: view:purchase.order:0
msgid "Approved purchase order" msgid "Approved purchase order"
msgstr "" msgstr "الموافقة على أمر الشراء"
#. module: purchase #. module: purchase
#: view:purchase.order:0 field:purchase.order,partner_id:0 #: view:purchase.order:0 field:purchase.order,partner_id:0
@ -149,7 +149,7 @@ msgstr "لا يوجد قائمة اسعار !"
#. module: purchase #. module: purchase
#: model:ir.model,name:purchase.model_purchase_config_wizard #: model:ir.model,name:purchase.model_purchase_config_wizard
msgid "purchase.config.wizard" msgid "purchase.config.wizard"
msgstr "" msgstr "شراء.ضبط.صندوق حوار"
#. module: purchase #. module: purchase
#: view:board.board:0 model:ir.actions.act_window,name:purchase.purchase_draft #: view:board.board:0 model:ir.actions.act_window,name:purchase.purchase_draft
@ -249,7 +249,7 @@ msgstr "يوم"
#. module: purchase #. module: purchase
#: selection:purchase.order,invoice_method:0 #: selection:purchase.order,invoice_method:0
msgid "Based on generated draft invoice" msgid "Based on generated draft invoice"
msgstr "" msgstr "على أساس توليد مسودة فاتورة"
#. module: purchase #. module: purchase
#: view:purchase.report:0 #: view:purchase.report:0
@ -259,7 +259,7 @@ msgstr "ترتيب اليوم"
#. module: purchase #. module: purchase
#: view:board.board:0 #: view:board.board:0
msgid "Monthly Purchases by Category" msgid "Monthly Purchases by Category"
msgstr "" msgstr "مشتريات شهرية حسب الفئة"
#. module: purchase #. module: purchase
#: model:ir.actions.act_window,name:purchase.action_purchase_line_product_tree #: model:ir.actions.act_window,name:purchase.action_purchase_line_product_tree
@ -269,7 +269,7 @@ msgstr "المشتريات"
#. module: purchase #. module: purchase
#: view:purchase.order:0 #: view:purchase.order:0
msgid "Purchase order which are in draft state" msgid "Purchase order which are in draft state"
msgstr "" msgstr "أمر الشراء التي هي في حالة مسودة"
#. module: purchase #. module: purchase
#: view:purchase.order:0 #: view:purchase.order:0
@ -392,7 +392,7 @@ msgstr "حركة مخزن"
#: code:addons/purchase/purchase.py:419 #: code:addons/purchase/purchase.py:419
#, python-format #, python-format
msgid "You must first cancel all invoices related to this purchase order." msgid "You must first cancel all invoices related to this purchase order."
msgstr "" msgstr "يجب عليك أولا إلغاء جميع الفواتير المتعلقة بهذا أمر الشراء."
#. module: purchase #. module: purchase
#: field:purchase.report,dest_address_id:0 #: field:purchase.report,dest_address_id:0
@ -436,13 +436,13 @@ msgstr "تم التحقق من الصلاحية عن طريق"
#. module: purchase #. module: purchase
#: view:purchase.report:0 #: view:purchase.report:0
msgid "Order in last month" msgid "Order in last month"
msgstr "" msgstr "طلب في الشهر الماضي"
#. module: purchase #. module: purchase
#: code:addons/purchase/purchase.py:412 #: code:addons/purchase/purchase.py:412
#, python-format #, python-format
msgid "You must first cancel all receptions related to this purchase order." msgid "You must first cancel all receptions related to this purchase order."
msgstr "" msgstr "يجب عليك أولا إلغاء جميع حفلات الاستقبال المتعلقة بهذا أمر الشراء."
#. module: purchase #. module: purchase
#: selection:purchase.order.line,state:0 #: selection:purchase.order.line,state:0
@ -467,7 +467,7 @@ msgstr "توضح انه قد تم عمل الاختيار"
#. module: purchase #. module: purchase
#: view:purchase.order:0 #: view:purchase.order:0
msgid "Purchase orders which are in exception state" msgid "Purchase orders which are in exception state"
msgstr "" msgstr "أوامر الشراء التي هي في حالة الاستثناء"
#. module: purchase #. module: purchase
#: report:purchase.order:0 field:purchase.report,validator:0 #: 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 #: model:ir.ui.menu,name:purchase.menu_action_picking_tree4_picking_to_invoice
#: selection:purchase.order,invoice_method:0 #: selection:purchase.order,invoice_method:0
msgid "Based on receptions" msgid "Based on receptions"
msgstr "" msgstr "استناداً على ما تم استقباله"
#. module: purchase #. module: purchase
#: constraint:res.company:0 #: constraint:res.company:0
@ -541,7 +541,7 @@ msgstr ""
#. module: purchase #. module: purchase
#: view:purchase.order:0 #: view:purchase.order:0
msgid "Purchase order which are in the exception state" msgid "Purchase order which are in the exception state"
msgstr "" msgstr "أمر الشراء التي هي في حالة استثناء"
#. module: purchase #. module: purchase
#: model:ir.actions.act_window,help:purchase.action_stock_move_report_po #: model:ir.actions.act_window,help:purchase.action_stock_move_report_po
@ -597,7 +597,7 @@ msgstr "السعر الإجمالي"
#. module: purchase #. module: purchase
#: model:ir.actions.act_window,name:purchase.action_import_create_supplier_installer #: model:ir.actions.act_window,name:purchase.action_import_create_supplier_installer
msgid "Create or Import Suppliers" msgid "Create or Import Suppliers"
msgstr "" msgstr "إنشاء أو استيراد الموردون"
#. module: purchase #. module: purchase
#: view:stock.picking:0 #: view:stock.picking:0
@ -662,7 +662,7 @@ msgstr ""
#. module: purchase #. module: purchase
#: report:purchase.order:0 #: report:purchase.order:0
msgid "Purchase Order Confirmation N°" msgid "Purchase Order Confirmation N°"
msgstr "" msgstr "تأكيد أمر الشراء ن°"
#. module: purchase #. module: purchase
#: model:ir.actions.act_window,help:purchase.action_purchase_order_report_all #: model:ir.actions.act_window,help:purchase.action_purchase_order_report_all
@ -748,7 +748,7 @@ msgstr "الاستقبالات"
#: code:addons/purchase/purchase.py:285 #: code:addons/purchase/purchase.py:285
#, python-format #, python-format
msgid "You cannot confirm a purchase order without any lines." msgid "You cannot confirm a purchase order without any lines."
msgstr "" msgstr "لا يمكنك تأكيد أمر الشراء دون أية أسطر."
#. module: purchase #. module: purchase
#: model:ir.actions.act_window,help:purchase.action_invoice_pending #: model:ir.actions.act_window,help:purchase.action_invoice_pending
@ -784,7 +784,7 @@ msgstr "يناير"
#. module: purchase #. module: purchase
#: model:ir.actions.server,name:purchase.ir_actions_server_edi_purchase #: model:ir.actions.server,name:purchase.ir_actions_server_edi_purchase
msgid "Auto-email confirmed purchase orders" msgid "Auto-email confirmed purchase orders"
msgstr "" msgstr "البريد التلقائي لتأكيد طلبات الشراء"
#. module: purchase #. module: purchase
#: model:process.transition,name:purchase.process_transition_approvingpurchaseorder0 #: model:process.transition,name:purchase.process_transition_approvingpurchaseorder0
@ -840,7 +840,7 @@ msgstr "دمج امر الشراء"
#. module: purchase #. module: purchase
#: view:purchase.report:0 #: view:purchase.report:0
msgid "Order in current month" msgid "Order in current month"
msgstr "" msgstr "طلب في الشهر الحالي"
#. module: purchase #. module: purchase
#: view:purchase.report:0 field:purchase.report,delay_pass:0 #: view:purchase.report:0 field:purchase.report,delay_pass:0
@ -881,7 +881,7 @@ msgstr "خطوط الاوامر الكلية للمستخدم لكل شهر"
#. module: purchase #. module: purchase
#: view:purchase.order:0 #: view:purchase.order:0
msgid "Approved purchase orders" msgid "Approved purchase orders"
msgstr "" msgstr "تأكيد أوامر الشراء"
#. module: purchase #. module: purchase
#: view:purchase.report:0 field:purchase.report,month:0 #: view:purchase.report:0 field:purchase.report,month:0
@ -891,7 +891,7 @@ msgstr "شهر"
#. module: purchase #. module: purchase
#: model:email.template,subject:purchase.email_template_edi_purchase #: model:email.template,subject:purchase.email_template_edi_purchase
msgid "${object.company_id.name} Order (Ref ${object.name or 'n/a' })" 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 #. module: purchase
#: report:purchase.quotation:0 #: report:purchase.quotation:0
@ -932,7 +932,7 @@ msgstr "ـكون هذه القائمة المختارة التي تم جمعها
#. module: purchase #. module: purchase
#: view:stock.picking:0 #: view:stock.picking:0
msgid "Is a Back Order" msgid "Is a Back Order"
msgstr "" msgstr "طلب عودة"
#. module: purchase #. module: purchase
#: model:process.node,note:purchase.process_node_invoiceafterpacking0 #: model:process.node,note:purchase.process_node_invoiceafterpacking0
@ -984,7 +984,7 @@ msgstr ""
#. module: purchase #. module: purchase
#: selection:purchase.config.wizard,default_method:0 #: selection:purchase.config.wizard,default_method:0
msgid "Pre-Generate Draft Invoices based on Purchase Orders" msgid "Pre-Generate Draft Invoices based on Purchase Orders"
msgstr "" msgstr "قبل انشاء مسودة فاتورة على طلبات الشراء"
#. module: purchase #. module: purchase
#: model:ir.actions.act_window,name:purchase.action_view_purchase_line_invoice #: model:ir.actions.act_window,name:purchase.action_view_purchase_line_invoice
@ -1010,7 +1010,7 @@ msgstr "عرض النتيجة"
#. module: purchase #. module: purchase
#: selection:purchase.config.wizard,default_method:0 #: selection:purchase.config.wizard,default_method:0
msgid "Based on Purchase Order Lines" msgid "Based on Purchase Order Lines"
msgstr "" msgstr "اعتماداً على سطور طلب الشراء"
#. module: purchase #. module: purchase
#: help:purchase.order,amount_untaxed:0 #: help:purchase.order,amount_untaxed:0
@ -1130,7 +1130,7 @@ msgstr "مرشحات مفصلة..."
#. module: purchase #. module: purchase
#: view:purchase.config.wizard:0 #: view:purchase.config.wizard:0
msgid "Invoicing Control on Purchases" msgid "Invoicing Control on Purchases"
msgstr "" msgstr "التحكم في الفاتورة عند الشراء"
#. module: purchase #. module: purchase
#: code:addons/purchase/wizard/purchase_order_group.py:48 #: code:addons/purchase/wizard/purchase_order_group.py:48

View File

@ -323,7 +323,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Purchase Order"> <search string="Search Purchase Order">
<field name="name" string="Reference"/> <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/> <separator/>
<filter icon="terp-document-new" name="draft" string="Quotations" domain="[('state','=','draft')]" help="Purchase orders which are in draft state"/> <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"/> <filter icon="terp-check" name="approved" string="Purchase Orders" domain="[('state','not in',('draft','cancel'))]" help="Approved purchase orders"/>

View File

@ -337,7 +337,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Sales Order"> <search string="Search Sales Order">
<field name="name" string="Sales Order" filter_domain="['|',('name','ilike',self),('client_order_ref','ilike',self)]"/> <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/> <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-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'))]"/> <filter icon="terp-check" string="Sales" name="sales" domain="[('state','in',('manual','progress'))]"/>

View File

@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openobject-addons\n" "Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-02-08 01:37+0100\n" "POT-Creation-Date: 2012-02-08 01:37+0100\n"
"PO-Revision-Date: 2012-01-13 21:14+0000\n" "PO-Revision-Date: 2012-10-08 15:06+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: almodhesh <Unknown>\n"
"Language-Team: Arabic <ar@li.org>\n" "Language-Team: Arabic <ar@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-08-28 06:34+0000\n" "X-Launchpad-Export-Date: 2012-10-09 04:51+0000\n"
"X-Generator: Launchpad (build 15864)\n" "X-Generator: Launchpad (build 16112)\n"
#. module: share #. module: share
#: field:share.wizard,embed_option_title:0 #: field:share.wizard,embed_option_title:0
@ -25,7 +25,7 @@ msgstr "عرض العنوان"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
msgid "Access granted!" msgid "Access granted!"
msgstr "" msgstr "منح الوصول!"
#. module: share #. module: share
#: field:share.wizard,user_type:0 #: field:share.wizard,user_type:0
@ -50,13 +50,15 @@ msgstr "مشاركة"
#. module: share #. module: share
#: field:share.wizard,share_root_url:0 #: field:share.wizard,share_root_url:0
msgid "Share Access URL" msgid "Share Access URL"
msgstr "" msgstr "مشاركة الوصول URL"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:782 #: code:addons/share/wizard/share_wizard.py:782
#, python-format #, python-format
msgid "You may use your current login (%s) and password to view them.\n" msgid "You may use your current login (%s) and password to view them.\n"
msgstr "" msgstr ""
"يمكنك استخدام معلومات تسجيل الدخول الخاصة بك الحالية(%s) وكلمة المرور "
"لعرضها.\n"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:601 #: code:addons/share/wizard/share_wizard.py:601
@ -83,24 +85,25 @@ msgstr ""
#. module: share #. module: share
#: field:share.wizard,embed_url:0 field:share.wizard.result.line,share_url:0 #: field:share.wizard,embed_url:0 field:share.wizard.result.line,share_url:0
msgid "Share URL" msgid "Share URL"
msgstr "" msgstr "مشاركة URL"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:776 #: code:addons/share/wizard/share_wizard.py:776
#, python-format #, python-format
msgid "These are your credentials to access this protected area:\n" msgid "These are your credentials to access this protected area:\n"
msgstr "" msgstr "هذا هو اعتمادك للوصول إلى هذه المناطق المحمية\n"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:643 #: code:addons/share/wizard/share_wizard.py:643
#, python-format #, python-format
msgid "You must be a member of the Share/User group to use the share wizard" msgid "You must be a member of the Share/User group to use the share wizard"
msgstr "" msgstr ""
"يجب أن تكون عضوا في المجموعة مشاركة / المستخدم لاستخدام مشاركة صندوق الحوار"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
msgid "Access info" msgid "Access info"
msgstr "" msgstr "معلومات الولوج"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
@ -111,12 +114,12 @@ msgstr "حصة"
#: code:addons/share/wizard/share_wizard.py:551 #: code:addons/share/wizard/share_wizard.py:551
#, python-format #, python-format
msgid "(Duplicated for modified sharing permissions)" msgid "(Duplicated for modified sharing permissions)"
msgstr "" msgstr "(تكرار للحصول على أذونات مشاركة تعديل)"
#. module: share #. module: share
#: help:share.wizard,domain:0 #: help:share.wizard,domain:0
msgid "Optional domain for further data filtering" msgid "Optional domain for further data filtering"
msgstr "" msgstr "المجال اختياري لمزيد من بيانات التصفية"
#. module: share #. module: share
#: sql_constraint:res.users:0 #: sql_constraint:res.users:0
@ -159,7 +162,7 @@ msgstr "إغلاق"
#: code:addons/share/wizard/share_wizard.py:640 #: code:addons/share/wizard/share_wizard.py:640
#, python-format #, python-format
msgid "Action and Access Mode are required to create a shared access" msgid "Action and Access Mode are required to create a shared access"
msgstr "" msgstr "نمط الإجراء والوصول لإنشاء مشاركة الوصول"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
@ -167,6 +170,7 @@ msgid ""
"Please select the action that opens the screen containing the data you want " "Please select the action that opens the screen containing the data you want "
"to share." "to share."
msgstr "" msgstr ""
"الرجاء اختيار الإجراء الذي يفتح شاشة تحتوي على البيانات التي تريد مشاركتها."
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:781 #: code:addons/share/wizard/share_wizard.py:781
@ -174,7 +178,7 @@ msgstr ""
msgid "" msgid ""
"The documents have been automatically added to your current OpenERP " "The documents have been automatically added to your current OpenERP "
"documents.\n" "documents.\n"
msgstr "" msgstr "المستندات تمت إضافتها بشكل تلقائي لمستندات اوبن اي ار بي الخاصة بك\n"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
@ -184,7 +188,7 @@ msgstr "إلغاء"
#. module: share #. module: share
#: field:res.groups,share:0 #: field:res.groups,share:0
msgid "Share Group" msgid "Share Group"
msgstr "" msgstr "مشاركة المجموعة"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:763 #: code:addons/share/wizard/share_wizard.py:763
@ -197,12 +201,12 @@ msgstr "لا بدّ من ذكر عنوان بريد إلكتروني"
msgid "" msgid ""
"Optionally, you may specify an additional domain restriction that will be " "Optionally, you may specify an additional domain restriction that will be "
"applied to the shared data." "applied to the shared data."
msgstr "" msgstr "اختيارياً، اختيار محدودية مجال لتطبيقها على المعلومات المشاركة."
#. module: share #. module: share
#: help:share.wizard,name:0 #: help:share.wizard,name:0
msgid "Title for the share (displayed to users as menu and shortcut name)" msgid "Title for the share (displayed to users as menu and shortcut name)"
msgstr "" msgstr "عنوان المشاركة (تعرض للمستخدمين كقائمة واسم مختصر)"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
@ -212,7 +216,7 @@ msgstr "خيارات"
#. module: share #. module: share
#: view:res.groups:0 #: view:res.groups:0
msgid "Regular groups only (no share groups" msgid "Regular groups only (no share groups"
msgstr "" msgstr "المجموعات المنتظمة فقط( لا مجموعات مشاركة)"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:787 #: code:addons/share/wizard/share_wizard.py:787
@ -222,27 +226,32 @@ msgid ""
"Sales, HR, etc.)\n" "Sales, HR, etc.)\n"
"It is open source and can be found on http://www.openerp.com." "It is open source and can be found on http://www.openerp.com."
msgstr "" msgstr ""
"OpenERP هي قوية ومجموعة من تطبيقات الأعمال سهل الاستعمال(إدارة علاقات "
"العملاء,المبيعات, موارد بشرية, الخ.)\n"
"هي مفتوحة المصدر ويمكنك الوصول إليها عن طريق هذا الرابط "
"http://www.openerp.com."
#. module: share #. module: share
#: field:share.wizard,action_id:0 #: field:share.wizard,action_id:0
msgid "Action to share" msgid "Action to share"
msgstr "" msgstr "إجراء للمشاركة"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
msgid "Optional: include a personal message" msgid "Optional: include a personal message"
msgstr "" msgstr "اختياري: تضمين رسالة خاصة"
#. module: share #. module: share
#: field:res.users,share:0 #: field:res.users,share:0
msgid "Share User" msgid "Share User"
msgstr "" msgstr "مشاركة المستخدم"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:647 #: code:addons/share/wizard/share_wizard.py:647
#, python-format #, python-format
msgid "Please indicate the emails of the persons to share with, one per line" msgid "Please indicate the emails of the persons to share with, one per line"
msgstr "" msgstr ""
"يرجى الإشارة إلى رسائل البريد الإلكتروني للأشخاص للمشاركة معه، واحد في كل سطر"
#. module: share #. module: share
#: field:share.wizard,embed_code:0 field:share.wizard.result.line,user_id:0 #: field:share.wizard,embed_code:0 field:share.wizard.result.line,user_id:0
@ -252,13 +261,13 @@ msgstr "مجهول"
#. module: share #. module: share
#: help:res.groups,share:0 #: help:res.groups,share:0
msgid "Group created to set access rights for sharing data with some users." msgid "Group created to set access rights for sharing data with some users."
msgstr "" msgstr "إنشاء مجموعة لتعيين حقوق الوصول لتبادل البيانات مع بعض المستخدمين."
#. module: share #. module: share
#: help:share.wizard,action_id:0 #: help:share.wizard,action_id:0
msgid "" msgid ""
"The action that opens the screen containing the data you wish to share." "The action that opens the screen containing the data you wish to share."
msgstr "" msgstr "الإجراء الذي يفتح شاشة تحتوي على البيانات التي ترغب في مشاركتها."
#. module: share #. module: share
#: constraint:res.users:0 #: constraint:res.users:0
@ -270,12 +279,12 @@ msgstr ""
#: code:addons/share/wizard/share_wizard.py:526 #: code:addons/share/wizard/share_wizard.py:526
#, python-format #, python-format
msgid "(Copy for sharing)" msgid "(Copy for sharing)"
msgstr "" msgstr "(نسخة من المشاركة)"
#. module: share #. module: share
#: field:share.wizard.result.line,newly_created:0 #: field:share.wizard.result.line,newly_created:0
msgid "Newly created" msgid "Newly created"
msgstr "" msgstr "منشأة حديثاً"
#. module: share #. module: share
#: code:addons/share/wizard/share_wizard.py:616 #: code:addons/share/wizard/share_wizard.py:616
@ -292,7 +301,7 @@ msgstr ""
#. module: share #. module: share
#: help:share.wizard,share_root_url:0 #: help:share.wizard,share_root_url:0
msgid "Main access page for users that are granted shared access" msgid "Main access page for users that are granted shared access"
msgstr "" msgstr "صفحة الوصول الرئيسية للمستخدمين لضمان مشاركة الوصول"
#. module: share #. module: share
#: sql_constraint:res.groups:0 #: sql_constraint:res.groups:0
@ -343,12 +352,12 @@ msgstr "ملخّص"
#: code:addons/share/wizard/share_wizard.py:493 #: code:addons/share/wizard/share_wizard.py:493
#, python-format #, python-format
msgid "Copied access for sharing" msgid "Copied access for sharing"
msgstr "" msgstr "نسخ الوصول للمشاركة"
#. module: share #. module: share
#: model:ir.actions.act_window,name:share.action_share_wizard_step1 #: model:ir.actions.act_window,name:share.action_share_wizard_step1
msgid "Share your documents" msgid "Share your documents"
msgstr "" msgstr "مشاركة المستندات الخاصة بك"
#. module: share #. module: share
#: view:share.wizard:0 #: view:share.wizard:0
@ -370,22 +379,22 @@ msgstr "share.wizard.result.line"
#. module: share #. module: share
#: help:share.wizard,user_type:0 #: help:share.wizard,user_type:0
msgid "Select the type of user(s) you would like to share data with." msgid "Select the type of user(s) you would like to share data with."
msgstr "" msgstr "استخدم نوع المستخدم الذي تود مشاركة المعلومات معه"
#. module: share #. module: share
#: field:share.wizard,view_type:0 #: field:share.wizard,view_type:0
msgid "Current View Type" msgid "Current View Type"
msgstr "" msgstr "نوع عرض الحالي"
#. module: share #. module: share
#: selection:share.wizard,access_mode:0 #: selection:share.wizard,access_mode:0
msgid "Can view" msgid "Can view"
msgstr "" msgstr "امكانية المشاهدة"
#. module: share #. module: share
#: selection:share.wizard,access_mode:0 #: selection:share.wizard,access_mode:0
msgid "Can edit" msgid "Can edit"
msgstr "" msgstr "امكانية التعديل"
#. module: share #. module: share
#: help:share.wizard,message:0 #: help:share.wizard,message:0
@ -528,3 +537,10 @@ msgstr "مشاركة مع..."
#~ msgid "Share with these people (one e-mail per line)" #~ msgid "Share with these people (one e-mail per line)"
#~ msgstr "المشاركة مع هؤلاء الناس (بريد إلكتروني واحد في كل سطر)" #~ msgstr "المشاركة مع هؤلاء الناس (بريد إلكتروني واحد في كل سطر)"
#~ msgid ""
#~ "An optional personal message, to be included in the e-mail notification."
#~ msgstr "رسالة اختيارية شخصية، ليتم تضمينها في اخطار البريد الإلكتروني."
#~ msgid "Use this link"
#~ msgstr "استخدم هذا الرابط"