[MERGE] Merge with base branch tempalte-aja

bzr revid: jke@openerp.com-20131107140448-kp3daxcqvymdfy7r
This commit is contained in:
jke-openerp 2013-11-07 15:04:48 +01:00
commit b92220ea7d
20 changed files with 725 additions and 158 deletions

View File

@ -21,5 +21,6 @@
import base_calendar
import crm_meeting
import controllers
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -47,11 +47,18 @@ If you need to manage your meetings, you should install the CRM module.
'base_calendar_data.xml',
'crm_meeting_data.xml',
],
'js': [
'static/src/js/*.js'
],
'qweb': ['static/src/xml/*.xml'],
'css': [
'static/src/css/base_calender.css'
],
'test' : ['test/base_calendar_test.yml'],
'installable': True,
'application': True,
'auto_install': False,
'images': ['images/base_calendar1.jpeg','images/base_calendar2.jpeg','images/base_calendar3.jpeg','images/base_calendar4.jpeg',],
'images': ['images/base_calendar1.jpeg','images/base_calendar2.jpeg','images/base_calendar3.jpeg','images/base_calendar4.jpeg'],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -28,10 +28,9 @@ from openerp.tools.translate import _
import pytz
import re
import time
import hashlib
from openerp import tools, SUPERUSER_ID
import openerp.service.report
months = {
1: "January", 2: "February", 3: "March", 4: "April", \
5: "May", 6: "June", 7: "July", 8: "August", 9: "September", \
@ -143,95 +142,6 @@ def real_id2base_calendar_id(real_id, recurrent_date):
return '%d-%s' % (real_id, recurrent_date)
return real_id
html_invitation = """
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>%(name)s</title>
</head>
<body>
<table border="0" cellspacing="10" cellpadding="0" width="100%%"
style="font-family: Arial, Sans-serif; font-size: 14">
<tr>
<td width="100%%">Hello,</td>
</tr>
<tr>
<td width="100%%">You are invited for <i>%(company)s</i> Event.</td>
</tr>
<tr>
<td width="100%%">Below are the details of event. Hours and dates expressed in %(timezone)s time.</td>
</tr>
</table>
<table cellspacing="0" cellpadding="5" border="0" summary=""
style="width: 90%%; font-family: Arial, Sans-serif; border: 1px Solid #ccc; background-color: #f6f6f6">
<tr valign="center" align="center">
<td bgcolor="DFDFDF">
<h3>%(name)s</h3>
</td>
</tr>
<tr>
<td>
<table cellpadding="8" cellspacing="0" border="0"
style="font-size: 14" summary="Eventdetails" bgcolor="f6f6f6"
width="90%%">
<tr>
<td width="21%%">
<div><b>Start Date</b></div>
</td>
<td><b>:</b></td>
<td>%(start_date)s</td>
<td width="15%%">
<div><b>End Date</b></div>
</td>
<td><b>:</b></td>
<td width="25%%">%(end_date)s</td>
</tr>
<tr valign="top">
<td><b>Description</b></td>
<td><b>:</b></td>
<td colspan="3">%(description)s</td>
</tr>
<tr valign="top">
<td>
<div><b>Location</b></div>
</td>
<td><b>:</b></td>
<td colspan="3">%(location)s</td>
</tr>
<tr valign="top">
<td>
<div><b>Event Attendees</b></div>
</td>
<td><b>:</b></td>
<td colspan="3">
<div>
<div>%(attendees)s</div>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table border="0" cellspacing="10" cellpadding="0" width="100%%"
style="font-family: Arial, Sans-serif; font-size: 14">
<tr>
<td width="100%%">From:</td>
</tr>
<tr>
<td width="100%%">%(user)s</td>
</tr>
<tr valign="top">
<td width="100%%">-<font color="a7a7a7">-------------------------</font></td>
</tr>
<tr>
<td width="100%%"> <font color="a7a7a7">%(sign)s</font></td>
</tr>
</table>
</body>
</html>
"""
class calendar_attendee(osv.osv):
"""
@ -390,6 +300,8 @@ property or property parameter."),
multi='event_end_date'),
'ref': fields.reference('Event Ref', selection=openerp.addons.base.res.res_request.referencable_models, size=128),
'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
'access_token':fields.char('Invitation Token', size=256),
}
_defaults = {
'state': 'needs-action',
@ -503,53 +415,43 @@ property or property parameter."),
@param email_from: email address for user sending the mail
@return: True
"""
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
for att in self.browse(cr, uid, ids, context=context):
sign = att.sent_by_uid and att.sent_by_uid.signature or ''
sign = '<br>'.join(sign and sign.split('\n') or [])
res_obj = att.ref
mail_id = []
data_pool = self.pool.get('ir.model.data')
mail_pool = self.pool.get('mail.mail')
template_pool = self.pool.get('email.template')
local_context = context.copy()
color = {
'needs-action' : 'grey',
'accepted' :'green',
'tentative' :'#FFFF00',
'declined':'red',
'delegated':'grey'
}
for attendee in self.browse(cr, uid, ids, context=context):
res_obj = attendee.ref
if res_obj:
att_infos = []
sub = res_obj.name
other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
for att2 in self.browse(cr, uid, other_invitation_ids):
att_infos.append(((att2.user_id and att2.user_id.name) or \
(att2.partner_id and att2.partner_id.name) or \
att2.email) + ' - Status: ' + att2.state.title())
#dates and times are gonna be expressed in `tz` time (local timezone of the `uid`)
tz = context.get('tz', pytz.timezone('UTC'))
#res_obj.date and res_obj.date_deadline are in UTC in database so we use context_timestamp() to transform them in the `tz` timezone
date_start = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
date_stop = False
if res_obj.date_deadline:
date_stop = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
body_vals = {'name': res_obj.name,
'start_date': date_start,
'end_date': date_stop,
'timezone': tz,
'description': res_obj.description or '-',
'location': res_obj.location or '-',
'attendees': '<br>'.join(att_infos),
'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
'sign': sign,
'company': company
}
body = html_invitation % body_vals
if mail_to and email_from:
model,template_id = data_pool.get_object_reference(cr, uid, 'base_calendar', "crm_email_template_meeting_invitation")
model,act_id = data_pool.get_object_reference(cr, uid, 'base_calendar', "view_crm_meeting_calendar")
action_id = self.pool.get('ir.actions.act_window').search(cr, uid, [('view_id','=',act_id)], context=context)
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
body = template_pool.browse(cr, uid, template_id, context=context).body_html
if attendee.email and email_from:
ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
vals = {'email_from': email_from,
'email_to': mail_to,
'state': 'outgoing',
'subject': sub,
'body_html': body,
'auto_delete': True}
local_context['att_obj'] = attendee
local_context['color'] = color
local_context['action_id'] = action_id[0]
local_context['dbname'] = cr.dbname
local_context['base_url'] = base_url
vals = template_pool.generate_email(cr, uid, template_id, res_obj.id, context=local_context)
if ics_file:
vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
'datas_fname': 'invitation.ics',
'datas': str(ics_file).encode('base64')})]
self.pool.get('mail.mail').create(cr, uid, vals, context=context)
return True
'datas_fname': 'invitation.ics',
'datas': str(ics_file).encode('base64')})]
if not attendee.partner_id.opt_out:
mail_id.append(mail_pool.create(cr, uid, vals, context=context))
if mail_id:
return mail_pool.send(cr, uid, mail_id, context=context)
return False
def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
"""
@ -590,8 +492,14 @@ property or property parameter."),
"""
if context is None:
context = {}
return self.write(cr, uid, ids, {'state': 'accepted'}, context)
meeting_obj = self.pool.get('crm.meeting')
res = self.write(cr, uid, ids, {'state': 'accepted'}, context)
for attandee in self.browse(cr, uid, ids, context=context):
meeting_ids = meeting_obj.search(cr, uid, [('attendee_ids', '=', attandee.id)], context=context)
if meeting_ids:
meeting_obj.message_post(cr, uid, get_real_ids(meeting_ids), body=_(("%s has accepted invitation") % (attandee.cn)), context=context)
return res
def do_decline(self, cr, uid, ids, context=None, *args):
"""
@ -605,7 +513,13 @@ property or property parameter."),
"""
if context is None:
context = {}
return self.write(cr, uid, ids, {'state': 'declined'}, context)
meeting_obj = self.pool.get('crm.meeting')
res = self.write(cr, uid, ids, {'state': 'declined'}, context)
for attandee in self.browse(cr, uid, ids, context=context):
meeting_ids = meeting_obj.search(cr, uid, [('attendee_ids', '=', attandee.id)], context=context)
if meeting_ids:
meeting_obj.message_post(cr, uid, get_real_ids(meeting_ids), body=_(("%s has declined invitation") % (attandee.cn)), context=context)
return res
def create(self, cr, uid, vals, context=None):
"""
@ -825,6 +739,23 @@ class calendar_alarm(osv.osv):
res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
return res
class res_partner(osv.osv):
_inherit = 'res.partner'
def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
datas = []
meeting = False
if meeting_id:
meeting = self.pool.get('crm.meeting').browse(cr, uid, get_real_ids(meeting_id),context)
for partner in self.browse(cr, uid, ids, context=context):
data = self.name_get(cr, uid, [partner.id], context)[0]
if meeting:
for attendee in meeting.attendee_ids:
if attendee.partner_id.id == partner.id:
data = (data[0], data[1], attendee.state)
datas.append(data)
return datas
def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
context=None):
"""Scheduler for event reminder
@ -1083,7 +1014,7 @@ class calendar_event(osv.osv):
('tentative', 'Uncertain'),
('cancelled', 'Cancelled'),
('confirmed', 'Confirmed'),
], 'Status', readonly=True),
],'Status', readonly=True),
'exdate': fields.text('Exception Date/Times', help="This property \
defines the list of date/time exceptions for a recurring calendar component."),
'exrule': fields.char('Exception Rule', size=352, help="Defines a \
@ -1149,6 +1080,11 @@ rule or repeating pattern of time to exclude from the recurring rule."),
'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
}
def new_invitation_token(self, cr, uid, record, partner_id):
db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
invitation_token = hashlib.sha256('%s-%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id, partner_id)).hexdigest()
return invitation_token
def create_attendees(self, cr, uid, ids, context):
att_obj = self.pool.get('calendar.attendee')
user_obj = self.pool.get('res.users')
@ -1162,24 +1098,25 @@ rule or repeating pattern of time to exclude from the recurring rule."),
for partner in event.partner_ids:
if partner.id in attendees:
continue
local_context = context.copy()
local_context.pop('default_state', None)
access_token = self.new_invitation_token(cr, uid, event, partner.id)
att_id = self.pool.get('calendar.attendee').create(cr, uid, {
'partner_id': partner.id,
'user_id': partner.user_ids and partner.user_ids[0].id or False,
'ref': self._name+','+str(event.id),
'email': partner.email
}, context=local_context)
'access_token': access_token,
'email': partner.email,
}, context=context)
if partner.email:
mail_to = mail_to + " " + partner.email
self.write(cr, uid, [event.id], {
'attendee_ids': [(4, att_id)]
}, context=context)
new_attendees.append(att_id)
if mail_to and current_user.email:
att_obj._send_mail(cr, uid, new_attendees, mail_to,
is_sent_mail = att_obj._send_mail(cr, uid, new_attendees, mail_to,
email_from = current_user.email, context=context)
if is_sent_mail:
self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), context=context)
return True
def default_organizer(self, cr, uid, context=None):

