[MERGE] forward port of branch saas-3 up to revid 9298 chm@openerp.com-20140311130852-3ft0v1mc9ht1any6

bzr revid: chs@openerp.com-20140311145205-s56fj113fsrnisc3
This commit is contained in:
Christophe Simonis 2014-03-11 15:52:05 +01:00
commit d504764eff
110 changed files with 1667 additions and 1078 deletions

View File

@ -168,7 +168,7 @@
on_change="onchange_partner_id(type,partner_id,date_invoice,payment_term, partner_bank_id,company_id)"
context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}"
domain="[('supplier', '=', True)]"/>
<field name="fiscal_position" widget="selection"/>
<field name="fiscal_position" options="{'no_create': True}"/>
<field name="origin"/>
<field name="supplier_invoice_number"/>
<label for="reference_type"/>
@ -183,7 +183,7 @@
<field domain="[('company_id', '=', company_id), ('type', '=', 'payable')]"
name="account_id" groups="account.group_account_user"/>
<field name="journal_id" groups="account.group_account_user"
on_change="onchange_journal_id(journal_id, context)" widget="selection"/>
on_change="onchange_journal_id(journal_id, context)" options="{'no_create': True}"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="check_total" groups="account.group_supplier_inv_check_total"/>
</group>
@ -253,7 +253,7 @@
<field domain="[('partner_id', '=', partner_id)]" name="partner_bank_id" on_change="onchange_partner_bank(partner_bank_id)"/>
<field name="user_id" string="Responsible" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_invoice']}"/>
<field name="name" invisible="1"/>
<field name="payment_term" widget="selection"/>
<field name="payment_term" options="{'no_create': True}"/>
</group>
<group>
<field name="move_id" groups="account.group_account_user"/>
@ -324,12 +324,12 @@
context="{'search_default_customer':1, 'show_address': 1}"
options='{"always_reload": True}'
domain="[('customer', '=', True)]"/>
<field name="fiscal_position" widget="selection" />
<field name="fiscal_position" options="{'no_create': True}" />
</group>
<group>
<field name="date_invoice"/>
<field name="journal_id" groups="account.group_account_user"
on_change="onchange_journal_id(journal_id, context)" widget="selection"/>
on_change="onchange_journal_id(journal_id, context)" options="{'no_create': True}"/>
<field domain="[('company_id', '=', company_id),('type','=', 'receivable')]"
name="account_id" groups="account.group_account_user"/>

View File

@ -149,7 +149,7 @@
<para style="terp_default_8">
<font color="white"> </font>
</para>
<pre style="terp_default_9_followup_id">[[ format(get_text(o,data['form']['followup_id'])) ]]</pre>
<para style="terp_default_9"><pre style="terp_default_9_followup_id">[[ format(get_text(o,data['form']['followup_id'])) ]]</pre></para>
<para style="terp_default_9">
<font color="white"> </font>
</para>

View File

