Merge lp:~openerp-dev/openobject-addons/trunk-email-invitation-template-aja into lp:openobject-addons
- trunk-email-invitation-template-aja
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~openerp-dev/openobject-addons/trunk-email-invitation-template-aja |
Merge into: | lp:openobject-addons |
Diff against target: |
1729 lines (+803/-540) 20 files modified
base_calendar/__init__.py (+1/-0) base_calendar/__openerp__.py (+8/-1) base_calendar/base_calendar.py (+159/-526) base_calendar/controllers/__init__.py (+3/-0) base_calendar/controllers/main.py (+77/-0) base_calendar/crm_meeting.py (+110/-3) base_calendar/crm_meeting_data.xml (+128/-0) base_calendar/crm_meeting_view.xml (+19/-7) base_calendar/doc/calnedar_attendee.rst (+23/-0) base_calendar/doc/changelog.rst (+29/-0) base_calendar/doc/crm_meeting.rst (+38/-0) base_calendar/security/calendar_security.xml (+1/-1) base_calendar/security/ir.model.access.csv (+1/-1) base_calendar/static/src/css/base_calender.css (+65/-0) base_calendar/static/src/js/base_calendar.js (+80/-0) base_calendar/static/src/xml/base_calendar.xml (+43/-0) portal/security/ir.model.access.csv (+1/-1) portal_crm/__openerp__.py (+1/-0) portal_crm/security/ir.model.access.csv (+3/-0) portal_hr_employees/hr_employee.py (+13/-0) |
To merge this branch: | bzr merge lp:~openerp-dev/openobject-addons/trunk-email-invitation-template-aja |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenERP Core Team | Pending | ||
Review via email: mp+190868@code.launchpad.net |
This proposal supersedes a proposal from 2013-07-03.
Commit message
Description of the change
Hello,
Email Template of Meeting Invitation:
-------
- REM : remove static HTML design of email of meeting invitation
- ADD : new better layout of email of meeting invitation using MAKO Template.
-ADD : Qweb template ,to directly allow any invited user to accept , decline a meeting, without log in to a system.
Calendar attendee:
-------------------
- ADD : field access_token and new_invitation_
- REF : do_accept/
Web Widget:
-----------------
- Many2Many_invite widget, to display a status of every invited attendees of meeting.
crm_meeting:
-------------------
-ADD : function field is_attendee,
-ADD : _find_user_attendee method to check attendee is internal user.
-ADD : _compute_time compute a time from date_start and duration to user's tz.
-ADD : do_accept/
crm_meeting_view:
----------------
-ADD : do_accept abd do_decline button in meeting form view
-ADD : add chatter , to show a log of meeting.
Calendar secutrity:
-------
- ADD : record rule , to restrict an user to show persional invitation on meeting.
Thank You.
- 8848. By ajay javiya (OpenERP)
-
[ADD]: change log
- 8849. By ajay javiya (OpenERP)
-
[MERGE]: with trunk
- 8850. By ajay javiya (OpenERP)
-
[IMP]: Fix trace back and improve emil template view
- 8851. By ajay javiya (OpenERP)
-
[IMP]:email template view for multiline description(what)
- 8852. By ajay javiya (OpenERP)
-
[IMP]:many2many widget for attendee status
- 8853. By ajay javiya (OpenERP)
-
[IMP]: code and allow to visible invitation tab to group with technical feature
- 8854. By ajay javiya (OpenERP)
-
[FIX]: Traceback , create an recurrent meeting and then accept/decline from invitation tab
- 8855. By ajay javiya (OpenERP)
-
[MERGE]: with trunk
- 8856. By ajay javiya (OpenERP)
-
[REM]: Rmove unwanted access rights and record rule
- 8857. By ajay javiya (OpenERP)
-
[IMP]: improve align in email template
- 8858. By ajay javiya (OpenERP)
-
[REV]: noupdate in data
- 8859. By ajay javiya (OpenERP)
-
[MERGE]: with trunk
- 8860. By ajay javiya (OpenERP)
-
[MERGE]: with trunk
- 8861. By ajay javiya (OpenERP)
-
[IMP]: if opt_out is checked invitation mail is not send to attendee
- 8862. By Antony Lesuisse (OpenERP)
-
slaughtering
- 8863. By Antony Lesuisse (OpenERP)
-
more slaughtering
Unmerged revisions
- 8863. By Antony Lesuisse (OpenERP)
-
more slaughtering
- 8862. By Antony Lesuisse (OpenERP)
-
slaughtering
Preview Diff
1 | === modified file 'base_calendar/__init__.py' |
2 | --- base_calendar/__init__.py 2012-11-29 22:26:45 +0000 |
3 | +++ base_calendar/__init__.py 2013-11-07 16:05:43 +0000 |
4 | @@ -21,5 +21,6 @@ |
5 | |
6 | import base_calendar |
7 | import crm_meeting |
8 | +import controllers |
9 | |
10 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
11 | |
12 | === modified file 'base_calendar/__openerp__.py' |
13 | --- base_calendar/__openerp__.py 2013-08-13 09:55:46 +0000 |
14 | +++ base_calendar/__openerp__.py 2013-11-07 16:05:43 +0000 |
15 | @@ -47,11 +47,18 @@ |
16 | 'base_calendar_data.xml', |
17 | 'crm_meeting_data.xml', |
18 | ], |
19 | + 'js': [ |
20 | + 'static/src/js/*.js' |
21 | + ], |
22 | + 'qweb': ['static/src/xml/*.xml'], |
23 | + 'css': [ |
24 | + 'static/src/css/base_calender.css' |
25 | + ], |
26 | 'test' : ['test/base_calendar_test.yml'], |
27 | 'installable': True, |
28 | 'application': True, |
29 | 'auto_install': False, |
30 | - 'images': ['images/base_calendar1.jpeg','images/base_calendar2.jpeg','images/base_calendar3.jpeg','images/base_calendar4.jpeg',], |
31 | + 'images': ['images/base_calendar1.jpeg','images/base_calendar2.jpeg','images/base_calendar3.jpeg','images/base_calendar4.jpeg'], |
32 | } |
33 | |
34 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
35 | |
36 | === modified file 'base_calendar/base_calendar.py' |
37 | --- base_calendar/base_calendar.py 2013-10-06 11:58:08 +0000 |
38 | +++ base_calendar/base_calendar.py 2013-11-07 16:05:43 +0000 |
39 | @@ -28,10 +28,9 @@ |
40 | import pytz |
41 | import re |
42 | import time |
43 | - |
44 | +import hashlib |
45 | from openerp import tools, SUPERUSER_ID |
46 | import openerp.service.report |
47 | - |
48 | months = { |
49 | 1: "January", 2: "February", 3: "March", 4: "April", \ |
50 | 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", \ |
51 | @@ -114,96 +113,6 @@ |
52 | return '%d-%s' % (real_id, recurrent_date) |
53 | return real_id |
54 | |
55 | -html_invitation = """ |
56 | -<html> |
57 | -<head> |
58 | -<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> |
59 | -<title>%(name)s</title> |
60 | -</head> |
61 | -<body> |
62 | -<table border="0" cellspacing="10" cellpadding="0" width="100%%" |
63 | - style="font-family: Arial, Sans-serif; font-size: 14"> |
64 | - <tr> |
65 | - <td width="100%%">Hello,</td> |
66 | - </tr> |
67 | - <tr> |
68 | - <td width="100%%">You are invited for <i>%(company)s</i> Event.</td> |
69 | - </tr> |
70 | - <tr> |
71 | - <td width="100%%">Below are the details of event. Hours and dates expressed in %(timezone)s time.</td> |
72 | - </tr> |
73 | -</table> |
74 | - |
75 | -<table cellspacing="0" cellpadding="5" border="0" summary="" |
76 | - style="width: 90%%; font-family: Arial, Sans-serif; border: 1px Solid #ccc; background-color: #f6f6f6"> |
77 | - <tr valign="center" align="center"> |
78 | - <td bgcolor="DFDFDF"> |
79 | - <h3>%(name)s</h3> |
80 | - </td> |
81 | - </tr> |
82 | - <tr> |
83 | - <td> |
84 | - <table cellpadding="8" cellspacing="0" border="0" |
85 | - style="font-size: 14" summary="Eventdetails" bgcolor="f6f6f6" |
86 | - width="90%%"> |
87 | - <tr> |
88 | - <td width="21%%"> |
89 | - <div><b>Start Date</b></div> |
90 | - </td> |
91 | - <td><b>:</b></td> |
92 | - <td>%(start_date)s</td> |
93 | - <td width="15%%"> |
94 | - <div><b>End Date</b></div> |
95 | - </td> |
96 | - <td><b>:</b></td> |
97 | - <td width="25%%">%(end_date)s</td> |
98 | - </tr> |
99 | - <tr valign="top"> |
100 | - <td><b>Description</b></td> |
101 | - <td><b>:</b></td> |
102 | - <td colspan="3">%(description)s</td> |
103 | - </tr> |
104 | - <tr valign="top"> |
105 | - <td> |
106 | - <div><b>Location</b></div> |
107 | - </td> |
108 | - <td><b>:</b></td> |
109 | - <td colspan="3">%(location)s</td> |
110 | - </tr> |
111 | - <tr valign="top"> |
112 | - <td> |
113 | - <div><b>Event Attendees</b></div> |
114 | - </td> |
115 | - <td><b>:</b></td> |
116 | - <td colspan="3"> |
117 | - <div> |
118 | - <div>%(attendees)s</div> |
119 | - </div> |
120 | - </td> |
121 | - </tr> |
122 | - </table> |
123 | - </td> |
124 | - </tr> |
125 | -</table> |
126 | -<table border="0" cellspacing="10" cellpadding="0" width="100%%" |
127 | - style="font-family: Arial, Sans-serif; font-size: 14"> |
128 | - <tr> |
129 | - <td width="100%%">From:</td> |
130 | - </tr> |
131 | - <tr> |
132 | - <td width="100%%">%(user)s</td> |
133 | - </tr> |
134 | - <tr valign="top"> |
135 | - <td width="100%%">-<font color="a7a7a7">-------------------------</font></td> |
136 | - </tr> |
137 | - <tr> |
138 | - <td width="100%%"> <font color="a7a7a7">%(sign)s</font></td> |
139 | - </tr> |
140 | -</table> |
141 | -</body> |
142 | -</html> |
143 | -""" |
144 | - |
145 | class calendar_attendee(osv.osv): |
146 | """ |
147 | Calendar Attendee Information |
148 | @@ -313,13 +222,6 @@ |
149 | ('group', 'Group'), ('resource', 'Resource'), \ |
150 | ('room', 'Room'), ('unknown', 'Unknown') ], \ |
151 | 'Invite Type', help="Specify the type of Invitation"), |
152 | - 'member': fields.char('Member', size=124, |
153 | - help="Indicate the groups that the attendee belongs to"), |
154 | - 'role': fields.selection([('req-participant', 'Participation required'), \ |
155 | - ('chair', 'Chair Person'), \ |
156 | - ('opt-participant', 'Optional Participation'), \ |
157 | - ('non-participant', 'For information Purpose')], 'Role', \ |
158 | - help='Participation role for the calendar user'), |
159 | 'state': fields.selection([('needs-action', 'Needs Action'), |
160 | ('tentative', 'Uncertain'), |
161 | ('declined', 'Declined'), |
162 | @@ -328,39 +230,13 @@ |
163 | help="Status of the attendee's participation"), |
164 | 'rsvp': fields.boolean('Required Reply?', |
165 | help="Indicats whether the favor of a reply is requested"), |
166 | - 'delegated_to': fields.function(_compute_data, \ |
167 | - string='Delegated To', type="char", size=124, store=True, \ |
168 | - multi='delegated_to', help="The users that the original \ |
169 | -request was delegated to"), |
170 | - 'delegated_from': fields.function(_compute_data, string=\ |
171 | - 'Delegated From', type="char", store=True, size=124, multi='delegated_from'), |
172 | - 'parent_ids': fields.many2many('calendar.attendee', 'calendar_attendee_parent_rel', \ |
173 | - 'attendee_id', 'parent_id', 'Delegrated From'), |
174 | - 'child_ids': fields.many2many('calendar.attendee', 'calendar_attendee_child_rel', \ |
175 | - 'attendee_id', 'child_id', 'Delegrated To'), |
176 | - 'sent_by': fields.function(_compute_data, string='Sent By', \ |
177 | - type="char", multi='sent_by', store=True, size=124, \ |
178 | - help="Specify the user that is acting on behalf of the calendar user"), |
179 | - 'sent_by_uid': fields.function(_compute_data, string='Sent By User', \ |
180 | - type="many2one", relation="res.users", multi='sent_by_uid'), |
181 | 'cn': fields.function(_compute_data, string='Common name', \ |
182 | type="char", size=124, multi='cn', store=True), |
183 | - 'dir': fields.char('URI Reference', size=124, help="Reference to the URI\ |
184 | -that points to the directory information corresponding to the attendee."), |
185 | - 'language': fields.function(_compute_data, string='Language', \ |
186 | - type="selection", selection=_lang_get, multi='language', \ |
187 | - store=True, help="To specify the language for text values in a\ |
188 | -property or property parameter."), |
189 | - 'user_id': fields.many2one('res.users', 'User'), |
190 | + 'dir': fields.char('URI Reference', size=124, help="Reference to the URI that points to the directory information corresponding to the attendee."), |
191 | 'partner_id': fields.many2one('res.partner', 'Contact'), |
192 | 'email': fields.char('Email', size=124, help="Email of Invited Person"), |
193 | - 'event_date': fields.function(_compute_data, string='Event Date', \ |
194 | - type="datetime", multi='event_date'), |
195 | - 'event_end_date': fields.function(_compute_data, \ |
196 | - string='Event End Date', type="datetime", \ |
197 | - multi='event_end_date'), |
198 | - 'ref': fields.reference('Event Ref', selection=openerp.addons.base.res.res_request.referencable_models, size=128), |
199 | 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"), |
200 | + 'access_token':fields.char('Invitation Token', size=256), |
201 | } |
202 | _defaults = { |
203 | 'state': 'needs-action', |
204 | @@ -369,10 +245,9 @@ |
205 | 'cutype': 'individual', |
206 | } |
207 | |
208 | - |
209 | def copy(self, cr, uid, id, default=None, context=None): |
210 | raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.')) |
211 | - |
212 | + |
213 | def onchange_partner_id(self, cr, uid, ids, partner_id,context=None): |
214 | """ |
215 | Make entry on email and availbility on change of partner_id field. |
216 | @@ -388,7 +263,7 @@ |
217 | return {'value': {'email': ''}} |
218 | partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) |
219 | return {'value': {'email': partner.email}} |
220 | - |
221 | + |
222 | def get_ics_file(self, cr, uid, event_obj, context=None): |
223 | """ |
224 | Returns iCalendar file for the event invitation. |
225 | @@ -474,53 +349,43 @@ |
226 | @param email_from: email address for user sending the mail |
227 | @return: True |
228 | """ |
229 | - company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name |
230 | - for att in self.browse(cr, uid, ids, context=context): |
231 | - sign = att.sent_by_uid and att.sent_by_uid.signature or '' |
232 | - sign = '<br>'.join(sign and sign.split('\n') or []) |
233 | - res_obj = att.ref |
234 | + mail_id = [] |
235 | + data_pool = self.pool.get('ir.model.data') |
236 | + mail_pool = self.pool.get('mail.mail') |
237 | + template_pool = self.pool.get('email.template') |
238 | + local_context = context.copy() |
239 | + color = { |
240 | + 'needs-action' : 'grey', |
241 | + 'accepted' :'green', |
242 | + 'tentative' :'#FFFF00', |
243 | + 'declined':'red', |
244 | + 'delegated':'grey' |
245 | + } |
246 | + for attendee in self.browse(cr, uid, ids, context=context): |
247 | + res_obj = attendee.ref |
248 | if res_obj: |
249 | - att_infos = [] |
250 | - sub = res_obj.name |
251 | - other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))]) |
252 | - |
253 | - for att2 in self.browse(cr, uid, other_invitation_ids): |
254 | - att_infos.append(((att2.user_id and att2.user_id.name) or \ |
255 | - (att2.partner_id and att2.partner_id.name) or \ |
256 | - att2.email) + ' - Status: ' + att2.state.title()) |
257 | - #dates and times are gonna be expressed in `tz` time (local timezone of the `uid`) |
258 | - tz = context.get('tz', pytz.timezone('UTC')) |
259 | - #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 |
260 | - date_start = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context) |
261 | - date_stop = False |
262 | - if res_obj.date_deadline: |
263 | - date_stop = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context) |
264 | - body_vals = {'name': res_obj.name, |
265 | - 'start_date': date_start, |
266 | - 'end_date': date_stop, |
267 | - 'timezone': tz, |
268 | - 'description': res_obj.description or '-', |
269 | - 'location': res_obj.location or '-', |
270 | - 'attendees': '<br>'.join(att_infos), |
271 | - 'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User', |
272 | - 'sign': sign, |
273 | - 'company': company |
274 | - } |
275 | - body = html_invitation % body_vals |
276 | - if mail_to and email_from: |
277 | + model,template_id = data_pool.get_object_reference(cr, uid, 'base_calendar', "crm_email_template_meeting_invitation") |
278 | + model,act_id = data_pool.get_object_reference(cr, uid, 'base_calendar', "view_crm_meeting_calendar") |
279 | + action_id = self.pool.get('ir.actions.act_window').search(cr, uid, [('view_id','=',act_id)], context=context) |
280 | + base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context) |
281 | + body = template_pool.browse(cr, uid, template_id, context=context).body_html |
282 | + if attendee.email and email_from: |
283 | ics_file = self.get_ics_file(cr, uid, res_obj, context=context) |
284 | - vals = {'email_from': email_from, |
285 | - 'email_to': mail_to, |
286 | - 'state': 'outgoing', |
287 | - 'subject': sub, |
288 | - 'body_html': body, |
289 | - 'auto_delete': True} |
290 | + local_context['att_obj'] = attendee |
291 | + local_context['color'] = color |
292 | + local_context['action_id'] = action_id[0] |
293 | + local_context['dbname'] = cr.dbname |
294 | + local_context['base_url'] = base_url |
295 | + vals = template_pool.generate_email(cr, uid, template_id, res_obj.id, context=local_context) |
296 | if ics_file: |
297 | vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics', |
298 | - 'datas_fname': 'invitation.ics', |
299 | - 'datas': str(ics_file).encode('base64')})] |
300 | - self.pool.get('mail.mail').create(cr, uid, vals, context=context) |
301 | - return True |
302 | + 'datas_fname': 'invitation.ics', |
303 | + 'datas': str(ics_file).encode('base64')})] |
304 | + if not attendee.partner_id.opt_out: |
305 | + mail_id.append(mail_pool.create(cr, uid, vals, context=context)) |
306 | + if mail_id: |
307 | + return mail_pool.send(cr, uid, mail_id, context=context) |
308 | + return False |
309 | |
310 | def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv): |
311 | """ |
312 | @@ -561,8 +426,13 @@ |
313 | """ |
314 | if context is None: |
315 | context = {} |
316 | - |
317 | - return self.write(cr, uid, ids, {'state': 'accepted'}, context) |
318 | + meeting_obj = self.pool.get('crm.meeting') |
319 | + res = self.write(cr, uid, ids, {'state': 'accepted'}, context) |
320 | + for attandee in self.browse(cr, uid, ids, context=context): |
321 | + meeting_ids = meeting_obj.search(cr, uid, [('attendee_ids', '=', attandee.id)], context=context) |
322 | + if meeting_ids: |
323 | + meeting_obj.message_post(cr, uid, get_real_ids(meeting_ids), body=_(("%s has accepted invitation") % (attandee.cn)), context=context) |
324 | + return res |
325 | |
326 | def do_decline(self, cr, uid, ids, context=None, *args): |
327 | """ |
328 | @@ -576,7 +446,13 @@ |
329 | """ |
330 | if context is None: |
331 | context = {} |
332 | - return self.write(cr, uid, ids, {'state': 'declined'}, context) |
333 | + meeting_obj = self.pool.get('crm.meeting') |
334 | + res = self.write(cr, uid, ids, {'state': 'declined'}, context) |
335 | + for attandee in self.browse(cr, uid, ids, context=context): |
336 | + meeting_ids = meeting_obj.search(cr, uid, [('attendee_ids', '=', attandee.id)], context=context) |
337 | + if meeting_ids: |
338 | + meeting_obj.message_post(cr, uid, get_real_ids(meeting_ids), body=_(("%s has declined invitation") % (attandee.cn)), context=context) |
339 | + return res |
340 | |
341 | def create(self, cr, uid, vals, context=None): |
342 | """ |
343 | @@ -598,291 +474,6 @@ |
344 | return res |
345 | |
346 | |
347 | -class res_alarm(osv.osv): |
348 | - """Resource Alarm """ |
349 | - _name = 'res.alarm' |
350 | - _description = 'Basic Alarm Information' |
351 | - |
352 | - _columns = { |
353 | - 'name':fields.char('Name', size=256, required=True), |
354 | - 'trigger_occurs': fields.selection([('before', 'Before'), \ |
355 | - ('after', 'After')], \ |
356 | - 'Triggers', required=True), |
357 | - 'trigger_interval': fields.selection([('minutes', 'Minutes'), \ |
358 | - ('hours', 'Hours'), \ |
359 | - ('days', 'Days')], 'Interval', \ |
360 | - required=True), |
361 | - 'trigger_duration': fields.integer('Duration', required=True), |
362 | - 'trigger_related': fields.selection([('start', 'The event starts'), \ |
363 | - ('end', 'The event ends')], \ |
364 | - 'Related to', required=True), |
365 | - 'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \ |
366 | -are both optional, but if one occurs, so MUST the other"""), |
367 | - 'repeat': fields.integer('Repeat'), |
368 | - 'active': fields.boolean('Active', help="If the active field is set to \ |
369 | -true, it will allow you to hide the event alarm information without removing it.") |
370 | - } |
371 | - _defaults = { |
372 | - 'trigger_interval': 'minutes', |
373 | - 'trigger_duration': 5, |
374 | - 'trigger_occurs': 'before', |
375 | - 'trigger_related': 'start', |
376 | - 'active': 1, |
377 | - } |
378 | - |
379 | - def do_alarm_create(self, cr, uid, ids, model, date, context=None): |
380 | - """ |
381 | - Create Alarm for event. |
382 | - @param cr: the current row, from the database cursor, |
383 | - @param uid: the current user's ID for security checks, |
384 | - @param ids: List of res alarm's IDs. |
385 | - @param model: Model name. |
386 | - @param date: Event date |
387 | - @param context: A standard dictionary for contextual values |
388 | - @return: True |
389 | - """ |
390 | - if context is None: |
391 | - context = {} |
392 | - alarm_obj = self.pool.get('calendar.alarm') |
393 | - res_alarm_obj = self.pool.get('res.alarm') |
394 | - ir_obj = self.pool.get('ir.model') |
395 | - model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0] |
396 | - |
397 | - model_obj = self.pool[model] |
398 | - for data in model_obj.browse(cr, uid, ids, context=context): |
399 | - |
400 | - basic_alarm = data.alarm_id |
401 | - cal_alarm = data.base_calendar_alarm_id |
402 | - if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm): |
403 | - new_res_alarm = None |
404 | - # Find for existing res.alarm |
405 | - duration = cal_alarm.trigger_duration |
406 | - interval = cal_alarm.trigger_interval |
407 | - occurs = cal_alarm.trigger_occurs |
408 | - related = cal_alarm.trigger_related |
409 | - domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)] |
410 | - alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context) |
411 | - if not alarm_ids: |
412 | - val = { |
413 | - 'trigger_duration': duration, |
414 | - 'trigger_interval': interval, |
415 | - 'trigger_occurs': occurs, |
416 | - 'trigger_related': related, |
417 | - 'name': str(duration) + ' ' + str(interval) + ' ' + str(occurs) |
418 | - } |
419 | - new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context) |
420 | - else: |
421 | - new_res_alarm = alarm_ids[0] |
422 | - cr.execute('UPDATE %s ' % model_obj._table + \ |
423 | - ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \ |
424 | - ' WHERE id=%s', |
425 | - (cal_alarm.id, new_res_alarm, data.id)) |
426 | - |
427 | - self.do_alarm_unlink(cr, uid, [data.id], model) |
428 | - if basic_alarm: |
429 | - vals = { |
430 | - 'action': 'display', |
431 | - 'description': data.description, |
432 | - 'name': data.name, |
433 | - 'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))], |
434 | - 'trigger_related': basic_alarm.trigger_related, |
435 | - 'trigger_duration': basic_alarm.trigger_duration, |
436 | - 'trigger_occurs': basic_alarm.trigger_occurs, |
437 | - 'trigger_interval': basic_alarm.trigger_interval, |
438 | - 'duration': basic_alarm.duration, |
439 | - 'repeat': basic_alarm.repeat, |
440 | - 'state': 'run', |
441 | - 'event_date': data[date], |
442 | - 'res_id': data.id, |
443 | - 'model_id': model_id, |
444 | - 'user_id': uid |
445 | - } |
446 | - alarm_id = alarm_obj.create(cr, uid, vals) |
447 | - cr.execute('UPDATE %s ' % model_obj._table + \ |
448 | - ' SET base_calendar_alarm_id=%s, alarm_id=%s ' |
449 | - ' WHERE id=%s', \ |
450 | - ( alarm_id, basic_alarm.id, data.id) ) |
451 | - return True |
452 | - |
453 | - def do_alarm_unlink(self, cr, uid, ids, model, context=None): |
454 | - """ |
455 | - Delete alarm specified in ids |
456 | - @param cr: the current row, from the database cursor, |
457 | - @param uid: the current user's ID for security checks, |
458 | - @param ids: List of res alarm's IDs. |
459 | - @param model: Model name for which alarm is to be cleared. |
460 | - @return: True |
461 | - """ |
462 | - if context is None: |
463 | - context = {} |
464 | - alarm_obj = self.pool.get('calendar.alarm') |
465 | - ir_obj = self.pool.get('ir.model') |
466 | - model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0] |
467 | - model_obj = self.pool[model] |
468 | - for data in model_obj.browse(cr, uid, ids, context=context): |
469 | - alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', data.id)]) |
470 | - if alarm_ids: |
471 | - alarm_obj.unlink(cr, uid, alarm_ids) |
472 | - cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\ |
473 | - where id=%%s' % model_obj._table,(data.id,)) |
474 | - return True |
475 | - |
476 | - |
477 | -class calendar_alarm(osv.osv): |
478 | - _name = 'calendar.alarm' |
479 | - _description = 'Event alarm information' |
480 | - _inherit = 'res.alarm' |
481 | - __attribute__ = {} |
482 | - |
483 | - _columns = { |
484 | - 'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'), |
485 | - 'name': fields.char('Summary', size=124, help="""Contains the text to be \ |
486 | - used as the message subject for email \ |
487 | - or contains the text to be used for display"""), |
488 | - 'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \ |
489 | - ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \ |
490 | - required=True, help="Defines the action to be invoked when an alarm is triggered"), |
491 | - 'description': fields.text('Description', help='Provides a more complete \ |
492 | - description of the calendar component, than that \ |
493 | - provided by the "SUMMARY" property'), |
494 | - 'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \ |
495 | - 'alarm_id', 'attendee_id', 'Attendees', readonly=True), |
496 | - 'attach': fields.binary('Attachment', help="""* Points to a sound resource,\ |
497 | - which is rendered when the alarm is triggered for audio, |
498 | - * File which is intended to be sent as message attachments for email, |
499 | - * Points to a procedure resource, which is invoked when\ |
500 | - the alarm is triggered for procedure."""), |
501 | - 'res_id': fields.integer('Resource ID'), |
502 | - 'model_id': fields.many2one('ir.model', 'Model'), |
503 | - 'user_id': fields.many2one('res.users', 'Owner'), |
504 | - 'event_date': fields.datetime('Event Date'), |
505 | - 'event_end_date': fields.datetime('Event End Date'), |
506 | - 'trigger_date': fields.datetime('Trigger Date', readonly="True"), |
507 | - 'state':fields.selection([ |
508 | - ('draft', 'Draft'), |
509 | - ('run', 'Run'), |
510 | - ('stop', 'Stop'), |
511 | - ('done', 'Done'), |
512 | - ], 'Status', select=True, readonly=True), |
513 | - } |
514 | - |
515 | - _defaults = { |
516 | - 'action': 'email', |
517 | - 'state': 'run', |
518 | - } |
519 | - |
520 | - def create(self, cr, uid, vals, context=None): |
521 | - """ |
522 | - Overrides orm create method. |
523 | - @param self: The object pointer |
524 | - @param cr: the current row, from the database cursor, |
525 | - @param vals: dictionary of fields value.{'name_of_the_field': value, ...} |
526 | - @param context: A standard dictionary for contextual values |
527 | - @return: new record id for calendar_alarm. |
528 | - """ |
529 | - if context is None: |
530 | - context = {} |
531 | - event_date = vals.get('event_date', False) |
532 | - if event_date: |
533 | - dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S") |
534 | - if vals['trigger_interval'] == 'days': |
535 | - delta = timedelta(days=vals['trigger_duration']) |
536 | - if vals['trigger_interval'] == 'hours': |
537 | - delta = timedelta(hours=vals['trigger_duration']) |
538 | - if vals['trigger_interval'] == 'minutes': |
539 | - delta = timedelta(minutes=vals['trigger_duration']) |
540 | - trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta) |
541 | - vals['trigger_date'] = trigger_date |
542 | - res = super(calendar_alarm, self).create(cr, uid, vals, context=context) |
543 | - return res |
544 | - |
545 | - def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \ |
546 | - context=None): |
547 | - """Scheduler for event reminder |
548 | - @param self: The object pointer |
549 | - @param cr: the current row, from the database cursor, |
550 | - @param uid: the current user's ID for security checks, |
551 | - @param ids: List of calendar alarm's IDs. |
552 | - @param use_new_cursor: False or the dbname |
553 | - @param context: A standard dictionary for contextual values |
554 | - """ |
555 | - if context is None: |
556 | - context = {} |
557 | - current_datetime = datetime.now() |
558 | - alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context) |
559 | - |
560 | - mail_to = "" |
561 | - |
562 | - for alarm in self.browse(cr, uid, alarm_ids, context=context): |
563 | - next_trigger_date = None |
564 | - update_vals = {} |
565 | - model_obj = self.pool[alarm.model_id.model] |
566 | - res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context) |
567 | - re_dates = [] |
568 | - |
569 | - if hasattr(res_obj, 'rrule') and res_obj.rrule: |
570 | - event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S') |
571 | - #exdate is a string and we need a list |
572 | - exdate = res_obj.exdate and res_obj.exdate.split(',') or [] |
573 | - recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule) |
574 | - |
575 | - trigger_interval = alarm.trigger_interval |
576 | - if trigger_interval == 'days': |
577 | - delta = timedelta(days=alarm.trigger_duration) |
578 | - if trigger_interval == 'hours': |
579 | - delta = timedelta(hours=alarm.trigger_duration) |
580 | - if trigger_interval == 'minutes': |
581 | - delta = timedelta(minutes=alarm.trigger_duration) |
582 | - delta = alarm.trigger_occurs == 'after' and delta or -delta |
583 | - |
584 | - for rdate in recurrent_dates: |
585 | - if rdate + delta > current_datetime: |
586 | - break |
587 | - if rdate + delta <= current_datetime: |
588 | - re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S")) |
589 | - rest_dates = recurrent_dates[len(re_dates):] |
590 | - next_trigger_date = rest_dates and rest_dates[0] or None |
591 | - |
592 | - else: |
593 | - re_dates = [alarm.trigger_date] |
594 | - |
595 | - if re_dates: |
596 | - if alarm.action == 'email': |
597 | - sub = '[OpenERP Reminder] %s' % (alarm.name) |
598 | - body = """<pre> |
599 | -Event: %s |
600 | -Event Date: %s |
601 | -Description: %s |
602 | - |
603 | -From: |
604 | - %s |
605 | - |
606 | ----- |
607 | -%s |
608 | -</pre> |
609 | -""" % (alarm.name, alarm.trigger_date, alarm.description, \ |
610 | - alarm.user_id.name, alarm.user_id.signature) |
611 | - mail_to = alarm.user_id.email |
612 | - for att in alarm.attendee_ids: |
613 | - mail_to = mail_to + " " + att.user_id.email |
614 | - if mail_to: |
615 | - vals = { |
616 | - 'state': 'outgoing', |
617 | - 'subject': sub, |
618 | - 'body_html': body, |
619 | - 'email_to': mail_to, |
620 | - 'email_from': tools.config.get('email_from', mail_to), |
621 | - } |
622 | - self.pool.get('mail.mail').create(cr, uid, vals, context=context) |
623 | - if next_trigger_date: |
624 | - update_vals.update({'trigger_date': next_trigger_date}) |
625 | - else: |
626 | - update_vals.update({'state': 'done'}) |
627 | - self.write(cr, uid, [alarm.id], update_vals) |
628 | - return True |
629 | - |
630 | - |
631 | - |
632 | class calendar_event(osv.osv): |
633 | _name = "calendar.event" |
634 | _description = "Calendar Event" |
635 | @@ -1011,12 +602,11 @@ |
636 | 'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}), |
637 | 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \ |
638 | 'Show Time as', states={'done': [('readonly', True)]}), |
639 | - 'base_calendar_url': fields.char('Caldav URL', size=264), |
640 | 'state': fields.selection([ |
641 | ('tentative', 'Uncertain'), |
642 | ('cancelled', 'Cancelled'), |
643 | ('confirmed', 'Confirmed'), |
644 | - ], 'Status', readonly=True), |
645 | + ],'Status', readonly=True), |
646 | 'exdate': fields.text('Exception Date/Times', help="This property \ |
647 | defines the list of date/time exceptions for a recurring calendar component."), |
648 | 'exrule': fields.char('Exception Rule', size=352, help="Defines a \ |
649 | @@ -1030,15 +620,11 @@ |
650 | ('yearly', 'Year(s)') |
651 | ], 'Recurrency', states={'done': [('readonly', True)]}, |
652 | help="Let the event automatically repeat at that interval"), |
653 | - 'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]}, |
654 | - help="Set an alarm at this time, before the event occurs" ), |
655 | - 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'), |
656 | + 'alarm_ids': fields.many2many('calendar.alarm', 'Remiders'), |
657 | 'recurrent_id': fields.integer('Recurrent ID'), |
658 | 'recurrent_id_date': fields.datetime('Recurrent ID date'), |
659 | 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'), |
660 | 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}), |
661 | - 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with organizer attribute of VEvent. |
662 | - 'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}), |
663 | 'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'), |
664 | 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"), |
665 | 'count': fields.integer('Repeat', help="Repeat x times"), |
666 | @@ -1078,6 +664,11 @@ |
667 | 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}), |
668 | } |
669 | |
670 | + def new_invitation_token(self, cr, uid, record, partner_id): |
671 | + db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid') |
672 | + invitation_token = hashlib.sha256('%s-%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id, partner_id)).hexdigest() |
673 | + return invitation_token |
674 | + |
675 | def create_attendees(self, cr, uid, ids, context): |
676 | att_obj = self.pool.get('calendar.attendee') |
677 | user_obj = self.pool.get('res.users') |
678 | @@ -1091,34 +682,27 @@ |
679 | for partner in event.partner_ids: |
680 | if partner.id in attendees: |
681 | continue |
682 | - local_context = context.copy() |
683 | - local_context.pop('default_state', None) |
684 | + access_token = self.new_invitation_token(cr, uid, event, partner.id) |
685 | att_id = self.pool.get('calendar.attendee').create(cr, uid, { |
686 | 'partner_id': partner.id, |
687 | 'user_id': partner.user_ids and partner.user_ids[0].id or False, |
688 | 'ref': self._name+','+str(event.id), |
689 | - 'email': partner.email |
690 | - }, context=local_context) |
691 | + 'access_token': access_token, |
692 | + 'email': partner.email, |
693 | + }, context=context) |
694 | if partner.email: |
695 | mail_to = mail_to + " " + partner.email |
696 | self.write(cr, uid, [event.id], { |
697 | 'attendee_ids': [(4, att_id)] |
698 | }, context=context) |
699 | new_attendees.append(att_id) |
700 | - |
701 | if mail_to and current_user.email: |
702 | - att_obj._send_mail(cr, uid, new_attendees, mail_to, |
703 | + is_sent_mail = att_obj._send_mail(cr, uid, new_attendees, mail_to, |
704 | email_from = current_user.email, context=context) |
705 | + if is_sent_mail: |
706 | + self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), context=context) |
707 | return True |
708 | |
709 | - def default_organizer(self, cr, uid, context=None): |
710 | - user_pool = self.pool.get('res.users') |
711 | - user = user_pool.browse(cr, uid, uid, context=context) |
712 | - res = user.name |
713 | - if user.email: |
714 | - res += " <%s>" %(user.email) |
715 | - return res |
716 | - |
717 | _defaults = { |
718 | 'end_type': 'count', |
719 | 'count': 1, |
720 | @@ -1130,7 +714,6 @@ |
721 | 'interval': 1, |
722 | 'active': 1, |
723 | 'user_id': lambda self, cr, uid, ctx: uid, |
724 | - 'organizer': default_organizer, |
725 | } |
726 | |
727 | def _check_closing_date(self, cr, uid, ids, context=None): |
728 | @@ -1623,53 +1206,103 @@ |
729 | return self.write(cr, uid, ids, {'state': 'confirmed'}, context) |
730 | |
731 | |
732 | -class calendar_todo(osv.osv): |
733 | - """ Calendar Task """ |
734 | - |
735 | - _name = "calendar.todo" |
736 | - _inherit = "calendar.event" |
737 | - _description = "Calendar Task" |
738 | - |
739 | - def _get_date(self, cr, uid, ids, name, arg, context=None): |
740 | - """ |
741 | - Get Date |
742 | - @param self: The object pointer |
743 | - @param cr: the current row, from the database cursor, |
744 | - @param uid: the current user's ID for security checks, |
745 | - @param ids: List of calendar todo's IDs. |
746 | - @param args: list of tuples of form [(‘name_of_the_field', ‘operator', value), ...]. |
747 | - @param context: A standard dictionary for contextual values |
748 | - """ |
749 | - |
750 | - res = {} |
751 | - for event in self.browse(cr, uid, ids, context=context): |
752 | - res[event.id] = event.date_start |
753 | - return res |
754 | - |
755 | - def _set_date(self, cr, uid, id, name, value, arg, context=None): |
756 | - """ |
757 | - Set Date |
758 | - @param self: The object pointer |
759 | - @param cr: the current row, from the database cursor, |
760 | - @param uid: the current user's ID for security checks, |
761 | - @param id: calendar's ID. |
762 | - @param value: Get Value |
763 | - @param args: list of tuples of form [('name_of_the_field', 'operator', value), ...]. |
764 | - @param context: A standard dictionary for contextual values |
765 | - """ |
766 | - |
767 | - assert name == 'date' |
768 | - return self.write(cr, uid, id, { 'date_start': value }, context=context) |
769 | +class calendar_alarm(osv.osv): |
770 | + _name = 'calendar.alarm' |
771 | + _description = 'Event alarm' |
772 | |
773 | _columns = { |
774 | - 'date': fields.function(_get_date, fnct_inv=_set_date, \ |
775 | - string='Duration', store=True, type='datetime'), |
776 | - 'duration': fields.integer('Duration'), |
777 | + 'name':fields.char('Name', size=256, required=True), # fields function |
778 | + 'type': fields.selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True), |
779 | + 'duration': fields.integer('Amount', required=True), |
780 | + 'interval': fields.selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], 'Unit', required=True), |
781 | + } |
782 | + _defaults = { |
783 | + 'type': 'notification', |
784 | + 'duration': 1, |
785 | + 'interval': 'hours', |
786 | } |
787 | |
788 | - __attribute__ = {} |
789 | - |
790 | - |
791 | + |
792 | +class res_partner(osv.osv): |
793 | + _inherit = 'res.partner' |
794 | + |
795 | + def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None): |
796 | + datas = [] |
797 | + meeting = False |
798 | + if meeting_id: |
799 | + meeting = self.pool.get('crm.meeting').browse(cr, uid, get_real_ids(meeting_id),context) |
800 | + for partner in self.browse(cr, uid, ids, context=context): |
801 | + data = self.name_get(cr, uid, [partner.id], context)[0] |
802 | + if meeting: |
803 | + for attendee in meeting.attendee_ids: |
804 | + if attendee.partner_id.id == partner.id: |
805 | + data = (data[0], data[1], attendee.state) |
806 | + datas.append(data) |
807 | + return datas |
808 | + |
809 | + def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None): |
810 | + if context is None: |
811 | + context = {} |
812 | + current_datetime = datetime.now() |
813 | + alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context) |
814 | + |
815 | + mail_to = "" |
816 | + |
817 | + for alarm in self.browse(cr, uid, alarm_ids, context=context): |
818 | + next_trigger_date = None |
819 | + update_vals = {} |
820 | + model_obj = self.pool[alarm.model_id.model] |
821 | + res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context) |
822 | + re_dates = [] |
823 | + |
824 | + if hasattr(res_obj, 'rrule') and res_obj.rrule: |
825 | + event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S') |
826 | + #exdate is a string and we need a list |
827 | + exdate = res_obj.exdate and res_obj.exdate.split(',') or [] |
828 | + recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule) |
829 | + |
830 | + trigger_interval = alarm.trigger_interval |
831 | + if trigger_interval == 'days': |
832 | + delta = timedelta(days=alarm.trigger_duration) |
833 | + if trigger_interval == 'hours': |
834 | + delta = timedelta(hours=alarm.trigger_duration) |
835 | + if trigger_interval == 'minutes': |
836 | + delta = timedelta(minutes=alarm.trigger_duration) |
837 | + delta = alarm.trigger_occurs == 'after' and delta or -delta |
838 | + |
839 | + for rdate in recurrent_dates: |
840 | + if rdate + delta > current_datetime: |
841 | + break |
842 | + if rdate + delta <= current_datetime: |
843 | + re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S")) |
844 | + rest_dates = recurrent_dates[len(re_dates):] |
845 | + next_trigger_date = rest_dates and rest_dates[0] or None |
846 | + |
847 | + else: |
848 | + re_dates = [alarm.trigger_date] |
849 | + |
850 | + if re_dates: |
851 | + if alarm.action == 'email': |
852 | + sub = '[OpenERP Reminder] %s' % (alarm.name) |
853 | + body = """ """ |
854 | + mail_to = alarm.user_id.email |
855 | + for att in alarm.attendee_ids: |
856 | + mail_to = mail_to + " " + att.user_id.email |
857 | + if mail_to: |
858 | + vals = { |
859 | + 'state': 'outgoing', |
860 | + 'subject': sub, |
861 | + 'body_html': body, |
862 | + 'email_to': mail_to, |
863 | + 'email_from': tools.config.get('email_from', mail_to), |
864 | + } |
865 | + self.pool.get('mail.mail').create(cr, uid, vals, context=context) |
866 | + if next_trigger_date: |
867 | + update_vals.update({'trigger_date': next_trigger_date}) |
868 | + else: |
869 | + update_vals.update({'state': 'done'}) |
870 | + self.write(cr, uid, [alarm.id], update_vals) |
871 | + return True |
872 | |
873 | |
874 | class ir_values(osv.osv): |
875 | |
876 | === added directory 'base_calendar/controllers' |
877 | === added file 'base_calendar/controllers/__init__.py' |
878 | --- base_calendar/controllers/__init__.py 1970-01-01 00:00:00 +0000 |
879 | +++ base_calendar/controllers/__init__.py 2013-11-07 16:05:43 +0000 |
880 | @@ -0,0 +1,3 @@ |
881 | +import main |
882 | + |
883 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
884 | |
885 | === added file 'base_calendar/controllers/main.py' |
886 | --- base_calendar/controllers/main.py 1970-01-01 00:00:00 +0000 |
887 | +++ base_calendar/controllers/main.py 2013-11-07 16:05:43 +0000 |
888 | @@ -0,0 +1,77 @@ |
889 | +import simplejson |
890 | +import urllib |
891 | +import openerp |
892 | +import openerp.addons.web.http as http |
893 | +from openerp.addons.web.http import request |
894 | +import openerp.addons.web.controllers.main as webmain |
895 | +import json |
896 | +from openerp.addons.web.http import SessionExpiredException |
897 | +from werkzeug.exceptions import BadRequest |
898 | + |
899 | +class meetting_invitation(http.Controller): |
900 | + |
901 | + def check_security(self, db, token): |
902 | + registry = openerp.modules.registry.RegistryManager.get(db) |
903 | + attendee_pool = registry.get('calendar.attendee') |
904 | + error_message = False |
905 | + with registry.cursor() as cr: |
906 | + attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token)]) |
907 | + if not attendee_id: |
908 | + # if token is not match |
909 | + error_message = """Invalid Invitation Token.""" |
910 | + elif request.session.uid and request.session.login != 'anonymous': |
911 | + # if valid session but user is not match |
912 | + attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0]) |
913 | + user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid) |
914 | + if attendee.user_id.id != user.id: |
915 | + 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) |
916 | + if error_message: |
917 | + raise BadRequest(error_message) |
918 | + return True |
919 | + |
920 | + @http.route('/meeting_invitation/accept', type='http', auth="none") |
921 | + def accept(self, db, token, action, id): |
922 | + # http://hostname:8069/meeting_invitation/accept?db=#token=&action=&id= |
923 | + self.check_security(db, token) |
924 | + registry = openerp.modules.registry.RegistryManager.get(db) |
925 | + attendee_pool = registry.get('calendar.attendee') |
926 | + with registry.cursor() as cr: |
927 | + attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token),('state','!=', 'accepted')]) |
928 | + if attendee_id: |
929 | + attendee_pool.do_accept(cr, openerp.SUPERUSER_ID, attendee_id) |
930 | + return self.view(db, token, action, id, view='form') |
931 | + |
932 | + @http.route('/meeting_invitation/decline', type='http', auth="none") |
933 | + def declined(self, db, token, action, id): |
934 | + # http://hostname:8069/meeting_invitation/decline?db=#token=&action=&id= |
935 | + self.check_security(db, token) |
936 | + registry = openerp.modules.registry.RegistryManager.get(db) |
937 | + attendee_pool = registry.get('calendar.attendee') |
938 | + with registry.cursor() as cr: |
939 | + attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token),('state','!=', 'declined')]) |
940 | + if attendee_id: |
941 | + attendee_pool.do_decline(cr, openerp.SUPERUSER_ID, attendee_id) |
942 | + return self.view(db, token, action, id, view='form') |
943 | + |
944 | + @http.route('/meeting_invitation/view', type='http', auth="none") |
945 | + def view(self, db, token, action, id, view='calendar'): |
946 | + # http://hostname:8069/meeting_invitation/view?db=#token=&action=&id= |
947 | + self.check_security(db, token) |
948 | + registry = openerp.modules.registry.RegistryManager.get(db) |
949 | + meeting_pool = registry.get('crm.meeting') |
950 | + attendee_pool = registry.get('calendar.attendee') |
951 | + with registry.cursor() as cr: |
952 | + attendee_data = meeting_pool.get_attendee(cr, openerp.SUPERUSER_ID, id); |
953 | + attendee = attendee_pool.search_read(cr, openerp.SUPERUSER_ID, [('access_token','=',token)],[]) |
954 | + if attendee: |
955 | + attendee_data['current_attendee'] = attendee[0] |
956 | + js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in webmain.manifest_list('js', db=db)) |
957 | + css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in webmain.manifest_list('css',db=db)) |
958 | + return webmain.html_template % { |
959 | + 'js': js, |
960 | + 'css': css, |
961 | + 'modules': simplejson.dumps(webmain.module_boot(db)), |
962 | + 'init': "s.base_calendar.event('%s', '%s', '%s', '%s' , '%s');" % (db, action, id, view, json.dumps(attendee_data)), |
963 | + } |
964 | + |
965 | + |
966 | |
967 | === modified file 'base_calendar/crm_meeting.py' |
968 | --- base_calendar/crm_meeting.py 2013-08-13 09:47:19 +0000 |
969 | +++ base_calendar/crm_meeting.py 2013-11-07 16:05:43 +0000 |
970 | @@ -22,9 +22,14 @@ |
971 | import time |
972 | |
973 | from openerp.osv import fields, osv |
974 | -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
975 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT |
976 | from openerp.tools.translate import _ |
977 | from base_calendar import get_real_ids, base_calendar_id2real_id |
978 | +from datetime import datetime, timedelta, date |
979 | +import pytz |
980 | +from openerp import tools |
981 | +import openerp |
982 | + |
983 | # |
984 | # crm.meeting is defined here so that it may be used by modules other than crm, |
985 | # without forcing the installation of crm. |
986 | @@ -43,6 +48,55 @@ |
987 | _description = "Meeting" |
988 | _order = "id desc" |
989 | _inherit = ["calendar.event", "mail.thread", "ir.needaction_mixin"] |
990 | + |
991 | + def _find_user_attendee(self, cr, uid, meeting_ids, context=None): |
992 | + attendee_pool = self.pool.get('calendar.attendee') |
993 | + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) |
994 | + for meeting_id in meeting_ids: |
995 | + for attendee in self.browse(cr,uid,meeting_id,context).attendee_ids: |
996 | + if user.partner_id.id == attendee.partner_id.id: |
997 | + return attendee |
998 | + return False |
999 | + |
1000 | + def _compute(self, cr, uid, ids, fields, arg, context=None): |
1001 | + res = {} |
1002 | + for meeting_id in ids: |
1003 | + res[meeting_id] = {} |
1004 | + attendee = self._find_user_attendee(cr, uid, [meeting_id], context) |
1005 | + for field in fields: |
1006 | + if field == 'is_attendee': |
1007 | + res[meeting_id][field] = True if attendee else False |
1008 | + elif field == 'attendee_status': |
1009 | + res[meeting_id][field] = attendee.state if attendee else 'needs-action' |
1010 | + elif field == 'event_time': |
1011 | + res[meeting_id][field] = self._compute_time(cr, uid, meeting_id, context=context) |
1012 | + return res |
1013 | + |
1014 | + |
1015 | + def _compute_time(self, cr, uid, meeting_id, context=None): |
1016 | + """ |
1017 | + Return date and time (from to from) based on duration with timezone in string : |
1018 | + eg. |
1019 | + 1) if user add duration for 2 hours, return : August-23-2013 at ( 04-30 To 06-30) (Europe/Brussels) |
1020 | + 2) if event all day ,return : AllDay, July-31-2013 |
1021 | + """ |
1022 | + if context is None: |
1023 | + context = {} |
1024 | + tz = context.get('tz', pytz.timezone('UTC')) |
1025 | + meeting = self.browse(cr, uid, meeting_id, context=context) |
1026 | + date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context) |
1027 | + date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context) |
1028 | + event_date = date.strftime('%B-%d-%Y') |
1029 | + event_time = date.strftime('%H-%M') |
1030 | + if meeting.allday: |
1031 | + time = _("AllDay , %s") % (event_date) |
1032 | + elif meeting.duration < 24: |
1033 | + duration = date + timedelta(hours= meeting.duration) |
1034 | + time = ("%s at ( %s To %s) (%s)") % (event_date, event_time, duration.strftime('%H-%M'), tz) |
1035 | + else : |
1036 | + 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) |
1037 | + return time |
1038 | + |
1039 | _columns = { |
1040 | 'create_date': fields.datetime('Creation Date', readonly=True), |
1041 | 'write_date': fields.datetime('Write Date', readonly=True), |
1042 | @@ -59,16 +113,29 @@ |
1043 | 'event_id', 'type_id', 'Tags'), |
1044 | 'attendee_ids': fields.many2many('calendar.attendee', 'meeting_attendee_rel',\ |
1045 | 'event_id', 'attendee_id', 'Invited People', states={'done': [('readonly', True)]}), |
1046 | + 'is_attendee': fields.function(_compute, string='Attendee', \ |
1047 | + type="boolean", multi='attendee'), |
1048 | + 'attendee_status': fields.function(_compute, string='Attendee Status', \ |
1049 | + type="selection", multi='attendee'), |
1050 | + 'event_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'), |
1051 | } |
1052 | _defaults = { |
1053 | 'state': 'open', |
1054 | } |
1055 | + |
1056 | + def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False): |
1057 | + if context is None: |
1058 | + context={} |
1059 | + if context.get('mymeetings',False): |
1060 | + partner_id = self.pool.get('res.users').browse(cr, uid, uid, context).partner_id.id |
1061 | + args += ['|', ('partner_ids', 'in', [partner_id]), ('user_id', '=', uid)] |
1062 | + return super(crm_meeting, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) |
1063 | |
1064 | - def message_get_subscription_data(self, cr, uid, ids, context=None): |
1065 | + def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None): |
1066 | res = {} |
1067 | for virtual_id in ids: |
1068 | real_id = base_calendar_id2real_id(virtual_id) |
1069 | - result = super(crm_meeting, self).message_get_subscription_data(cr, uid, [real_id], context=context) |
1070 | + result = super(crm_meeting, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context) |
1071 | res[virtual_id] = result[real_id] |
1072 | return res |
1073 | |
1074 | @@ -123,8 +190,48 @@ |
1075 | subtype=None, parent_id=False, attachments=None, context=None, **kwargs): |
1076 | if isinstance(thread_id, str): |
1077 | thread_id = get_real_ids(thread_id) |
1078 | + if context.get('default_date'): |
1079 | + del context['default_date'] |
1080 | 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) |
1081 | |
1082 | + def do_decline(self, cr, uid, ids, context=None): |
1083 | + attendee_pool = self.pool.get('calendar.attendee') |
1084 | + attendee = self._find_user_attendee(cr, uid, ids, context) |
1085 | + return attendee_pool.do_decline(cr, uid, [attendee.id], context=context) |
1086 | + |
1087 | + def do_accept(self, cr, uid, ids, context=None): |
1088 | + attendee_pool = self.pool.get('calendar.attendee') |
1089 | + attendee = self._find_user_attendee(cr, uid, ids, context) |
1090 | + return attendee_pool.do_accept(cr, uid, [attendee.id], context=context) |
1091 | + |
1092 | + def get_attendee(self, cr, uid, meeting_id, context=None): |
1093 | + invitation = {'meeting':{}, 'attendee': [], 'logo': ''} |
1094 | + attendee_pool = self.pool.get('calendar.attendee') |
1095 | + company_logo = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.logo |
1096 | + meeting = self.browse(cr, uid, int(meeting_id), context) |
1097 | + invitation['meeting'] = { |
1098 | + 'event':meeting.name, |
1099 | + 'organizer': meeting.organizer, |
1100 | + 'where': meeting.location, |
1101 | + 'when':meeting.event_time |
1102 | + } |
1103 | + invitation['logo'] = company_logo.replace('\n','\\n') if company_logo else '' |
1104 | + for attendee in meeting.attendee_ids: |
1105 | + invitation['attendee'].append({'name':attendee.cn,'status': attendee.state}) |
1106 | + return invitation |
1107 | + |
1108 | + def get_interval(self, cr, uid, ids, date, interval, context=None): |
1109 | + date = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT) |
1110 | + if interval == 'day': |
1111 | + res = str(date.day) |
1112 | + elif interval == 'month': |
1113 | + res = date.strftime('%B') + " " + str(date.year) |
1114 | + elif interval == 'dayname': |
1115 | + res = date.strftime('%A') |
1116 | + elif interval == 'time': |
1117 | + res = date.strftime('%I:%M %p') |
1118 | + return res |
1119 | + |
1120 | class mail_message(osv.osv): |
1121 | _inherit = "mail.message" |
1122 | |
1123 | |
1124 | === modified file 'base_calendar/crm_meeting_data.xml' |
1125 | --- base_calendar/crm_meeting_data.xml 2012-11-29 22:26:45 +0000 |
1126 | +++ base_calendar/crm_meeting_data.xml 2013-11-07 16:05:43 +0000 |
1127 | @@ -28,5 +28,133 @@ |
1128 | <field name="name">Meeting</field> |
1129 | <field name="object">crm.meeting</field> |
1130 | </record> |
1131 | + <record id="crm_email_template_meeting_invitation" model="email.template"> |
1132 | + <field name="name">CRM Meeting Invitation</field> |
1133 | + <field name="email_from">${object.user_id.email or ''}</field> |
1134 | + <field name="subject">${object.name}</field> |
1135 | + <field name="email_to" >${ctx['att_obj'].email}</field> |
1136 | + <field name="model_id" ref="base_calendar.model_crm_meeting"/> |
1137 | + |
1138 | + <field name="auto_delete" eval="True"/> |
1139 | + <field name="body_html"><![CDATA[ |
1140 | + <html> |
1141 | + <head> |
1142 | + <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> |
1143 | + <title>${object.name}</title> |
1144 | + </head> |
1145 | + <body> |
1146 | + <div style="border-radius: 2px; max-width: 1200px; height: auto;margin-left: auto;margin-right: auto;background-color:#f9f9f9;"> |
1147 | + <div style="height:auto;text-align: center;font-size : 30px;color: #8A89BA;"> |
1148 | + <strong>${object.name}</strong> |
1149 | + </div> |
1150 | + <div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px"> |
1151 | + <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> |
1152 | + </div> |
1153 | + <div style="height: auto;margin-left:12px;margin-top:30px;"> |
1154 | + <table> |
1155 | + <tr> |
1156 | + <td> |
1157 | + <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> |
1158 | + <div style="font-size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F;background-color: #E1E2F8;width: 130px;"> |
1159 | + ${object.get_interval(object.date,'day')} |
1160 | + </div> |
1161 | + <div style='font-size:12px;text-align:center;font-weight:bold;color:#ffffff;background-color:#8a89ba'>${object.get_interval(object.date, 'month')}</div> |
1162 | + <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> |
1163 | + </td> |
1164 | + <td> |
1165 | + <table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;"> |
1166 | + % if object.location : |
1167 | + <tr style=" height: 30px;"> |
1168 | + <td style="vertical-align:top;"> |
1169 | + <div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> |
1170 | + Where |
1171 | + </div> |
1172 | + </td> |
1173 | + <td colspan="1" style="vertical-align:top;"> |
1174 | + <div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" > |
1175 | + : ${object.location} |
1176 | + <span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.location}">View Map</a>) |
1177 | + </span> |
1178 | + </div> |
1179 | + </td> |
1180 | + </tr> |
1181 | + % endif |
1182 | + % if not object.location : |
1183 | + <tr style=" height: 30px;color:#909090"> |
1184 | + <td> |
1185 | + <div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> |
1186 | + Where |
1187 | + </div> |
1188 | + </td> |
1189 | + <td colspan="1"> |
1190 | + <div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px;" > |
1191 | + : - |
1192 | + </div> |
1193 | + </td> |
1194 | + </tr> |
1195 | + % endif |
1196 | + % if object.description : |
1197 | + <tr style=" height:auto;"> |
1198 | + <td style="vertical-align:top;"> |
1199 | + <div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> |
1200 | + What |
1201 | + </div> |
1202 | + </td> |
1203 | + <td colspan="3" style="vertical-align:text-top;"> |
1204 | + <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> |
1205 | + : ${object.description or ''} |
1206 | + </div> |
1207 | + </td> |
1208 | + </tr> |
1209 | + % endif |
1210 | + % if not object.description : |
1211 | + <tr style=" height: 30px;color:#909090"> |
1212 | + <td style="vertical-align:top;"> |
1213 | + <div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> |
1214 | + What |
1215 | + </div> |
1216 | + </td> |
1217 | + <td colspan="3" style="vertical-align:text-top;"> |
1218 | + <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> |
1219 | + : - |
1220 | + </div> |
1221 | + </td> |
1222 | + </tr> |
1223 | + % endif |
1224 | + <tr style=" height: 30px;"> |
1225 | + <td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> |
1226 | + <div> |
1227 | + Attendees |
1228 | + </div> |
1229 | + </td> |
1230 | + <td colspan="3"> |
1231 | + : <div style='display:inline-block; border-radius: 50%; width:10px; height:10px;background:grey;'></div> |
1232 | + <span style="margin-left:5px">You</span> |
1233 | + |
1234 | + % for attendee in object.attendee_ids: |
1235 | + % if attendee.cn != ctx['att_obj'].cn: |
1236 | + <div style='display:inline-block; border-radius: 50%; width:10px; height:10px;background:${ctx['color'][attendee.state]};'></div> |
1237 | + <span style="margin-left:5px">${attendee.cn}</span> |
1238 | + % endif |
1239 | + % endfor |
1240 | + </td> |
1241 | + </tr> |
1242 | + </table> |
1243 | + </td> |
1244 | + </tr> |
1245 | + </table> |
1246 | + </div> |
1247 | + <div style="height: auto;width:300px; margin:0 auto;padding-top:20px;"> |
1248 | + <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> |
1249 | + <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> |
1250 | + </div> |
1251 | + <div style="padding-top:10px;"> |
1252 | + -- </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> |
1253 | + </div> |
1254 | + </div> |
1255 | + </body> |
1256 | + </html> |
1257 | + ]]></field> |
1258 | + </record> |
1259 | </data> |
1260 | </openerp> |
1261 | |
1262 | === modified file 'base_calendar/crm_meeting_view.xml' |
1263 | --- base_calendar/crm_meeting_view.xml 2013-09-04 14:11:22 +0000 |
1264 | +++ base_calendar/crm_meeting_view.xml 2013-11-07 16:05:43 +0000 |
1265 | @@ -32,8 +32,16 @@ |
1266 | <field name="model">crm.meeting</field> |
1267 | <field name="arch" type="xml"> |
1268 | <form string="Meetings" version="7.0"> |
1269 | - <field name="state" invisible="True"/> |
1270 | + <header> |
1271 | + <button name="do_accept" type="object" |
1272 | + string="Accept" attrs="{'invisible':['|',('is_attendee','=',False),('attendee_status','=','accepted')]}"/> |
1273 | + <button name="do_decline" type="object" |
1274 | + string="Decline" attrs="{'invisible':['|',('is_attendee','=',False),('attendee_status','=','declined')]}"/> |
1275 | + <field name="state" invisible="True"/> |
1276 | + </header> |
1277 | <sheet> |
1278 | + <field name="is_attendee" invisible="1"/> |
1279 | + <field name="attendee_status" invisible="1"/> |
1280 | <div class="oe_title"> |
1281 | <div class="oe_edit_only"> |
1282 | <label for="name"/> |
1283 | @@ -43,7 +51,7 @@ |
1284 | </h1> |
1285 | <label for="partner_ids" class="oe_edit_only"/> |
1286 | <h2> |
1287 | - <field name="partner_ids" widget="many2many_tags" |
1288 | + <field name="partner_ids" widget="many2manyattendee" |
1289 | context="{'force_email':True}" |
1290 | on_change="onchange_partner_ids(partner_ids)"/> |
1291 | </h2> |
1292 | @@ -133,16 +141,16 @@ |
1293 | </group> |
1294 | </group> |
1295 | </page> |
1296 | - <page string="Invitations"> |
1297 | + <page string="Invitations" groups="base.group_no_one"> |
1298 | <field name="attendee_ids" widget="one2many" mode="tree"> |
1299 | - <tree string="Invitation details" editable="top"> |
1300 | + <tree string="Invitation details" editable="top" > |
1301 | <field name="partner_id" on_change="onchange_partner_id(partner_id)"/> |
1302 | <field name="email" string="Mail To"/> |
1303 | <field name="state"/> |
1304 | <button name="do_tentative" |
1305 | states="needs-action,declined,accepted" |
1306 | string="Uncertain" type="object" |
1307 | - icon="terp-crm"/> |
1308 | + icon="terp-crm" /> |
1309 | <button name="do_accept" string="Accept" |
1310 | states="needs-action,tentative,declined" |
1311 | type="object" icon="gtk-apply"/> |
1312 | @@ -181,6 +189,10 @@ |
1313 | |
1314 | </notebook> |
1315 | </sheet> |
1316 | + <div class="oe_chatter"> |
1317 | + <field name="message_follower_ids" widget="mail_followers"/> |
1318 | + <field name="message_ids" widget="mail_thread" /> |
1319 | + </div> |
1320 | </form> |
1321 | </field> |
1322 | </record> |
1323 | @@ -238,7 +250,7 @@ |
1324 | <field name="categ_ids"/> |
1325 | <field name="user_id"/> |
1326 | <separator/> |
1327 | - <filter string="My Meetings" help="My Meetings" domain="[('user_id','=',uid)]"/> |
1328 | + <filter string="My Meetings" help="My Meetings" name="mymeetings" context='{"mymeetings": 1}'/> |
1329 | <filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/> |
1330 | </search> |
1331 | </field> |
1332 | @@ -252,7 +264,7 @@ |
1333 | <field name="view_mode">calendar,tree,form,gantt</field> |
1334 | <field name="view_id" ref="view_crm_meeting_calendar"/> |
1335 | <field name="search_view_id" ref="view_crm_meeting_search"/> |
1336 | - <field name="context">{"calendar_default_user_id": uid}</field> |
1337 | + <field name="context">{"search_default_mymeetings": 1}</field> |
1338 | <field name="help" type="html"> |
1339 | <p class="oe_view_nocontent_create"> |
1340 | Click to schedule a new meeting. |
1341 | |
1342 | === added directory 'base_calendar/doc' |
1343 | === added file 'base_calendar/doc/calnedar_attendee.rst' |
1344 | --- base_calendar/doc/calnedar_attendee.rst 1970-01-01 00:00:00 +0000 |
1345 | +++ base_calendar/doc/calnedar_attendee.rst 2013-11-07 16:05:43 +0000 |
1346 | @@ -0,0 +1,23 @@ |
1347 | +.. _calendar_attendee: |
1348 | +calendar.attendee: |
1349 | +================= |
1350 | + |
1351 | +Fields |
1352 | +++++++ |
1353 | + - ``access_token`` : |
1354 | + unique value(token) for every new attendee. |
1355 | + |
1356 | +Methods |
1357 | ++++++++ |
1358 | + - ``do_accept``: |
1359 | + REF : post message in chatter when attendee accepted an invitation. |
1360 | + - ``do_decline``: |
1361 | + REF : post message in chatter when attendee declined an invitation. |
1362 | + |
1363 | +calendar.event: |
1364 | +=============== |
1365 | +Methods |
1366 | ++++++++ |
1367 | + - ``new_invitation_token``: |
1368 | + generate a unique token for every new attendee. |
1369 | + |
1370 | |
1371 | === added file 'base_calendar/doc/changelog.rst' |
1372 | --- base_calendar/doc/changelog.rst 1970-01-01 00:00:00 +0000 |
1373 | +++ base_calendar/doc/changelog.rst 2013-11-07 16:05:43 +0000 |
1374 | @@ -0,0 +1,29 @@ |
1375 | +.. _changelog: |
1376 | + |
1377 | +Changelog |
1378 | +========= |
1379 | + |
1380 | +Email Template of Meeting Invitation: |
1381 | ++++++++++++++++++++++++++++++++++++++ |
1382 | + - remove static code of HTML design of email of meeting invitation |
1383 | + - added new better layout of email of meeting invitation using MAKO Template. |
1384 | + |
1385 | +Web controller: |
1386 | ++++++++++++++++ |
1387 | + - ``accept`` : |
1388 | + handle request ('meeting_invitation/accept') ,when accepted an invitation it change the status of invitation as accepted , user do need to login in system. |
1389 | + - ``declined``: |
1390 | + handle request ('meeting_invitation/decline') ,when declined an invitation it change the status of invitation as declined , user do need to login in system. |
1391 | + - ``view``: |
1392 | + 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. |
1393 | + - ``check_security``: |
1394 | + check token is valid and user is not allow to accept/decline invitation mail of other user from email template URL. |
1395 | + |
1396 | +Web Widget: |
1397 | ++++++++++++ |
1398 | + - ``Field Many2Many_invite``(widget): |
1399 | + display a status button in left side of every invited attendees of meeting , in many2many. |
1400 | + |
1401 | +Qweb Template: |
1402 | +++++++++++++++ |
1403 | + - 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. |
1404 | |
1405 | === added file 'base_calendar/doc/crm_meeting.rst' |
1406 | --- base_calendar/doc/crm_meeting.rst 1970-01-01 00:00:00 +0000 |
1407 | +++ base_calendar/doc/crm_meeting.rst 2013-11-07 16:05:43 +0000 |
1408 | @@ -0,0 +1,38 @@ |
1409 | +.. _crm_meeting: |
1410 | + |
1411 | +Fields: |
1412 | ++++++++ |
1413 | + - ``is_attendee`` : |
1414 | + function field , that defined whether loged in user is attendee or not. |
1415 | + - ``attendee_status``: |
1416 | + function field , that defined login user status, either accepted, declined or needs-action. |
1417 | + - ``event_time``: |
1418 | + function field, defined an event_time in user's tz. |
1419 | + |
1420 | +Methods: |
1421 | +++++++++ |
1422 | + - ``_find_user_attendee``: |
1423 | + return attendee if attendee is internal user else false. |
1424 | + - ``_compute_time``: |
1425 | + compute a time from date_start and duration with user's tz. |
1426 | + - ``search``: |
1427 | + search a current user's meetings |
1428 | + - ``do_accept/do_decline``: |
1429 | + trigger when ,user accept/decline from the meeting form view. |
1430 | + - ``get_attendee``: |
1431 | + get detail of attendees meeting. |
1432 | + - ``get_interval``: |
1433 | + call from email template that return formate of date, as per value pass from the email template. |
1434 | + |
1435 | +views: |
1436 | +++++++ |
1437 | + - ``do_accept``: |
1438 | + 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. |
1439 | + - ``do_decline``: |
1440 | + 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. |
1441 | + - ``chatter(message_ids)``: |
1442 | + show a log of meeting. |
1443 | + |
1444 | +security: |
1445 | ++++++++++ |
1446 | + - added record rule to restrict an user to show personal invitation on meeting , so user can't change other's status , from invitation tab. |
1447 | |
1448 | === modified file 'base_calendar/security/calendar_security.xml' |
1449 | --- base_calendar/security/calendar_security.xml 2012-06-28 06:40:05 +0000 |
1450 | +++ base_calendar/security/calendar_security.xml 2013-11-07 16:05:43 +0000 |
1451 | @@ -5,5 +5,5 @@ |
1452 | <field name="name">Survey / User</field> |
1453 | <field name="users" eval="[(4, ref('base.user_root'))]"/> |
1454 | </record> |
1455 | - </data> |
1456 | + </data> |
1457 | </openerp> |
1458 | |
1459 | === modified file 'base_calendar/security/ir.model.access.csv' |
1460 | --- base_calendar/security/ir.model.access.csv 2012-12-12 17:55:58 +0000 |
1461 | +++ base_calendar/security/ir.model.access.csv 2013-11-07 16:05:43 +0000 |
1462 | @@ -7,7 +7,7 @@ |
1463 | access_calendar_attendee_survey_user,calendar.attendee,model_calendar_attendee,base.group_survey_user,1,0,0,0 |
1464 | access_crm_meeting_manager,crm.meeting.manager,model_crm_meeting,base.group_sale_manager,1,1,1,1 |
1465 | access_crm_meeting,crm.meeting,model_crm_meeting,base.group_sale_salesman,1,1,1,0 |
1466 | -access_crm_meeting_all,crm.meeting_allll,model_crm_meeting,base.group_user,1,0,0,0 |
1467 | +access_crm_meeting_all,crm.meeting_allll,model_crm_meeting,base.group_user,1,1,0,0 |
1468 | access_crm_meeting_partner_manager,crm.meeting.partner.manager,model_crm_meeting,base.group_partner_manager,1,1,1,1 |
1469 | access_crm_meeting_type_sale_manager,crm.meeting.type.manager,model_crm_meeting_type,base.group_sale_manager,1,1,1,0 |
1470 | access_crm_meeting_type_sale_user,crm.meeting.type.user,model_crm_meeting_type,base.group_user,1,0,0,0 |
1471 | |
1472 | === added directory 'base_calendar/static/src/css' |
1473 | === added file 'base_calendar/static/src/css/base_calender.css' |
1474 | --- base_calendar/static/src/css/base_calender.css 1970-01-01 00:00:00 +0000 |
1475 | +++ base_calendar/static/src/css/base_calender.css 2013-11-07 16:05:43 +0000 |
1476 | @@ -0,0 +1,65 @@ |
1477 | +.openerp .oe_invitation , .text-core .text-tag .oe_invitation{ |
1478 | + width : 13px; |
1479 | + height : 13px; |
1480 | + margin-bottom : -4px; |
1481 | + display : inline-block; |
1482 | +} |
1483 | +.openerp .needs-action , .tentative,.text-core .text-tag .custom-edit, .text-core .text-tag .tentative { |
1484 | + background : url(/web/static/src/img/icons/gtk-normal.png) no-repeat; |
1485 | + background-size : 11px 11px; |
1486 | +} |
1487 | +.openerp .accepted , .text-core .text-tag .accepted { |
1488 | + background : url(/web/static/src/img/icons/gtk-yes.png) no-repeat; |
1489 | + background-size : 11px 11px; |
1490 | +} |
1491 | +.openerp .declined , .text-core .text-tag .declined { |
1492 | + background : url(/web/static/src/img/icons/gtk-no.png) no-repeat; |
1493 | + background-size : 11px 11px; |
1494 | +} |
1495 | +.cal_meeting { |
1496 | + font-size : 24px; |
1497 | + font-style: bold; |
1498 | + text-align : justify; |
1499 | + color : #8A89BA; |
1500 | +} |
1501 | +.cal_lable { |
1502 | + width: 50px; |
1503 | + color : #808080; |
1504 | +} |
1505 | +.invitation_block { |
1506 | + padding : 50px 0 0 30px; |
1507 | + font-size : 14px; |
1508 | + background : #f9f9f9; |
1509 | +} |
1510 | +.attendee_accepted { |
1511 | + background : url(/web/static/src/img/icons/gtk-apply.png) no-repeat; |
1512 | + background-size : 15px 15px; |
1513 | + padding-left: 20px; |
1514 | +} |
1515 | +.attendee_declined { |
1516 | + background : url(/web/static/src/img/icons/gtk-cancel.png) no-repeat; |
1517 | + background-size : 15px 15px; |
1518 | + padding-left: 20px; |
1519 | +} |
1520 | +.event_status { |
1521 | + border : 1px solid; |
1522 | + height : 20px; |
1523 | + width : auto; |
1524 | + background: #808080; |
1525 | + color : #FFFFFF; |
1526 | + padding: 5px 10px; |
1527 | + width: 400px; |
1528 | +} |
1529 | +.cal_inline { |
1530 | + display: inline; |
1531 | +} |
1532 | +.cal_tag { |
1533 | + padding-right : 10px; |
1534 | + font-style : italic; |
1535 | + font-size : 17px; |
1536 | + vertical-align:bottom; |
1537 | +} |
1538 | +.cal_image { |
1539 | + height: 30px; |
1540 | + width : 100px; |
1541 | +} |
1542 | |
1543 | === added directory 'base_calendar/static/src/js' |
1544 | === added file 'base_calendar/static/src/js/base_calendar.js' |
1545 | --- base_calendar/static/src/js/base_calendar.js 1970-01-01 00:00:00 +0000 |
1546 | +++ base_calendar/static/src/js/base_calendar.js 2013-11-07 16:05:43 +0000 |
1547 | @@ -0,0 +1,80 @@ |
1548 | +openerp.base_calendar = function(instance) { |
1549 | +var _t = instance.web._t; |
1550 | +var QWeb = instance.web.qweb; |
1551 | +instance.base_calendar = {} |
1552 | + |
1553 | + instance.base_calendar.invitation = instance.web.Widget.extend({ |
1554 | + |
1555 | + init: function(parent, db, action, id, view, attendee_data) { |
1556 | + this._super(); |
1557 | + this.db = db; |
1558 | + this.action = action; |
1559 | + this.id = id; |
1560 | + this.view = view; |
1561 | + this.attendee_data = attendee_data; |
1562 | + }, |
1563 | + start: function() { |
1564 | + var self = this; |
1565 | + if(instance.session.session_is_valid(self.db) && instance.session.username != "anonymous") { |
1566 | + self.redirect_meeting_view(self.db,self.action,self.id,self.view); |
1567 | + } else { |
1568 | + self.open_invitation_form(self.attendee_data); |
1569 | + } |
1570 | + }, |
1571 | + open_invitation_form : function(invitation){ |
1572 | + this.$el.html(QWeb.render('invitation_view', {'invitation': JSON.parse(invitation)})); |
1573 | + }, |
1574 | + redirect_meeting_view : function(db, action, meeting_id, view){ |
1575 | + var self = this; |
1576 | + var action_url = ''; |
1577 | + if(view == "form") { |
1578 | + action_url = _.str.sprintf('/?db=%s#id=%s&view_type=%s&model=crm.meeting', db, meeting_id, view, meeting_id); |
1579 | + } else { |
1580 | + action_url = _.str.sprintf('/?db=%s#view_type=%s&model=crm.meeting&action=%s',self.db,self.view,self.action); |
1581 | + } |
1582 | + var reload_page = function(){ |
1583 | + return location.replace(action_url); |
1584 | + } |
1585 | + reload_page(); |
1586 | + }, |
1587 | + }); |
1588 | + |
1589 | + instance.web.form.Many2ManyAttendee = instance.web.form.FieldMany2ManyTags.extend({ |
1590 | + tag_template: "many2manyattendee", |
1591 | + initialize_texttext: function() { |
1592 | + return _.extend(this._super(),{ |
1593 | + html : { |
1594 | + 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>' |
1595 | + } |
1596 | + }); |
1597 | + }, |
1598 | + map_tag: function(value){ |
1599 | + return _.map(value, function(el) {return {name: el[1], id:el[0], state: el[2]};}) |
1600 | + }, |
1601 | + get_render_data: function(ids){ |
1602 | + var self = this; |
1603 | + var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); |
1604 | + return dataset.call('get_attendee_detail',[ids, self.getParent().datarecord.id || false]); |
1605 | + }, |
1606 | + render_tag: function(data){ |
1607 | + this._super(data); |
1608 | + var self = this; |
1609 | + if (! self.get("effective_readonly")) { |
1610 | + var tag_element = self.tags.tagElements(); |
1611 | + _.each(data,function(value, key){ |
1612 | + $(tag_element[key]).find(".custom-edit").addClass(data[key][2]) |
1613 | + }); |
1614 | + } |
1615 | + } |
1616 | + }); |
1617 | + instance.web.form.widgets = instance.web.form.widgets.extend({ |
1618 | + 'many2manyattendee' : 'instance.web.form.Many2ManyAttendee', |
1619 | + }); |
1620 | + |
1621 | + instance.base_calendar.event = function (db, action, id, view, attendee_data) { |
1622 | + instance.session.session_bind(instance.session.origin).done(function () { |
1623 | + new instance.base_calendar.invitation(null,db,action,id,view,attendee_data).appendTo($("body").addClass('openerp')); |
1624 | + }); |
1625 | + } |
1626 | +}; |
1627 | +//vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: |
1628 | |
1629 | === added directory 'base_calendar/static/src/xml' |
1630 | === added file 'base_calendar/static/src/xml/base_calendar.xml' |
1631 | --- base_calendar/static/src/xml/base_calendar.xml 1970-01-01 00:00:00 +0000 |
1632 | +++ base_calendar/static/src/xml/base_calendar.xml 2013-11-07 16:05:43 +0000 |
1633 | @@ -0,0 +1,43 @@ |
1634 | +<?xml version="1.0" encoding="UTF-8"?> |
1635 | +<template> |
1636 | + <t t-name="many2manyattendee"> |
1637 | + <t t-set="i" t-value="0"/> |
1638 | + <t t-foreach="elements" t-as="el"> |
1639 | + <span class="oe_tag" t-att-data-index="i"> |
1640 | + <a t-attf-class="oe_invitation #{el[2]}"/> |
1641 | + <t t-esc="el[1]"/> |
1642 | + </span> |
1643 | + <t t-set="i" t-value="i + 1"/> |
1644 | + </t> |
1645 | + </t> |
1646 | + <t t-name="invitation_view"> |
1647 | + <div class="oe_right"><b><t t-esc="invitation['current_attendee'].cn"/> (<t t-esc="invitation['current_attendee'].email"/>)</b></div> |
1648 | + <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> |
1649 | + <div class="invitation_block"> |
1650 | + <t t-if="invitation['current_attendee'].state != 'needs-action'"> |
1651 | + <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> |
1652 | + </t> |
1653 | + <div class="cal_meeting"><t t-esc="invitation['meeting'].event"/></div> |
1654 | + <table calss="invitation_block"> |
1655 | + <tr> |
1656 | + <td class="cal_lable">When</td> |
1657 | + <td>: <t t-esc="invitation['meeting'].when"/></td> |
1658 | + </tr> |
1659 | + <tr> |
1660 | + <td class="cal_lable">Where</td> |
1661 | + <td>: <t t-esc="invitation['meeting'].where or '-'"/></td> |
1662 | + </tr> |
1663 | + <tr> |
1664 | + <td class="cal_lable">Who</td> |
1665 | + <td> |
1666 | + <span>: <t t-esc="invitation['meeting'].organizer"/> - <a class="cal_lable">Organizer</a></span> |
1667 | + <t t-foreach="invitation['attendee']" t-as="att"> |
1668 | + <br/> |
1669 | + <span class="cal_status"><a t-attf-class="oe_invitation #{att.status}"/><t t-esc="att.name"/></span> |
1670 | + </t> |
1671 | + </td> |
1672 | + </tr> |
1673 | + </table> |
1674 | + </div> |
1675 | + </t> |
1676 | +</template> |
1677 | |
1678 | === modified file 'portal/security/ir.model.access.csv' |
1679 | --- portal/security/ir.model.access.csv 2013-04-29 09:33:29 +0000 |
1680 | +++ portal/security/ir.model.access.csv 2013-11-07 16:05:43 +0000 |
1681 | @@ -6,4 +6,4 @@ |
1682 | access_res_partner_portal,res.partner.portal,base.model_res_partner,portal.group_portal,1,0,0,0 |
1683 | access_acquirer,portal.payment.acquirer,portal.model_portal_payment_acquirer,,1,0,0,0 |
1684 | access_acquirer_all,portal.payment.acquirer,portal.model_portal_payment_acquirer,base.group_system,1,1,1,1 |
1685 | -access_ir_attachment_group_portal,ir.attachment group_portal,base.model_ir_attachment,portal.group_portal,1,0,1,0 |
1686 | \ No newline at end of file |
1687 | +access_ir_attachment_group_portal,ir.attachment group_portal,base.model_ir_attachment,portal.group_portal,1,0,1,0 |
1688 | |
1689 | === modified file 'portal_crm/__openerp__.py' |
1690 | --- portal_crm/__openerp__.py 2012-11-29 22:26:45 +0000 |
1691 | +++ portal_crm/__openerp__.py 2013-11-07 16:05:43 +0000 |
1692 | @@ -33,6 +33,7 @@ |
1693 | 'depends': ['crm','portal'], |
1694 | 'data': [ |
1695 | 'contact_view.xml', |
1696 | + 'security/ir.model.access.csv', |
1697 | ], |
1698 | 'test': [ |
1699 | 'test/contact_form.yml', |
1700 | |
1701 | === added directory 'portal_crm/security' |
1702 | === added file 'portal_crm/security/ir.model.access.csv' |
1703 | --- portal_crm/security/ir.model.access.csv 1970-01-01 00:00:00 +0000 |
1704 | +++ portal_crm/security/ir.model.access.csv 2013-11-07 16:05:43 +0000 |
1705 | @@ -0,0 +1,3 @@ |
1706 | +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
1707 | +access_crm_meeting_portal,crm.meeting.portal,base_calendar.model_crm_meeting,portal.group_portal,1,1,0,0 |
1708 | +access_crm_meeting_type_portal,crm.meeting.type.portal,base_calendar.model_crm_meeting_type,portal.group_portal,1,0,0,0 |
1709 | |
1710 | === modified file 'portal_hr_employees/hr_employee.py' |
1711 | --- portal_hr_employees/hr_employee.py 2012-12-10 11:16:54 +0000 |
1712 | +++ portal_hr_employees/hr_employee.py 2013-11-07 16:05:43 +0000 |
1713 | @@ -56,3 +56,16 @@ |
1714 | _defaults = { |
1715 | 'visibility': 'private', |
1716 | } |
1717 | + |
1718 | +class calendar_attendee(osv.osv): |
1719 | + _inherit = 'calendar.attendee' |
1720 | + |
1721 | + def create(self, cr, uid, vals, context=None): |
1722 | + user_pool = self.pool.get('res.users') |
1723 | + partner_id = vals.get('partner_id') |
1724 | + users = user_pool.search_read(cr, uid, [('partner_id','=', partner_id)],['employee_ids'], context=context) |
1725 | + for user in users: |
1726 | + if user['employee_ids']: |
1727 | + vals['state'] = 'accepted' |
1728 | + return super(calendar_attendee, self).create(cr, uid, vals, context=context) |
1729 | + |