View File

@ -0,0 +1,3 @@
import main
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,77 @@
import simplejson
import urllib
import openerp
import openerp.addons.web.http as http
from openerp.addons.web.http import request
import openerp.addons.web.controllers.main as webmain
import json
from openerp.addons.web.http import SessionExpiredException
from werkzeug.exceptions import BadRequest
class meetting_invitation(http.Controller):
def check_security(self, db, token):
registry = openerp.modules.registry.RegistryManager.get(db)
attendee_pool = registry.get('calendar.attendee')
error_message = False
with registry.cursor() as cr:
attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token)])
if not attendee_id:
# if token is not match
error_message = """Invalid Invitation Token."""
elif request.session.uid and request.session.login != 'anonymous':
# if valid session but user is not match
attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
if attendee.user_id.id != user.id:
error_message = """Invitation cannot be forwarded via email. This event/meeting belongs to %s and you are logged in as %s. Please ask organizer to add you.""" % (attendee.email, user.email)
if error_message:
raise BadRequest(error_message)
return True
@http.route('/meeting_invitation/accept', type='http', auth="none")
def accept(self, db, token, action, id):
# http://hostname:8069/meeting_invitation/accept?db=#token=&action=&id=
self.check_security(db, token)
registry = openerp.modules.registry.RegistryManager.get(db)
attendee_pool = registry.get('calendar.attendee')
with registry.cursor() as cr:
attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token),('state','!=', 'accepted')])
if attendee_id:
attendee_pool.do_accept(cr, openerp.SUPERUSER_ID, attendee_id)
return self.view(db, token, action, id, view='form')
@http.route('/meeting_invitation/decline', type='http', auth="none")
def declined(self, db, token, action, id):
# http://hostname:8069/meeting_invitation/decline?db=#token=&action=&id=
self.check_security(db, token)
registry = openerp.modules.registry.RegistryManager.get(db)
attendee_pool = registry.get('calendar.attendee')
with registry.cursor() as cr:
attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token),('state','!=', 'declined')])
if attendee_id:
attendee_pool.do_decline(cr, openerp.SUPERUSER_ID, attendee_id)
return self.view(db, token, action, id, view='form')
@http.route('/meeting_invitation/view', type='http', auth="none")
def view(self, db, token, action, id, view='calendar'):
# http://hostname:8069/meeting_invitation/view?db=#token=&action=&id=
self.check_security(db, token)
registry = openerp.modules.registry.RegistryManager.get(db)
meeting_pool = registry.get('crm.meeting')
attendee_pool = registry.get('calendar.attendee')
with registry.cursor() as cr:
attendee_data = meeting_pool.get_attendee(cr, openerp.SUPERUSER_ID, id);
attendee = attendee_pool.search_read(cr, openerp.SUPERUSER_ID, [('access_token','=',token)],[])
if attendee:
attendee_data['current_attendee'] = attendee[0]
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in webmain.manifest_list('js', db=db))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in webmain.manifest_list('css',db=db))
return webmain.html_template % {
'js': js,
'css': css,
'modules': simplejson.dumps(webmain.module_boot(db)),
'init': "s.base_calendar.event('%s', '%s', '%s', '%s' , '%s');" % (db, action, id, view, json.dumps(attendee_data)),
}