@ -15,6 +15,7 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
#----------------------------------------------------------
# helpers
#----------------------------------------------------------
@ -30,11 +31,15 @@ def fragment_to_query_string(func):
var s = l.search ? (l.search === '?' ? '' : '&') : '?';
r = l.pathname + l.search + s + q;
}
if (r == l.pathname) {
r = '/';
}
window.location = r;
</script></head><body></body></html>"""
return func(self, *a, **kw)
return wrapper
#----------------------------------------------------------
# Controller
#----------------------------------------------------------
@ -111,6 +116,7 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home):
response.qcontext.update(providers=providers)
return response
class OAuthController(http.Controller):
@http.route('/auth_oauth/signin', type='http', auth='none')

View File

@ -13,15 +13,15 @@
<template id="auth_signup.fields" name="Auth Signup/ResetPassword form fields">
<t t-call="web.database_select"/>
<div class="form-group field-login">
<label for="login" class="control-label">Your Email</label>
<input type="text" name="login" t-att-value="login" id="login" class="form-control" autofocus="autofocus"
required="required" t-att-readonly="'readonly' if only_passwords else None"/>
</div>
<div class="form-group field-name">
<label for="name" class="control-label">Your Name</label>
<input type="text" name="name" t-att-value="name" id="name" class="form-control" placeholder="e.g. John Doe"
required="required" t-att-autofocus="'autofocus' if not only_passwords else None" t-att-readonly="'readonly' if only_passwords else None"/>
</div>
<div class="form-group field-login">
<label for="login" class="control-label">Your Email</label>
<input type="text" name="login" t-att-value="login" id="login" class="form-control"
required="required" t-att-readonly="'readonly' if only_passwords else None"/>
</div>

View File

@ -114,6 +114,8 @@ class ir_import(orm.TransientModel):
elif field['type'] == 'one2many' and depth:
f['fields'] = self.get_fields(
cr, uid, field['relation'], context=context, depth=depth-1)
if self.pool['res.users'].has_group(cr, uid, 'base.group_no_one'):
f['fields'].append({'id' : '.id', 'name': '.id', 'string': _("Database ID"), 'required': False, 'fields': []})
fields.append(f)

View File

@ -37,8 +37,6 @@ from openerp.tools.translate import _
from openerp.http import request
from operator import itemgetter
from werkzeug.exceptions import BadRequest
import logging
_logger = logging.getLogger(__name__)
@ -63,6 +61,7 @@ def calendar_id2real_id(calendar_id=None, with_date=False):
return int(real_id)
return calendar_id and int(calendar_id) or calendar_id
def get_real_ids(ids):
if isinstance(ids, (str, int, long)):
return calendar_id2real_id(ids)
@ -76,6 +75,7 @@ class calendar_attendee(osv.Model):
Calendar Attendee Information
"""
_name = 'calendar.attendee'
_rec_name = 'cn'
_description = 'Attendee information'
def _compute_data(self, cr, uid, ids, name, arg, context=None):
@ -223,7 +223,7 @@ class calendar_attendee(osv.Model):
})
for attendee in self.browse(cr, uid, ids, context=context):
if attendee.email and email_from:
if attendee.email and email_from and attendee.email != email_from:
ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
@ -334,22 +334,22 @@ class calendar_alarm_manager(osv.AbstractModel):
res = {}
base_request = """
SELECT
crm.id,
crm.date - interval '1' minute * calcul_delta.max_delta AS first_alarm,
cal.id,
cal.date - interval '1' minute * calcul_delta.max_delta AS first_alarm,
CASE
WHEN crm.recurrency THEN crm.end_date - interval '1' minute * calcul_delta.min_delta
ELSE crm.date_deadline - interval '1' minute * calcul_delta.min_delta
WHEN cal.recurrency THEN cal.end_date - interval '1' minute * calcul_delta.min_delta
ELSE cal.date_deadline - interval '1' minute * calcul_delta.min_delta
END as last_alarm,
crm.date as first_event_date,
cal.date as first_event_date,
CASE
WHEN crm.recurrency THEN crm.end_date
ELSE crm.date_deadline
WHEN cal.recurrency THEN cal.end_date
ELSE cal.date_deadline
END as last_event_date,
calcul_delta.min_delta,
calcul_delta.max_delta,
crm.rrule AS rule
cal.rrule AS rule
FROM
calendar_event AS crm
calendar_event AS cal
RIGHT JOIN
(
SELECT
@ -359,11 +359,11 @@ class calendar_alarm_manager(osv.AbstractModel):
LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
WHERE alarm.type in %s
GROUP BY rel.calendar_event_id
) AS calcul_delta ON calcul_delta.calendar_event_id = crm.id
) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id
"""
filter_user = """
LEFT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = crm.id
RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
AND part_rel.res_partner_id = %s
"""
@ -384,21 +384,14 @@ class calendar_alarm_manager(osv.AbstractModel):
#Add filter on hours
tuple_params += (seconds, seconds,)
cr.execute("""
SELECT
*
FROM (
"""
+ base_request
+ """
) AS ALL_EVENTS
WHERE
ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%s' second )
AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%s' second )
""", tuple_params)
cr.execute("""SELECT *
FROM ( %s ) AS ALL_EVENTS
WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second )
AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%%s' second )
""" % base_request, tuple_params)
for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
res[event_id].update({
res[event_id] = {
'event_id': event_id,
'first_alarm': first_alarm,
'last_alarm': last_alarm,
@ -407,7 +400,7 @@ class calendar_alarm_manager(osv.AbstractModel):
'min_duration': min_duration,
'max_duration': max_duration,
'rrule': rule
})
}
return res
@ -433,83 +426,86 @@ class calendar_alarm_manager(osv.AbstractModel):
res.append(alert)
return res
def get_next_mail(self,cr,uid,context=None):
cron = self.pool.get('ir.cron').search(cr,uid,[('model','ilike',self._name)],context=context)
def get_next_mail(self, cr, uid, context=None):
cron = self.pool.get('ir.cron').search(cr, uid, [('model', 'ilike', self._name)], context=context)
if cron and len(cron) == 1:
cron = self.pool.get('ir.cron').browse(cr,uid,cron[0],context=context)
cron = self.pool.get('ir.cron').browse(cr, uid, cron[0], context=context)
else:
raise ("Cron for " + self._name + " not identified :( !")
if cron.interval_type=="weeks":
if cron.interval_type == "weeks":
cron_interval = cron.interval_number * 7 * 24 * 60 * 60
elif cron.interval_type=="days":
cron_interval = cron.interval_number * 24 * 60 * 60
elif cron.interval_type=="hours":
elif cron.interval_type == "days":
cron_interval = cron.interval_number * 24 * 60 * 60
elif cron.interval_type == "hours":
cron_interval = cron.interval_number * 60 * 60
elif cron.interval_type=="minutes":
elif cron.interval_type == "minutes":
cron_interval = cron.interval_number * 60
elif cron.interval_type=="seconds":
cron_interval = cron.interval_number
elif cron.interval_type == "seconds":
cron_interval = cron.interval_number
if not cron_interval:
raise ("Cron delay for " + self._name + " can not be calculated :( !")
all_events = self.get_next_potential_limit_alarm(cr,uid,cron_interval,notif=False,context=context)
all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
for event in all_events: #.values()
max_delta = all_events[event]['max_duration'];
curEvent = self.pool.get('calendar.event').browse(cr,uid,event,context=context)
for event in all_events: # .values()
max_delta = all_events[event]['max_duration']
curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
if curEvent.recurrency:
bFound = False
LastFound = False
for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr,uid,curEvent, context=context) :
in_date_format = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S');
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,cron_interval,notif=False,context=context)
for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
in_date_format = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S')
LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
if LastFound:
for alert in LastFound:
self.do_mail_reminder(cr,uid,alert,context=context)
self.do_mail_reminder(cr, uid, alert, context=context)
if not bFound: # if it's the first alarm for this recurrent event
bFound = True
if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event
break
else:
in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S');
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,cron_interval,notif=False,context=context)
in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
if LastFound:
for alert in LastFound:
self.do_mail_reminder(cr,uid,alert,context=context)
self.do_mail_reminder(cr, uid, alert, context=context)
def get_next_notif(self,cr,uid,context=None):
def get_next_notif(self, cr, uid, context=None):
ajax_check_every_seconds = 300
partner = self.pool.get('res.users').browse(cr,uid,uid,context=context).partner_id;
partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
all_notif = []
all_events = self.get_next_potential_limit_alarm(cr,uid,ajax_check_every_seconds,partner_id=partner.id,mail=False,context=context)
if not partner:
return []
all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
for event in all_events: # .values()
max_delta = all_events[event]['max_duration'];
curEvent = self.pool.get('calendar.event').browse(cr,uid,event,context=context)
max_delta = all_events[event]['max_duration']
curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
if curEvent.recurrency:
bFound = False
LastFound = False
for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr,uid,curEvent, context=context) :
in_date_format = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S');
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,ajax_check_every_seconds,after=partner.cal_last_notif,mail=False,context=context)
for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
in_date_format = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S')
LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner.calendar_last_notif_ack, mail=False, context=context)
if LastFound:
for alert in LastFound:
all_notif.append(self.do_notif_reminder(cr,uid,alert,context=context))
if not bFound: #if it's the first alarm for this recurrent event
bFound = True
if bFound and not LastFound: #if the precedent event had alarm but not this one, we can stop the search fot this event
all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
if not bFound: # if it's the first alarm for this recurrent event
bFound = True
if bFound and not LastFound: # if the precedent event had alarm but not this one, we can stop the search fot this event
break
else:
in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S');
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,ajax_check_every_seconds,partner.cal_last_notif,mail=False,context=context)
in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, partner.calendar_last_notif_ack, mail=False, context=context)
if LastFound:
for alert in LastFound:
all_notif.append(self.do_notif_reminder(cr,uid,alert,context=context))
return all_notif
all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
return all_notif
def do_mail_reminder(self, cr, uid, alert, context=None):
if context is None:
@ -520,7 +516,7 @@ class calendar_alarm_manager(osv.AbstractModel):
alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
if alarm.type == 'email':
res = self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, event.attendee_ids, template_xmlid='calendar_template_meeting_reminder', context=context)
res = self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], template_xmlid='calendar_template_meeting_reminder', context=context)
return res
@ -777,7 +773,7 @@ class calendar_event(osv.Model):
else:
result[event] = ""
return result
def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
if not isinstance(ids, list):
ids = [ids]
@ -790,7 +786,7 @@ class calendar_event(osv.Model):
data.update(update_data)
self.write(cr, uid, ids, data, context=context)
return True
def _tz_get(self, cr, uid, context=None):
return [(x.lower(), x) for x in pytz.all_timezones]
@ -798,7 +794,7 @@ class calendar_event(osv.Model):
'location': {
'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
},
'date': {
'date': {
'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
},
}
@ -979,7 +975,7 @@ class calendar_event(osv.Model):
}
return res
def get_search_fields(self,browse_event,order_fields,r_date=None):
def get_search_fields(self, browse_event, order_fields, r_date=None):
sort_fields = {}
for ord in order_fields:
if ord == 'id' and r_date:
@ -989,17 +985,17 @@ class calendar_event(osv.Model):
'If we sort on FK, we obtain a browse_record, so we need to sort on name_get'
if type(browse_event[ord]) is openerp.osv.orm.browse_record:
name_get = browse_event[ord].name_get()
if len(name_get) and len(name_get[0])>=2:
if len(name_get) and len(name_get[0]) >= 2:
sort_fields[ord] = name_get[0][1]
return sort_fields
def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
"""Gives virtual event ids for recurring events
"""Gives virtual event ids for recurring events
This method gives ids of dates that comes between start date and end date of calendar views
@param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
@param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
"""
if not context:
@ -1024,7 +1020,7 @@ class calendar_event(osv.Model):
for ev in self.browse(cr, uid, ids_to_browse, context=context):
if not ev.recurrency or not ev.rrule:
result.append(ev.id)
result_data.append(self.get_search_fields(ev,order_fields))
result_data.append(self.get_search_fields(ev, order_fields))
continue
rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
@ -1070,7 +1066,7 @@ class calendar_event(osv.Model):
if [True for item in new_pile if not item]:
continue
result_data.append(self.get_search_fields(ev,order_fields,r_date=r_date))
result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
if order_fields:
def comparer(left, right):
@ -1081,7 +1077,7 @@ class calendar_event(osv.Model):
return 0
sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
comparers = [ ((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
if isinstance(event_id, (str, int, long)):
@ -1089,7 +1085,6 @@ class calendar_event(osv.Model):
else:
return ids
def compute_rule_string(self, data):
"""
Compute rule string according to value type RECUR of iCalendar from the values given.
@ -1123,7 +1118,7 @@ class calendar_event(osv.Model):
data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
freq = data.get('rrule_type', False) # day/week/month/year
res = ''
@ -1219,12 +1214,12 @@ class calendar_event(osv.Model):
res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
return res
def onchange_rec_day(self,cr,uid,id,date,mo,tu,we,th,fr,sa,su):
def onchange_rec_day(self, cr, uid, id, date, mo, tu, we, th, fr, sa, su):
""" set the start date according to the first occurence of rrule"""
rrule_obj = self._get_empty_rrule_data()
rrule_obj.update({
'byday':True,
'rrule_type':'weekly',
'byday': True,
'rrule_type': 'weekly',
'mo': mo,
'tu': tu,
'we': we,
@ -1232,12 +1227,11 @@ class calendar_event(osv.Model):
'fr': fr,
'sa': sa,
'su': su,
'interval':1
'interval': 1
})
str_rrule = self.compute_rule_string(rrule_obj)
first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
return {'value': { 'date' : first_occurence.strftime("%Y-%m-%d") + ' 00:00:00' } }
first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
return {'value': {'date': first_occurence.strftime("%Y-%m-%d") + ' 00:00:00'}}
def check_partners_email(self, cr, uid, partner_ids, context=None):
""" Verify that selected partner_ids have an email_address defined.
@ -1251,12 +1245,10 @@ class calendar_event(osv.Model):
warning_msg = _('The following contacts have no email address :')
for partner in partner_wo_email_lst:
warning_msg += '\n- %s' % (partner.name)
return {'warning':
{
'title': _('Email addresses not found'),
'message': warning_msg,
}
}
return {'warning': {
'title': _('Email addresses not found'),
'message': warning_msg,
}}
# ----------------------------------------
# OpenChatter
@ -1365,8 +1357,7 @@ class calendar_event(osv.Model):
rrule_type=False,
rrule='',
recurrency=False,
end_date = datetime.strptime(values.get('date', False) or data.get('date'),"%Y-%m-%d %H:%M:%S")
+ timedelta(hours=values.get('duration', False) or data.get('duration'))
end_date=datetime.strptime(values.get('date', False) or data.get('date'), "%Y-%m-%d %H:%M:%S") + timedelta(hours=values.get('duration', False) or data.get('duration'))
)
#do not copy the id
@ -1381,29 +1372,25 @@ class calendar_event(osv.Model):
new_id = self._detach_one_event(cr, uid, ids[0], context=context)
return {
'type': 'ir.actions.act_window',
'res_model': 'calendar.event',
'view_mode': 'form',
'res_id': new_id,
'target': 'current',
'flags': {'form': {'action_buttons': True, 'options' : { 'mode' : 'edit' } } }
'type': 'ir.actions.act_window',
'res_model': 'calendar.event',
'view_mode': 'form',
'res_id': new_id,
'target': 'current',
'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
}
def write(self, cr, uid, ids, values, context=None):
def _only_changes_to_apply_on_real_ids(field_names):
''' return True if changes are only to be made on the real ids'''
''' return True if changes are only to be made on the real ids'''
for field in field_names:
if field in ['date','active']:
return True
if field in ['date', 'active']:
return True
return False
context = context or {}
if isinstance(ids, (str,int, long)):
if isinstance(ids, (str, int, long)):
if len(str(ids).split('-')) == 1:
ids = [int(ids)]
else:
@ -1428,9 +1415,9 @@ class calendar_event(osv.Model):
continue
else:
data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration'])
if data.get('rrule'):
if data.get('rrule'):
new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
# set end_date for calendar searching
@ -1528,7 +1515,7 @@ class calendar_event(osv.Model):
continue
if r['class'] == 'private':
for f in r.keys():
if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count'):
if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date'):
if isinstance(r[f], list):
r[f] = []
else:
@ -1554,7 +1541,7 @@ class calendar_event(osv.Model):
ids_to_unlink = []
# One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it
for event_id in ids:
for event_id in ids:
if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL
if self.browse(cr, uid, event_id).recurrent_id:
ids_to_exclure.append(event_id)
@ -1587,7 +1574,7 @@ class mail_message(osv.Model):
def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
if doc_model == 'calendar.event':
order = context.get('order', self._order)
order = context.get('order', self._order)
for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
@ -1619,26 +1606,27 @@ class ir_http(osv.AbstractModel):
def _auth_method_calendar(self):
token = request.params['token']
db = request.params['db']
db = request.params['db']
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)])
attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
if not attendee_id:
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.partner_id.id != user.partner_id.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 attendee.partner_id.id != user.partner_id.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
class invite_wizard(osv.osv_memory):
_inherit = 'mail.wizard.invite'

View File

@ -136,7 +136,7 @@
<strong>${object.event_id.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 ${object.cn}</strong> ,<br/><p style="margin-left:12px">${object.event_id.user_id.partner_id.name} invited you for the ${object.event_id.name} meeting of ${object.event_id.user_id.company_id.name}.</p>
<strong style="margin-left:12px">Dear ${object.cn}</strong> ,<br/><p style="margin-left:12px">${object.event_id.user_id.partner_id.name} invited you for the ${object.event_id.name} meeting of ${object.event_id.user_id.company_id.name}.</p>
</div>
<div style="height: auto;margin-left:12px;margin-top:30px;">
<table>
@ -151,50 +151,58 @@
</td>
<td>
<table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;">
% if object.event_id.location:
<tr style=" height: 30px;">
<tr>
<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.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% if object.event_id.location:
<div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Where
</div>
% endif
</td>
<td style="vertical-align:top;">
% if object.event_id.location:
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% endif
</td>
</tr>
% endif
% if object.event_id.description :
<tr style=" height:auto;">
<tr>
<td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
% if object.event_id.description :
<div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
% endif
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${object.event_id.description}
</div>
<td style="vertical-align:text-top;">
% if object.event_id.description :
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${object.event_id.description}
</div>
% endif
</td>
</tr>
% endif
% if not object.event_id.allday and object.event_id.duration:
<tr style=" height:auto;">
<tr>
<td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Duration
</div>
% if not object.event_id.allday and object.event_id.duration:
<div style="height:auto; width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Duration
</div>
% endif
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% if not object.event_id.allday and object.event_id.duration:
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% endif
</td>
</tr>
% endif
</tr>
<tr style=" height: 30px;">
<td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
<div>
@ -219,9 +227,9 @@
</table>
</div>
<div style="height: auto;width:450px; margin:0 auto;padding-top:20px;padding-bottom:40px;">
<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']}/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a>
<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="/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a>
</div>
</div>
</body>
@ -259,13 +267,13 @@
<strong>${object.event_id.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 ${object.cn}</strong> ,<br/>
<strong style="margin-left:12px">Dear ${object.cn}</strong> ,<br/>
<p style="margin-left:12px">The date of the meeting has been changed...<br/>
The meeting created by ${object.event_id.user_id.partner_id.name} is now scheduled for : ${object.event_id.date}.</p>
The meeting created by ${object.event_id.user_id.partner_id.name} is now scheduled for : ${object.event_id.display_time}.</p>
</div>
<div style="height: auto;margin-left:12px;margin-top:30px;">
<table>
<tr>
<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.event_id.get_interval(object.event_id.date, 'dayname')}</div>
<div style="font-size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F;background-color: #E1E2F8;width: 130px;">
@ -276,50 +284,58 @@
</td>
<td>
<table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;">
% if object.event_id.location:
<tr style=" height: 30px;">
<tr>
<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.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% if object.event_id.location:
<div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Where
</div>
% endif
</td>
<td style="vertical-align:top;">
% if object.event_id.location:
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% endif
</td>
</tr>
% endif
% if object.event_id.description :
<tr style=" height:auto;">
<tr>
<td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
% if object.event_id.description :
<div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
% endif
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${object.event_id.description}
</div>
<td style="vertical-align:text-top;">
% if object.event_id.description :
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${object.event_id.description}
</div>
% endif
</td>
</tr>
% endif
% if not object.event_id.allday and object.event_id.duration:
<tr style=" height:auto;">
<tr>
<td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Duration
</div>
% if not object.event_id.allday and object.event_id.duration:
<div style="height:auto; width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Duration
</div>
% endif
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% if not object.event_id.allday and object.event_id.duration:
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% endif
</td>
</tr>
% endif
</tr>
<tr style=" height: 30px;">
<td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
<div>
@ -341,12 +357,12 @@
</table>
</td>
</tr>
</table>
</table>
</div>
<div style="height: auto;width:450px; margin:0 auto;padding-top:20px;padding-bottom:40px;">
<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']}/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a>
<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="/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a>
</div>
</div>
</body>
@ -384,8 +400,8 @@
<strong>${object.event_id.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 ${object.cn}</strong> ,<br/>
<p style="margin-left:12px">this it a rmeinder for the event below : </p>
<strong style="margin-left:12px">Dear ${object.cn}</strong> ,<br/>
<p style="margin-left:12px">this it a reminder for the event below : </p>
</div>
<div style="height: auto;margin-left:12px;margin-top:30px;">
<table>
@ -400,50 +416,58 @@
</td>
<td>
<table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;">
% if object.event_id.location:
<tr style=" height: 30px;">
<tr>
<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.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% if object.event_id.location:
<div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Where
</div>
% endif
</td>
<td style="vertical-align:top;">
% if object.event_id.location:
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% endif
</td>
</tr>
% endif
% if object.event_id.description :
<tr style=" height:auto;">
<tr>
<td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
% if object.event_id.description :
<div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
What
</div>
% endif
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${object.event_id.description}
</div>
<td style="vertical-align:text-top;">
% if object.event_id.description :
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${object.event_id.description}
</div>
% endif
</td>
</tr>
% endif
% if not object.event_id.allday and object.event_id.duration:
<tr style=" height:auto;">
<tr>
<td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Duration
</div>
% if not object.event_id.allday and object.event_id.duration:
<div style="height:auto; width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
Duration
</div>
% endif
</td>
<td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% if not object.event_id.allday and object.event_id.duration:
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% endif
</td>
</tr>
% endif
</tr>
<tr style=" height: 30px;">
<td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
<div>

View File

@ -20,7 +20,7 @@
<record id="calendar_event_1" model="calendar.event">
<field eval="1" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="partner_ids" eval="[(6,0,[ref('base.partner_root'),ref('base.res_partner_1'),ref('base.res_partner_6')])]"/>
<field name="partner_ids" eval="[(6,0,[ref('base.res_partner_6')])]"/>
<field name="name">Follow-up for Project proposal</field>
<field name="description">Meeting to discuss project plan and hash out the details of implementation.</field>
<field eval="time.strftime('%Y-%m-03 10:20:00')" name="date"/>

View File

@ -1,21 +1,19 @@
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 meeting_invitation(http.Controller):
@http.route('/calendar/meeting/accept', type='http', auth="calendar")
def accept(self, db, token, action, id,**kwargs):
def accept(self, db, token, action, id, **kwargs):
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')])
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')
@ -25,7 +23,7 @@ class meeting_invitation(http.Controller):
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')])
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')
@ -36,13 +34,13 @@ class meeting_invitation(http.Controller):
meeting_pool = registry.get('calendar.event')
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)],[])
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))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in webmain.manifest_list('css', db=db))
return webmain.html_template % {
'js': js,
@ -50,7 +48,7 @@ class meeting_invitation(http.Controller):
'modules': simplejson.dumps(webmain.module_boot(db)),
'init': "s.calendar.event('%s', '%s', '%s', '%s' , '%s');" % (db, action, id, 'form', json.dumps(attendee_data)),
}
# Function used, in RPC to check every 5 minutes, if notification to do for an event or not
@http.route('/calendar/notify', type='json', auth="none")
def notify(self):
@ -58,15 +56,14 @@ class meeting_invitation(http.Controller):
uid = request.session.uid
context = request.session.context
with registry.cursor() as cr:
res = registry.get("calendar.alarm_manager").get_next_notif(cr,uid,context=context)
res = registry.get("calendar.alarm_manager").get_next_notif(cr, uid, context=context)
return res
@http.route('/calendar/notify_ack', type='json', auth="none")
def notify_ack(self, type=''):
registry = openerp.modules.registry.RegistryManager.get(request.session.db)
uid = request.session.uid
context = request.session.context
with registry.cursor() as cr:
res = registry.get("res.partner").calendar_last_notif(cr,uid,context=context)
res = registry.get("res.partner").calendar_last_notif_ack(cr, uid, context=context)
return res

View File

@ -1,17 +1,20 @@
openerp.calendar = function(instance) {
var _t = instance.web._t;
var QWeb = instance.web.qweb;
instance.calendar = {};
instance.web.WebClient = instance.web.WebClient.extend({
get_notif_box: function(me) {
return $(me).closest(".ui-notify-message-style");
},
get_next_notif: function() {
var self= this;
this.rpc("/calendar/notify")
.then(
.done(
function(result) {
_.each(result, function(res) {
setTimeout(function() {
@ -44,21 +47,33 @@ openerp.calendar = function(instance) {
},res.timer * 1000);
});
}
);
)
.fail(function (err, ev) {
if (err.code === -32098) {
// Prevent the CrashManager to display an error
// in case of an xhr error not due to a server error
ev.preventDefault();
}
});
},
check_notifications: function() {
var self= this;
self.get_next_notif();
setInterval(function(){
self.intervalNotif = setInterval(function(){
self.get_next_notif();
}, 5 * 60 * 1000 );
}, 5 * 60 * 1000 );
},
//Override the show_application of addons/web/static/src/js/chrome.js
show_application: function() {
this._super();
this.check_notifications();
},
},
//Override addons/web/static/src/js/chrome.js
on_logout: function() {
this._super();
clearInterval(self.intervalNotif);
},
});

View File

@ -23,15 +23,13 @@ from openerp.osv import fields, osv
import logging
_logger = logging.getLogger(__name__)
#
# calendar.event is defined in module calendar
#
class calendar_event(osv.Model):
""" Model for Calendar Event """
_inherit = 'calendar.event'
_columns = {
'phonecall_id': fields.many2one ('crm.phonecall', 'Phonecall'),
'opportunity_id': fields.many2one ('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]"),
'phonecall_id': fields.many2one('crm.phonecall', 'Phonecall'),
'opportunity_id': fields.many2one('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]"),
}
def create(self, cr, uid, vals, context=None):
@ -48,31 +46,11 @@ class calendar_attendee(osv.osv):
_inherit = 'calendar.attendee'
_description = 'Calendar Attendee'
def _compute_data(self, cr, uid, ids, name, arg, context=None):
"""
@param self: The object pointer
@param cr: the current row, from the database cursor,
@param uid: the current users ID for security checks,
@param ids: List of compute datas IDs
@param context: A standard dictionary for contextual values
"""
name = name[0]
result = super(calendar_attendee, self)._compute_data(cr, uid, ids, name, arg, context=context)
for attdata in self.browse(cr, uid, ids, context=context):
id = attdata.id
result[id] = {}
if name == 'categ_id':
if attdata.ref and 'categ_id' in attdata.ref._columns:
result[id][name] = (attdata.ref.categ_id.id, attdata.ref.categ_id.name,)
else:
result[id][name] = False
return result
def _noop(self, cr, uid, ids, name, arg, context=None):
return dict.fromkeys(ids, False)
_columns = {
'categ_id': fields.function(_compute_data, \
string='Event Type', type="many2one", \
relation="crm.case.categ", multi='categ_id'),
'categ_id': fields.function(_noop, string='Event Type', deprecated="Unused Field - TODO : Remove it in trunk", type="many2one", relation="crm.case.categ"),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -407,7 +407,7 @@ class crm_lead(format_address, osv.osv):
'probability = 0 %, select "Change Probability Automatically".\n'
'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
for stage_id, lead_ids in stages_leads.items():
self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
self.write(cr, uid, lead_ids, {'stage_id': stage_id, 'date_closed': fields.datetime.now()}, context=context)
return True
def case_mark_won(self, cr, uid, ids, context=None):
@ -428,7 +428,7 @@ class crm_lead(format_address, osv.osv):
'probability = 100 % and select "Change Probability Automatically".\n'
'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
for stage_id, lead_ids in stages_leads.items():
self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
self.write(cr, uid, lead_ids, {'stage_id': stage_id, 'date_closed': fields.datetime.now()}, context=context)
return True
def case_escalate(self, cr, uid, ids, context=None):

View File

@ -58,7 +58,7 @@
<field name="view_mode">tree,calendar</field>
<field name="view_id" ref="crm_case_inbound_phone_tree_view"/>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="context">{'search_default_state': 'done', 'default_state': 'done'}</field>
<field name="search_view_id" ref="crm.view_crm_case_phonecalls_filter"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">

View File

@ -167,6 +167,7 @@
<search string="Search Phonecalls">
<field name="name" string="Phonecalls"/>
<field name="date"/>
<field name="state"/>
<separator/>
<filter icon="terp-gtk-go-back-rtl" string="To Do" name="current" domain="[('state','=','open')]"/>
<separator/>

View File

@ -65,7 +65,7 @@
Salesman create a mass convert wizard and convert all the leads.
-
!python {model: crm.lead2opportunity.partner.mass}: |
context.update({'active_model': 'crm.lead', 'active_ids': [ref("test_crm_lead_01"), ref("test_crm_lead_02"), ref("test_crm_lead_03"), ref("test_crm_lead_04"), ref("test_crm_lead_05"), ref("test_crm_lead_06")], 'active_id': ref("test_crm_lead_01")})
context.update({'active_model': 'crm.lead', 'active_ids': [ref("test_crm_lead_01"), ref("test_crm_lead_02"), ref("test_crm_lead_03"), ref("test_crm_lead_04"), ref("test_crm_lead_05"), ref("test_crm_lead_06")], 'active_id': ref("test_crm_lead_01"), 'no_force_assignation': False})
id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('test_res_user_01'), ref('test_res_user_02'), ref('test_res_user_03'), ref('test_res_user_04')])], 'section_id': ref('crm.section_sales_department'), 'deduplicate': False}, context=context)
self.mass_convert(cr, uid, [id], context=context)
-

View File

@ -46,12 +46,12 @@ class crm_lead2opportunity_partner(osv.osv_memory):
results = []
if partner_id:
# Search for opportunities that have the same partner and that arent done or cancelled
ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), '|', ('probability', '=', False), ('probability', '<', '100')])
ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), '|', ('stage_id.probability', '=', False), ('stage_id.probability', '<', '100')])
for id in ids:
results.append(id)
email = re.findall(r'([^ ,<@]+@[^> ,]+)', email or '')
if email:
ids = lead_obj.search(cr, uid, [('email_from', '=ilike', email[0]), '|', ('probability', '=', False), ('probability', '<', '100')])
ids = lead_obj.search(cr, uid, [('email_from', '=ilike', email[0]), '|', ('stage_id.probability', '=', False), ('stage_id.probability', '<', '100')])
for id in ids:
results.append(id)
return list(set(results))
@ -69,11 +69,10 @@ class crm_lead2opportunity_partner(osv.osv_memory):
if context.get('active_id'):
tomerge = [int(context['active_id'])]
email = False
partner_id = res.get('partner_id')
lead = lead_obj.browse(cr, uid, int(context['active_id']), context=context)
email = lead.partner_id and lead.partner_id.email or lead.email_from
#TOFIX: use mail.mail_message.to_mail
tomerge.extend(self._get_duplicated_leads(cr, uid, partner_id, email))
tomerge = list(set(tomerge))
@ -100,10 +99,8 @@ class crm_lead2opportunity_partner(osv.osv_memory):
else:
user_in_section = False
if not user_in_section:
section_id = False
section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
if section_ids:
section_id = section_ids[0]
result = self.pool['crm.lead'].on_change_user(cr, uid, ids, user_id, context=context)
section_id = result.get('value') and result['value'].get('section_id') and result['value']['section_id'] or False
return {'value': {'section_id': section_id}}
def view_init(self, cr, uid, fields, context=None):
@ -126,14 +123,17 @@ class crm_lead2opportunity_partner(osv.osv_memory):
lead_ids = vals.get('lead_ids', [])
team_id = vals.get('section_id', False)
data = self.browse(cr, uid, ids, context=context)[0]
for lead_id in lead_ids:
partner_id = self._create_partner(cr, uid, lead_id, data.action, data.partner_id, context=context)
# FIXME: cannot pass user_ids as the salesman allocation only works in batch
res = lead.convert_opportunity(cr, uid, [lead_id], partner_id, [], team_id, context=context)
# FIXME: must perform salesman allocation in batch separately here
leads = lead.browse(cr, uid, lead_ids, context=context)
for lead_id in leads:
partner_id = self._create_partner(cr, uid, lead_id.id, data.action, lead_id.partner_id.id, context=context)
res = lead.convert_opportunity(cr, uid, [lead_id.id], partner_id, [], team_id, context=context)
user_ids = vals.get('user_ids', False)
if context.get('no_force_assignation'):
leads_to_allocate = [lead_id.id for lead_id in leads if not lead_id.user_id]
else:
leads_to_allocate = lead_ids
if user_ids:
lead.allocate_salesman(cr, uid, lead_ids, user_ids, team_id=team_id, context=context)
lead.allocate_salesman(cr, uid, leads_to_allocate, user_ids, team_id=team_id, context=context)
return res
def action_apply(self, cr, uid, ids, context=None):
@ -144,15 +144,19 @@ class crm_lead2opportunity_partner(osv.osv_memory):
if context is None:
context = {}
lead_obj = self.pool['crm.lead']
w = self.browse(cr, uid, ids, context=context)[0]
opp_ids = [o.id for o in w.opportunity_ids]
if w.name == 'merge':
lead_id = self.pool.get('crm.lead').merge_opportunity(cr, uid, opp_ids, w.user_id.id, w.section_id.id, context=context)
lead_id = lead_obj.merge_opportunity(cr, uid, opp_ids, context=context)
lead_ids = [lead_id]
lead = self.pool.get('crm.lead').read(cr, uid, lead_id, ['type'], context=context)
lead = lead_obj.read(cr, uid, lead_id, ['type', 'user_id'], context=context)
if lead['type'] == "lead":
context.update({'active_ids': lead_ids})
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'section_id': w.section_id.id}, context=context)
elif not context.get('no_force_assignation') or not lead['user_id']:
lead_obj.write(cr, uid, lead_id, {'user_id': w.user_id.id, 'section_id': w.section_id.id}, context=context)
else:
lead_ids = context.get('active_ids', [])
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'section_id': w.section_id.id}, context=context)
@ -186,11 +190,13 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
_columns = {
'user_ids': fields.many2many('res.users', string='Salesmen'),
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
'deduplicate': fields.boolean('Apply deduplication', help='Merge with existing leads/opportunities of each partner'),
'deduplicate': fields.boolean('Apply deduplication', help='Merge with existing leads/opportunities of each partner'),
'action': fields.selection([
('each_exist_or_create', 'Use existing partner or create'),
('nothing', 'Do not link to a customer')
], 'Related Customer', required=True),
# Uncomment me in trunk
# 'force_assignation': fields.boolean('Force assignation', help='If unchecked, this will leave the salesman of duplicated opportunities'),
}
_defaults = {
@ -266,6 +272,10 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
active_ids = active_ids.difference(merged_lead_ids)
active_ids = active_ids.union(remaining_lead_ids)
ctx['active_ids'] = list(active_ids)
# Remove me in trunk
ctx['no_force_assignation'] = ctx.get('no_force_assignation', True)
# Uncomment me in trunk
# ctx['no_force_assignation'] = not data.force_assignation
return self.action_apply(cr, uid, ids, context=ctx)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -58,6 +58,8 @@
<group string="Assign opportunities to">
<field name="section_id" groups="base.group_multi_salesteams"/>
<field name="user_ids" widget="many2many_tags"/>
<!-- Uncomment me in trunk -->
<!-- <field name="force_assignation" /> -->
</group>
<label for="opportunity_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
<group attrs="{'invisible': [('deduplicate', '=', False)]}">

View File

@ -9,7 +9,7 @@
<field name="arch" type="xml">
<form string="Merge Leads/Opportunities" version="7.0">
<group string="Assign opportunities to">
<field name="user_id" class="oe_inline" on_change="on_change_user(user_id, context)"/>
<field name="user_id" class="oe_inline" on_change="on_change_user(user_id, section_id, context)"/>
<field name="section_id" class="oe_inline"/>
</group>
<group string="Select Leads/Opportunities">

View File

@ -1,18 +1,19 @@
openerp.crm_partner_assign = function (instance) {
instance.crm_partner_assign = instance.crm_partner_assign || {};
instance.crm_partner_assign.next_or_list = function(parent) {
if (parent.inner_widget.active_view === "form"){
var form = parent.inner_widget.views.form.controller;
form.dataset.remove_ids([form.dataset.ids[form.dataset.index]]);
form.reload();
if (!form.dataset.ids.length){
parent.inner_widget.switch_mode('list');
}
}
else{
parent.inner_widget.views[parent.inner_widget.active_view].controller.reload();
}
parent.do_action({ type: 'ir.actions.act_window_close' });
var view = parent.inner_widget.active_view;
var controller = parent.inner_widget.views[view].controller;
if (view === "form"){
if (controller.dataset.size()) {
controller.execute_pager_action('next');
} else {
controller.do_action('history_back');
}
}
controller.do_action({ type: 'ir.actions.act_window_close' });
if (view === "list"){
controller.records.remove(controller.records.get(parent.dialog_widget.action.context.active_id));
}
};
instance.web.client_actions.add("next_or_list", "instance.crm_partner_assign.next_or_list");
}

View File

@ -24,6 +24,8 @@ import base64
import datetime
import dateutil.relativedelta as relativedelta
import logging
import lxml
import urlparse
import openerp
from openerp import SUPERUSER_ID
@ -61,6 +63,15 @@ try:
'quote': quote,
'urlencode': urlencode,
'datetime': datetime,
'len': len,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'filter': filter,
'reduce': reduce,
'map': map,
'round': round,
# dateutil.relativedelta is an old-style class and cannot be directly
# instanciated wihtin a jinja2 expression, so a lambda "proxy" is
@ -70,6 +81,7 @@ try:
except ImportError:
_logger.warning("jinja2 not available, templating features will not work!")
class email_template(osv.osv):
"Templates for sending email"
_name = "email.template"
@ -82,7 +94,48 @@ class email_template(osv.osv):
res['model_id'] = self.pool['ir.model'].search(cr, uid, [('model', '=', res.pop('model'))], context=context)[0]
return res
def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
def _replace_local_links(self, cr, uid, html, context=None):
""" Post-processing of html content to replace local links to absolute
links, using web.base.url as base url. """
if not html:
return html
# form a tree
root = lxml.html.fromstring(html)
if not len(root) and root.text is None and root.tail is None:
html = '<div>%s</div>' % html
root = lxml.html.fromstring(html)
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
(base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url)
def _process_link(url):
new_url = url
(scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
if not scheme and not netloc:
new_url = urlparse.urlunparse((base_scheme, base_netloc, path, params, query, fragment))
return new_url
# check all nodes, replace :
# - img src -> check URL
# - a href -> check URL
for node in root.iter():
if node.tag == 'a':
node.set('href', _process_link(node.get('href')))
elif node.tag == 'img' and not node.get('src', 'data').startswith('data'):
node.set('src', _process_link(node.get('src')))
html = lxml.html.tostring(root, pretty_print=False, method='html')
# this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
if html.startswith('<div>') and html.endswith('</div>'):
html = html[5:-6]
return html
def render_post_process(self, cr, uid, html, context=None):
html = self._replace_local_links(cr, uid, html, context=context)
return html
def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):
"""Render the given template text, replace mako expressions ``${expr}``
with the result of evaluating these expressions with
an evaluation context containing:
@ -125,6 +178,10 @@ class email_template(osv.osv):
if render_result == u"False":
render_result = u""
results[res_id] = render_result
if post_process:
for res_id, result in results.iteritems():
results[res_id] = self.render_post_process(cr, uid, result, context=context)
return results
def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None):
@ -183,7 +240,7 @@ class email_template(osv.osv):
'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False,
help="Optional preferred server for outgoing mails. If not set, the highest "
"priority one will be used."),
'body_html': fields.html('Body', translate=True, help="Rich-text/HTML version of the message (placeholders may be used here)"),
'body_html': fields.html('Body', translate=True, sanitize=False, help="Rich-text/HTML version of the message (placeholders may be used here)"),
'report_name': fields.char('Report Filename', translate=True,
help="Name to use for the generated report file (may contain placeholders)\n"
"The extension can be omitted and will then come from the report type."),
@ -356,17 +413,20 @@ class email_template(osv.osv):
results = dict()
for template, template_res_ids in templates_to_res_ids.iteritems():
# generate fields value for all res_ids linked to the current template
for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']:
generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context)
for field in fields:
generated_field_values = self.render_template_batch(
cr, uid, getattr(template, field), template.model, template_res_ids,
post_process=(field == 'body_html'),
context=context)
for res_id, field_value in generated_field_values.iteritems():
results.setdefault(res_id, dict())[field] = field_value
# update values for all res_ids
for res_id in template_res_ids:
values = results[res_id]
if template.user_signature:
if 'body_html' in fields and template.user_signature:
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
if values['body_html']:
if values.get('body_html'):
values['body'] = tools.html_sanitize(values['body_html'])
values.update(
mail_server_id=template.mail_server_id.id or False,

View File

@ -162,16 +162,18 @@ class mail_compose_message(osv.TransientModel):
partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)
return partner_ids
def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None):
def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
""" Call email_template.generate_email(), get fields relevant for
mail.compose.message, transform email_cc and email_to into partner_ids """
# filter template values
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id']
if fields is None:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
returned_fields = fields + ['attachments']
values = dict.fromkeys(res_ids, False)
template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context)
template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=context)
for res_id in res_ids:
res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field))
res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field))
res_id_values['body'] = res_id_values.pop('body_html', '')
# transform email_to, email_cc into partner_ids
@ -189,7 +191,10 @@ class mail_compose_message(osv.TransientModel):
""" Override to handle templates. """
# generate template-based values
if wizard.template_id:
template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context)
template_values = self.generate_email_for_composer_batch(
cr, uid, wizard.template_id.id, res_ids,
fields=['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'],
context=context)
else:
template_values = dict.fromkeys(res_ids, dict())
# generate composer values
@ -206,8 +211,8 @@ class mail_compose_message(osv.TransientModel):
template_values[res_id].update(composer_values[res_id])
return template_values
def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context)
def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):
return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context, post_process=post_process)
# Compatibility methods
def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):

