summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorchm@openerp.com <>2014-03-11 16:46:16 +0100
committerchm@openerp.com <>2014-03-11 16:46:16 +0100
commit53f59c16d6df857fe6be56a9b889d0cfe752d89f (patch)
tree594c3263e88f3ca208d0e8e46269e6eadb853dc8
parente5fbd2ba1a7bef6711270ee8d19cc7f5bdbfd777 (diff)
parentd504764eff52354d4993863d72d1342bcdf704fb (diff)
[MERGE] from trunk
bzr revid: chm@openerp.com-20140311154616-tb35jfweo3xuzqws
-rw-r--r--addons/account/account_invoice_view.xml10
-rw-r--r--addons/account_followup/report/account_followup_print.rml2
-rw-r--r--addons/auth_oauth/controllers/main.py6
-rw-r--r--addons/auth_signup/views/auth_signup_login.xml12
-rw-r--r--addons/base_import/models.py2
-rw-r--r--addons/calendar/calendar.py224
-rw-r--r--addons/calendar/calendar_data.xml248
-rw-r--r--addons/calendar/calendar_demo.xml2
-rw-r--r--addons/calendar/controllers/main.py27
-rw-r--r--addons/calendar/static/src/js/base_calendar.js25
-rw-r--r--addons/crm/calendar_event.py34
-rw-r--r--addons/crm/crm_lead.py4
-rw-r--r--addons/crm/crm_phonecall_menu.xml2
-rw-r--r--addons/crm/crm_phonecall_view.xml1
-rw-r--r--addons/crm/test/lead2opportunity_assign_salesmen.yml2
-rw-r--r--addons/crm/wizard/crm_lead_to_opportunity.py44
-rw-r--r--addons/crm/wizard/crm_lead_to_opportunity_view.xml2
-rw-r--r--addons/crm/wizard/crm_merge_opportunities_view.xml2
-rw-r--r--addons/crm_partner_assign/static/src/js/next.js25
-rw-r--r--addons/email_template/email_template.py72
-rw-r--r--addons/email_template/wizard/mail_compose_message.py19
-rw-r--r--addons/event/report/report_event_registration.py2
-rw-r--r--addons/event/report/report_event_registration_view.xml1
-rw-r--r--addons/google_calendar/google_calendar.py465
-rw-r--r--addons/hr_expense/hr_expense.py4
-rw-r--r--addons/hr_holidays/hr_holidays.py3
-rw-r--r--addons/hr_payroll_account/hr_payroll_account.py7
-rw-r--r--addons/l10n_multilang/l10n_multilang.py2
-rw-r--r--addons/mail/ir_attachment.py9
-rw-r--r--addons/mail/mail_mail.py4
-rw-r--r--addons/mail/mail_message.py4
-rw-r--r--addons/mail/mail_thread.py2
-rw-r--r--addons/mail/static/src/xml/mail.xml6
-rw-r--r--addons/mail/wizard/mail_compose_message.py10
-rw-r--r--addons/membership/membership_demo.xml6
-rw-r--r--addons/mrp/mrp.py5
-rw-r--r--addons/mrp_repair/__openerp__.py1
-rw-r--r--addons/mrp_repair/mrp_repair.py19
-rw-r--r--addons/mrp_repair/test/test_mrp_repair_fee.yml23
-rw-r--r--addons/payment/models/payment_acquirer.py7
-rw-r--r--addons/payment/models/res_config.py9
-rw-r--r--addons/payment_ogone/models/ogone.py8
-rw-r--r--addons/payment_paypal/models/paypal.py1
-rw-r--r--addons/payment_paypal/views/res_config_view.xml2
-rw-r--r--addons/point_of_sale/controllers/main.py4
-rw-r--r--addons/point_of_sale/point_of_sale.py18
-rw-r--r--addons/point_of_sale/static/src/js/devices.js19
-rw-r--r--addons/point_of_sale/static/src/js/models.js99
-rw-r--r--addons/portal/static/src/css/portal.css10
-rw-r--r--addons/portal_sale/portal_sale.py4
-rw-r--r--addons/portal_sale/portal_sale_view.xml4
-rw-r--r--addons/portal_sale/res_config_view.xml2
-rw-r--r--addons/procurement/procurement_view.xml2
-rw-r--r--addons/product/__openerp__.py1
-rw-r--r--addons/product/_common.py16
-rw-r--r--addons/product/pricelist.py9
-rw-r--r--addons/product/product.py21
-rw-r--r--addons/product/product_view.xml1
-rw-r--r--addons/product/test/product_uom.yml26
-rw-r--r--addons/product/tests/__init__.py5
-rw-r--r--addons/product/tests/test_uom.py37
-rw-r--r--addons/product_margin/product_margin.py4
-rw-r--r--addons/project_issue/project_issue.py7
-rw-r--r--addons/purchase/purchase_view.xml2
-rw-r--r--addons/sale/sale_view.xml4
-rw-r--r--addons/sale_crm/sale_crm_view.xml2
-rw-r--r--addons/sale_stock/sale_stock_view.xml2
-rw-r--r--addons/stock/wizard/stock_fill_inventory.py7
-rw-r--r--addons/website/controllers/main.py14
-rw-r--r--addons/website/models/ir_ui_view.py26
-rw-r--r--addons/website/models/res_config.py2
-rw-r--r--addons/website/models/website.py30
-rw-r--r--addons/website/static/lib/jquery.placeholder/jquery.placeholder.js185
-rw-r--r--addons/website/static/src/css/editor.css21
-rw-r--r--addons/website/static/src/css/editor.sass23
-rw-r--r--addons/website/static/src/css/snippets.css78
-rw-r--r--addons/website/static/src/css/snippets.sass64
-rw-r--r--addons/website/static/src/css/website.css15
-rw-r--r--addons/website/static/src/css/website.sass21
-rw-r--r--addons/website/static/src/js/website.ace.js17
-rw-r--r--addons/website/static/src/js/website.editor.js121
-rw-r--r--addons/website/static/src/js/website.js2
-rw-r--r--addons/website/static/src/js/website.mobile.js3
-rw-r--r--addons/website/static/src/js/website.snippets.animation.js4
-rw-r--r--addons/website/static/src/js/website.snippets.editor.js70
-rw-r--r--addons/website/static/src/xml/website.editor.xml20
-rw-r--r--addons/website/static/src/xml/website.snippets.xml10
-rw-r--r--addons/website/tests/test_views.py4
-rw-r--r--addons/website/views/snippets.xml12
-rw-r--r--addons/website/views/website_templates.xml6
-rw-r--r--addons/website_blog/static/src/css/website_blog.css3
-rw-r--r--addons/website_crm/views/website_crm.xml2
-rw-r--r--addons/website_event/data/event_data.xml2
-rw-r--r--addons/website_event/views/website_event.xml12
-rw-r--r--addons/website_event_sale/static/src/js/website.tour.event_sale.js6
-rw-r--r--addons/website_event_track/models/event.py1
-rw-r--r--addons/website_event_track/security/event.xml4
-rw-r--r--addons/website_event_track/views/website_event.xml7
-rw-r--r--addons/website_hr_recruitment/controllers/main.py34
-rw-r--r--addons/website_hr_recruitment/static/description/index.html4
-rw-r--r--addons/website_hr_recruitment/views/templates.xml15
-rw-r--r--addons/website_mail/controllers/email_designer.py4
-rw-r--r--addons/website_mail/models/email_template.py52
-rw-r--r--addons/website_sale/controllers/main.py81
-rw-r--r--addons/website_sale/static/src/css/website_sale.css8
-rw-r--r--addons/website_sale/static/src/css/website_sale.sass6
-rw-r--r--addons/website_sale/static/src/js/website.tour.sale.js10
-rw-r--r--addons/website_sale/static/src/js/website_sale.js8
-rw-r--r--addons/website_sale/views/website_sale.xml46
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&amp;id=#{view.id}">
+ <t t-esc="view.name"/>
+ </a> (id <t t-esc="view.id"/>)
+ </li>
+ </ul>
+ </div>
+ </t>
<t t-name="website.editor.table.panel">
<table class="editorbar-panel">
<tr t-foreach="rows"><td t-foreach="cols">&#8203;</td></tr>
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&amp;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) &gt; 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&amp;id='+str(message.id)+'&amp;method=download_attachment&amp;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&amp;field=datas&amp;id=' + str(attachment.id) + '&amp;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&amp;field=image_small&amp;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>