diff options
| author | chm@openerp.com <> | 2014-03-11 16:46:16 +0100 |
|---|---|---|
| committer | chm@openerp.com <> | 2014-03-11 16:46:16 +0100 |
| commit | 53f59c16d6df857fe6be56a9b889d0cfe752d89f (patch) | |
| tree | 594c3263e88f3ca208d0e8e46269e6eadb853dc8 | |
| parent | e5fbd2ba1a7bef6711270ee8d19cc7f5bdbfd777 (diff) | |
| parent | d504764eff52354d4993863d72d1342bcdf704fb (diff) | |
[MERGE] from trunk
bzr revid: chm@openerp.com-20140311154616-tb35jfweo3xuzqws
109 files changed, 1665 insertions, 1059 deletions
diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index faa2aaee3be..e58cf7721c7 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -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"/> diff --git a/addons/account_followup/report/account_followup_print.rml b/addons/account_followup/report/account_followup_print.rml index a363126635c..6eadaed8675 100644 --- a/addons/account_followup/report/account_followup_print.rml +++ b/addons/account_followup/report/account_followup_print.rml @@ -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> diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index b62fb31d8b0..22a240e310b 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -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') diff --git a/addons/auth_signup/views/auth_signup_login.xml b/addons/auth_signup/views/auth_signup_login.xml index ae813cc14ac..727adbfd3de 100644 --- a/addons/auth_signup/views/auth_signup_login.xml +++ b/addons/auth_signup/views/auth_signup_login.xml @@ -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> diff --git a/addons/base_import/models.py b/addons/base_import/models.py index f49db56e755..9f29848ad46 100644 --- a/addons/base_import/models.py +++ b/addons/base_import/models.py @@ -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) diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index ca4d00eb85e..784358dceed 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -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' diff --git a/addons/calendar/calendar_data.xml b/addons/calendar/calendar_data.xml index cf00059913d..4435e8a4324 100644 --- a/addons/calendar/calendar_data.xml +++ b/addons/calendar/calendar_data.xml @@ -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> diff --git a/addons/calendar/calendar_demo.xml b/addons/calendar/calendar_demo.xml index 9e0b44c89cb..4ac308a29c2 100644 --- a/addons/calendar/calendar_demo.xml +++ b/addons/calendar/calendar_demo.xml @@ -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"/> diff --git a/addons/calendar/controllers/main.py b/addons/calendar/controllers/main.py index 941c92dcc3d..98625be42da 100644 --- a/addons/calendar/controllers/main.py +++ b/addons/calendar/controllers/main.py @@ -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 - diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index 18fd4dfa20e..eed958e20d9 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -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); + }, }); diff --git a/addons/crm/calendar_event.py b/addons/crm/calendar_event.py index df50d99b72b..d505be1b174 100644 --- a/addons/crm/calendar_event.py +++ b/addons/crm/calendar_event.py @@ -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 user’s ID for security checks, - @param ids: List of compute data’s 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: diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 6f522f0118f..fd9462d9c34 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -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): diff --git a/addons/crm/crm_phonecall_menu.xml b/addons/crm/crm_phonecall_menu.xml index 6380f80c89f..f28a49a55e7 100644 --- a/addons/crm/crm_phonecall_menu.xml +++ b/addons/crm/crm_phonecall_menu.xml @@ -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"> diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml index 9675913a0b5..60b5934ac53 100644 --- a/addons/crm/crm_phonecall_view.xml +++ b/addons/crm/crm_phonecall_view.xml @@ -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/> diff --git a/addons/crm/test/lead2opportunity_assign_salesmen.yml b/addons/crm/test/lead2opportunity_assign_salesmen.yml index de88a96cb50..12d212ec5fd 100644 --- a/addons/crm/test/lead2opportunity_assign_salesmen.yml +++ b/addons/crm/test/lead2opportunity_assign_salesmen.yml @@ -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) - diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index f779237ea71..33bdb46452d 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -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: diff --git a/addons/crm/wizard/crm_lead_to_opportunity_view.xml b/addons/crm/wizard/crm_lead_to_opportunity_view.xml index 58fbe244ba4..0f8f676d92d 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_view.xml +++ b/addons/crm/wizard/crm_lead_to_opportunity_view.xml @@ -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)]}"> diff --git a/addons/crm/wizard/crm_merge_opportunities_view.xml b/addons/crm/wizard/crm_merge_opportunities_view.xml index d43993b8389..f88a6fe6f17 100644 --- a/addons/crm/wizard/crm_merge_opportunities_view.xml +++ b/addons/crm/wizard/crm_merge_opportunities_view.xml @@ -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"> diff --git a/addons/crm_partner_assign/static/src/js/next.js b/addons/crm_partner_assign/static/src/js/next.js index 73ef09edb2e..b3a4fff2732 100644 --- a/addons/crm_partner_assign/static/src/js/next.js +++ b/addons/crm_partner_assign/static/src/js/next.js @@ -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"); }
\ No newline at end of file diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index b6899a7da2b..c1329eb5a4e 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -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, diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index beede1122dc..71f99471afa 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -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): diff --git a/addons/event/report/report_event_registration.py b/addons/event/report/report_event_registration.py index ca78412bd8e..e76b5c60d1f 100644 --- a/addons/event/report/report_event_registration.py +++ b/addons/event/report/report_event_registration.py @@ -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, diff --git a/addons/event/report/report_event_registration_view.xml b/addons/event/report/report_event_registration_view.xml index c6c2191c9c0..ed0124b5459 100644 --- a/addons/event/report/report_event_registration_view.xml +++ b/addons/event/report/report_event_registration_view.xml @@ -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'}"/> diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index 734710be4d0..f36062b6be2 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -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)
\ No newline at end of file + 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) diff --git a/addons/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py index e25b5ac0894..f57b142cae9 100644 --- a/addons/hr_expense/hr_expense.py +++ b/addons/hr_expense/hr_expense.py @@ -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 diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index f7fa6a7e47d..932a290b4a1 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -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: diff --git a/addons/hr_payroll_account/hr_payroll_account.py b/addons/hr_payroll_account/hr_payroll_account.py index ccab06c0dc1..ad8527fce43 100644 --- a/addons/hr_payroll_account/hr_payroll_account.py +++ b/addons/hr_payroll_account/hr_payroll_account.py @@ -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)) diff --git a/addons/l10n_multilang/l10n_multilang.py b/addons/l10n_multilang/l10n_multilang.py index 937ba332694..4d70d2f40e7 100644 --- a/addons/l10n_multilang/l10n_multilang.py +++ b/addons/l10n_multilang/l10n_multilang.py @@ -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) diff --git a/addons/mail/ir_attachment.py b/addons/mail/ir_attachment.py index 99c5a9e054e..a5d8705c0fb 100644 --- a/addons/mail/ir_attachment.py +++ b/addons/mail/ir_attachment.py @@ -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 } diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index a8726c16a0d..a33eaaea15d 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -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 diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 40a51df91d2..4c05d7a6d1f 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -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 diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index c72ef8a77cb..0f39c77e990 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -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 diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 6ce588e82a2..28cbbca1f2c 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -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> diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 2d09a925ca7..e5ff2b51fcd 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -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: diff --git a/addons/membership/membership_demo.xml b/addons/membership/membership_demo.xml index 379eaf0c1e3..219e4c50e23 100644 --- a/addons/membership/membership_demo.xml +++ b/addons/membership/membership_demo.xml @@ -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> diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index d6973bad803..91d50e93236 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -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 diff --git a/addons/mrp_repair/__openerp__.py b/addons/mrp_repair/__openerp__.py index c929ee2f43e..bde728a5e41 100644 --- a/addons/mrp_repair/__openerp__.py +++ b/addons/mrp_repair/__openerp__.py @@ -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, diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py index f04aa68ca6a..b1aec60a4f4 100644 --- a/addons/mrp_repair/mrp_repair.py +++ b/addons/mrp_repair/mrp_repair.py @@ -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), }), } diff --git a/addons/mrp_repair/test/test_mrp_repair_fee.yml b/addons/mrp_repair/test/test_mrp_repair_fee.yml new file mode 100644 index 00000000000..6aac605d0d9 --- /dev/null +++ b/addons/mrp_repair/test/test_mrp_repair_fee.yml @@ -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 diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 1a60bc13700..9ad77b3c4cf 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -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, diff --git a/addons/payment/models/res_config.py b/addons/payment/models/res_config.py index a0e9b1f7a11..70668a296da 100644 --- a/addons/payment/models/res_config.py +++ b/addons/payment/models/res_config.py @@ -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.'), } diff --git a/addons/payment_ogone/models/ogone.py b/addons/payment_ogone/models/ogone.py index 4018d3ec437..2ae9722b655 100644 --- a/addons/payment_ogone/models/ogone.py +++ b/addons/payment_ogone/models/ogone.py @@ -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 = { diff --git a/addons/payment_paypal/models/paypal.py b/addons/payment_paypal/models/paypal.py index 13945bab6e8..a5e31e9571c 100644 --- a/addons/payment_paypal/models/paypal.py +++ b/addons/payment_paypal/models/paypal.py @@ -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) diff --git a/addons/payment_paypal/views/res_config_view.xml b/addons/payment_paypal/views/res_config_view.xml index fbe23f71229..463ddb77c63 100644 --- a/addons/payment_paypal/views/res_config_view.xml +++ b/addons/payment_paypal/views/res_config_view.xml @@ -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> diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index b6848c8421f..0a611376916 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -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) diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index fbdf48aacf1..e53d3f378af 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -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) diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index 683cfaeb08b..5db91924bce 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -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); } diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 2197783473d..c8c87f1b3af 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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(); - - function rec_flush(index){ - if(index < orders.length){ - self._flush_order(orders[index].id).always(function(){ - rec_flush(index+1); - }) - }else{ - tried_all.resolve(); - } + 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 + }); + }); + }, + + // 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 || {}; + + var self = this; + var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length; - return tried_all; + // 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){ diff --git a/addons/portal/static/src/css/portal.css b/addons/portal/static/src/css/portal.css index e32f3b0e639..5157b1a8027 100644 --- a/addons/portal/static/src/css/portal.css +++ b/addons/portal/static/src/css/portal.css @@ -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); diff --git a/addons/portal_sale/portal_sale.py b/addons/portal_sale/portal_sale.py index 85bd70a35d4..aef730ab0a2 100644 --- a/addons/portal_sale/portal_sale.py +++ b/addons/portal_sale/portal_sale.py @@ -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): diff --git a/addons/portal_sale/portal_sale_view.xml b/addons/portal_sale/portal_sale_view.xml index ac56cc9ddfc..772e01af100 100644 --- a/addons/portal_sale/portal_sale_view.xml +++ b/addons/portal_sale/portal_sale_view.xml @@ -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> diff --git a/addons/portal_sale/res_config_view.xml b/addons/portal_sale/res_config_view.xml index 15bca2ea6bd..e761d6fae3f 100644 --- a/addons/portal_sale/res_config_view.xml +++ b/addons/portal_sale/res_config_view.xml @@ -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> diff --git a/addons/procurement/procurement_view.xml b/addons/procurement/procurement_view.xml index e0f345943ac..faec656cf20 100644 --- a/addons/procurement/procurement_view.xml +++ b/addons/procurement/procurement_view.xml @@ -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"/> diff --git a/addons/product/__openerp__.py b/addons/product/__openerp__.py index 194cc91b019..c628fca6aa8 100644 --- a/addons/product/__openerp__.py +++ b/addons/product/__openerp__.py @@ -63,7 +63,6 @@ Print product labels with barcode. ], 'test': [ 'product_pricelist_demo.yml', - 'test/product_uom.yml', 'test/product_pricelist.yml', ], 'installable': True, diff --git a/addons/product/_common.py b/addons/product/_common.py index f5f53fe8689..c05dcee66a2 100644 --- a/addons/product/_common.py +++ b/addons/product/_common.py @@ -18,12 +18,18 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## -def rounding(f, r): - if not r: - return f - return round(f / r) * r +from openerp import tools +import math -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: +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 math.ceil(f / r) * r diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py index 65c4311f34a..3c2edf74ea3 100644 --- a/addons/product/pricelist.py +++ b/addons/product/pricelist.py @@ -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) diff --git a/addons/product/product.py b/addons/product/product.py index 83e57e36489..6b0e22238cd 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -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): diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index c0798aa74dd..4946a71f80b 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -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"> diff --git a/addons/product/test/product_uom.yml b/addons/product/test/product_uom.yml deleted file mode 100644 index 663b03e992a..00000000000 --- a/addons/product/test/product_uom.yml +++ /dev/null @@ -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." diff --git a/addons/product/tests/__init__.py b/addons/product/tests/__init__.py new file mode 100644 index 00000000000..850189ac0f4 --- /dev/null +++ b/addons/product/tests/__init__.py @@ -0,0 +1,5 @@ +from . import test_uom + +fast_suite = [ + test_uom, +] diff --git a/addons/product/tests/test_uom.py b/addons/product/tests/test_uom.py new file mode 100644 index 00000000000..3d3ba04b689 --- /dev/null +++ b/addons/product/tests/test_uom.py @@ -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.") diff --git a/addons/product_margin/product_margin.py b/addons/product_margin/product_margin.py index 1bbfe8c285b..c344abb3410 100644 --- a/addons/product_margin/product_margin.py +++ b/addons/product_margin/product_margin.py @@ -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 diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index f787ed1544e..6f072f5abd1 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -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' diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index 4fa0e760df1..209bad47495 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -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> diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index 35901b0131e..c9ffb94b985 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -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> diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml index c1e6498b429..88d7fe8be6c 100644 --- a/addons/sale_crm/sale_crm_view.xml +++ b/addons/sale_crm/sale_crm_view.xml @@ -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> diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml index 6d0214fba67..a926a16dc14 100644 --- a/addons/sale_stock/sale_stock_view.xml +++ b/addons/sale_stock/sale_stock_view.xml @@ -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" diff --git a/addons/stock/wizard/stock_fill_inventory.py b/addons/stock/wizard/stock_fill_inventory.py index 3d741ead4be..3f9abcfe659 100644 --- a/addons/stock/wizard/stock_fill_inventory.py +++ b/addons/stock/wizard/stock_fill_inventory.py @@ -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)): diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 95c7ad82e2b..eff28b61fcd 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -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) diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 27af03310a9..857b99d40d2 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -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) diff --git a/addons/website/models/res_config.py b/addons/website/models/res_config.py index 1ae491388a3..dd86f824602 100644 --- a/addons/website/models/res_config.py +++ b/addons/website/models/res_config.py @@ -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), diff --git a/addons/website/models/website.py b/addons/website/models/website.py index ffb10d8850d..966810297ea 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -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', diff --git a/addons/website/static/lib/jquery.placeholder/jquery.placeholder.js b/addons/website/static/lib/jquery.placeholder/jquery.placeholder.js new file mode 100644 index 00000000000..4e296b7ac4e --- /dev/null +++ b/addons/website/static/lib/jquery.placeholder/jquery.placeholder.js @@ -0,0 +1,185 @@ +/*! http://mths.be/placeholder v2.0.7 by @mathias */ +;(function(window, document, $) { + + // Opera Mini v7 doesn’t 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));
\ No newline at end of file diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index 4fb4496e692..da0fad43caf 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -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; diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass index 2e6513f7c1d..4571dcd257e 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -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. diff --git a/addons/website/static/src/css/snippets.css b/addons/website/static/src/css/snippets.css index 4baa2b06f2d..1416f2f899f 100644 --- a/addons/website/static/src/css/snippets.css +++ b/addons/website/static/src/css/snippets.css @@ -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: -20px; 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; diff --git a/addons/website/static/src/css/snippets.sass b/addons/website/static/src/css/snippets.sass index 1be99e27f19..8d337aadf3c 100644 --- a/addons/website/static/src/css/snippets.sass +++ b/addons/website/static/src/css/snippets.sass @@ -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: -20px 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 diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index fb6dd4552b1..310f6b8156e 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -390,22 +390,15 @@ div.carousel .carousel-indicators .active { } .parallax { - position: relative; background-size: cover; +} +.parallax > div { + position: relative; 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 { +.parallax > div > div { display: table-cell; vertical-align: middle; padding: 32px 0; diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index 787dca1257f..563dcae95ec 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -322,21 +322,16 @@ div.carousel 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 */ diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js index ed7037e903c..a8af74bb52d 100644 --- a/addons/website/static/src/js/website.ace.js +++ b/addons/website/static/src/js/website.ace.js @@ -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; }, diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index dc82c0c17b9..a5cf89b3a4e 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -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(); + } + } + } })(); diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index b99004243a2..249058d91fb 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -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) { diff --git a/addons/website/static/src/js/website.mobile.js b/addons/website/static/src/js/website.mobile.js index 4acf2d78db8..59c2328e09b 100644 --- a/addons/website/static/src/js/website.mobile.js +++ b/addons/website/static/src/js/website.mobile.js @@ -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(); }, diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index 8d7b634e177..55da2b9df66 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -72,6 +72,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({ diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js index b615f8d60e5..ce24f7e7780 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -19,13 +19,23 @@ edit: function () { var self = this; $("[data-oe-model] *, [data-oe-type=html] *").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(true); + 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); @@ -166,13 +176,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"); @@ -189,7 +200,6 @@ bind_snippet_click_editor: function () { var self = this; - var snipped_event_flag = false; $("#wrapwrap").on('click', function (event) { if (snipped_event_flag) { return; @@ -197,7 +207,7 @@ snipped_event_flag = true; setTimeout(function () {snipped_event_flag = false;}, 0); - var $target = $(event.srcElement); + var $target = $(event.srcElement || event.target); if (!$target.is(website.snippet.globalSelector)) { $target = $target.parents(website.snippet.globalSelector).first(); @@ -381,14 +391,13 @@ setTimeout(function () { $("#oe_snippets").trigger('snippet-dropped', $target); - // after rte website.snippet.start_animation(true, $target); + // drop_and_build_snippet self.create_overlay($target); if ($target.data("snippet-editor")) { $target.data("snippet-editor").drop_and_build_snippet($target); } - for (var k in website.snippet.templateOptions) { $target.find(website.snippet.templateOptions[k].selector).each(function () { var $snippet = $(this); @@ -399,6 +408,23 @@ }); } + // reset snippet for rte + $target.removeData("snippet-editor"); + if ($target.data("overlay")) { + $target.data("overlay").remove(); + $target.removeData("overlay"); + } + $target.find("[data-snippet-id]").each(function () { + var $snippet = $(this); + $snippet.removeData("snippet-editor"); + if ($snippet.data("overlay")) { + $snippet.data("overlay").remove(); + $snippet.removeData("overlay"); + } + }); + self.create_overlay($target); + // end + self.make_active($target); },0); } else { @@ -543,6 +569,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); @@ -759,6 +799,7 @@ this.$el.find('li:has(li[data-class].active)').addClass("active"); } }); + website.snippet.options.slider = website.snippet.Option.extend({ drop_and_build_snippet: function() { var id = $(".carousel").length; @@ -1154,7 +1195,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(); @@ -1200,6 +1240,7 @@ this._super(compass, beginClass, current); }, }); + website.snippet.options["resize"] = website.snippet.options.marginAndResize.extend({ getSize: function () { this.grid = this._super(); @@ -1234,7 +1275,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; diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index 8caac7c6683..09aa3783c3f 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -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&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">​</td></tr> diff --git a/addons/website/static/src/xml/website.snippets.xml b/addons/website/static/src/xml/website.snippets.xml index d73e0c3934a..ae109139e3c 100644 --- a/addons/website/static/src/xml/website.snippets.xml +++ b/addons/website/static/src/xml/website.snippets.xml @@ -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> diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 8041a7e5f1c..1adb11712ec 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -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) diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml index 35861331338..ebbece93f64 100644 --- a/addons/website/views/snippets.xml +++ b/addons/website/views/snippets.xml @@ -776,7 +776,7 @@ <span class="oe_snippet_thumbnail_title">Parallax</span> </div> <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> @@ -788,10 +788,9 @@ <span class="oe_snippet_thumbnail_title">Parallax Slider</span> </div> <section class="oe_snippet_body 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"> <!-- Indicators --> <ol class="carousel-indicators mb0"> @@ -838,8 +837,7 @@ </div> </div> </div> - </div> - </div> + </div></div></div> </section> </div> @@ -862,7 +860,7 @@ <li class="dropdown-submenu"> <a tabindex="-1" href="#">Style</a> <ul class="dropdown-menu"> - <li data-class="readable"><a>Blog Style</a></li> + <li data-class="readable"><a>Narrow</a></li> </ul> </li> </div> diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index ee7d4cab9ef..580957b81d6 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -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> diff --git a/addons/website_blog/static/src/css/website_blog.css b/addons/website_blog/static/src/css/website_blog.css index 96045adb914..f7355ce783d 100644 --- a/addons/website_blog/static/src/css/website_blog.css +++ b/addons/website_blog/static/src/css/website_blog.css @@ -1,5 +1,4 @@ -@charset "utf-8"; -@import url(compass/css3.css); +@charset "UTF-8"; .css_website_mail .has-error { border-color: red; } diff --git a/addons/website_crm/views/website_crm.xml b/addons/website_crm/views/website_crm.xml index fa0489924ce..64ae477679a 100644 --- a/addons/website_crm/views/website_crm.xml +++ b/addons/website_crm/views/website_crm.xml @@ -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"> diff --git a/addons/website_event/data/event_data.xml b/addons/website_event/data/event_data.xml index f46d18001ed..763e887172c 100644 --- a/addons/website_event/data/event_data.xml +++ b/addons/website_event/data/event_data.xml @@ -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"/> diff --git a/addons/website_event/views/website_event.xml b/addons/website_event/views/website_event.xml index b057aaa7c5d..e490b1a0687 100644 --- a/addons/website_event/views/website_event.xml +++ b/addons/website_event/views/website_event.xml @@ -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> diff --git a/addons/website_event_sale/static/src/js/website.tour.event_sale.js b/addons/website_event_sale/static/src/js/website.tour.event_sale.js index 7991831400c..3c74cc2317e 100644 --- a/addons/website_event_sale/static/src/js/website.tour.event_sale.js +++ b/addons/website_event_sale/static/src/js/website.tour.event_sale.js @@ -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", diff --git a/addons/website_event_track/models/event.py b/addons/website_event_track/models/event.py index e99318f1ced..cfba63cfc6b 100644 --- a/addons/website_event_track/models/event.py +++ b/addons/website_event_track/models/event.py @@ -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'] diff --git a/addons/website_event_track/security/event.xml b/addons/website_event_track/security/event.xml index 56ca1ff5ec1..52f8e3c7286 100644 --- a/addons/website_event_track/security/event.xml +++ b/addons/website_event_track/security/event.xml @@ -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"/> diff --git a/addons/website_event_track/views/website_event.xml b/addons/website_event_track/views/website_event.xml index b9db5aaa2f0..03f6f750b6f 100644 --- a/addons/website_event_track/views/website_event.xml +++ b/addons/website_event_track/views/website_event.xml @@ -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/> diff --git a/addons/website_hr_recruitment/controllers/main.py b/addons/website_hr_recruitment/controllers/main.py index c50bc6a0428..8140a39dc9b 100644 --- a/addons/website_hr_recruitment/controllers/main.py +++ b/addons/website_hr_recruitment/controllers/main.py @@ -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'] + + 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 = { - 'name': _('Online Form'), - 'user_id': False, '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 ['phone', 'email_from', 'partner_name', 'description', 'department_id', 'job_id']: + 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) - job_id = request.registry['hr.applicant'].create(cr, SUPERUSER_ID, value, context=context) + 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, } diff --git a/addons/website_hr_recruitment/static/description/index.html b/addons/website_hr_recruitment/static/description/index.html index 1a56da0e82c..7af603b64dc 100644 --- a/addons/website_hr_recruitment/static/description/index.html +++ b/addons/website_hr_recruitment/static/description/index.html @@ -51,14 +51,14 @@ <section class="oe_container"> <div class="oe_row"> <h2 class="oe_slogan">Post Your Jobs on Best Job Boards</h2> - <h3 class="oe_slogan">LinkedIn, Monster, Kraigslist, Careerbuilder,...</h3> + <h3 class="oe_slogan">LinkedIn, Monster, Craigslist, Careerbuilder,...</h3> <div class="oe_span6"> <img class="oe_picture oe_screenshot" src="jobs3.png"> </div> <div class="oe_span6"> <p class='oe_mt32'> Connect automatically to most famous job board websites; - linkedIn, Monster, Kraigslist, ... Every job position has a new + linkedIn, Monster, Craigslist, ... Every job position has a new email address automatically assigned to route applications automatically to the right job position. </p><p> diff --git a/addons/website_hr_recruitment/views/templates.xml b/addons/website_hr_recruitment/views/templates.xml index 3e057d249f2..9dab6c6accc 100644 --- a/addons/website_hr_recruitment/views/templates.xml +++ b/addons/website_hr_recruitment/views/templates.xml @@ -161,34 +161,35 @@ <form class="form-horizontal mt32" action="/jobs/thankyou" method="post" enctype="multipart/form-data"> <input type="hidden" t-att-value="job and job.department_id.id or False" name="department_id"/> <input type="hidden" t-att-value="job and job.id or False" name="job_id"/> - <div class="form-group"> + <div t-attf-class="form-group #{error.get('partner_name') and 'has-error' or ''}"> <label class="col-md-3 col-sm-4 control-label" for="partner_name">Your Name</label> <div class="col-md-7 col-sm-8"> - <input type="text" class="form-control" name="partner_name" required="True" /> + <input type="text" t-att-value="default.get('partner_name')" class="form-control" name="partner_name" required="True"/> </div> </div> - <div class="form-group"> + <div t-attf-class="form-group #{error.get('email_from') and 'has-error' or ''}"> <label class="col-md-3 col-sm-4 control-label" for="email_from">Your Email</label> <div class="col-md-7 col-sm-8"> - <input type="email" class="form-control" name="email_from" required="True" /> + <input type="email" t-att-value="default.get('email_from')" class="form-control" name="email_from" required="True"/> </div> </div> - <div class="form-group"> + <div t-attf-class="form-group #{error.get('phone') and 'has-error' or ''}"> <label class="col-md-3 col-sm-4 control-label" for="phone">Your Phone</label> <div class="col-md-7 col-sm-8"> - <input type="text" class="form-control" name="phone" required="True" /> + <input type="text" t-att-value="default.get('phone')" class="form-control" name="phone" required="True"/> </div> </div> <div class="form-group"> <label class="col-md-3 col-sm-4 control-label" for="description">Short Introduction</label> <div class="col-md-7 col-sm-8"> - <textarea class="form-control" name="description" style="min-height: 120px"/> + <textarea class="form-control" t-esc="default.get('description')" name="description" style="min-height: 120px"/> </div> </div> <div class="form-group"> <label class="col-md-3 col-sm-4 control-label" for="ufile">Resume</label> <div class="col-md-7 col-sm-8"> <input class="input-file" id="fileInput" type="file" name="ufile"/> + <span t-if="error.get('ufile')">Please send again your resume.</span> </div> </div> <div class="form-group"> diff --git a/addons/website_mail/controllers/email_designer.py b/addons/website_mail/controllers/email_designer.py index 81bc703b702..3eb3c32ac96 100644 --- a/addons/website_mail/controllers/email_designer.py +++ b/addons/website_mail/controllers/email_designer.py @@ -7,7 +7,7 @@ from openerp.addons.web.http import request class WebsiteEmailDesigner(http.Controller): - @http.route('/website_mail/email_designer/<model("email.template"):template>/', type='http', auth="public", website=True, multilang=True) + @http.route('/website_mail/email_designer/<model("email.template"):template>/', type='http', auth="user", website=True, multilang=True) def index(self, template, **kw): values = { 'template': template, @@ -15,6 +15,6 @@ class WebsiteEmailDesigner(http.Controller): print template return request.website.render("website_mail.designer_index", values) - @http.route(['/website_mail/snippets'], type='json', auth="public", website=True) + @http.route(['/website_mail/snippets'], type='json', auth="user", website=True) def snippets(self): return request.website._render('website_mail.email_designer_snippets') diff --git a/addons/website_mail/models/email_template.py b/addons/website_mail/models/email_template.py index 6d84f91e5ab..87daac6aabb 100644 --- a/addons/website_mail/models/email_template.py +++ b/addons/website_mail/models/email_template.py @@ -19,9 +19,6 @@ # ############################################################################## -import lxml -import urlparse - from openerp.osv import osv, fields from openerp.tools.translate import _ @@ -39,52 +36,3 @@ class EmailTemplate(osv.Model): help='Link to the website', ), } - - def _postprocess_html_replace_links(self, cr, uid, body_html, context=None): - """ Post-processing of body_html. Indeed the content generated by the - website builder contains references to local addresses, for example - for images. This method changes those addresses to absolute addresses. """ - html = body_html - if not body_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 create(self, cr, uid, values, context=None): - if 'body_html' in values: - values['body_html'] = self._postprocess_html_replace_links(cr, uid, values['body_html'], context=context) - return super(EmailTemplate, self).create(cr, uid, values, context=context) - - def write(self, cr, uid, ids, values, context=None): - if 'body_html' in values: - values['body_html'] = self._postprocess_html_replace_links(cr, uid, values['body_html'], context=context) - return super(EmailTemplate, self).write(cr, uid, ids, values, context=context) diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 9fb4ce50b0e..129643f0029 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -58,14 +58,19 @@ class table_compute(object): self.table = {} def _check_place(self, posx, posy, sizex, sizey): + res = True for y in range(sizey): for x in range(sizex): if posx+x>=PPR: - return False + res = False + break row = self.table.setdefault(posy+y, {}) if row.setdefault(posx+x) is not None: - return False - return True + res = False + break + for x in range(PPR): + self.table[posy+y].setdefault(x, None) + return res def process(self, products): # Compute products positions on the grid @@ -106,8 +111,10 @@ class table_compute(object): for col in range(len(rows)): cols = rows[col].items() cols.sort() - rows[col] = map(lambda x: x[1], cols) - return filter(bool, rows) + x += len(cols) + rows[col] = [c for c in map(lambda x: x[1], cols) if c != False] + + return rows class Ecommerce(http.Controller): @@ -181,19 +188,29 @@ class Ecommerce(http.Controller): return request.redirect(url) - def attributes_to_ids(self, attributes): - obj = request.registry.get('product.attribute.line') - domain = [] + def attributes_to_ids(self, cr, uid, attributes): + req = """ + SELECT product_tmpl_id as id, count(*) as nb_match + FROM product_attribute_line + WHERE 1!=1 + """ + nb = 0 for key_val in attributes: - domain.append(("attribute_id", "=", key_val[0])) + attribute_id = key_val[0] if isinstance(key_val[1], list): - domain.append(("value", ">=", key_val[1][0])) - domain.append(("value", "<=", key_val[1][1])) + req += " OR ( attribute_id = %s AND value >= %s AND value <= %s)" % \ + (attribute_id, key_val[1][0], key_val[1][1]) + nb += 1 else: - domain.append(("value_id", "in", key_val[1:])) - att_ids = obj.search(request.cr, request.uid, domain, context=request.context) - att = obj.read(request.cr, request.uid, att_ids, ["product_tmpl_id"], context=request.context) - return [r["product_tmpl_id"][0] for r in att] + for value_id in key_val[1:]: + req += " OR ( attribute_id = %s AND value_id = %s)" % \ + (attribute_id, value_id) + nb += 1 + + req += " GROUP BY product_tmpl_id" + cr.execute(req) + result = cr.fetchall() + return [id for id, nb_match in result if nb_match >= nb] @http.route(['/shop/pricelist'], type='http', auth="public", website=True, multilang=True) def shop_promo(self, promo=None, **post): @@ -221,7 +238,7 @@ class Ecommerce(http.Controller): if filters: filters = simplejson.loads(filters) if filters: - ids = self.attributes_to_ids(filters) + ids = self.attributes_to_ids(cr, uid, filters) domain.append(('id', 'in', ids or [0])) url = "/shop/" @@ -292,7 +309,7 @@ class Ecommerce(http.Controller): } return request.website.render("website_sale.product", values) - @http.route(['/shop/product/comment'], type='http', auth="public", methods=['POST'], website=True) + @http.route(['/shop/product/<int:product_template_id>/comment'], type='http', auth="public", methods=['POST'], website=True) def product_comment(self, product_template_id, **post): cr, uid, context = request.cr, request.uid, request.context if post.get('comment'): @@ -324,7 +341,7 @@ class Ecommerce(http.Controller): # must have a draft sale order with lines at this point, otherwise reset order = self.get_order() if order and order.state != 'draft': - request.registry['website'].sale_reset_order(cr, uid, context=context) + request.registry['website'].ecommerce_reset(cr, uid, context=context) return request.redirect('/shop/') self.get_pricelist() @@ -422,12 +439,11 @@ class Ecommerce(http.Controller): partner = None public_id = request.registry['website'].get_public_user(cr, uid, context) - if not request.uid == public_id: + if request.uid != public_id: partner = orm_user.browse(cr, uid, uid, context).partner_id elif order.partner_id: - domain = [("active", "=", False), ("partner_id", "=", order.partner_id.id)] - user_ids = request.registry['res.users'].search(cr, SUPERUSER_ID, domain, context=context) - if not user_ids or public_id not in user_ids: + public_partner = orm_user.browse(cr, SUPERUSER_ID, public_id, context=context).partner_id.id + if public_partner != order.partner_id.id: partner = orm_partner.browse(cr, SUPERUSER_ID, order.partner_id.id, context) if partner: @@ -501,9 +517,8 @@ class Ecommerce(http.Controller): if request.uid != public_id: partner_id = orm_user.browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id elif order.partner_id: - domain = [("active", "=", False), ("partner_id", "=", order.partner_id.id)] - user_ids = request.registry['res.users'].search(cr, SUPERUSER_ID, domain, context=context) - if not user_ids or public_id not in user_ids: + public_partner = orm_user.browse(cr, SUPERUSER_ID, public_id, context=context).partner_id.id + if public_partner != order.partner_id.id: partner_id = order.partner_id.id if partner_id: @@ -705,8 +720,8 @@ class Ecommerce(http.Controller): - UDPATE ME """ cr, uid, context = request.cr, request.uid, request.context - email_act = None sale_order_obj = request.registry['sale.order'] + email_act = None if transaction_id is None: tx = context.get('website_sale_transaction') @@ -719,8 +734,10 @@ class Ecommerce(http.Controller): order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context) assert order.website_session_id == request.session['website_session_id'] - if not tx or not order: + if not order: return request.redirect('/shop/') + elif order.amount_total and not tx: + return request.redirect('/shop/mycart') if not order.amount_total or tx.state == 'done': # confirm the quotation @@ -734,6 +751,16 @@ class Ecommerce(http.Controller): # cancel the quotation sale_order_obj.action_cancel(cr, SUPERUSER_ID, [order.id], context=request.context) + # send the email + if email_act and email_act.get('context'): + composer_values = {} + email_ctx = email_act['context'] + public_id = request.registry['website'].get_public_user(cr, uid, context) + if uid == public_id: + composer_values['email_from'] = request.registry['res.users'].browse(cr, SUPERUSER_ID, public_id, context=context).company_id.email + composer_id = request.registry['mail.compose.message'].create(cr, SUPERUSER_ID, composer_values, context=email_ctx) + request.registry['mail.compose.message'].send_mail(cr, SUPERUSER_ID, [composer_id], context=email_ctx) + # clean context and session, then redirect to the confirmation page request.registry['website'].ecommerce_reset(cr, uid, context=context) diff --git a/addons/website_sale/static/src/css/website_sale.css b/addons/website_sale/static/src/css/website_sale.css index d115ca37b90..d9880bd7025 100644 --- a/addons/website_sale/static/src/css/website_sale.css +++ b/addons/website_sale/static/src/css/website_sale.css @@ -1,3 +1,4 @@ +@charset "utf-8"; /* ---- Default Styles ---- */ .oe_product { border: 1px solid rgba(100, 100, 100, 0.2); @@ -52,7 +53,7 @@ right: 0; bottom: 0; overflow: hidden; - padding: 0 15px; + padding: 0 15px 24px 0; max-height: 110px; min-height: 56px; border-top: 1px solid rgba(255, 255, 255, 0.2); @@ -75,11 +76,6 @@ overflow: hidden; margin-bottom: 30px; } -.oe_product .oe_subdescription.oe_shadow { - position: absolute; - opacity: 0.3; - max-height: none; -} .oe_mycart .input-group-addon { padding-left: 6px; diff --git a/addons/website_sale/static/src/css/website_sale.sass b/addons/website_sale/static/src/css/website_sale.sass index 9cf1f5dd012..7c5bde43509 100644 --- a/addons/website_sale/static/src/css/website_sale.sass +++ b/addons/website_sale/static/src/css/website_sale.sass @@ -47,7 +47,7 @@ right: 0 bottom: 0 overflow: hidden - padding: 0 15px + padding: 0 15px 24px 0 max-height: 110px min-height: 56px border-top: 1px solid rgba(255,255,255,0.2) @@ -66,10 +66,6 @@ max-height: 42px overflow: hidden margin-bottom: 30px - &.oe_shadow - position: absolute - opacity: .3 - max-height: none .oe_mycart .input-group-addon diff --git a/addons/website_sale/static/src/js/website.tour.sale.js b/addons/website_sale/static/src/js/website.tour.sale.js index 83696c4e3cb..f85a8b22ad7 100644 --- a/addons/website_sale/static/src/js/website.tour.sale.js +++ b/addons/website_sale/static/src/js/website.tour.sale.js @@ -22,11 +22,11 @@ { title: "click on add to cart", waitFor: 'input[name="product_id"]:eq(1)[checked]', - element: 'form[action="/shop/add_cart/"] button', + element: 'form[action="/shop/add_cart/"] .btn', }, { title: "add suggested", - element: 'form[action="/shop/add_cart/"] button.btn-link:contains("Add to Cart")', + element: 'form[action="/shop/add_cart/"] .btn-link:contains("Add to Cart")', }, { title: "add one more iPod", @@ -51,7 +51,7 @@ }, { title: "test with input error", - element: 'form[action="/shop/confirm_order/"] button', + element: 'form[action="/shop/confirm_order/"] .btn:contains("Confirm")', onload: function (tour) { $("input[name='phone']").val(""); }, @@ -59,7 +59,7 @@ { title: "test without input error", waitFor: 'form[action="/shop/confirm_order/"] .has-error', - 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"); @@ -79,7 +79,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", diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 3c9bb6ea05c..8298f6b22c0 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -61,6 +61,10 @@ $(document).ready(function () { return false; }); + $('.a-submit').on('click', function () { + $(this).closest('form').submit(); + }); + // change price when they are variants $('form.js_add_cart_json label').on('mouseup', function (ev) { ev.preventDefault(); @@ -107,6 +111,10 @@ $(document).ready(function () { $min.val( ui.values[ 0 ] ); $max.val( ui.values[ 1 ] ); $form.submit(); + }, + slide: function( event, ui ) { + $min.val( ui.values[ 0 ] ); + $max.val( ui.values[ 1 ] ); } }); $min.val( $slider.slider( "values", 0 ) ); diff --git a/addons/website_sale/views/website_sale.xml b/addons/website_sale/views/website_sale.xml index abbcaa26ab9..31517b8bfbb 100644 --- a/addons/website_sale/views/website_sale.xml +++ b/addons/website_sale/views/website_sale.xml @@ -50,27 +50,27 @@ <div class="input-group"> <input type="text" name="search" class="search-query form-control" placeholder="Search..." t-att-value="search.get('search') or ''"/> <span class="input-group-btn"> - <button class="btn btn-default" type="submit"><i class="fa fa-search"/></button> + <a class="btn btn-default a-submit"><i class="fa fa-search"/></a> </span> </div> </form> </template> -<template id="products_cart" name="Shopping cart"> +<template id="products_cart" name="Product Cell"> <div itemscope="itemscope" itemtype="http://schema.org/Product"> <div class="ribbon-wrapper"> <div class="ribbon btn btn-danger">Sale</div> </div> <div class="oe_product_image"> <a itemprop="url" t-attf-href="/shop/product/{{ slug(product) }}/?{{ keep_query('search', 'filters', category=(category and int(category)), page=(pager['page']['num'] if pager['page']['num']>1 else None)) }}"> - <span itemprop="image" t-field="product.image" t-field-options='{"widget": "image"}'/> + <img itemprop="image" class="img img-responsive" t-attf-src="/website/image/product.template/#{product.id}/image#{'' if product_image_big else '?max_width=300&max_height=300'}"/> </a> </div> <section> <h5><strong><a itemprop="name" t-attf-href="/shop/product/{{ slug(product) }}/?{{ keep_query('search', 'filters', category=(category and int(category)), page=(pager['page']['num'] if pager['page']['num']>1 else None)) }}" t-field="product.name"/></strong></h5> <div itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer" class="product_price" t-if="product.product_variant_ids"> <b> - <t t-if="abs(product.product_variant_ids[0].lst_price - product.product_variant_ids[0].price) > 0.2"> + <t t-if="product.product_variant_ids[0].lst_price != product.product_variant_ids[0].price"> <del class="text-danger" t-field="product.product_variant_ids[0].lst_price" t-field-options='{ "widget": "monetary", @@ -130,7 +130,7 @@ <div class="css_options" t-ignore="true" groups="base.group_website_publisher"> <div t-attf-class="dropdown js_options" t-att-data-id="product.id"> - <a class="btn btn-default" t-att-id="'dopprod-%s' % product.id" role="button" data-toggle="dropdown">Options <span class="caret"></span></a> + <button class="btn btn-default" t-att-id="'dopprod-%s' % product.id" role="button" data-toggle="dropdown">Options <span class="caret"></span></button> <ul class="dropdown-menu" role="menu" t-att-aria-labelledby="'dopprod-%s' % product.id"> <li class='dropdown-submenu'> <a tabindex="-1" href="#">Size</a> @@ -187,12 +187,13 @@ </div> </div> + <t t-set="product_image_big" t-value="td_product['x']+td_product['y'] > 2"/> <t t-call="website_sale.products_cart"/> </div> </td> </t> - <td t-if="td_product == None"/> + <td t-if="not td_product"/> </t> </tr> </tbody> @@ -220,8 +221,9 @@ <template id="product_description" inherit_option_id="website_sale.products_cart" name="Product Description"> <xpath expr="//div[@class='product_price']" position="before"> - <div class="text-info oe_subdescription oe_shadow" t-field="product.description_sale"/> - <div itemprop="description" class="text-info oe_subdescription" t-field="product.description_sale"/> + <div class="text-info oe_subdescription" contenteditable="false"> + <div itemprop="description" t-field="product.description_sale"></div> + </div> </xpath> </template> @@ -231,7 +233,7 @@ <xpath expr="//div[@class='product_price']" position="inside"> <form action="/shop/add_cart/" method="post" style="display: inline-block;"> <input name="product_id" t-att-value="product.product_variant_ids[0].id" type="hidden"/> - <button type="submit" class="fa fa-shopping-cart"/> + <a class="btn btn-default btn-xs fa fa-shopping-cart a-submit"/> </form> </xpath> </template> @@ -338,7 +340,7 @@ <span itemprop="priceCurrency" style="display:none;" t-esc="website.pricelist_id.currency_id.name"/> </h4> </div> - <button class="btn btn-primary btn-lg mt8">Add to Cart</button> + <a class="btn btn-primary btn-lg mt8 a-submit">Add to Cart</a> <hr t-if="product.description_sale"/> <div><p t-field="product.description_sale" class="text-muted"/></div> <hr/> @@ -421,11 +423,11 @@ <div> <div class="oe_msg_attachment" t-foreach='message.attachment_ids' t-as='attachment'> <a t-att-href="'/mail/download_attachment?model=mail.message&id='+str(message.id)+'&method=download_attachment&attachment_id='+str(attachment.id)" target="_blank"> - <t t-if="attachment.file_type == 'webimage'"> + <t t-if="attachment.file_type_icon == 'webimage'"> <img t-att-src="'/web/binary/image?model=ir.attachment&field=datas&id=' + str(attachment.id) + '&resize=100,80'" class='oe_attachment_embedded'></img> </t> - <t t-if="attachment.file_type != 'webimage'"> + <t t-if="attachment.file_type_icon != 'webimage'"> <img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type + '.png'" class='oe_attachment_webimage'></img> </t> @@ -443,7 +445,7 @@ <img class="img pull-left img-rounded" t-att-src="'/website/image?model=res.partner&field=image_small&id='+str(user_id.partner_id.id)" style="width: 50px; margin-right: 10px;"/> <div class="pull-left mb32" style="width: 75%%"> <textarea rows="3" name="comment" class="form-control" placeholder="Write a comment..."></textarea> - <button type="submit" class="btn btn-primary mt8">Post</button> + <a class="btn btn-primary mt8 a-submit">Post</a> </div> </form> </div> @@ -610,9 +612,9 @@ <t t-set="attribute" t-value="Ecommerce.has_search_filter(attribute_id.id)"/> <div style="margin: 0 20px;" class="js_slider" t-att-data-id="attribute_id.id" - t-att-data-value-min="attribute and attribute[1][0] or attribute_id.float_min" + t-att-data-value-min="attribute and attribute[1][0] or attribute_id.float_min or '0'" t-att-data-value-max="attribute and attribute[1][1] or attribute_id.float_max" - t-att-data-min="attribute_id.float_min" + t-att-data-min="attribute_id.float_min or '0'" t-att-data-max="attribute_id.float_max"></div> </li> </t> @@ -675,7 +677,7 @@ <td class="text-center"> <form action="/shop/add_cart/" method="post"> <input name="product_id" t-att-value="product.product_variant_ids[0].id" type="hidden"/> - <button type="submit" class="btn btn-link"><strong>Add to Cart</strong></button> + <a class="btn btn-link a-submit"><strong>Add to Cart</strong></a> </form> </td> </tr> @@ -694,7 +696,7 @@ <div class="input-group"> <input name="promo" class='form-control' type="text" placeholder="code..." t-att-value="website_sale_order.pricelist_id.code or ''"/> <div class="input-group-btn"> - <button class="btn btn-default">Apply</button> + <a class="btn btn-default a-submit">Apply</a> </div> </div> </form> @@ -786,7 +788,7 @@ <div class="form-group col-lg-6" groups="sale.group_delivery_invoice_address"> <label> <input type="checkbox" name="shipping_different" t-att-checked="shipping"/> - Ship to a different address + <span>Ship to a different address</span> </label> </div> </div> @@ -833,13 +835,13 @@ </select> </div> </div> - <button type="submit" class="btn btn-default btn-primary pull-right mb32">Confirm <span class="fa fa-long-arrow-right"/></button> + <a class="btn btn-default btn-primary pull-right mb32 a-submit">Confirm <span class="fa fa-long-arrow-right"/></a> </div> <div class="col-lg-offset-1 col-lg-3 col-md-3 text-muted"> <h3 class="page-header mt16">Your Order <small><a href="/shop/mycart"><span class="fa fa-arrow-right"/> change</a></small></h3> <div class="row"> <div class="col-sm-6 text-right">Subtotal:</div> - <div class="col-sm-6"><span t-esc="website_sale_order.amount_untaxed" t-field-options='{ + <div class="col-sm-6"><span t-field="website_sale_order.amount_untaxed" t-field-options='{ "widget": "monetary", "display_currency": "website.pricelist_id.currency_id" }'/></div> @@ -971,9 +973,9 @@ <div class="js_payment mb64 row" t-if="not website_sale_order.amount_total" id="payment_method"> <div class="col-lg-8 col-sm-8"> <form target="_self" action="/shop/payment/validate/" method="post" class="pull-right"> - <button width="100px" class="btn btn-primary" name="submit"> + <a style="width:100px;" class="btn btn-primary a-submit"> <span>Pay Now <span class="fa fa-long-arrow-right"></span></span> - </button> + </a> </form> </div> </div> |