View File

@ -52,7 +52,7 @@ class report_event_registration(osv.osv):
# TOFIX this request won't select events that have no registration
cr.execute(""" CREATE VIEW report_event_registration AS (
SELECT
e.id::char || '/' || coalesce(r.id::char,'') AS id,
e.id::varchar || '/' || coalesce(r.id::varchar,'') AS id,
e.id AS event_id,
e.user_id AS user_id,
r.user_id AS user_id_registration,

View File

@ -35,7 +35,6 @@
<group expand="1" string="Group By...">
<filter string="Participant / Contact" icon="terp-personal" context="{'group_by':'name_registration'}" help="Registration contact"/>
<filter string="Register" icon="terp-personal" context="{'group_by':'user_id_registration'}" help="Registration contact" groups="base.group_no_one"/>
<filter string="Speaker" name="speaker" icon="terp-personal+" context="{'group_by': 'speaker_id'}" groups="base.group_no_one"/>
<filter string="Event Responsible" name="user_id" icon="terp-personal" context="{'group_by': 'user_id'}"/>
<filter string="Event" name="event" icon="terp-crm" context="{'group_by':'event_id', 'max_reg_event_visible':0}"/>
<filter string="Event Type" icon="terp-crm" context="{'group_by':'event_type'}"/>

View File

@ -21,30 +21,22 @@
import operator
import simplejson
import re
import urllib
import warnings
from openerp import tools
from openerp import SUPERUSER_ID
from openerp.tools.translate import _
from openerp.addons.web.http import request
import werkzeug.utils
from datetime import datetime, timedelta, date
from datetime import datetime, timedelta
from dateutil import parser
import pytz
from openerp.osv import fields, osv
from openerp.osv import osv
from collections import namedtuple
import logging
_logger = logging.getLogger(__name__)
class Meta(type):
""" This Meta class allow to define class as a structure, and so instancied variable
""" This Meta class allow to define class as a structure, and so instancied variable
in __init__ to avoid to have side effect alike 'static' variable """
def __new__(typ, name, parents, attrs):
methods = dict((k, v) for k, v in attrs.iteritems()
@ -63,9 +55,11 @@ class Meta(type):
methods['__getitem__'] = getattr
return type.__new__(typ, name, parents, methods)
class Struct(object):
__metaclass__ = Meta
class OpenerpEvent(Struct):
event = False
found = False
@ -77,6 +71,7 @@ class OpenerpEvent(Struct):
attendee_id = False
synchro = False
class GmailEvent(Struct):
event = False
found = False
@ -85,53 +80,51 @@ class GmailEvent(Struct):
update = False
status = False
class SyncEvent(object):
def __init__(self):
self.OE = OpenerpEvent()
self.GG = GmailEvent()
self.GG = GmailEvent()
self.OP = None
def __getitem__(self, key):
return getattr(self,key)
def __getitem__(self, key):
return getattr(self, key)
def compute_OP(self):
#If event are already in Gmail and in OpenERP
#If event are already in Gmail and in OpenERP
if self.OE.found and self.GG.found:
#If the event has been deleted from one side, we delete on other side !
if self.OE.status != self.GG.status:
self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"),
'The event has been deleted from one side, we delete on other side !' )
#If event is not deleted !
'The event has been deleted from one side, we delete on other side !')
#If event is not deleted !
elif self.OE.status and self.GG.status:
if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]:
if self.OE.update < self.GG.update:
tmpSrc = 'GG'
elif self.OE.update > self.GG.update:
tmpSrc = 'OE'
assert tmpSrc in ['GG','OE']
assert tmpSrc in ['GG', 'OE']
#if self.OP.action == None:
if self[tmpSrc].isRecurrence:
if self[tmpSrc].status:
self.OP = Update(tmpSrc, 'Only need to update, because i\'m active')
self.OP = Update(tmpSrc, 'Only need to update, because i\'m active')
else:
self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence')
elif self[tmpSrc].isInstance:
self.OP= Update(tmpSrc, 'Only need to update, because already an exclu');
self.OP = Update(tmpSrc, 'Only need to update, because already an exclu')
else:
self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event');
#end-if self.OP.action == None:
self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event')
else:
if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]:
self.OP = Update('OE','Event already updated by another user, but not synchro with my google calendar')
self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar')
#import ipdb; ipdb.set_trace();
else:
self.OP = NothingToDo("",'Not update needed')
self.OP = NothingToDo("", 'Not update needed')
else:
self.OP = NothingToDo("", "Both are already deleted");
self.OP = NothingToDo("", "Both are already deleted")
# New in openERP... Create on create_events of synchronize function
elif self.OE.found and not self.GG.found:
@ -139,64 +132,73 @@ class SyncEvent(object):
if self.OE.status:
self.OP = Delete('OE', 'Removed from GOOGLE')
else:
self.OP = NothingToDo("","Already Deleted in gmail and unlinked in OpenERP")
self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP")
elif self.GG.found and not self.OE.found:
tmpSrc = 'GG'
if not self.GG.status and not self.GG.isInstance:
# don't need to make something... because event has been created and deleted before the synchronization
self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly')
# don't need to make something... because event has been created and deleted before the synchronization
self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly')
else:
if self.GG.isInstance:
if self[tmpSrc].status:
self.OP = Exclude(tmpSrc, 'Need to create the new exclu')
else:
self.OP = Exclude(tmpSrc, 'Need to copy and Exclude')
else:
self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL')
if self.GG.isInstance:
if self[tmpSrc].status:
self.OP = Exclude(tmpSrc, 'Need to create the new exclu')
else:
self.OP = Exclude(tmpSrc, 'Need to copy and Exclude')
else:
self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL')
def __str__(self):
return self.__repr__()
def __repr__(self):
myPrint = "---- A SYNC EVENT ---"
myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id)
myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False))
myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name)
myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', False))
myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found)
myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence)
myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance)
myPrint += "\n Synchro OE: %10s " % (self.OE.synchro)
myPrint += "\n Update OE: %10s " % (self.OE.update)
myPrint += "\n Update GG: %10s " % (self.GG.update)
myPrint += "\n Status OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status)
myPrint = "---- A SYNC EVENT ---"
myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id)
myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False))
myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name)
myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', False))
myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found)
myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence)
myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance)
myPrint += "\n Synchro OE: %10s " % (self.OE.synchro)
myPrint += "\n Update OE: %10s " % (self.OE.update)
myPrint += "\n Update GG: %10s " % (self.GG.update)
myPrint += "\n Status OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status)
if (self.OP is None):
myPrint += "\n Action %s" % "---!!!---NONE---!!!---"
myPrint += "\n Action %s" % "---!!!---NONE---!!!---"
else:
myPrint += "\n Action %s" % type(self.OP).__name__
myPrint += "\n Source %s" % (self.OP.src)
myPrint += "\n comment %s" % (self.OP.info)
myPrint += "\n Action %s" % type(self.OP).__name__
myPrint += "\n Source %s" % (self.OP.src)
myPrint += "\n comment %s" % (self.OP.info)
return myPrint
class SyncOperation(object):
def __init__(self, src,info, **kw):
def __init__(self, src, info, **kw):
self.src = src
self.info = info
for k,v in kw.items():
setattr(self,k,v)
for k, v in kw.items():
setattr(self, k, v)
def __str__(self):
return 'in__STR__'
class Create(SyncOperation):
pass
class Update(SyncOperation):
pass
class Delete(SyncOperation):
pass
class NothingToDo(SyncOperation):
pass
class Exclude(SyncOperation):
pass
@ -205,9 +207,9 @@ class google_calendar(osv.AbstractModel):
STR_SERVICE = 'calendar'
_name = 'google.%s' % STR_SERVICE
def generate_data(self, cr, uid, event, context=None):
def generate_data(self, cr, uid, event, context=None):
if event.allday:
start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) , context=context).isoformat('T').split('T')[0]
start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0]
end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration), context=context).isoformat('T').split('T')[0]
type = 'date'
vstype = 'dateTime'
@ -220,72 +222,71 @@ class google_calendar(osv.AbstractModel):
for attendee in event.attendee_ids:
attendee_list.append({
'email':attendee.email or 'NoEmail@mail.com',
'displayName':attendee.partner_id.name,
'responseStatus':attendee.state or 'needsAction',
'email': attendee.email or 'NoEmail@mail.com',
'displayName': attendee.partner_id.name,
'responseStatus': attendee.state or 'needsAction',
})
data = {
"summary": event.name or '',
"description": event.description or '',
"start":{
type:start_date,
vstype:None,
'timeZone':'UTC'
},
"end":{
type:end_date,
vstype:None,
'timeZone':'UTC'
},
"attendees":attendee_list,
"location":event.location or '',
"visibility":event['class'] or 'public',
"start": {
type: start_date,
vstype: None,
'timeZone': 'UTC'
},
"end": {
type: end_date,
vstype: None,
'timeZone': 'UTC'
},
"attendees": attendee_list,
"location": event.location or '',
"visibility": event['class'] or 'public',
}
if event.recurrency and event.rrule:
data["recurrence"]=["RRULE:"+event.rrule]
data["recurrence"] = ["RRULE:" + event.rrule]
if not event.active:
data["state"] = "cancelled"
if not self.get_need_synchro_attendee(cr,uid,context=context):
if not self.get_need_synchro_attendee(cr, uid, context=context):
data.pop("attendees")
return data
def create_an_event(self, cr, uid,event, context=None):
def create_an_event(self, cr, uid, event, context=None):
gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid,event, context=context)
data = self.generate_data(cr, uid, event, context=context)
url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary',urllib.quote('id,updated'),self.get_token(cr,uid,context))
url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib.quote('id,updated'), self.get_token(cr, uid, context))
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
data_json = simplejson.dumps(data)
return gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context)
def delete_an_event(self, cr, uid,event_id, context=None):
def delete_an_event(self, cr, uid, event_id, context=None):
gs_pool = self.pool['google.service']
params = {
'access_token' : self.get_token(cr,uid,context)
}
'access_token': self.get_token(cr, uid, context)
}
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
url = "/calendar/v3/calendars/%s/events/%s" % ('primary',event_id)
url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event_id)
return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context)
def get_event_dict(self,cr,uid,token=False,nextPageToken=False,context=None):
def get_event_dict(self, cr, uid, token=False, nextPageToken=False, context=None):
if not token:
token = self.get_token(cr,uid,context)
token = self.get_token(cr, uid, context)
gs_pool = self.pool['google.service']
params = {
'fields': 'items,nextPageToken',
'access_token' : token,
'maxResults':1000,
'timeMin': self.get_start_time_to_synchro(cr,uid,context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"),
'fields': 'items,nextPageToken',
'access_token': token,
'maxResults': 1000,
'timeMin': self.get_start_time_to_synchro(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"),
}
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
@ -301,50 +302,50 @@ class google_calendar(osv.AbstractModel):
google_events_dict[google_event['id']] = google_event
if content.get('nextPageToken', False):
google_events_dict.update(self.get_event_dict(cr,uid,token,content['nextPageToken'],context=context))
return google_events_dict
google_events_dict.update(self.get_event_dict(cr, uid, token, content['nextPageToken'], context=context))
return google_events_dict
def update_to_google(self, cr, uid, oe_event, google_event, context):
calendar_event = self.pool['calendar.event']
gs_pool = self.pool['google.service']
url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'],'id,updated', self.get_token(cr,uid,context))
url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context))
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
data = self.generate_data(cr,uid ,oe_event, context)
data = self.generate_data(cr, uid, oe_event, context)
data['sequence'] = google_event.get('sequence', 0)
data_json = simplejson.dumps(data)
content = gs_pool._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context)
update_date = datetime.strptime(content['updated'],"%Y-%m-%dT%H:%M:%S.%fz")
calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date':update_date})
update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date})
if context['curr_attendee']:
self.pool['calendar.attendee'].write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date},context)
self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context)
def update_an_event(self, cr, uid,event, context=None):
def update_an_event(self, cr, uid, event, context=None):
gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid,event, context=context)
data = self.generate_data(cr, uid, event, context=context)
url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id)
headers = {}
data['access_token'] = self.get_token(cr,uid,context)
data['access_token'] = self.get_token(cr, uid, context)
response = gs_pool._do_request(cr, uid, url, data, headers, type='GET', context=context)
#TO_CHECK : , if http fail, no event, do DELETE ?
return response
def update_recurrent_event_exclu(self, cr, uid,instance_id,event_ori_google_id,event_new, context=None):
def update_recurrent_event_exclu(self, cr, uid, instance_id, event_ori_google_id, event_new, context=None):
gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid,event_new, context=context)
data = self.generate_data(cr, uid, event_new, context=context)
data['recurringEventId'] = event_ori_google_id
data['originalStartTime'] = event_new.recurrent_id_date
url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id,self.get_token(cr,uid,context))
headers = { 'Content-type': 'application/json'}
url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token(cr, uid, context))
headers = {'Content-type': 'application/json'}
data['sequence'] = self.get_sequence(cr, uid, instance_id, context)
@ -353,41 +354,41 @@ class google_calendar(osv.AbstractModel):
def update_from_google(self, cr, uid, event, single_event_dict, type, context):
if context is None:
context= []
context = []
calendar_event = self.pool['calendar.event']
res_partner_obj = self.pool['res.partner']
calendar_attendee_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users']
myPartnerID = user_obj.browse(cr,uid,uid,context).partner_id.id
myPartnerID = user_obj.browse(cr, uid, uid, context).partner_id.id
attendee_record = []
partner_record = [(4,myPartnerID)]
partner_record = [(4, myPartnerID)]
result = {}
if single_event_dict.get('attendees',False):
if single_event_dict.get('attendees', False):
for google_attendee in single_event_dict['attendees']:
if type == "write":
for oe_attendee in event['attendee_ids']:
if oe_attendee.email == google_attendee['email']:
calendar_attendee_obj.write(cr, uid,[oe_attendee.id] ,{'state' : google_attendee['responseStatus']},context=context)
calendar_attendee_obj.write(cr, uid, [oe_attendee.id], {'state': google_attendee['responseStatus']}, context=context)
google_attendee['found'] = True
continue
if google_attendee.get('found',False):
if google_attendee.get('found', False):
continue
if self.get_need_synchro_attendee(cr,uid,context=context):
attendee_id = res_partner_obj.search(cr, uid,[('email', '=', google_attendee['email'])], context=context)
if not attendee_id:
attendee_id = [res_partner_obj.create(cr, uid,{'email': google_attendee['email'],'customer': False, 'name': google_attendee.get("displayName",False) or google_attendee['email'] }, context=context)]
if self.get_need_synchro_attendee(cr, uid, context=context):
attendee_id = res_partner_obj.search(cr, uid, [('email', '=', google_attendee['email'])], context=context)
if not attendee_id:
attendee_id = [res_partner_obj.create(cr, uid, {'email': google_attendee['email'], 'customer': False, 'name': google_attendee.get("displayName", False) or google_attendee['email']}, context=context)]
attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context)
partner_record.append((4, attendee.get('id')))
attendee['partner_id'] = attendee.pop('id')
attendee['state'] = google_attendee['responseStatus']
attendee_record.append((0, 0, attendee))
UTC = pytz.timezone('UTC')
if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled
if single_event_dict['start'].get('dateTime',False) and single_event_dict['end'].get('dateTime',False):
if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled
if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False):
date = parser.parse(single_event_dict['start']['dateTime'])
date_deadline = parser.parse(single_event_dict['end']['dateTime'])
delta = date_deadline.astimezone(UTC) - date.astimezone(UTC)
@ -402,8 +403,8 @@ class google_calendar(osv.AbstractModel):
delta = (d_end - d_start)
allday = True
result['duration'] = (delta.seconds / 60) / 60.0 + delta.days *24
update_date = datetime.strptime(single_event_dict['updated'],"%Y-%m-%dT%H:%M:%S.%fz")
result['duration'] = (delta.seconds / 60) / 60.0 + delta.days * 24
update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
result.update({
'date': date,
'date_deadline': date_deadline,
@ -413,17 +414,17 @@ class google_calendar(osv.AbstractModel):
'attendee_ids': attendee_record,
'partner_ids': list(set(partner_record)),
'name': single_event_dict.get('summary','Event'),
'description': single_event_dict.get('description',False),
'location':single_event_dict.get('location',False),
'class':single_event_dict.get('visibility','public'),
'oe_update_date':update_date,
'name': single_event_dict.get('summary', 'Event'),
'description': single_event_dict.get('description', False),
'location': single_event_dict.get('location', False),
'class': single_event_dict.get('visibility', 'public'),
'oe_update_date': update_date,
# 'google_internal_event_id': single_event_dict.get('id',False),
})
if single_event_dict.get("recurrence",False):
if single_event_dict.get("recurrence", False):
rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:]
result['rrule']=rrule
result['rrule'] = rrule
if type == "write":
res = calendar_event.write(cr, uid, event['id'], result, context=context)
@ -435,77 +436,71 @@ class google_calendar(osv.AbstractModel):
res = calendar_event.create(cr, uid, result, context=context)
if context['curr_attendee']:
self.pool['calendar.attendee'].write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date,'google_internal_event_id': single_event_dict.get('id',False)},context)
self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date, 'google_internal_event_id': single_event_dict.get('id', False)}, context)
return res
def synchronize_events(self, cr, uid, ids, context=None):
gc_obj = self.pool['google.calendar']
# Create all new events from OpenERP into Gmail, if that is not recurrent event
self.create_new_events(cr, uid, context=context)
self.bind_recurring_events_to_google(cr, uid, context)
res = self.update_events(cr, uid, context)
return {
"status" : res and "need_refresh" or "no_new_event_form_google",
"url" : ''
}
"status": res and "need_refresh" or "no_new_event_form_google",
"url": ''
}
def create_new_events(self, cr, uid, context=None):
gc_pool = self.pool['google.calendar']
ev_obj = self.pool['calendar.event']
att_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users']
myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id
myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
context_norecurrent = context.copy()
context_norecurrent['virtual_id'] = False
my_att_ids = att_obj.search(cr, uid,[ ('partner_id', '=', myPartnerID),
('google_internal_event_id', '=', False),
'|',
('event_id.date_deadline','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")),
('event_id.end_date','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")),
], context=context_norecurrent)
my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
('google_internal_event_id', '=', False),
'|',
('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
], context=context_norecurrent)
for att in att_obj.browse(cr,uid,my_att_ids,context=context):
for att in att_obj.browse(cr, uid, my_att_ids, context=context):
if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0:
response = self.create_an_event(cr,uid,att.event_id,context=context)
update_date = datetime.strptime(response['updated'],"%Y-%m-%dT%H:%M:%S.%fz")
ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date':update_date})
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date':update_date})
cr.commit()
response = self.create_an_event(cr, uid, att.event_id, context=context)
update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date})
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date})
cr.commit()
def bind_recurring_events_to_google(self, cr, uid, context):
def bind_recurring_events_to_google(self, cr, uid, context):
ev_obj = self.pool['calendar.event']
att_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users']
myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id
myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
context_norecurrent = context.copy()
context_norecurrent['virtual_id'] = False
context_norecurrent['active_test'] = False
my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '=', False)], context=context_norecurrent)
for att in att_obj.browse(cr,uid,my_att_ids,context=context):
my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent)
for att in att_obj.browse(cr, uid, my_att_ids, context=context):
if att.event_id.recurrent_id and att.event_id.recurrent_id > 0:
new_google_internal_event_id = False
source_event_record = ev_obj.browse(cr, uid, att.event_id.recurrent_id, context)
source_attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',source_event_record.id)], context=context)
source_attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', source_event_record.id)], context=context)
source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context)[0]
if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id:
new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.split(' ')[0].replace('-','')
new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.split(' ')[0].replace('-', '')
elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id:
new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.replace('-','').replace(' ','T').replace(':','') + 'Z'
new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.replace('-', '').replace(' ', 'T').replace(':', '') + 'Z'
if new_google_internal_event_id:
#TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE !
res = self.update_recurrent_event_exclu(cr, uid,new_google_internal_event_id, source_attendee_record.google_internal_event_id,att.event_id, context=context)
self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context)
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context)
cr.commit()
@ -523,16 +518,16 @@ class google_calendar(osv.AbstractModel):
context_novirtual['active_test'] = False
all_event_from_google = self.get_event_dict(cr, uid, context=context)
# Select all events from OpenERP which have been already synchronized in gmail
my_att_ids = att_obj.search(cr, uid,[ ('partner_id', '=', myPartnerID),
('google_internal_event_id', '!=', False),
'|',
('event_id.date_deadline','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")),
('event_id.end_date','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")),
], context=context_novirtual)
my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
('google_internal_event_id', '!=', False),
'|',
('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
], context=context_novirtual)
event_to_synchronize = {}
for att in att_obj.browse(cr,uid,my_att_ids,context=context):
for att in att_obj.browse(cr, uid, my_att_ids, context=context):
event = att.event_id
base_event_id = att.google_internal_event_id.split('_')[0]
@ -544,7 +539,7 @@ class google_calendar(osv.AbstractModel):
event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent()
ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id]
ev_to_sync.OE.attendee_id = att.id
ev_to_sync.OE.event = event
ev_to_sync.OE.found = True
@ -566,40 +561,35 @@ class google_calendar(osv.AbstractModel):
event_to_synchronize[base_event_id][event_id] = SyncEvent()
ev_to_sync = event_to_synchronize[base_event_id][event_id]
ev_to_sync.GG.event = event
ev_to_sync.GG.found = True
ev_to_sync.GG.isRecurrence = bool(event.get('recurrence',''))
ev_to_sync.GG.isInstance = bool(event.get('recurringEventId',0))
ev_to_sync.GG.update = event.get('updated',None) # if deleted, no date without browse event
ev_to_sync.GG.isRecurrence = bool(event.get('recurrence', ''))
ev_to_sync.GG.isInstance = bool(event.get('recurringEventId', 0))
ev_to_sync.GG.update = event.get('updated', None) # if deleted, no date without browse event
if ev_to_sync.GG.update:
ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T',' ').replace('Z','')
ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T', ' ').replace('Z', '')
ev_to_sync.GG.status = (event.get('status') != 'cancelled')
######################
######################
# PRE-PROCESSING #
######################
for base_event in event_to_synchronize:
for current_event in event_to_synchronize[base_event]:
event_to_synchronize[base_event][current_event].compute_OP()
event_to_synchronize[base_event][current_event].compute_OP()
#print event_to_synchronize[base_event]
#print "========================================================"
######################
# DO ACTION #
######################
######################
for base_event in event_to_synchronize:
event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(),key=operator.itemgetter(0))
event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(), key=operator.itemgetter(0))
for current_event in event_to_synchronize[base_event]:
cr.commit()
event = current_event[1] # event is an Sync Event !
event = current_event[1] # event is an Sync Event !
actToDo = event.OP
actSrc = event.OP.src
# if not isinstance(actToDo, NothingToDo):
# print event
context['curr_attendee'] = event.OE.attendee_id
if isinstance(actToDo, NothingToDo):
@ -607,135 +597,134 @@ class google_calendar(osv.AbstractModel):
elif isinstance(actToDo, Create):
context_tmp = context.copy()
context_tmp['NewMeeting'] = True
if actSrc == 'GG':
if actSrc == 'GG':
res = self.update_from_google(cr, uid, False, event.GG.event, "create", context=context_tmp)
event.OE.event_id = res
meeting = calendar_event.browse(cr,uid,res,context=context)
attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',res)], context=context)
self.pool['calendar.attendee'].write(cr, uid, attendee_record_id, {'oe_synchro_date':meeting.oe_update_date, 'google_internal_event_id':event.GG.event['id']}, context=context_tmp)
elif actSrc == 'OE':
meeting = calendar_event.browse(cr, uid, res, context=context)
attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', res)], context=context)
self.pool['calendar.attendee'].write(cr, uid, attendee_record_id, {'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']}, context=context_tmp)
elif actSrc == 'OE':
raise "Should be never here, creation for OE is done before update !"
#TODO Add to batch
elif isinstance(actToDo, Update):
if actSrc == 'GG':
if actSrc == 'GG':
self.update_from_google(cr, uid, event.OE.event, event.GG.event, 'write', context)
elif actSrc == 'OE':
elif actSrc == 'OE':
self.update_to_google(cr, uid, event.OE.event, event.GG.event, context)
elif isinstance(actToDo, Exclude):
if actSrc == 'OE':
self.delete_an_event(cr,uid,current_event[0],context=context)
elif actSrc == 'GG':
self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'GG':
new_google_event_id = event.GG.event['id'].split('_')[1]
if 'T' in new_google_event_id:
new_google_event_id = new_google_event_id.replace('T','')[:-1]
new_google_event_id = new_google_event_id.replace('T', '')[:-1]
else:
new_google_event_id = new_google_event_id + "000000"
if event.GG.status:
parent_event = {}
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id , new_google_event_id)
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)
else:
if event_to_synchronize[base_event][0][1].OE.event_id:
parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id
calendar_event.unlink(cr,uid,"%s-%s" % (parent_oe_id,new_google_event_id),unlink_level=1,context=context)
parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id
calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), unlink_level=1, context=context)
elif isinstance(actToDo, Delete):
if actSrc == 'GG':
self.delete_an_event(cr,uid,current_event[0],context=context)
elif actSrc == 'OE':
calendar_event.unlink(cr,uid,event.OE.event_id,unlink_level=0,context=context)
self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'OE':
calendar_event.unlink(cr, uid, event.OE.event_id, unlink_level=0, context=context)
return True
def check_and_sync(self, cr, uid, oe_event, google_event, context):
if datetime.strptime(oe_event.oe_update_date,"%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'],"%Y-%m-%dT%H:%M:%S.%fz"):
if datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"):
self.update_to_google(cr, uid, oe_event, google_event, context)
elif datetime.strptime(oe_event.oe_update_date,"%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'],"%Y-%m-%dT%H:%M:%S.%fz"):
elif datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"):
self.update_from_google(cr, uid, oe_event, google_event, 'write', context)
def get_sequence(self,cr,uid,instance_id,context=None):
def get_sequence(self, cr, uid, instance_id, context=None):
gs_pool = self.pool['google.service']
params = {
'fields': 'sequence',
'access_token' : self.get_token(cr,uid,context)
}
'fields': 'sequence',
'access_token': self.get_token(cr, uid, context)
}
headers = {'Content-type': 'application/json'}
url = "/calendar/v3/calendars/%s/events/%s" % ('primary',instance_id)
url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id)
content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context)
return content.get('sequence',0)
#################################
return content.get('sequence', 0)
#################################
## MANAGE CONNEXION TO GMAIL ##
#################################
def get_token(self,cr,uid,context=None):
current_user = self.pool['res.users'].browse(cr,uid,uid,context=context)
def get_token(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)):
self.do_refresh_token(cr,uid,context=context)
self.do_refresh_token(cr, uid, context=context)
current_user.refresh()
return current_user.google_calendar_token
def do_refresh_token(self,cr,uid,context=None):
current_user = self.pool['res.users'].browse(cr,uid,uid,context=context)
def do_refresh_token(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
gs_pool = self.pool['google.service']
refresh = current_user.google_calendar_rtoken
all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken, self.STR_SERVICE, context=context)
vals = {}
vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
self.pool['res.users'].write(cr,SUPERUSER_ID,uid,vals,context=context)
self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
def need_authorize(self,cr,uid,context=None):
current_user = self.pool['res.users'].browse(cr,uid,uid,context=context)
return current_user.google_calendar_rtoken == False
def need_authorize(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
return current_user.google_calendar_rtoken is False
def get_calendar_scope(self,RO=False):
def get_calendar_scope(self, RO=False):
readonly = RO and '.readonly' or ''
return 'https://www.googleapis.com/auth/calendar%s' % (readonly)
def authorize_google_uri(self,cr,uid,from_url='http://www.openerp.com',context=None):
url = self.pool['google.service']._get_authorize_uri(cr,uid,from_url,self.STR_SERVICE,scope=self.get_calendar_scope(),context=context)
def authorize_google_uri(self, cr, uid, from_url='http://www.openerp.com', context=None):
url = self.pool['google.service']._get_authorize_uri(cr, uid, from_url, self.STR_SERVICE, scope=self.get_calendar_scope(), context=context)
return url
def can_authorize_google(self,cr,uid,context=None):
def can_authorize_google(self, cr, uid, context=None):
return self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager')
def set_all_tokens(self,cr,uid,authorization_code,context=None):
def set_all_tokens(self, cr, uid, authorization_code, context=None):
gs_pool = self.pool['google.service']
all_token = gs_pool._get_google_token_json(cr, uid, authorization_code,self.STR_SERVICE,context=context)
all_token = gs_pool._get_google_token_json(cr, uid, authorization_code, self.STR_SERVICE, context=context)
vals = {}
vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token')
vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
self.pool['res.users'].write(cr,SUPERUSER_ID,uid,vals,context=context)
self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
def get_start_time_to_synchro(self, cr, uid, context=None) :
def get_start_time_to_synchro(self, cr, uid, context=None):
# WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4
number_of_week = 13
return datetime.now()-timedelta(weeks=number_of_week)
return datetime.now() - timedelta(weeks=number_of_week)
def get_need_synchro_attendee(self, cr, uid, context=None):
# WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4
return True
class res_users(osv.Model):
class res_users(osv.Model):
_inherit = 'res.users'
_columns = {
'google_calendar_rtoken': fields.char('Refresh Token'),
'google_calendar_token': fields.char('User token'),
'google_calendar_token': fields.char('User token'),
'google_calendar_token_validity': fields.datetime('Token Validity'),
}
}
class calendar_event(osv.Model):
@ -743,7 +732,7 @@ class calendar_event(osv.Model):
def write(self, cr, uid, ids, vals, context=None):
if context is None:
context= {}
context = {}
sync_fields = set(['name', 'description', 'date', 'date_closed', 'date_deadline', 'attendee_ids', 'location', 'class'])
if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context:
vals['oe_update_date'] = datetime.now()
@ -773,19 +762,17 @@ class calendar_attendee(osv.Model):
'google_internal_event_id': fields.char('Google Calendar Event Id', size=256),
'oe_synchro_date': fields.datetime('OpenERP Synchro Date'),
}
_sql_constraints = [('google_id_uniq','unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')]
_sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')]
def write(self, cr, uid, ids, vals, context=None):
if context is None:
context = {}
for id in ids:
ref = vals.get('event_id',self.browse(cr,uid,id,context=context).event_id.id)
ref = vals.get('event_id', self.browse(cr, uid, id, context=context).event_id.id)
# If attendees are updated, we need to specify that next synchro need an action
# Except if it come from an update_from_google
if not context.get('curr_attendee', False) and not context.get('NewMeeting', False):
self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date':datetime.now()},context)
return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context)
self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date': datetime.now()}, context)
return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context)