View File

@ -22,9 +22,14 @@
import time
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _
from base_calendar import get_real_ids, base_calendar_id2real_id
from datetime import datetime, timedelta, date
import pytz
from openerp import tools
import openerp
#
# crm.meeting is defined here so that it may be used by modules other than crm,
# without forcing the installation of crm.
@ -43,6 +48,55 @@ class crm_meeting(osv.Model):
_description = "Meeting"
_order = "id desc"
_inherit = ["calendar.event", "mail.thread", "ir.needaction_mixin"]
def _find_user_attendee(self, cr, uid, meeting_ids, context=None):
attendee_pool = self.pool.get('calendar.attendee')
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
for meeting_id in meeting_ids:
for attendee in self.browse(cr,uid,meeting_id,context).attendee_ids:
if user.partner_id.id == attendee.partner_id.id:
return attendee
return False
def _compute(self, cr, uid, ids, fields, arg, context=None):
res = {}
for meeting_id in ids:
res[meeting_id] = {}
attendee = self._find_user_attendee(cr, uid, [meeting_id], context)
for field in fields:
if field == 'is_attendee':
res[meeting_id][field] = True if attendee else False
elif field == 'attendee_status':
res[meeting_id][field] = attendee.state if attendee else 'needs-action'
elif field == 'event_time':
res[meeting_id][field] = self._compute_time(cr, uid, meeting_id, context=context)
return res
def _compute_time(self, cr, uid, meeting_id, context=None):
"""
Return date and time (from to from) based on duration with timezone in string :
eg.
1) if user add duration for 2 hours, return : August-23-2013 at ( 04-30 To 06-30) (Europe/Brussels)
2) if event all day ,return : AllDay, July-31-2013
"""
if context is None:
context = {}
tz = context.get('tz', pytz.timezone('UTC'))
meeting = self.browse(cr, uid, meeting_id, context=context)
date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
event_date = date.strftime('%B-%d-%Y')
event_time = date.strftime('%H-%M')
if meeting.allday:
time = _("AllDay , %s") % (event_date)
elif meeting.duration < 24:
duration = date + timedelta(hours= meeting.duration)
time = ("%s at ( %s To %s) (%s)") % (event_date, event_time, duration.strftime('%H-%M'), tz)
else :
time = ("%s at %s To\n %s at %s (%s)") % (event_date, event_time, date_deadline.strftime('%B-%d-%Y'), date_deadline.strftime('%H-%M'), tz)
return time
_columns = {
'create_date': fields.datetime('Creation Date', readonly=True),
'write_date': fields.datetime('Write Date', readonly=True),
@ -59,16 +113,29 @@ class crm_meeting(osv.Model):
'event_id', 'type_id', 'Tags'),
'attendee_ids': fields.many2many('calendar.attendee', 'meeting_attendee_rel',\
'event_id', 'attendee_id', 'Invited People', states={'done': [('readonly', True)]}),
'is_attendee': fields.function(_compute, string='Attendee', \
type="boolean", multi='attendee'),
'attendee_status': fields.function(_compute, string='Attendee Status', \
type="selection", multi='attendee'),
'event_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
}
_defaults = {
'state': 'open',
}
def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
if context is None:
context={}
if context.get('mymeetings',False):
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context).partner_id.id
args += ['|', ('partner_ids', 'in', [partner_id]), ('user_id', '=', uid)]
return super(crm_meeting, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
def message_get_subscription_data(self, cr, uid, ids, context=None):
def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
res = {}
for virtual_id in ids:
real_id = base_calendar_id2real_id(virtual_id)
result = super(crm_meeting, self).message_get_subscription_data(cr, uid, [real_id], context=context)
result = super(crm_meeting, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
res[virtual_id] = result[real_id]
return res
@ -123,8 +190,48 @@ class crm_meeting(osv.Model):
subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
if isinstance(thread_id, str):
thread_id = get_real_ids(thread_id)
if context.get('default_date'):
del context['default_date']
return super(crm_meeting, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs)
def do_decline(self, cr, uid, ids, context=None):
attendee_pool = self.pool.get('calendar.attendee')
attendee = self._find_user_attendee(cr, uid, ids, context)
return attendee_pool.do_decline(cr, uid, [attendee.id], context=context)
def do_accept(self, cr, uid, ids, context=None):
attendee_pool = self.pool.get('calendar.attendee')
attendee = self._find_user_attendee(cr, uid, ids, context)
return attendee_pool.do_accept(cr, uid, [attendee.id], context=context)
def get_attendee(self, cr, uid, meeting_id, context=None):
invitation = {'meeting':{}, 'attendee': [], 'logo': ''}
attendee_pool = self.pool.get('calendar.attendee')
company_logo = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.logo
meeting = self.browse(cr, uid, int(meeting_id), context)
invitation['meeting'] = {
'event':meeting.name,
'organizer': meeting.organizer,
'where': meeting.location,
'when':meeting.event_time
}
invitation['logo'] = company_logo.replace('\n','\\n') if company_logo else ''
for attendee in meeting.attendee_ids:
invitation['attendee'].append({'name':attendee.cn,'status': attendee.state})
return invitation
def get_interval(self, cr, uid, ids, date, interval, context=None):
date = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT)
if interval == 'day':
res = str(date.day)
elif interval == 'month':
res = date.strftime('%B') + " " + str(date.year)
elif interval == 'dayname':
res = date.strftime('%A')
elif interval == 'time':
res = date.strftime('%I:%M %p')
return res
class mail_message(osv.osv):
_inherit = "mail.message"

View File

@ -28,5 +28,133 @@
<field name="name">Meeting</field>
<field name="object">crm.meeting</field>
</record>
<record id="crm_email_template_meeting_invitation" model="email.template">
<field name="name">CRM Meeting Invitation</field>
<field name="email_from">${object.user_id.email or ''}</field>
<field name="subject">${object.name}</field>
<field name="email_to" >${ctx['att_obj'].email}</field>
<field name="model_id" ref="base_calendar.model_crm_meeting"/>
<field name="auto_delete" eval="True"/>
<field name="body_html"><![CDATA[
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>${object.name}</title>
</head>
<body>
<div style="border-radius: 2px; max-width: 1200px; height: auto;margin-left: auto;margin-right: auto;background-color:#f9f9f9;">
<div style="height:auto;text-align: center;font-size : 30px;color: #8A89BA;">
<strong>${object.name}</strong>
</div>
<div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px">
<strong style="margin-left:12px">Hello ${ctx['att_obj'].cn}</strong> ,<br/><p style="margin-left:12px">${object.organizer} invited you for the ${object.name} meeting of ${object.user_id.company_id.name}.</p>
</div>
<div style="height: auto;margin-left:12px;margin-top:30px;">
<table>
<tr>
<td>
<div style="border-top-left-radius:3px;border-top-right-radius:3px;font-size:12px;border-collapse:separate;text-align:center;font-weight:bold;color:#ffffff;width:130px;min-height: 18px;border-color:#ffffff;background:#8a89ba;padding-top: 4px;">${object.get_interval(object.date, 'dayname')}</div>
<div style="font-size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F;background-color: #E1E2F8;width: 130px;">
${object.get_interval(object.date,'day')}
</div>
<div style='font-size:12px;text-align:center;font-weight:bold;color:#ffffff;background-color:#8a89ba'>${object.get_interval(object.date, 'month')}</div>
<div style="border-collapse:separate;color:#8a89ba;text-align:center;width: 128px;font-size:12px;border-bottom-right-radius:3px;font-weight:bold;border:1px solid;border-bottom-left-radius:3px;">${object.get_interval(object.date, 'time')}</div>
</td>
<td>
<table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;">
% if object.location :
<tr style=" height: 30px;">
<td style="vertical-align:top;">
<div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Where
</div>
</td>
<td colspan="1" style="vertical-align:top;">
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.location}">View Map</a>)
</span>
</div>
</td>
</tr>
% endif
% if not object.location :
<tr style=" height: 30px;color:#909090">
<td>
<div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Where
</div>
</td>
<td colspan="1">
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px;" >
: -
</div>
</td>
</tr>
% endif
% if object.description :
<tr style=" height:auto;">
<td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${object.description or ''}
</div>
</td>
</tr>
% endif
% if not object.description :
<tr style=" height: 30px;color:#909090">
<td style="vertical-align:top;">
<div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: -
</div>
</td>
</tr>
% endif
<tr style=" height: 30px;">
<td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
<div>
Attendees
</div>
</td>
<td colspan="3">
: <div style='display:inline-block; border-radius: 50%; width:10px; height:10px;background:grey;'></div>
<span style="margin-left:5px">You</span>
% for attendee in object.attendee_ids:
% if attendee.cn != ctx['att_obj'].cn:
<div style='display:inline-block; border-radius: 50%; width:10px; height:10px;background:${ctx['color'][attendee.state]};'></div>
<span style="margin-left:5px">${attendee.cn}</span>
% endif
% endfor
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div style="height: auto;width:300px; margin:0 auto;padding-top:20px;">
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#8A89BA;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/meeting_invitation/accept?db=${ctx['dbname']}&token=${ctx['att_obj'].access_token}&action=${ctx['action_id']}&id=${object.id}">Accept</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/meeting_invitation/decline?db=${ctx['dbname']}&token=${ctx['att_obj'].access_token}&action=${ctx['action_id']}&id=${object.id}">Decline</a>
</div>
<div style="padding-top:10px;">
-- </br> Sent by ${object.user_id.name} from ${object.user_id.company_id.name}. View this meeting detail <a href="${ctx['base_url']}/meeting_invitation/view?db=${ctx['dbname']}&token=${ctx['att_obj'].access_token}&action=${ctx['action_id']}&id=${object.id}">directly in OpenERP.</a>
</div>
</div>
</body>
</html>
]]></field>
</record>
</data>
</openerp>