View File

@ -263,6 +263,10 @@ class hr_expense_expense(osv.osv):
#convert eml into an osv-valid format
lines = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, exp.employee_id.address_home_id, exp.date_confirm, context=context)), eml)
journal_id = move_obj.browse(cr, uid, move_id, context).journal_id
# post the journal entry if 'Skip 'Draft' State for Manual Entries' is checked
if journal_id.entry_posted:
move_obj.button_validate(cr, uid, [move_id], context)
move_obj.write(cr, uid, [move_id], {'line_id': lines}, context=context)
self.write(cr, uid, ids, {'account_move_id': move_id, 'state': 'done'}, context=context)
return True

View File

@ -370,7 +370,8 @@ class hr_holidays(osv.osv):
'date': record.date_from,
'end_date': record.date_to,
'date_deadline': record.date_to,
'state': 'open', # to block that meeting date in the calendar
'state': 'open', # to block that meeting date in the calendar
'class': 'confidential'
}
#Add the partner_id (if exist) as an attendee
if record.user_id and record.user_id.partner_id:

View File

@ -23,7 +23,7 @@ import time
from datetime import date, datetime, timedelta
from openerp.osv import fields, osv
from openerp.tools import config
from openerp.tools import config, float_compare
from openerp.tools.translate import _
class hr_payslip(osv.osv):
@ -86,6 +86,7 @@ class hr_payslip(osv.osv):
def process_sheet(self, cr, uid, ids, context=None):
move_pool = self.pool.get('account.move')
period_pool = self.pool.get('account.period')
precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Payroll')
timenow = time.strftime('%Y-%m-%d')
for slip in self.browse(cr, uid, ids, context=context):
@ -149,7 +150,7 @@ class hr_payslip(osv.osv):
line_ids.append(credit_line)
credit_sum += credit_line[2]['credit'] - credit_line[2]['debit']
if debit_sum > credit_sum:
if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1:
acc_id = slip.journal_id.default_credit_account_id.id
if not acc_id:
raise osv.except_osv(_('Configuration Error!'),_('The Expense Journal "%s" has not properly configured the Credit Account!')%(slip.journal_id.name))
@ -165,7 +166,7 @@ class hr_payslip(osv.osv):
})
line_ids.append(adjust_credit)
elif debit_sum < credit_sum:
elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1:
acc_id = slip.journal_id.default_debit_account_id.id
if not acc_id:
raise osv.except_osv(_('Configuration Error!'),_('The Expense Journal "%s" has not properly configured the Debit Account!')%(slip.journal_id.name))

View File

@ -138,7 +138,7 @@ class wizard_multi_charts_accounts(osv.osv_memory):
def _process_taxes_translations(self, cr, uid, obj_multi, company_id, langs, field, context=None):
obj_tax_template = self.pool.get('account.tax.template')
obj_tax = self.pool.get('account.tax')
in_ids = sorted([x.id for x in obj_multi.chart_template_id.tax_template_ids])
in_ids = [x.id for x in obj_multi.chart_template_id.tax_template_ids]
out_ids = obj_tax.search(cr, uid, [('company_id', '=', company_id)], order='id')
return self.process_translations(cr, uid, langs, obj_tax_template, field, in_ids, obj_tax, out_ids, context=context)

View File

@ -186,12 +186,11 @@ class IrAttachment(osv.Model):
def get_attachment_type(self, cr, uid, ids, name, args, context=None):
result = {}
for attachment in self.browse(cr, uid, ids, context=context):
fileext = os.path.splitext(attachment.datas_fname)[1].lower()
if not fileext or not fileext[1:] in self._fileext_to_type:
return 'unknown'
result[attachment.id] = self._fileext_to_type[fileext[1:]]
fileext = os.path.splitext(attachment.datas_fname or '')[1].lower()[1:]
result[attachment.id] = self._fileext_to_type.get(fileext, 'unknown')
return result
_columns = {
'file_type': fields.function(get_attachment_type, type='char', string='File Type'),
'file_type_icon': fields.function(get_attachment_type, type='char', string='File Type Icon'),
'file_type': fields.related('file_type_icon', type='char'), # FIXME remove in trunk
}

View File

@ -160,8 +160,8 @@ class mail_mail(osv.Model):
fragment['message_id'] = mail.mail_message_id.id
elif mail.model and mail.res_id:
fragment.update(model=mail.model, res_id=mail.res_id)
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
url = urljoin(base_url, "/web?%s#%s" % (urlencode(query), urlencode(fragment)))
return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small></span>""") % url
else:
return None

View File

@ -351,12 +351,12 @@ class mail_message(osv.Model):
partner_tree = dict((partner[0], partner) for partner in partners)
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type'], context=context)
attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type_icon'], context=context)
attachments_tree = dict((attachment['id'], {
'id': attachment['id'],
'filename': attachment['datas_fname'],
'name': attachment['name'],
'file_type': attachment['file_type'],
'file_type_icon': attachment['file_type_icon'],
}) for attachment in attachments)
# 3. Update message dictionaries

View File

@ -209,7 +209,7 @@ class mail_thread(osv.AbstractModel):
], context=context)
for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
thread_subtype_dict = res[fol.res_id]['message_subtype_data']
for subtype in fol.subtype_ids:
for subtype in [st for st in fol.subtype_ids if st.name in thread_subtype_dict]:
thread_subtype_dict[subtype.name]['followed'] = True
res[fol.res_id]['message_subtype_data'] = thread_subtype_dict

View File

@ -88,10 +88,10 @@
-->
<t t-name="mail.thread.message.attachments">
<t t-foreach='widget.attachment_ids' t-as='attachment'>
<t t-if="attachment.file_type !== 'webimage'">
<t t-if="attachment.file_type_icon !== 'webimage'">
<div t-attf-class="oe_attachment #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank">
<img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type + '.png'"></img>
<img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type_icon + '.png'"></img>
<div class='oe_name'><t t-raw='attachment.name' /></div>
</a>
<div class='oe_delete oe_e' title="Delete this attachment" t-att-data-id="attachment.id">[</div>
@ -100,7 +100,7 @@
</div>
</div>
</t>
<t t-if="attachment.file_type === 'webimage'">
<t t-if="attachment.file_type_icon === 'webimage'">
<div t-attf-class="oe_attachment oe_preview #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank">
<img t-att-src="widget.attachments_resize_image(attachment.id, [100,80])"></img>

View File

@ -352,10 +352,10 @@ class mail_compose_message(osv.TransientModel):
:return dict results: for each res_id, the generated template values for
subject, body, email_from and reply_to
"""
subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context)
bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context)
emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context)
replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context)
subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context=context)
bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context=context, post_process=True)
emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context=context)
replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context=context)
results = dict.fromkeys(res_ids, False)
for res_id in res_ids:
@ -367,7 +367,7 @@ class mail_compose_message(osv.TransientModel):
}
return results
def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):
""" Render the given template text, replace mako-like expressions ``${expr}``
with the result of evaluating these expressions with an evaluation context
containing:

View File

@ -6,7 +6,7 @@
<record id="membership_0" model="product.product">
<field name="membership">True</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-12-01')" name="membership_date_to"/>
<field eval="time.strftime('%Y-12-31')" name="membership_date_to"/>
<field name="name">Gold Membership</field>
<field name="list_price">180</field>
<field name="categ_id" ref="product.product_category_1"/>
@ -16,7 +16,7 @@
<record id="membership_1" model="product.product">
<field name="membership">True</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-12-01')" name="membership_date_to"/>
<field eval="time.strftime('%Y-12-31')" name="membership_date_to"/>
<field name="name">Silver Membership</field>
<field name="categ_id" ref="product.product_category_1"/>
<field name="list_price">80</field>
@ -26,7 +26,7 @@
<record id="membership_2" model="product.product">
<field name="membership">True</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-12-01')" name="membership_date_to"/>
<field eval="time.strftime('%Y-12-31')" name="membership_date_to"/>
<field name="name">Basic Membership</field>
<field name="categ_id" ref="product.product_category_1"/>
<field name="list_price">40</field>

View File

@ -29,6 +29,7 @@ from openerp.tools import float_compare
from openerp.tools.translate import _
from openerp import tools, SUPERUSER_ID
from openerp import SUPERUSER_ID
from openerp.addons.product import _common
#----------------------------------------------------------
# Work Centers
@ -322,7 +323,7 @@ class mrp_bom(osv.osv):
"""
routing_obj = self.pool.get('mrp.routing')
factor = factor / (bom.product_efficiency or 1.0)
factor = rounding(factor, bom.product_rounding)
factor = _common.ceiling(factor, bom.product_rounding)
if factor < bom.product_rounding:
factor = bom.product_rounding
result = []
@ -378,6 +379,8 @@ class mrp_bom(osv.osv):
def rounding(f, r):
# TODO for trunk: log deprecation warning
# _logger.warning("Deprecated rounding method, please use tools.float_round to round floats.")
import math
if not r:
return f

View File

@ -59,6 +59,7 @@ The following topics should be covered by this module:
'test/test_mrp_repair_b4inv.yml',
'test/test_mrp_repair_afterinv.yml',
'test/test_mrp_repair_cancel.yml',
'test/test_mrp_repair_fee.yml',
],
'installable': True,
'auto_install': False,

View File

@ -108,10 +108,12 @@ class mrp_repair(osv.osv):
return res
def _get_lines(self, cr, uid, ids, context=None):
result = {}
for line in self.pool.get('mrp.repair.line').browse(cr, uid, ids, context=context):
result[line.repair_id.id] = True
return result.keys()
return self.pool['mrp.repair'].search(
cr, uid, [('operations', 'in', ids)], context=context)
def _get_fee_lines(self, cr, uid, ids, context=None):
return self.pool['mrp.repair'].search(
cr, uid, [('fees_lines', 'in', ids)], context=context)
_columns = {
'name': fields.char('Repair Reference',size=24, required=True, states={'confirmed':[('readonly',True)]}),
@ -160,18 +162,21 @@ class mrp_repair(osv.osv):
'repaired': fields.boolean('Repaired', readonly=True),
'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount',
store={
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10),
'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
}),
'amount_tax': fields.function(_amount_tax, string='Taxes',
store={
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10),
'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
}),
'amount_total': fields.function(_amount_total, string='Total',
store={
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10),
'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
}),
}

View File

@ -0,0 +1,23 @@
-
Testing total amount update function
-
I check the total amount of mrp_repair_rmrp1 is 100
-
!assert {model: mrp.repair, id: mrp_repair_rmrp1, string=amount_total should be 100}:
- amount_total == 100
-
I add a new fee line
-
!record {model: mrp.repair, id: mrp_repair_rmrp1}:
fees_lines:
- name: 'Assembly Service Cost'
product_id: product.product_assembly
product_uom_qty: 1.0
product_uom: product.product_uom_hour
price_unit: 12.0
to_invoice: True
-
I check the total amount of mrp_repair_rmrp1 is now 112
-
!assert {model: mrp.repair, id: mrp_repair_rmrp1, string=amount_total should be 112}:
- amount_total == 112

View File

@ -275,9 +275,12 @@ class PaymentAcquirer(osv.Model):
</div>""" % (amount, payment_header)
return result % html_block.decode("utf-8")
def render_payment_block(self, cr, uid, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, context=None):
def render_payment_block(self, cr, uid, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, company_id=None, context=None):
html_forms = []
acquirer_ids = self.search(cr, uid, [('website_published', '=', True), ('validation', '=', 'automatic')], context=context)
domain = [('website_published', '=', True), ('validation', '=', 'automatic')]
if company_id:
domain.append(('company_id', '=', company_id))
acquirer_ids = self.search(cr, uid, domain, context=context)
for acquirer_id in acquirer_ids:
button = self.render(
cr, uid, acquirer_id,

View File

@ -9,14 +9,11 @@ class AccountPaymentConfig(osv.TransientModel):
_columns = {
'module_payment_paypal': fields.boolean(
'Manage Payments Using Paypal',
help='Blahblahblah\n'
'-It installs the module payment_paypal.'),
help='-It installs the module payment_paypal.'),
'module_payment_ogone': fields.boolean(
'Manage Payments Using Ogone',
help='Blahblahblah\n'
'-It installs the module payment_ogone.'),
help='-It installs the module payment_ogone.'),
'module_payment_adyen': fields.boolean(
'Manage Payments Using Adyen',
help='Blahblahblah\n'
'-It installs the module payment_adyen.'),
help='-It installs the module payment_adyen.'),
}

View File

@ -30,10 +30,10 @@ class PaymentAcquirerOgone(osv.Model):
@TDETODO: complete me
"""
return {
'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard.asp' % env,
'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % env,
'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect.asp' % env,
'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % env,
'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard_utf8.asp' % (env,),
'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect_utf8.asp' % (env,),
'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect_utf8.asp' % (env,),
'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (env,),
}
_columns = {

View File

@ -70,6 +70,7 @@ class AcquirerPaypal(osv.Model):
else:
paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button')
self.create(cr, uid, {
'name': 'paypal',
'paypal_email_account': company_paypal_account,
'view_template_id': paypal_view.id,
}, context=context)

View File

@ -8,7 +8,7 @@
<field name="inherit_id" ref="account.view_account_config_settings"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='payment_acquirer']" version="7.0" position="inside">
<button name='%(payment.acquirer_list)d' type="action"
<button name='%(payment.action_payment_acquirer)d' type="action"
string="Configure payment acquiring methods" class="oe_link"/>
</xpath>
</field>

View File

@ -8,7 +8,7 @@ import random
from openerp import http
from openerp.http import request
from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template
from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template, login_redirect
_logger = logging.getLogger(__name__)
@ -59,7 +59,7 @@ class PosController(http.Controller):
def a(self, debug=False, **k):
if not request.session.uid:
return http.local_redirect('/web/login?redirect=/pos/web')
return login_redirect()
js_list = manifest_list('js',db=request.db, debug=debug)
css_list = manifest_list('css',db=request.db, debug=debug)

View File

@ -514,13 +514,18 @@ class pos_order(osv.osv):
_order = "id desc"
def create_from_ui(self, cr, uid, orders, context=None):
#_logger.info("orders: %r", orders)
# Keep only new orders
submitted_references = [o['data']['name'] for o in orders]
existing_orders = self.search_read(cr, uid, domain=[('pos_reference', 'in', submitted_references)], fields=['pos_reference'], context=context)
existing_references = set([o['pos_reference'] for o in existing_orders])
orders_to_save = [o for o in orders if o['data']['name'] not in existing_references]
order_ids = []
for tmp_order in orders:
for tmp_order in orders_to_save:
to_invoice = tmp_order['to_invoice']
order = tmp_order['data']
order_id = self.create(cr, uid, {
'name': order['name'],
'user_id': order['user_id'] or False,
@ -542,7 +547,6 @@ class pos_order(osv.osv):
if order['amount_return']:
session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
cash_journal = session.cash_journal_id
cash_statement = False
if not cash_journal:
cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids)
if not len(cash_journal_ids):
@ -556,7 +560,11 @@ class pos_order(osv.osv):
'journal': cash_journal.id,
}, context=context)
order_ids.append(order_id)
self.signal_paid(cr, uid, [order_id])
try:
self.signal_paid(cr, uid, [order_id])
except Exception as e:
_logger.error('Could not mark POS Order as Paid: %s', tools.ustr(e))
if to_invoice:
self.action_invoice(cr, uid, [order_id], context)

View File