View File

@ -32,8 +32,16 @@
<field name="model">crm.meeting</field>
<field name="arch" type="xml">
<form string="Meetings" version="7.0">
<field name="state" invisible="True"/>
<header>
<button name="do_accept" type="object"
string="Accept" attrs="{'invisible':['|',('is_attendee','=',False),('attendee_status','=','accepted')]}"/>
<button name="do_decline" type="object"
string="Decline" attrs="{'invisible':['|',('is_attendee','=',False),('attendee_status','=','declined')]}"/>
<field name="state" invisible="True"/>
</header>
<sheet>
<field name="is_attendee" invisible="1"/>
<field name="attendee_status" invisible="1"/>
<div class="oe_title">
<div class="oe_edit_only">
<label for="name"/>
@ -43,7 +51,7 @@
</h1>
<label for="partner_ids" class="oe_edit_only"/>
<h2>
<field name="partner_ids" widget="many2many_tags"
<field name="partner_ids" widget="many2manyattendee"
context="{'force_email':True}"
on_change="onchange_partner_ids(partner_ids)"/>
</h2>
@ -133,16 +141,16 @@
</group>
</group>
</page>
<page string="Invitations">
<page string="Invitations" groups="base.group_no_one">
<field name="attendee_ids" widget="one2many" mode="tree">
<tree string="Invitation details" editable="top">
<tree string="Invitation details" editable="top" >
<field name="partner_id" on_change="onchange_partner_id(partner_id)"/>
<field name="email" string="Mail To"/>
<field name="state"/>
<button name="do_tentative"
states="needs-action,declined,accepted"
string="Uncertain" type="object"
icon="terp-crm"/>
icon="terp-crm" />
<button name="do_accept" string="Accept"
states="needs-action,tentative,declined"
type="object" icon="gtk-apply"/>
@ -181,6 +189,10 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread" />
</div>
</form>
</field>
</record>
@ -238,7 +250,7 @@
<field name="categ_ids"/>
<field name="user_id"/>
<separator/>
<filter string="My Meetings" help="My Meetings" domain="[('user_id','=',uid)]"/>
<filter string="My Meetings" help="My Meetings" name="mymeetings" context='{"mymeetings": 1}'/>
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
</search>
</field>
@ -252,7 +264,7 @@
<field name="view_mode">calendar,tree,form,gantt</field>
<field name="view_id" ref="view_crm_meeting_calendar"/>
<field name="search_view_id" ref="view_crm_meeting_search"/>
<field name="context">{"calendar_default_user_id": uid}</field>
<field name="context">{"search_default_mymeetings": 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to schedule a new meeting.

View File

@ -0,0 +1,23 @@
.. _calendar_attendee:
calendar.attendee:
=================
Fields
++++++
- ``access_token`` :
unique value(token) for every new attendee.
Methods
+++++++
- ``do_accept``:
REF : post message in chatter when attendee accepted an invitation.
- ``do_decline``:
REF : post message in chatter when attendee declined an invitation.
calendar.event:
===============
Methods
+++++++
- ``new_invitation_token``:
generate a unique token for every new attendee.

View File

@ -0,0 +1,29 @@
.. _changelog:
Changelog
=========
Email Template of Meeting Invitation:
+++++++++++++++++++++++++++++++++++++
- remove static code of HTML design of email of meeting invitation
- added new better layout of email of meeting invitation using MAKO Template.
Web controller:
+++++++++++++++
- ``accept`` :
handle request ('meeting_invitation/accept') ,when accepted an invitation it change the status of invitation as accepted , user do need to login in system.
- ``declined``:
handle request ('meeting_invitation/decline') ,when declined an invitation it change the status of invitation as declined , user do need to login in system.
- ``view``:
handle request ('meeting_invitation/view') ,when user click on accept,declined link button , it redirect user to form view if user is already login and if user has not been login it redirect to a simple qweb template to inform user has accepted/declined a meeting ,if user click on directly in openerp it redirect user to a meeting calendar view , if user is not login then it redirect to a qweb template.
- ``check_security``:
check token is valid and user is not allow to accept/decline invitation mail of other user from email template URL.
Web Widget:
+++++++++++
- ``Field Many2Many_invite``(widget):
display a status button in left side of every invited attendees of meeting , in many2many.
Qweb Template:
++++++++++++++
- added template ,to directly allow any invited user to accept , decline a meeting , if user do not need to login in the system to accept or decline an invitation.

View File

@ -0,0 +1,38 @@
.. _crm_meeting:
Fields:
+++++++
- ``is_attendee`` :
function field , that defined whether loged in user is attendee or not.
- ``attendee_status``:
function field , that defined login user status, either accepted, declined or needs-action.
- ``event_time``:
function field, defined an event_time in user's tz.
Methods:
++++++++
- ``_find_user_attendee``:
return attendee if attendee is internal user else false.
- ``_compute_time``:
compute a time from date_start and duration with user's tz.
- ``search``:
search a current user's meetings
- ``do_accept/do_decline``:
trigger when ,user accept/decline from the meeting form view.
- ``get_attendee``:
get detail of attendees meeting.
- ``get_interval``:
call from email template that return formate of date, as per value pass from the email template.
views:
++++++
- ``do_accept``:
Accept button in meeting form view that is allow a user to accept a meeting ,that is visible to only attendee and if attendee state is other than accepted.
- ``do_decline``:
Decline button in meeting form view that is allow a user to accept a meeting ,that is visible to only attendee and if attendee state is other than declined.
- ``chatter(message_ids)``:
show a log of meeting.
security:
+++++++++
- added record rule to restrict an user to show personal invitation on meeting , so user can't change other's status , from invitation tab.

View File

@ -5,5 +5,5 @@
<field name="name">Survey / User</field>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
</data>
</data>
</openerp>

View File

@ -7,7 +7,7 @@ access_calendar_event,calendar.event,model_calendar_event,base.group_user,1,1,1,
access_calendar_attendee_survey_user,calendar.attendee,model_calendar_attendee,base.group_survey_user,1,0,0,0
access_crm_meeting_manager,crm.meeting.manager,model_crm_meeting,base.group_sale_manager,1,1,1,1
access_crm_meeting,crm.meeting,model_crm_meeting,base.group_sale_salesman,1,1,1,0
access_crm_meeting_all,crm.meeting_allll,model_crm_meeting,base.group_user,1,0,0,0
access_crm_meeting_all,crm.meeting_allll,model_crm_meeting,base.group_user,1,1,0,0
access_crm_meeting_partner_manager,crm.meeting.partner.manager,model_crm_meeting,base.group_partner_manager,1,1,1,1
access_crm_meeting_type_sale_manager,crm.meeting.type.manager,model_crm_meeting_type,base.group_sale_manager,1,1,1,0
access_crm_meeting_type_sale_user,crm.meeting.type.user,model_crm_meeting_type,base.group_user,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
7 access_calendar_attendee_survey_user calendar.attendee model_calendar_attendee base.group_survey_user 1 0 0 0
8 access_crm_meeting_manager crm.meeting.manager model_crm_meeting base.group_sale_manager 1 1 1 1
9 access_crm_meeting crm.meeting model_crm_meeting base.group_sale_salesman 1 1 1 0
10 access_crm_meeting_all crm.meeting_allll model_crm_meeting base.group_user 1 0 1 0 0
11 access_crm_meeting_partner_manager crm.meeting.partner.manager model_crm_meeting base.group_partner_manager 1 1 1 1
12 access_crm_meeting_type_sale_manager crm.meeting.type.manager model_crm_meeting_type base.group_sale_manager 1 1 1 0
13 access_crm_meeting_type_sale_user crm.meeting.type.user model_crm_meeting_type base.group_user 1 0 0 0

View File

@ -0,0 +1,65 @@
.openerp .oe_invitation , .text-core .text-tag .oe_invitation{
width : 13px;
height : 13px;
margin-bottom : -4px;
display : inline-block;
}
.openerp .needs-action , .tentative,.text-core .text-tag .custom-edit, .text-core .text-tag .tentative {
background : url(/web/static/src/img/icons/gtk-normal.png) no-repeat;
background-size : 11px 11px;
}
.openerp .accepted , .text-core .text-tag .accepted {
background : url(/web/static/src/img/icons/gtk-yes.png) no-repeat;
background-size : 11px 11px;
}
.openerp .declined , .text-core .text-tag .declined {
background : url(/web/static/src/img/icons/gtk-no.png) no-repeat;
background-size : 11px 11px;
}
.cal_meeting {
font-size : 24px;
font-style: bold;
text-align : justify;
color : #8A89BA;
}
.cal_lable {
width: 50px;
color : #808080;
}
.invitation_block {
padding : 50px 0 0 30px;
font-size : 14px;
background : #f9f9f9;
}
.attendee_accepted {
background : url(/web/static/src/img/icons/gtk-apply.png) no-repeat;
background-size : 15px 15px;
padding-left: 20px;
}
.attendee_declined {
background : url(/web/static/src/img/icons/gtk-cancel.png) no-repeat;
background-size : 15px 15px;
padding-left: 20px;
}
.event_status {
border : 1px solid;
height : 20px;
width : auto;
background: #808080;
color : #FFFFFF;
padding: 5px 10px;
width: 400px;
}
.cal_inline {
display: inline;
}
.cal_tag {
padding-right : 10px;
font-style : italic;
font-size : 17px;
vertical-align:bottom;
}
.cal_image {
height: 30px;
width : 100px;
}

View File

@ -0,0 +1,80 @@
openerp.base_calendar = function(instance) {
var _t = instance.web._t;
var QWeb = instance.web.qweb;
instance.base_calendar = {}
instance.base_calendar.invitation = instance.web.Widget.extend({
init: function(parent, db, action, id, view, attendee_data) {
this._super();
this.db = db;
this.action = action;
this.id = id;
this.view = view;
this.attendee_data = attendee_data;
},
start: function() {
var self = this;
if(instance.session.session_is_valid(self.db) && instance.session.username != "anonymous") {
self.redirect_meeting_view(self.db,self.action,self.id,self.view);
} else {
self.open_invitation_form(self.attendee_data);
}
},
open_invitation_form : function(invitation){
this.$el.html(QWeb.render('invitation_view', {'invitation': JSON.parse(invitation)}));
},
redirect_meeting_view : function(db, action, meeting_id, view){
var self = this;
var action_url = '';
if(view == "form") {
action_url = _.str.sprintf('/?db=%s#id=%s&view_type=%s&model=crm.meeting', db, meeting_id, view, meeting_id);
} else {
action_url = _.str.sprintf('/?db=%s#view_type=%s&model=crm.meeting&action=%s',self.db,self.view,self.action);
}
var reload_page = function(){
return location.replace(action_url);
}
reload_page();
},
});
instance.web.form.Many2ManyAttendee = instance.web.form.FieldMany2ManyTags.extend({
tag_template: "many2manyattendee",
initialize_texttext: function() {
return _.extend(this._super(),{
html : {
tag: '<div class="text-tag"><div class="text-button"><a class="oe_invitation custom-edit"/><span class="text-label"/><a class="text-remove"/></div></div>'
}
});
},
map_tag: function(value){
return _.map(value, function(el) {return {name: el[1], id:el[0], state: el[2]};})
},
get_render_data: function(ids){
var self = this;
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
return dataset.call('get_attendee_detail',[ids, self.getParent().datarecord.id || false]);
},
render_tag: function(data){
this._super(data);
var self = this;
if (! self.get("effective_readonly")) {
var tag_element = self.tags.tagElements();
_.each(data,function(value, key){
$(tag_element[key]).find(".custom-edit").addClass(data[key][2])
});
}
}
});
instance.web.form.widgets = instance.web.form.widgets.extend({
'many2manyattendee' : 'instance.web.form.Many2ManyAttendee',
});
instance.base_calendar.event = function (db, action, id, view, attendee_data) {
instance.session.session_bind(instance.session.origin).done(function () {
new instance.base_calendar.invitation(null,db,action,id,view,attendee_data).appendTo($("body").addClass('openerp'));
});
}
};
//vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-name="many2manyattendee">
<t t-set="i" t-value="0"/>
<t t-foreach="elements" t-as="el">
<span class="oe_tag" t-att-data-index="i">
<a t-attf-class="oe_invitation #{el[2]}"/>
<t t-esc="el[1]"/>
</span>
<t t-set="i" t-value="i + 1"/>
</t>
</t>
<t t-name="invitation_view">
<div class="oe_right"><b><t t-esc="invitation['current_attendee'].cn"/> (<t t-esc="invitation['current_attendee'].email"/>)</b></div>
<div class="oe_left"><img class="cal_inline cal_image" t-attf-src="data:image/png;base64,#{invitation['logo']}"/><p class="cal_tag cal_inline">Calendar</p></div>
<div class="invitation_block">
<t t-if="invitation['current_attendee'].state != 'needs-action'">
<div class="event_status"><a t-attf-class="attendee_#{invitation['current_attendee'].state}"><b t-if="invitation['current_attendee'].state == 'accepted'">Yes I'm going.</b><b t-if="invitation['current_attendee'].state == 'declined'">No I'm not going.</b></a></div>
</t>
<div class="cal_meeting"><t t-esc="invitation['meeting'].event"/></div>
<table calss="invitation_block">
<tr>
<td class="cal_lable">When</td>
<td>: <t t-esc="invitation['meeting'].when"/></td>
</tr>
<tr>
<td class="cal_lable">Where</td>
<td>: <t t-esc="invitation['meeting'].where or '-'"/></td>
</tr>
<tr>
<td class="cal_lable">Who</td>
<td>
<span>: <t t-esc="invitation['meeting'].organizer"/> - <a class="cal_lable">Organizer</a></span>
<t t-foreach="invitation['attendee']" t-as="att">
<br/>
<span class="cal_status"><a t-attf-class="oe_invitation #{att.status}"/><t t-esc="att.name"/></span>
</t>
</td>
</tr>
</table>
</div>
</t>
</template>

View File

@ -6,4 +6,4 @@ access_mail_followers_portal,mail.followers.portal,mail.model_mail_followers,gro
access_res_partner_portal,res.partner.portal,base.model_res_partner,portal.group_portal,1,0,0,0
access_acquirer,portal.payment.acquirer,portal.model_portal_payment_acquirer,,1,0,0,0
access_acquirer_all,portal.payment.acquirer,portal.model_portal_payment_acquirer,base.group_system,1,1,1,1
access_ir_attachment_group_portal,ir.attachment group_portal,base.model_ir_attachment,portal.group_portal,1,0,1,0
access_ir_attachment_group_portal,ir.attachment group_portal,base.model_ir_attachment,portal.group_portal,1,0,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
6 access_res_partner_portal res.partner.portal base.model_res_partner portal.group_portal 1 0 0 0
7 access_acquirer portal.payment.acquirer portal.model_portal_payment_acquirer 1 0 0 0
8 access_acquirer_all portal.payment.acquirer portal.model_portal_payment_acquirer base.group_system 1 1 1 1
9 access_ir_attachment_group_portal ir.attachment group_portal base.model_ir_attachment portal.group_portal 1 0 1 0

View File

@ -33,6 +33,7 @@ This module adds a contact page (with a contact form creating a lead when submit
'depends': ['crm','portal'],
'data': [
'contact_view.xml',
'security/ir.model.access.csv',
],
'test': [
'test/contact_form.yml',

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_crm_meeting_portal,crm.meeting.portal,base_calendar.model_crm_meeting,portal.group_portal,1,1,0,0
access_crm_meeting_type_portal,crm.meeting.type.portal,base_calendar.model_crm_meeting_type,portal.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_crm_meeting_portal crm.meeting.portal base_calendar.model_crm_meeting portal.group_portal 1 1 0 0
3 access_crm_meeting_type_portal crm.meeting.type.portal base_calendar.model_crm_meeting_type portal.group_portal 1 0 0 0

View File

@ -56,3 +56,16 @@ class hr_employee(osv.osv):
_defaults = {
'visibility': 'private',
}
class calendar_attendee(osv.osv):
_inherit = 'calendar.attendee'
def create(self, cr, uid, vals, context=None):
user_pool = self.pool.get('res.users')
partner_id = vals.get('partner_id')
users = user_pool.search_read(cr, uid, [('partner_id','=', partner_id)],['employee_ids'], context=context)
for user in users:
if user['employee_ids']:
vals['state'] = 'accepted'
return super(calendar_attendee, self).create(cr, uid, vals, context=context)