@ -672,7 +672,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
// returns true if the ean is a valid EAN codebar number by checking the control digit.
// ean must be a string
check_ean: function(ean){
return this.ean_checksum(ean) === Number(ean[ean.length-1]);
return /^\d+$/.test(ean) && this.ean_checksum(ean) === Number(ean[ean.length-1]);
},
// returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
sanitize_ean:function(ean){
@ -757,8 +757,13 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
scan: function(code){
if(code.length < 3){
return;
}else if(code.length === 13 && /^\d+$/.test(code)){
}else if(code.length === 13 && this.check_ean(code)){
var parse_result = this.parse_ean(code);
}else if(code.length === 12 && this.check_ean('0'+code)){
// many barcode scanners strip the leading zero of ean13 barcodes.
// This is because ean-13 are UCP-A with an additional zero at the beginning,
// so by stripping zeros you get retrocompatibility with UCP-A systems.
var parse_result = this.parse_ean('0'+code);
}else if(this.pos.db.get_product_by_reference(code)){
var parse_result = {
encoding: 'reference',
@ -767,12 +772,16 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
prefix: '',
};
}else{
var parse_result = {
encoding: 'error',
type: 'error',
code: code,
prefix: '',
};
return;
}
if (parse_result.type === 'error') { //most likely a checksum error, raise warning
console.warn('WARNING: barcode checksum error:',parse_result);
}else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
if(this.action_callback['product']){
this.action_callback['product'](parse_result);
}

View File

@ -421,62 +421,67 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// it is therefore important to only call this method from inside a mutex
// this method returns a deferred indicating wether the sending was successful or not
// there is a timeout parameter which is set to 2 seconds by default.
_flush_order: function(order_id, options){
var self = this;
options = options || {};
timeout = typeof options.timeout === 'number' ? options.timeout : 7500;
this.set('synch',{state:'connecting', pending: this.get('synch').pending});
var order = this.db.get_order(order_id);
order.to_invoice = options.to_invoice || false;
if(!order){
// flushing a non existing order always fails
return (new $.Deferred()).reject();
}
// we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice,
// then we want to notify the user that we are waiting on something )
var rpc = (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{shadow: !options.to_invoice, timeout:timeout});
rpc.fail(function(unused,event){
// prevent an error popup creation by the rpc failure
// we want the failure to be silent as we send the orders in the background
event.preventDefault();
console.error('Failed to send order:',order);
});
rpc.done(function(){
self.db.remove_order(order_id);
var pending = self.db.get_orders().length;
self.set('synch',{state: pending ? 'connecting' : 'connected', pending:pending});
});
return rpc;
_flush_order: function( order_id, options) {
return this._flush_all_orders([this.db.get_order(order_id)], options);
},
// attempts to send all the locally stored orders. As with _flush_order, it should only be
// called from within a mutex.
// this method returns a deferred that always succeeds when all orders have been tried to be sent,
// even if none of them could actually be sent.
_flush_all_orders: function(){
_flush_all_orders: function () {
var self = this;
var orders = this.db.get_orders();
var tried_all = new $.Deferred();
self.set('synch', {
state: 'connecting',
pending: self.get('synch').pending
});
return self._save_to_server(self.db.get_orders()).done(function () {
var pending = self.db.get_orders().length;
self.set('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
});
},
function rec_flush(index){
if(index < orders.length){
self._flush_order(orders[index].id).always(function(){
rec_flush(index+1);
})
}else{
tried_all.resolve();
}
// send an array of orders to the server
// available options:
// - timeout: timeout for the rpc call in ms
_save_to_server: function (orders, options) {
if (!orders || !orders.length) {
var result = $.Deferred();
result.resolve();
return result;
}
rec_flush(0);
options = options || {};
return tried_all;
var self = this;
var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length;
// we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice,
// then we want to notify the user that we are waiting on something )
var posOrderModel = new instance.web.Model('pos.order');
return posOrderModel.call('create_from_ui',
[_.map(orders, function (order) {
order.to_invoice = options.to_invoice || false;
return order;
})],
undefined,
{
shadow: !options.to_invoice,
timeout: timeout
}
).then(function () {
_.each(orders, function (order) {
self.db.remove_order(order.id);
});
}).fail(function (unused, event){
// prevent an error popup creation by the rpc failure
// we want the failure to be silent as we send the orders in the background
event.preventDefault();
console.error('Failed to send orders:', orders);
});
},
scan_product: function(parsed_code){

View File

@ -6,12 +6,18 @@
z-index: 0;
}
.openerp .oe_form .oe_form_embedded_html.view_portal_payment_options {
overflow: visible;
}
.openerp .payment_acquirers {
margin: -40px 0 -32px -24px;
position: relative;
padding: 10px 15px;
right: -125px; /* improved margin according bootstrap3 */
width: 650px;
margin-left: 80px;
background: #729FCF;
background-image: -webkit-gradient(linear, left top, left bottom, from(#729FCF), to(#3465A4));
background-image: -webkit-linear-gradient(top, #729FCF, #3465A4);

View File

@ -39,7 +39,7 @@ class sale_order(osv.Model):
if this.state not in ('draft', 'cancel') and not this.invoiced:
result[this.id] = payment_acquirer.render_payment_block(
cr, uid, this.name, this.amount_total, this.pricelist_id.currency_id.id,
partner_id=this.partner_id.id, context=context)
partner_id=this.partner_id.id, company_id=this.company_id.id, context=context)
return result
def action_quotation_send(self, cr, uid, ids, context=None):
@ -90,7 +90,7 @@ class account_invoice(osv.Model):
if this.type == 'out_invoice' and this.state not in ('draft', 'done') and not this.reconciled:
result[this.id] = payment_acquirer.render_payment_block(
cr, uid, this.number, this.residual, this.currency_id.id,
partner_id=this.partner_id.id, context=context)
partner_id=this.partner_id.id, company_id=this.company_id.id, context=context)
return result
def action_invoice_sent(self, cr, uid, ids, context=None):

View File

@ -9,7 +9,7 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<notebook version="7.0" position="before">
<field name="portal_payment_options" groups="portal_sale.group_payment_options"/>
<field name="portal_payment_options" groups="portal_sale.group_payment_options" class="view_portal_payment_options"/>
</notebook>
</field>
</record>
@ -19,7 +19,7 @@
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<notebook version="7.0" position="before">
<field name="portal_payment_options" groups="portal_sale.group_payment_options"/>
<field name="portal_payment_options" groups="portal_sale.group_payment_options" class="view_portal_payment_options"/>
</notebook>
</field>
</record>

View File

@ -11,7 +11,7 @@
<div>
<field name="group_payment_options" class="oe_inline"/>
<label for="group_payment_options"/>
<button name='%(payment.acquirer_list)d' type="action"
<button name='%(payment.action_payment_acquirer)d' type="action"
string="Configure payment acquiring methods" class="oe_link"/>
</div>
</xpath>

View File

@ -222,7 +222,7 @@
<field name="product_id" on_change="onchange_product_id(product_id)"/>
</group>
<group>
<field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" widget="selection" groups="stock.group_locations"/>
<field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" options="{'no_create': True}" groups="stock.group_locations"/>
<field name="product_uom" groups="product.group_uom"/>
<field name="location_id" groups="stock.group_locations"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>

View File

@ -63,7 +63,6 @@ Print product labels with barcode.
],
'test': [
'product_pricelist_demo.yml',
'test/product_uom.yml',
'test/product_pricelist.yml',
],
'installable': True,

View File

@ -18,12 +18,18 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import tools
import math
def rounding(f, r):
# TODO for trunk: log deprecation warning
# _logger.warning("Deprecated rounding method, please use tools.float_round to round floats.")
return tools.float_round(f, precision_rounding=r)
# TODO for trunk: add rounding method parameter to tools.float_round and use this method as hook
def ceiling(f, r):
if not r:
return f
return round(f / r) * r
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
return math.ceil(f / r) * r

View File

@ -21,8 +21,7 @@
import time
from _common import rounding
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
@ -180,7 +179,8 @@ class product_pricelist(osv.osv):
if ((v.date_start is False) or (v.date_start <= date)) and ((v.date_end is False) or (v.date_end >= date)):
version = v
break
if not version:
raise osv.except_osv(_('Warning!'), _("At least one pricelist has no active version !\nPlease create or activate one."))
categ_ids = {}
for p in products:
categ = p.categ_id
@ -269,7 +269,8 @@ class product_pricelist(osv.osv):
if price is not False:
price_limit = price
price = price * (1.0+(rule.price_discount or 0.0))
price = rounding(price, rule.price_round) #TOFIX: rounding with tools.float_rouding
if rule.price_round:
price = tools.float_round(price, precision_rounding=rule.price_round)
price += (rule.price_surcharge or 0.0)
if rule.price_min_margin:
price = max(price, price_limit+rule.price_min_margin)

View File

@ -22,7 +22,7 @@
import math
import re
from _common import rounding
from _common import ceiling
from openerp import SUPERUSER_ID
from openerp import tools
@ -178,7 +178,7 @@ class product_uom(osv.osv):
return qty
amount = qty / from_unit.factor
if to_unit:
amount = rounding(amount * to_unit.factor, to_unit.rounding)
amount = ceiling(amount * to_unit.factor, to_unit.rounding)
return amount
def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
@ -548,15 +548,10 @@ class product_product(osv.osv):
_product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
res = dict.fromkeys(ids, 0.0)
res = {}
product_uom_obj = self.pool.get('product.uom')
# retrieve pricelist
pricelist = None
if context.get('pricelist'):
pricelist = self.pool['product.pricelist'].browse(cr, uid, context.get('pricelist'), context=context)
base_currency = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id
for id in ids:
res.setdefault(id, 0.0)
for product in self.browse(cr, uid, ids, context=context):
if 'uom' in context:
uom = product.uos_id or product.uom_id
@ -564,11 +559,7 @@ class product_product(osv.osv):
uom.id, product.list_price, context['uom'])
else:
res[product.id] = product.list_price
res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
# update the result, according to the eventual pricelist currency
if pricelist and pricelist.currency_id:
res[product.id] = self.pool['res.currency'].compute(
cr, uid, base_currency.id, pricelist.currency_id.id, res[product.id], round=False, context=context)
res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
return res
def _save_product_lst_price(self, cr, uid, product_id, field_name, field_value, arg, context=None):

View File

@ -221,7 +221,6 @@
<kanban>
<field name="color"/>
<field name="type"/>
<field name="image_small"/>
<field name="list_price"/>
<templates>
<t t-name="kanban-box">

View File

@ -1,26 +0,0 @@
-
In order to test conversation of UOM,
-
I convert Grams into TON with price.
-
!python {model: product.uom}: |
from_uom_id = ref("product_uom_gram")
to_uom_id = ref("product_uom_ton")
price = 2
qty = 1020000
price = self._compute_price(cr, uid, from_uom_id, price, to_uom_id)
qty = self._compute_qty(cr, uid, from_uom_id, qty, to_uom_id)
assert qty == 1.02, "Qty is not correspond."
assert price == 2000000.0, "Price is not correspond."
-
I convert Liters into Gallons with price.
-
!python {model: product.uom}: |
from_uom_id = ref("product_uom_litre")
to_uom_id = ref("product_uom_gal")
price = 2
qty = 30.28
price = self._compute_price(cr, uid, from_uom_id, price, to_uom_id)
qty = self._compute_qty(cr, uid, from_uom_id, qty, to_uom_id)
assert qty == 8, "Qty does not correspond."
assert round(price,2) == 7.57, "Price does not correspond."

View File

@ -0,0 +1,5 @@
from . import test_uom
fast_suite = [
test_uom,
]

View File

@ -0,0 +1,37 @@
from openerp.tests.common import TransactionCase
class TestUom(TransactionCase):
"""Tests for unit of measure conversion"""
def setUp(self):
super(TestUom, self).setUp()
self.product = self.registry('product.product')
self.uom = self.registry('product.uom')
self.imd = self.registry('ir.model.data')
def test_10_conversion(self):
cr, uid = self.cr, self.uid
gram_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_gram')[1]
tonne_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_ton')[1]
qty = self.uom._compute_qty(cr, uid, gram_id, 1020000, tonne_id)
self.assertEquals(qty, 1.02, "Converted quantity does not correspond.")
price = self.uom._compute_price(cr, uid, gram_id, 2, tonne_id)
self.assertEquals(price, 2000000.0, "Converted price does not correspond.")
def test_20_rounding(self):
cr, uid = self.cr, self.uid
unit_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_unit')[1]
categ_unit_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_categ_unit')[1]
score_id = self.uom.create(cr, uid, {
'name': 'Score',
'factor_inv': 20,
'uom_type': 'bigger',
'rounding': 1.0,
'category_id': categ_unit_id
})
qty = self.uom._compute_qty(cr, uid, unit_id, 2, score_id)
self.assertEquals(qty, 1, "Converted quantity should be rounded up.")

View File

@ -53,9 +53,9 @@ class product_product(osv.osv):
states = ('draft', 'open', 'paid')
sqlstr="""select
sum(l.price_unit * l.quantity)/sum(l.quantity) as avg_unit_price,
sum(l.price_unit * l.quantity)/sum(nullif(l.quantity,0)) as avg_unit_price,
sum(l.quantity) as num_qty,
sum(l.quantity * (l.price_subtotal/l.quantity)) as total,
sum(l.quantity * (l.price_subtotal/(nullif(l.quantity,0)))) as total,
sum(l.quantity * pt.list_price) as sale_expected,
sum(l.quantity * pt.standard_price) as normal_cost
from account_invoice_line l

View File

@ -48,8 +48,9 @@ class project_issue(osv.Model):
_mail_post_access = 'read'
_track = {
'stage_id': {
'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence == 1,
'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence != 1,
# this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence <= 1,
'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1,
},
'user_id': {
'project_issue.mt_issue_assigned': lambda self, cr, uid, obj, ctx=None: obj.user_id and obj.user_id.id,
@ -74,7 +75,7 @@ class project_issue(osv.Model):
def _get_default_stage_id(self, cr, uid, context=None):
""" Gives default stage_id """
project_id = self._get_default_project_id(cr, uid, context=context)
return self.stage_find(cr, uid, [], project_id, [('sequence', '=', 1)], context=context)
return self.stage_find(cr, uid, [], project_id, [('fold', '=', False)], context=context)
def _resolve_project_id_from_context(self, cr, uid, context=None):
""" Returns ID of project based on the value of 'default_project_id'

View File

@ -200,7 +200,7 @@
<group>
<field name="date_order"/>
<field name="origin" attrs="{'invisible': [('origin','=',False)]}"/>
<field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" widget="selection" groups="stock.group_locations"/>
<field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" options="{'no_create': True}" groups="stock.group_locations"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
</group>
</group>

View File

@ -205,8 +205,8 @@
<field groups="base.group_no_one" name="origin"/>
</group>
<group name="sale_pay">
<field name="payment_term" widget="selection"/>
<field name="fiscal_position" widget="selection"/>
<field name="payment_term" options="{'no_create': True}"/>
<field name="fiscal_position" options="{'no_create': True}"/>
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
</group>
<group>

View File

@ -27,7 +27,7 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="section_id" widget="selection" groups="base.group_multi_salesteams"/>
<field name="section_id" options="{'no_create': True}" groups="base.group_multi_salesteams"/>
<field name="categ_ids" widget="many2many_tags"/>
</field>
</field>

View File

@ -27,7 +27,7 @@
<field name="company_id" readonly="True"/>
</field>
<field name="fiscal_position" position="after">
<field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" widget="selection" groups="stock.group_locations"/>
<field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" options="{'no_create': True}" groups="stock.group_locations"/>
</field>
<field name="product_id" position="replace">
<field name="product_id"

View File

@ -106,15 +106,16 @@ class stock_fill_inventory(osv.osv_memory):
datas = {}
res[location] = {}
move_ids = move_obj.search(cr, uid, ['|',('location_dest_id','=',location),('location_id','=',location),('state','=','done')], context=context)
local_context = dict(context)
local_context['raise-exception'] = False
for move in move_obj.browse(cr, uid, move_ids, context=context):
lot_id = move.prodlot_id.id
prod_id = move.product_id.id
if move.location_dest_id.id != move.location_id.id:
if move.location_dest_id.id == location:
qty = uom_obj._compute_qty(cr, uid, move.product_uom.id,move.product_qty, move.product_id.uom_id.id)
qty = uom_obj._compute_qty_obj(cr, uid, move.product_uom,move.product_qty, move.product_id.uom_id, context=local_context)
else:
qty = -uom_obj._compute_qty(cr, uid, move.product_uom.id,move.product_qty, move.product_id.uom_id.id)
qty = -uom_obj._compute_qty_obj(cr, uid, move.product_uom,move.product_qty, move.product_id.uom_id, context=local_context)
if datas.get((prod_id, lot_id)):

View File

@ -256,20 +256,18 @@ class Website(openerp.addons.web.controllers.main.Home):
u"Image size excessive, uploaded images must be smaller "
u"than 42 million pixel")
attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, {
Attachments = request.registry['ir.attachment']
attachment_id = Attachments.create(request.cr, request.uid, {
'name': upload.filename,
'datas': image_data.encode('base64'),
'datas_fname': upload.filename,
'res_model': 'ir.ui.view',
}, request.context)
url = website.urlplus('/website/image', {
'model': 'ir.attachment',
'id': attachment_id,
'field': 'datas',
'max_height': MAX_IMAGE_HEIGHT,
'max_width': MAX_IMAGE_WIDTH,
})
[attachment] = Attachments.read(
request.cr, request.uid, [attachment_id], ['website_url'],
context=request.context)
url = attachment['website_url']
except Exception, e:
logger.exception("Failed to upload image to attachment")
message = unicode(e)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import copy
import re
import simplejson
import werkzeug
@ -158,6 +159,29 @@ class view(osv.osv):
return super(view, self).render(cr, uid, id_or_xml_id, values=values, engine=engine, context=context)
def _pretty_arch(self, arch):
# remove_blank_string does not seem to work on HTMLParser, and
# pretty-printing with lxml more or less requires stripping
# whitespace: http://lxml.de/FAQ.html#why-doesn-t-the-pretty-print-option-reformat-my-xml-output
# so serialize to XML, parse as XML (remove whitespace) then serialize
# as XML (pretty print)
arch_no_whitespace = etree.fromstring(
etree.tostring(arch, encoding='utf-8'),
parser=etree.XMLParser(encoding='utf-8', remove_blank_text=True))
arch_pretty_indent_2 = etree.tostring(
arch_no_whitespace, encoding='unicode', pretty_print=True)
# pretty_print uses a fixed indent level of 2, we want an indent of 4,
# double up leading spaces.
def repl(m):
indent = len(m.group(0)) / 2
return u' ' * 4 * indent
# FIXME: If py2.7 only, can use re.M in sub and don't have to do replacement line by line
return u'\n'.join(
re.sub(ur'^((?: )+)', repl, line)
for line in arch_pretty_indent_2.split(u'\n')
)
def save(self, cr, uid, res_id, value, xpath=None, context=None):
""" Update a view section. The view section may embed fields to write
@ -183,5 +207,5 @@ class view(osv.osv):
arch = self.replace_arch_section(cr, uid, res_id, xpath, arch_section, context=context)
self.write(cr, uid, res_id, {
'arch': etree.tostring(arch, encoding='utf-8').decode('utf-8')
'arch': self._pretty_arch(arch)
}, context=context)

View File

@ -3,7 +3,7 @@ from openerp.osv import fields, osv
class website_config_settings(osv.osv_memory):
_name = 'website.config.settings'
_inherit = 'base.config.settings'
_inherit = 'res.config.settings'
_columns = {
'website_id': fields.many2one('website', string="website", required=True),

View File

@ -9,6 +9,7 @@ import urlparse
import werkzeug
import werkzeug.exceptions
import werkzeug.utils
import werkzeug.wrappers
# optional python-slugify import (https://github.com/un33k/python-slugify)
try:
@ -594,13 +595,40 @@ class ir_attachment(osv.osv):
return super(ir_attachment, self).create(
cr, uid, values, context=context)
def try_remove(self, cr, uid, ids, context=None):
""" Removes a web-based image attachment if it is used by no view
(template)
Returns a dict mapping attachments which would not be removed (if any)
mapped to the views preventing their removal
"""
Views = self.pool['ir.ui.view']
attachments_to_remove = []
# views blocking removal of the attachment
removal_blocked_by = {}
for attachment in self.browse(cr, uid, ids, context=context):
# in-document URLs are html-escaped, a straight search will not
# find them
url = werkzeug.utils.escape(attachment.website_url)
ids = Views.search(cr, uid, [('arch', 'like', url)], context=context)
if ids:
removal_blocked_by[attachment.id] = Views.read(
cr, uid, ids, ['name'], context=context)
else:
attachments_to_remove.append(attachment.id)
if attachments_to_remove:
self.unlink(cr, uid, attachments_to_remove, context=context)
return removal_blocked_by
class res_partner(osv.osv):
_inherit = "res.partner"
def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
partner = self.browse(cr, uid, ids[0], context=context)
params = {
'center': '%s, %s %s, %s' % (partner.street, partner.city, partner.zip, partner.country_id and partner.country_id.name_get()[0][1] or ''),
'center': '%s, %s %s, %s' % (partner.street or '', partner.city or '', partner.zip or '', partner.country_id and partner.country_id.name_get()[0][1] or ''),
'size': "%sx%s" % (height, width),
'zoom': zoom,
'sensor': 'false',

View File

@ -0,0 +1,185 @@
/*! http://mths.be/placeholder v2.0.7 by @mathias */
;(function(window, document, $) {
// Opera Mini v7 doesnt support placeholder although its DOM seems to indicate so
var isOperaMini = Object.prototype.toString.call(window.operamini) == '[object OperaMini]';
var isInputSupported = 'placeholder' in document.createElement('input') && !isOperaMini;
var isTextareaSupported = 'placeholder' in document.createElement('textarea') && !isOperaMini;
var prototype = $.fn;
var valHooks = $.valHooks;
var propHooks = $.propHooks;
var hooks;
var placeholder;
if (isInputSupported && isTextareaSupported) {
placeholder = prototype.placeholder = function() {
return this;
};
placeholder.input = placeholder.textarea = true;
} else {
placeholder = prototype.placeholder = function() {
var $this = this;
$this
.filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
.not('.placeholder')
.bind({
'focus.placeholder': clearPlaceholder,
'blur.placeholder': setPlaceholder
})
.data('placeholder-enabled', true)
.trigger('blur.placeholder');
return $this;
};
placeholder.input = isInputSupported;
placeholder.textarea = isTextareaSupported;
hooks = {
'get': function(element) {
var $element = $(element);
var $passwordInput = $element.data('placeholder-password');
if ($passwordInput) {
return $passwordInput[0].value;
}
return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
},
'set': function(element, value) {
var $element = $(element);
var $passwordInput = $element.data('placeholder-password');
if ($passwordInput) {
return $passwordInput[0].value = value;
}
if (!$element.data('placeholder-enabled')) {
return element.value = value;
}
if (value == '') {
element.value = value;
// Issue #56: Setting the placeholder causes problems if the element continues to have focus.
if (element != safeActiveElement()) {
// We can't use `triggerHandler` here because of dummy text/password inputs :(
setPlaceholder.call(element);
}
} else if ($element.hasClass('placeholder')) {
clearPlaceholder.call(element, true, value) || (element.value = value);
} else {
element.value = value;
}
// `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363
return $element;
}
};
if (!isInputSupported) {
valHooks.input = hooks;
propHooks.value = hooks;
}
if (!isTextareaSupported) {
valHooks.textarea = hooks;
propHooks.value = hooks;
}
$(function() {
// Look for forms
$(document).delegate('form', 'submit.placeholder', function() {
// Clear the placeholder values so they don't get submitted
var $inputs = $('.placeholder', this).each(clearPlaceholder);
setTimeout(function() {
$inputs.each(setPlaceholder);
}, 10);
});
});
// Clear placeholder values upon page reload
$(window).bind('beforeunload.placeholder', function() {
$('.placeholder').each(function() {
this.value = '';
});
});
}
function args(elem) {
// Return an object of element attributes
var newAttrs = {};
var rinlinejQuery = /^jQuery\d+$/;
$.each(elem.attributes, function(i, attr) {
if (attr.specified && !rinlinejQuery.test(attr.name)) {
newAttrs[attr.name] = attr.value;
}
});
return newAttrs;
}
function clearPlaceholder(event, value) {
var input = this;
var $input = $(input);
if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) {
if ($input.data('placeholder-password')) {
$input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
// If `clearPlaceholder` was called from `$.valHooks.input.set`
if (event === true) {
return $input[0].value = value;
}
$input.focus();
} else {
input.value = '';
$input.removeClass('placeholder');
input == safeActiveElement() && input.select();
}
}
}
function setPlaceholder() {
var $replacement;
var input = this;
var $input = $(input);
var id = this.id;
if (input.value == '') {
if (input.type == 'password') {
if (!$input.data('placeholder-textinput')) {
try {
$replacement = $input.clone().attr({ 'type': 'text' });
} catch(e) {
$replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
}
$replacement
.removeAttr('name')
.data({
'placeholder-password': $input,
'placeholder-id': id
})
.bind('focus.placeholder', clearPlaceholder);
$input
.data({
'placeholder-textinput': $replacement,
'placeholder-id': id
})
.before($replacement);
}
$input = $input.removeAttr('id').hide().prev().attr('id', id).show();
// Note: `$input[0] != input` now!
}
$input.addClass('placeholder');
$input[0].value = $input.attr('placeholder');
} else {
$input.removeClass('placeholder');
}
}
function safeActiveElement() {
// Avoid IE9 `document.activeElement` of death
// https://github.com/mathiasbynens/jquery-placeholder/pull/99
try {
return document.activeElement;
} catch (err) {}
}
}(this, document, jQuery));

View File

@ -26,6 +26,7 @@
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
-ms-filter: "alpha(opacity=50)";
}
/* ---- OpenERP Style ---- {{{ */
@ -250,6 +251,26 @@ ul.oe_menu_editor .disclose {
.existing-attachments .pager .disabled {
display: none;
}
.existing-attachments .existing-attachment-cell {
position: relative;
}
.existing-attachments .existing-attachment-cell .img {
border: 1px solid #848490;
}
.existing-attachments .existing-attachment-cell .existing-attachment-remove {
position: absolute;
top: 0;
left: 15px;
cursor: pointer;
background: white;
padding: 2px;
border: 1px solid #848490;
border-top: none;
border-left: none;
-moz-border-radius-bottomright: 8px;
-webkit-border-bottom-right-radius: 8px;
border-bottom-right-radius: 8px;
}
.cke_widget_wrapper {
position: static !important;

View File

@ -23,6 +23,7 @@
background: transparent
border: none
+box-shadow(none)
-ms-filter: "alpha(opacity=50)"
// }}}
@ -210,9 +211,27 @@ ul.oe_menu_editor
.font-icons-selected
background-color: #ddd
$attachment-border-color: #848490
.existing-attachments
.pager .disabled
display: none
.existing-attachments .pager .disabled
display: none
.existing-attachment-cell
position: relative
.img
border: 1px solid $attachment-border-color
.existing-attachment-remove
position: absolute
top: 0
left: 15px // padding-left on col-*
cursor: pointer
background: white
padding: 2px
border: 1px solid $attachment-border-color
border-top: none
border-left: none
+border-bottom-right-radius(8px)
// wrapper positioned relatively for drag&drop widget which is disabled below.
// Breaks completely horribly crazy products listing page, so take it out.

View File

@ -80,10 +80,8 @@
-moz-user-select: none;
user-select: none;
cursor: move;
pointer-events: none;
}
.oe_snippet .oe_snippet_thumbnail {
pointer-events: auto;
text-align: center;
height: 100%;
background: transparent;
@ -200,6 +198,7 @@
.oe_overlay {
display: none;
height: 0;
position: absolute;
background: transparent;
-webkit-border-radius: 3px;
@ -214,20 +213,12 @@
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
pointer-events: none;
}
.oe_overlay.oe_active {
display: block;
border-style: dashed;
border-width: 1px;
-webkit-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.3), 0px 0px 0px 1px rgba(255, 255, 255, 0.3) inset;
-moz-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.3), 0px 0px 0px 1px rgba(255, 255, 255, 0.3) inset;
box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.3), 0px 0px 0px 1px rgba(255, 255, 255, 0.3) inset;
border-color: rgba(0, 0, 0, 0.3);
}
.oe_overlay .oe_handle {
display: block !important;
pointer-events: auto;
position: absolute;
top: 50%;
left: 50%;
@ -238,7 +229,18 @@
height: 16px;
margin: -2px;
}
.oe_overlay .oe_handle > div {
z-index: 1;
position: absolute;
border-style: dashed;
border-width: 1px;
border-color: #666666;
-webkit-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
-moz-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
}
.oe_overlay .oe_handle.e:before, .oe_overlay .oe_handle.w:before, .oe_overlay .oe_handle.s:before, .oe_overlay .oe_handle.n:before, .oe_overlay .oe_handle.size .oe_handle_button {
z-index: 2;
position: relative;
top: 50%;
left: 50%;
@ -270,56 +272,70 @@
-moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
}
.oe_overlay .oe_handle.e, .oe_overlay .oe_handle.w {
top: 4px;
height: 100%;
}
.oe_overlay .oe_handle.e:before, .oe_overlay .oe_handle.w:before {
content: "\f0d9-\f0da";
line-height: 16px;
}
.oe_overlay .oe_handle.e > div, .oe_overlay .oe_handle.w > div {
width: 0;
height: 100%;
top: 2px;
left: 8px;
}
.oe_overlay .oe_handle.e {
left: auto;
top: 2px;
height: 100%;
right: -6px;
cursor: w-resize;
}
.oe_overlay .oe_handle.w {
top: 2px;
height: 100%;
left: -6px;
cursor: e-resize;
}
.oe_overlay .oe_handle.s, .oe_overlay .oe_handle.n {
left: 2px;
width: 100%;
}
.oe_overlay .oe_handle.s:before, .oe_overlay .oe_handle.n:before {
z-index: 0;
content: "\f07d";
text-align: center;
padding: 1px;
}
.oe_overlay .oe_handle.s > div, .oe_overlay .oe_handle.n > div {
width: 100%;
height: 0;
top: 7px;
left: 1px;
}
.oe_overlay .oe_handle.s {
top: auto;
left: 2px;
width: 100%;
bottom: -6px;
cursor: n-resize;
}
.oe_overlay .oe_handle.n {
left: 2px;
width: 100%;
top: -6px;
cursor: s-resize;
}
.oe_overlay .oe_handle.n > div {
top: 5px;
}
.oe_overlay .oe_handle.size {
z-index: 3;
top: auto;
left: 2px;
width: 100%;
left: 50%;
bottom: -6px;
}
.oe_overlay .oe_handle.size .oe_handle_button {
z-index: 1;
z-index: 3;
content: "Resize";
width: 64px;
text-align: center;
margin-left: -32px;
margin-top: -10px;
cursor: row-resize;
left: 0px;
top: 9px;
}
.oe_overlay .oe_handle.size .oe_handle_button:hover {
background: rgba(30, 30, 30, 0.8);
@ -328,25 +344,20 @@
-moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
}
.oe_overlay .oe_handle.size div {
border-style: dashed;
border-width: 0 0 1px 0;
border-color: rgba(0, 0, 0, 0.5);
position: relative;
top: 8px;
}
.oe_overlay .icon.btn {
display: inline-block;
}
.oe_overlay .oe_overlay_options {
position: absolute;
width: 100%;
left: 50% !important;
text-align: center;
top: -11px;
z-index: 1002;
}
.oe_overlay .oe_overlay_options > .btn-group {
left: -50%;
}
.oe_overlay .oe_overlay_options .btn, .oe_overlay .oe_overlay_options a {
pointer-events: auto;
cursor: pointer;
}
.oe_overlay .oe_overlay_options .dropdown {
@ -505,7 +516,6 @@
box-shadow: 0px 3px 17px rgba(99, 53, 150, 0.59);
}
.oe_snippet_editor .oe_snippet > * {
pointer-events: none;
margin-top: 16px;
line-height: 1em;
zoom: 0.6;

View File

@ -64,9 +64,7 @@
overflow: hidden
+user-select(none)
cursor: move
pointer-events: none
.oe_snippet_thumbnail
pointer-events: auto
text-align: center
height: 100%
background: transparent
@ -150,22 +148,17 @@
.oe_overlay
display: none
height: 0
position: absolute
background: transparent
//@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.02) ,rgba(255,255,255,.02) 35px, rgba(0,0,0,.02) 35px, rgba(0,0,0,.02) 75px))
+border-radius(3px)
@include transition(opacity 100ms linear)
+box-sizing(border-box)
pointer-events: none
&.oe_active
display: block
border-style: dashed
border-width: 1px
+box-shadow(0px 0px 0px 1px rgba(255,255,255,0.3), 0px 0px 0px 1px rgba(255,255,255,0.3) inset)
border-color: rgba(0, 0, 0, 0.3)
.oe_handle
display: block !important
pointer-events: auto
position: absolute
top: 50%
left: 50%
@ -173,7 +166,15 @@
width: 16px
height: 16px
margin: -2px
> div
z-index: 1
position: absolute
border-style: dashed
border-width: 1px
border-color: #666666
+box-shadow(0px 0px 0px 1px rgba(255,255,255,0.5), 0px 0px 0px 1px rgba(255,255,255,0.5) inset)
&.e:before, &.w:before, &.s:before, &.n:before, &.size .oe_handle_button
z-index: 2
position: relative
top: 50%
left: 50%
@ -196,72 +197,74 @@
color: #fff
+box-shadow(0 0 5px 3px rgba(255,255,255,.7))
&.e, &.w
top: 4px
height: 100%
&:before
content: "\f0d9-\f0da"
line-height: 16px
> div
width: 0
height: 100%
top: 2px
left: 8px
&.e
left: auto
top: 2px
height: 100%
right: -6px
cursor: w-resize
&.w
top: 2px
height: 100%
left: -6px
cursor: e-resize
&.s, &.n
left: 2px
width: 100%
&:before
z-index: 0
content: "\f07d"
text-align: center
padding: 1px
> div
width: 100%
height: 0
top: 7px
left: 1px
&.s
top: auto
left: 2px
width: 100%
bottom: -6px
cursor: n-resize
&.n
left: 2px
width: 100%
top: -6px
cursor: s-resize
> div
top: 5px
&.size
z-index: 3
top: auto
left: 2px
width: 100%
left: 50%
bottom: -6px
.oe_handle_button
z-index: 1
z-index: 3
content: "Resize"
width: 64px
text-align: center
margin-left: -32px
margin-top: -10px
cursor: row-resize
left: 0px
top: 9px
&:hover
background: rgba(30, 30, 30, .8)
color: #fff
+box-shadow(0 0 5px 3px rgba(255,255,255,.7))
div
border-style: dashed
border-width: 0 0 1px 0
border-color: rgba(0, 0, 0, 0.5)
position: relative
top: 8px
.icon.btn
display: inline-block
.oe_overlay_options
position: absolute
width: 100%
left: 50% !important
text-align: center
top: -11px
z-index: 1002
> .btn-group
left: -50%
.btn, a
pointer-events: auto
cursor: pointer
.dropdown
display: inline-block
@ -392,7 +395,6 @@
border: 2px solid rgb(151, 137, 255)
box-shadow: 0px 3px 17px rgba(99, 53, 150, 0.59)
& > *
pointer-events: none
margin-top: 16px
line-height: 1em
zoom: 0.6

View File

@ -388,22 +388,15 @@ div.carousel[data-snippet-id="slider"] .carousel-indicators .active {
}
.parallax {
position: relative;
background-size: cover;
display: table;
width: 100%;
min-height: 100px;
}
.parallax.oe_small {
min-height: 200px;
}
.parallax.oe_medium {
min-height: 300px;
}
.parallax.oe_big {
min-height: 450px;
}
.parallax > div {
position: relative;
display: table;
width: 100%;
min-height: 200px;
}
.parallax > div > div {
display: table-cell;
vertical-align: middle;
padding: 32px 0;

View File

@ -320,21 +320,16 @@ div.carousel[data-snippet-id="slider"]
background-color: grey
.parallax
position: relative
background-size: cover
display: table
width: 100%
min-height: 100px
&.oe_small
min-height: 200px
&.oe_medium
min-height: 300px
&.oe_big
min-height: 450px
> div
display: table-cell
vertical-align: middle
padding: 32px 0
position: relative
display: table
width: 100%
min-height: 200px
> div
display: table-cell
vertical-align: middle
padding: 32px 0
/* Background */

View File

@ -39,12 +39,17 @@
this.xml = text;
},
isWellFormed: function () {
var error;
if (document.implementation.createDocument) {
var dom = new DOMParser().parseFromString(this.xml, "text/xml");
var error = dom.getElementsByTagName("parsererror");
return error.length === 0 || error;
} else if (window.ActiveXObject) {
// TODO test in IE
// use try catch for ie
try {
var dom = new DOMParser().parseFromString(this.xml, "text/xml");
error = dom.getElementsByTagName("parsererror");
return error.length === 0 || $(error).text();
} catch (e) {}
}
if (window.ActiveXObject) {
// IE
var msDom = new ActiveXObject("Microsoft.XMLDOM");
msDom.async = false;
msDom.loadXML(this.xml);
@ -284,7 +289,7 @@
def.reject("server", session, error);
});
} else {
def.reject(null, session, $(isWellFormed).text());
def.reject(null, session, isWellFormed);
}
return def;
},

View File

@ -20,6 +20,10 @@
website.form(this.pathname, 'POST');
});
$(document).on('click', '.cke_editable label', function (ev) {
ev.preventDefault();
});
$(document).on('submit', '.cke_editable form', function (ev) {
// Disable form submition in editable mode
ev.preventDefault();
@ -66,8 +70,10 @@
// only enable editors manually
CKEDITOR.disableAutoInline = true;
// EDIT ALL THE THINGS
CKEDITOR.dtd.$editable = $.extend(
{}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline);
CKEDITOR.dtd.$editable = _.omit(
$.extend({}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline),
// well maybe not *all* the things
'ul', 'ol', 'li', 'table', 'tr', 'th', 'td');
// Disable removal of empty elements on CKEDITOR activation. Empty
// elements are used for e.g. support of FontAwesome icons
CKEDITOR.dtd.$removeEmpty = {};
@ -377,8 +383,7 @@
});
},
upcast: function (el) {
return el.attributes['class']
&& (/\bfa\b/.test(el.attributes['class']));
return el.hasClass('fa');
}
});
}
@ -625,11 +630,11 @@
var $link_button = this.make_hover_button(_t("Change"), function () {
var sel = new CKEDITOR.dom.element(previous);
editor.getSelection().selectElement(sel);
if (previous.tagName.toUpperCase() === 'A') {
link_dialog(editor);
} else if(sel.hasClass('fa')) {
if(sel.hasClass('fa')) {
new website.editor.FontIconsDialog(editor, previous)
.appendTo(document.body);
} else if (previous.tagName.toUpperCase() === 'A') {
link_dialog(editor);
}
$link_button.hide();
previous = null;
@ -650,6 +655,7 @@
if (previous && previous === this) { return; }
var selected = new CKEDITOR.dom.element(this);
// FIXME: fa nodes may not be editable widgets (?)
if (!is_editable_node(selected) && !selected.hasClass('fa')) {
return;
}
@ -837,7 +843,6 @@
document.execCommand("enableInlineTableEditing", false, "false");
} catch (e) {}
// detect & setup any CKEDITOR widget within a newly dropped
// snippet. There does not seem to be a simple way to do it for
// HTML not inserted via ckeditor APIs:
@ -1436,6 +1441,7 @@
this.page += $target.hasClass('previous') ? -1 : 1;
this.display_attachments();
},
'click .existing-attachment-remove': 'try_remove',
}),
init: function (parent) {
this.image = null;
@ -1458,7 +1464,7 @@
kwargs: {
fields: ['name', 'website_url'],
domain: [['res_model', '=', 'ir.ui.view']],
order: 'name',
order: 'id desc',
context: website.get_context(),
}
});
@ -1468,6 +1474,7 @@
this.display_attachments();
},
display_attachments: function () {
this.$('.help-block').empty();
var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
var from = this.page * per_screen;
@ -1495,6 +1502,34 @@
}
this.close()
},
try_remove: function (e) {
var $help_block = this.$('.help-block').empty();
var self = this;
var id = parseInt($(e.target).data('id'), 10);
var attachment = _.findWhere(this.records, {id: id});
return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'ir.attachment',
method: 'try_remove',
args: [],
kwargs: {
ids: [id],
context: website.get_context()
}
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.records = _.without(self.records, attachment);
self.display_attachments();
return;
}
$help_block.replaceWith(openerp.qweb.render(
'website.editor.dialog.image.existing.error', {
views: prevented[id]
}
));
});
},
});
function get_selected_link(editor) {
@ -1684,6 +1719,9 @@
// ignore mutation if the *only* change is .cke_focus
return change.length !== 1 || change[0] === 'cke_focus';
case 'childList':
setTimeout(function () {
fixup_browser_crap(m.addedNodes);
}, 0);
// Remove ignorable nodes from addedNodes or removedNodes,
// if either set remains non-empty it's considered to be an
// impactful change. Otherwise it's ignored.
@ -1721,4 +1759,69 @@
}
return output;
}
var programmatic_styles = {
float: 1,
display: 1,
position: 1,
top: 1,
left: 1,
right: 1,
bottom: 1,
};
function fixup_browser_crap(nodes) {
if (!nodes || !nodes.length) { return; }
/**
* Checks that the node only has a @style, not e.g. @class or whatever
*/
function has_only_style(node) {
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
if (attr.attributeName !== 'style') {
return false;
}
}
return true;
}
function has_programmatic_style(node) {
for (var i = 0; i < node.style.length; i++) {
var style = node.style[i];
if (programmatic_styles[style]) {
return true;
}
}
return false;
}
for (var i=0; i<nodes.length; ++i) {
var node = nodes[i];
if (node.nodeType !== document.ELEMENT_NODE) { continue; }
if (node.nodeName === 'SPAN'
&& has_only_style(node)
&& !has_programmatic_style(node)) {
// On backspace, webkit browsers create a <span> with a bunch of
// inline styles "remembering" where they come from. Refs:
// http://www.neotericdesign.com/blog/2013/3/working-around-chrome-s-contenteditable-span-bug
// https://code.google.com/p/chromium/issues/detail?id=226941
// https://bugs.webkit.org/show_bug.cgi?id=114791
// http://dev.ckeditor.com/ticket/9998
var child, parent = node.parentNode;
while (child = node.firstChild) {
parent.insertBefore(child, node);
}
parent.removeChild(node);
// chances are we had e.g.
// <p>foo</p>
// <p>bar</p>
// merged the lines getting this in webkit
// <p>foo<span>bar</span></p>
// after unwrapping the span, we have 2 text nodes
// <p>[foo][bar]</p>
// where we probably want only one. Normalize will merge
// adjacent text nodes. However, does not merge text and cdata
parent.normalize();
}
}
}
})();

View File

@ -69,6 +69,8 @@
website.is_editable = website.is_editable || $('html').data('editable');
website.is_editable_button= website.is_editable_button || $('html').data('editable');
dom_ready.resolve();
// fix for ie
if($.fn.placeholder) $('input, textarea').placeholder();
});
website.init_kanban = function ($kanban) {

View File

@ -17,6 +17,9 @@
'hidden.bs.modal': 'destroy'
},
start: function () {
if (!window.location.origin) { // fix for ie9
window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: '') + '/';
}
document.getElementById("mobile-viewport").src = window.location.origin + window.location.pathname + "#mobile-preview";
this.$el.modal();
},

View File

@ -64,6 +64,10 @@
start: function () {
this.$target.carousel({interval: 10000});
},
stop: function () {
this.$target.carousel('pause');
this.$target.removeData("bs.carousel");
},
});
website.snippet.animationRegistry.parallax = website.snippet.Animation.extend({

View File

@ -19,13 +19,23 @@
edit: function () {
var self = this;
$("body").off('click');
website.snippet.stop_animation();
window.snippets = this.snippets = new website.snippet.BuildingBlock(this);
this.snippets.appendTo(this.$el);
this.on('rte:ready', this, function () {
self.snippets.$button.removeClass("hidden");
website.snippet.stop_animation();
website.snippet.start_animation();
$(website.snippet.readyAnimation).each(function() {
var animation = $(this).data("snippet-view");
if (animation) {
animation.$target.on('focus', '*', function(){
animation.stop();
});
animation.$target.on('blur', '*', function(){
animation.start();
});
}
});
});
return this._super.apply(this, arguments);
@ -190,13 +200,14 @@
var mt = parseInt($target.css("margin-top") || 0);
var mb = parseInt($target.css("margin-bottom") || 0);
$el.css({
'position': 'absolute',
'width': $target.outerWidth(),
'height': $target.outerHeight() + mt + mb+1,
'top': pos.top - mt,
'top': pos.top - mt - 5,
'left': pos.left
});
$el.find(".oe_handle.size").css("bottom", (mb-7)+'px');
$el.find(">.e,>.w").css({'height': $target.outerHeight() + mt + mb+1});
$el.find(">.s").css({'top': $target.outerHeight() + mt + mb});
$el.find(">.size").css({'top': $target.outerHeight() + mt});
$el.find(">.s,>.n").css({'width': $target.outerWidth()-2});
},
show: function () {
this.$el.removeClass("hidden");
@ -213,14 +224,8 @@
bind_snippet_click_editor: function () {
var self = this;
var snipped_event_flag = false;
$("#wrapwrap").on('click', function (event) {
if (snipped_event_flag) {
return;
}
snipped_event_flag = true;
setTimeout(function () {snipped_event_flag = false;}, 0);
var $target = $(event.srcElement);
var $target = $(event.srcElement || event.target);
if (!$target.attr("data-snippet-id")) {
$target = $target.parents("[data-snippet-id]:first");
}
@ -421,7 +426,6 @@
$target.data("overlay").remove();
$target.removeData("overlay");
}
self.create_overlay($target);
$target.find("[data-snippet-id]").each(function () {
var $snippet = $(this);
$snippet.removeData("snippet-editor");
@ -429,10 +433,10 @@
$snippet.data("overlay").remove();
$snippet.removeData("overlay");
}
self.create_overlay($snippet);
});
// end
self.create_overlay($target);
self.make_active($target);
},0);
} else {
@ -577,6 +581,20 @@
var $target = $(this);
if (!$target.data('overlay')) {
var $zone = $(openerp.qweb.render('website.snippet_overlay'));
// fix for pointer-events: none with ie9
if (document.body && document.body.addEventListener) {
$zone.on("click mousedown mousedown", function passThrough(event) {
event.preventDefault();
$target.each(function() {
// check if clicked point (taken from event) is inside element
event.srcElement = this;
$(this).trigger(event.type);
});
return false;
});
}
$zone.appendTo('#oe_manipulators');
$zone.data('target',$target);
$target.data('overlay',$zone);
@ -789,6 +807,7 @@
this.parent = parent;
this.$target = $(dom);
this.$overlay = this.$target.data('overlay');
this.$overlay.find('a[data-toggle="dropdown"]').dropdown();
this.snippet_id = this.$target.data("snippet-id");
this._readXMLData();
this.load_style_options();
@ -1216,7 +1235,6 @@
on_clone: function () {
var $clone = this.$target.clone(false);
var _class = $clone.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, '');
_class += ' col-md-1';
$clone.attr("class", _class);
this.$target.after($clone);
this.hide_remove_button();
@ -1264,6 +1282,11 @@
});
website.snippet.editorRegistry.slider = website.snippet.editorRegistry.resize.extend({
getSize: function () {
this.grid = this._super();
this.grid.size = 8;
return this.grid;
},
drop_and_build_snippet: function() {
var id = $(".carousel").length;
this.id = "myCarousel" + id;
@ -1299,7 +1322,6 @@
this.$editor.find(".js_add").on('click', function () {self.on_add_slide(); return false;});
this.$editor.find(".js_remove").on('click', function () {self.on_remove_slide(); return false;});
this.$target.carousel('pause');
this.rebind_event();
},
on_add_slide: function () {
@ -1352,11 +1374,6 @@
});
website.snippet.editorRegistry.carousel = website.snippet.editorRegistry.slider.extend({
getSize: function () {
this.grid = this._super();
this.grid.size = 8;
return this.grid;
},
clean_for_save: function () {
this._super();
this.$target.css("background-image", "");
@ -1442,7 +1459,10 @@
self.$target.data("snippet-view").set_values();
});
this.$target.attr('contentEditable', 'false');
this.$target.find('> div > .oe_structure').attr('contentEditable', 'true');
this.$target.find('> div > .oe_structure').attr('contentEditable', 'true'); // saas-3 retro-compatibility
this.$target.find('> div > div:not(.oe_structure) > .oe_structure').attr('contentEditable', 'true');
},
scroll: function () {
var self = this;

View File

@ -47,10 +47,10 @@
},
{
waitFor: '.oe_overlay_options .oe_options:visible',
element: '#wrap [data-snippet-id=carousel]:first .carousel-caption',
element: '#wrap [data-snippet-id=carousel]:first .carousel-caption > div',
placement: 'top',
title: _("Customize banner's text"),
content: _("Click in the text and start editing it."),
title: _t("Customize banner's text"),
content: _t("Click in the text and start editing it."),
popover: { next: _t("Continue") },
},
{
@ -68,6 +68,13 @@
content: _t("Let's add another building block to your page."),
popover: { fixed: true },
},
{
element: 'a[href="#snippet_feature"]',
placement: 'bottom',
title: _t("Feature blocks list"),
content: _t("Click on 'Features' to see the feature blocks list."),
popover: { fixed: true },
},
{
snippet: 'features',
placement: 'bottom',

View File

@ -140,8 +140,9 @@
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 class="modal-title">Select a Picture</h3>
</div>
<div class="modal-body">
<div class="modal-body has-error">
<div class="existing-attachments"/>
<div class="help-block"/>
</div>
<div class="modal-footer">
<a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
@ -157,12 +158,27 @@
<li class="next disabled"><a href="#">Next →</a></li>
</ul>
<div class="row mt16" t-foreach="rows" t-as="row">
<div class="col-sm-2" t-foreach="row" t-as="attachment">
<div class="col-sm-2 existing-attachment-cell"
t-foreach="row" t-as="attachment">
<i class="fa fa-times existing-attachment-remove" t-att-data-id="attachment.id"/>
<img t-att-src="attachment.website_url" t-att-alt="attachment.name" class="img img-responsive"/>
</div>
</div>
</div>
</t>
<t t-name="website.editor.dialog.image.existing.error">
<div class="help-block">
<p>The image could not be deleted because it is used in the
following pages or views:</p>
<ul t-foreach="views" t-as="view">
<li>
<a t-attf-href="/web#model=ir.ui.view&amp;id=#{view.id}">
<t t-esc="view.name"/>
</a> (id <t t-esc="view.id"/>)
</li>
</ul>
</div>
</t>
<t t-name="website.editor.table.panel">
<table class="editorbar-panel">
<tr t-foreach="rows"><td t-foreach="cols">&#8203;</td></tr>

View File

@ -46,11 +46,11 @@
<div t-name="website.snippets.resize" data-snippet-id='resize'>
<!-- custom data for the widget -->
<div class='oe_handles'>
<div class='oe_handle n'></div>
<div class='oe_handle e'></div>
<div class='oe_handle w'></div>
<div class='oe_handle size'><div class="oe_handle_button size">Resize</div><div class="oe_border"/></div>
<div class='oe_handle s'></div>
<div class='oe_handle n'><div></div></div>
<div class='oe_handle e'><div></div></div>
<div class='oe_handle w'><div></div></div>
<div class='oe_handle size'><div class="oe_handle_button size">Resize</div></div>
<div class='oe_handle s'><div></div></div>
</div>
<div class='oe_snippet_thumbnail'>Margin resize</div>
</div>

View File

@ -13,8 +13,8 @@ class TestViewSaving(common.TransactionCase):
def eq(self, a, b):
self.assertEqual(a.tag, b.tag)
self.assertEqual(a.attrib, b.attrib)
self.assertEqual(a.text, b.text)
self.assertEqual(a.tail, b.tail)
self.assertEqual((a.text or '').strip(), (b.text or '').strip())
self.assertEqual((a.tail or '').strip(), (b.tail or '').strip())
for ca, cb in itertools.izip_longest(a, b):
self.eq(ca, cb)

View File

@ -783,7 +783,7 @@
</ul>
</li>
<section class="oe_snippet_body parallax"
style="height: 320px; background-image: url('/website/static/src/img/banner/mountains.jpg')"
style="background-image: url('/website/static/src/img/banner/mountains.jpg')"
data-scroll-background-ratio="0.3">
<div><div class="oe_structure"/></div>
</section>
@ -795,10 +795,9 @@
<span class="oe_snippet_thumbnail_title">Parallax Slider</span>
</div>
<section class="oe_snippet_body parallax" data-snippet-id="parallax"
style="height: 320px; background-image: url('/website/static/src/img/parallax/quote.png')"
style="background-image: url('/website/static/src/img/parallax/quote.png')"
data-scroll-background-ratio="0.3">
<div>
<div class="oe_structure">
<div><div><div class="oe_structure">
<div id="myQuoteCarousel" class="carousel quotecarousel slide mb0" data-snippet-id="slider">
<!-- Indicators -->
<ol class="carousel-indicators mb0">
@ -845,8 +844,7 @@
</div>
</div>
</div>
</div>
</div>
</div></div></div>
</section>
</div>
@ -879,7 +877,7 @@
<li class="dropdown-submenu">
<a tabindex="-1" href="#">Style</a>
<ul class="dropdown-menu">
<li data-class="readable"><a>Readability</a></li>
<li data-class="readable"><a>Narrow</a></li>
</ul>
</li>
</div>

View File

@ -80,8 +80,8 @@
<script type="text/javascript" src="/website/static/src/js/website.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script>
<script t-if="not translatable" type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script>
<t t-raw="head or ''" name='layout_head'/>
</head>
@ -113,7 +113,7 @@
</b>
</a>
<ul class="dropdown-menu js_usermenu" role="menu">
<li><a href="/web" role="menuitem">Administration</a></li>
<li><a href="/web" role="menuitem">My Account</a></li>
<li class="divider"/>
<li><a t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a></li>
</ul>
@ -272,6 +272,8 @@
<script type="text/javascript" src='/website/static/lib/nearest/jquery.nearest.js'></script>
<script type="text/javascript" src="/website/static/lib/MutationObservers/MutationObserver.js"></script>
<script type="text/javascript" src="/website/static/lib/jquery.placeholder/jquery.placeholder.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.editor.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.editor.newpage.js" groups="base.group_website_designer"></script>
<script type="text/javascript" src="/website/static/src/js/website.menu.js" groups="base.group_website_designer"></script>

View File

@ -1,5 +1,4 @@
@charset "utf-8";
@import url(compass/css3.css);
@charset "UTF-8";
.css_website_mail .has-error {
border-color: red;
}

View File

@ -32,7 +32,7 @@
<div t-attf-class="form-group #{error and 'description' in error and 'has-error' or ''}">
<label class="col-md-3 col-sm-4 control-label" for="description">Your Question</label>
<div class="col-md-7 col-sm-8">
<textarea class="form-control" name="description" style="min-height: 120px" required="True" t-attf-value="#{description or ''}"/>
<textarea class="form-control" name="description" style="min-height: 120px" required="True"><t t-esc="description or ''"/></textarea>
</div>
</div>
<div class="form-group">

View File

@ -11,7 +11,7 @@
<record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website Home</field>
<field name="target">self</field>
<field name="url">/event?tutorial.event=true</field>
<field name="url">/event#tutorial.event=true</field>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_open_website"/>

View File

@ -59,7 +59,7 @@
</t>
</div>
<div>
<i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="event.date_begin"> </span> <i>to</i> <span itemprop="endDate" t-field="event.date_end"> </span>
<i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="event.date_begin" t-field-options='{"hide_seconds":"True"}'> </span> <i>to</i> <span itemprop="endDate" t-field="event.date_end" t-field-options='{"hide_seconds":"True"}'> </span>
</div>
<div itemprop="location" t-field="event.address_id" t-field-options='{
"widget": "contact",
@ -221,8 +221,8 @@
<div itemscope="itemscope" itemtype="http://schema.org/Event" class="container">
<h1 itemprop="name" class="text-center" t-field="event.name"></h1>
<h4 class="text-center text-muted">
<i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="event.date_begin"/> to
<span itemprop="endDate" t-field="event.date_end"/>
<i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="event.date_begin" t-field-options='{"hide_seconds":"True"}'/> to
<span itemprop="endDate" t-field="event.date_end" t-field-options='{"hide_seconds":"True"}'/>
</h4>
<h4 class="text-center text-muted"
t-field="event.address_id" t-field-options='{
@ -265,7 +265,7 @@
<t t-raw="comment.body"/>
<small class="pull-right muted text-right">
<div t-field="comment.author_id"/>
<div t-field="comment.date"/>
<div t-field="comment.date" t-field-options='{"hide_seconds":"True"}'/>
</small>
</div>
</li>
@ -294,8 +294,8 @@
<h4>When</h4>
</div>
<div class="panel-body">
<i class="fa fa-clock-o"></i> from <span t-field="event.date_begin"> </span><br/>
<i class="fa fa-clock-o"></i> to <span t-field="event.date_end"> </span>
<i class="fa fa-clock-o"></i> From <span t-field="event.date_begin" t-field-options='{"hide_seconds":"True"}'> </span><br/>
<i class="fa fa-clock-o"></i> To <span t-field="event.date_end" t-field-options='{"hide_seconds":"True"}'> </span>
</div>
</div>

View File

@ -39,12 +39,12 @@
{
title: "Order Now",
waitFor: 'select[name="ticket-2"] option:contains(3):selected',
element: 'button.btn-primary:contains("Order Now")',
element: '.btn-primary:contains("Order Now")',
},
{
title: "Complete checkout",
waitFor: '#top_menu .my_cart_quantity:contains(5)',
element: 'form[action="/shop/confirm_order/"] button',
element: 'form[action="/shop/confirm_order/"] .btn:contains("Confirm")',
onload: function (tour) {
if ($("input[name='name']").val() === "")
$("input[name='name']").val("website_sale-test-shoptest");
@ -64,7 +64,7 @@
{
title: "Pay Now",
waitFor: '#payment_method label:has(input:checked):has(img[title="transfer"])',
element: '.oe_sale_acquirer_button button[name="submit"]:visible',
element: '.oe_sale_acquirer_button .btn[name="submit"]:visible',
},
{
title: "finish",

View File

@ -59,6 +59,7 @@ class event_track_location(osv.osv):
class event_track(osv.osv):
_name = "event.track"
_description = 'Event Tracks'
_order = 'priority, date'
_inherit = ['mail.thread', 'ir.needaction_mixin', 'website.seo.metadata']

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<data noupdate="1">
<record id="event_track_public" model="ir.rule">
<field name="name">event tracks: Public</field>
<field name="model_id" ref="website_event_track.model_event_track"/>
<field name="domain_force">[('website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>

View File

@ -152,7 +152,7 @@
<t t-esc="speaker.name"/>,
</t>
</li>
<li class="text-muted fa fa-calendar"> <span t-field="track.date"/></li>
<li class="text-muted fa fa-calendar"> <span t-field="track.date" t-field-options='{"hide_seconds":"True"}'/></li>
<li class="text-muted fa fa-map-marker" t-if="track.location_id">
<span t-field="track.location_id"/>
</li>
@ -203,7 +203,8 @@
<h2 t-field="track.name" class="text-center"/>
<h3 t-field="event.name" class="text-center text-muted"/>
<ul t-if="track.tag_ids" class="text-center text-muted list-inline">
<li t-foreach="track.tag_ids" t-as="tag_id" class="fa fa-tags">
<li t-foreach="track.tag_ids" t-as="tag_id">
<span class="fa fa-tags"></span>
<a t-attf-href="/event/#{ slug(event) }/track/tag/#{ slug(tag_id) }">
<span t-field="tag_id.name"/>
</a>
@ -235,7 +236,7 @@
</div>
<div class="panel-body">
<b>Date</b><br/>
<span t-field="track.date"/><br/>
<span t-field="track.date" t-field-options='{"hide_seconds":"True"}'/><br/>
<b>Duration</b><br/>
<span t-field="track.duration"/> minutes<br/>
<b>Location</b><br/>

View File

@ -7,7 +7,6 @@ from openerp.tools.translate import _
from openerp.addons.web.http import request
class website_hr_recruitment(http.Controller):
@http.route([
'/jobs',
'/jobs/department/<model("hr.department"):department>',
@ -61,27 +60,48 @@ class website_hr_recruitment(http.Controller):
@http.route(['/jobs/apply/<model("hr.job"):job>'], type='http', auth="public", website=True, multilang=True)
def jobs_apply(self, job):
return request.website.render("website_hr_recruitment.apply", { 'job': job })
error = {}
default = {}
if 'website_hr_recruitment_error' in request.session:
error = request.session.pop('website_hr_recruitment_error')
default = request.session.pop('website_hr_recruitment_default')
return request.website.render("website_hr_recruitment.apply", { 'job': job, 'error': error, 'default': default})
@http.route(['/jobs/thankyou'], methods=['POST'], type='http', auth="public", website=True, multilang=True)
def jobs_thankyou(self, **post):
cr, uid, context = request.cr, request.uid, request.context
imd = request.registry['ir.model.data']
value = {
'name': _('Online Form'),
'user_id': False,
'source_id' : imd.xmlid_to_res_id(cr, SUPERUSER_ID, 'hr_recruitment.source_website_company'),
}
for f in ['phone', 'email_from', 'partner_name', 'description', 'department_id', 'job_id']:
value[f] = post.get(f)
job_id = request.registry['hr.applicant'].create(cr, SUPERUSER_ID, value, context=context)
error = {}
for field_name in ["partner_name", "phone", "email_from"]:
if not post.get(field_name):
error[field_name] = 'missing'
if error:
request.session['website_hr_recruitment_error'] = error
ufile = post.pop('ufile')
if ufile:
error['ufile'] = 'reset'
request.session['website_hr_recruitment_default'] = post
return request.redirect('/jobs/apply/%s' % post.get("job_id"))
value = {
'source_id' : imd.xmlid_to_res_id(cr, SUPERUSER_ID, 'hr_recruitment.source_website_company'),
'name': '%s\'s Application' % post.get('partner_name'),
}
for f in ['email_from', 'partner_name', 'description']:
value[f] = post.get(f)
for f in ['department_id', 'job_id']:
value[f] = int(post.get(f) or 0)
# Retro-compatibility for saas-3. "phone" field should be replace by "partner_phone" in the template in trunk.
value['partner_phone'] = post.pop('phone', False)
applicant_id = request.registry['hr.applicant'].create(cr, SUPERUSER_ID, value, context=context)
if post['ufile']:
attachment_value = {
'name': post['ufile'].filename,
'res_name': value['partner_name'],
'res_model': 'hr.applicant',
'res_id': job_id,
'res_id': applicant_id,
'datas': base64.encodestring(post['ufile'].read()),
'datas_fname': post['ufile'].filename,
}

Some files were not shown because too many files have changed in this diff Show More