diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 40daace4eda..91e960c97be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,10 @@ Contributing to Odoo TL;DR -* Use this [template](https://github.com/odoo/odoo/wiki/Contributing#reporting-issues) when reporting issues, and please search for duplicates first! -* The issues not linked to pull requests will be handled with a low priority. +* If you [make a pull request](https://github.com/odoo/odoo/wiki/Contributing#making-pull-requests), + do not create an issue! Use the PR description for that +* Issues are handled with a much lower priority than pull requests +* Use this [template](https://github.com/odoo/odoo/wiki/Contributing#reporting-issues) + when reporting issues. Please search for duplicates first! * Pull requests must be made against the [correct version](https://github.com/odoo/odoo/wiki/Contributing#against-which-version-should-i-submit-a-patch) * There are restrictions on the kind of [changes allowed in stable series](https://github.com/odoo/odoo/wiki/Contributing#what-does-stable-mean) diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py index 57adb817be8..d91d7696162 100644 --- a/addons/account/account_move_line.py +++ b/addons/account/account_move_line.py @@ -1155,9 +1155,10 @@ class account_move_line(osv.osv): move_ids = set() for line in self.browse(cr, uid, ids, context=context): move_ids.add(line.move_id.id) - context['journal_id'] = line.journal_id.id - context['period_id'] = line.period_id.id - result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context) + localcontext = dict(context) + localcontext['journal_id'] = line.journal_id.id + localcontext['period_id'] = line.period_id.id + result = super(account_move_line, self).unlink(cr, uid, [line.id], context=localcontext) move_ids = list(move_ids) if check and move_ids: move_obj.validate(cr, uid, move_ids, context=context) diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index ef6767713b6..aa31a536df1 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -125,9 +125,9 @@ class calendar_attendee(osv.Model): def ics_datetime(idate, allday=False): if idate: if allday: - return datetime.strptime(idate.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT).replace(tzinfo=pytz.timezone('UTC')) + return openerp.fields.Date.from_string(idate) else: - return datetime.strptime(idate.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT).replace(tzinfo=pytz.timezone('UTC')) + return openerp.fields.Datetime.from_string(idate).replace(tzinfo=pytz.timezone('UTC')) return False try: @@ -374,12 +374,12 @@ class calendar_alarm_manager(osv.AbstractModel): tuple_params += (partner_id, ) #Add filter on hours - tuple_params += (seconds, seconds,) + tuple_params += (seconds,) cr.execute("""SELECT * FROM ( %s WHERE cal.active = True ) 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 ) + AND ALL_EVENTS.last_event_date > (now() at time zone 'utc') """ % base_request, tuple_params) for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall(): @@ -396,20 +396,30 @@ class calendar_alarm_manager(osv.AbstractModel): return res - def do_check_alarm_for_one_date(self, cr, uid, one_date, event, event_maxdelta, in_the_next_X_seconds, after=False, notif=True, mail=True, context=None): - res = [] - alarm_type = [] + def do_check_alarm_for_one_date(self, cr, uid, one_date, event, event_maxdelta, in_the_next_X_seconds, after=False, notif=True, mail=True, missing=False, context=None): + # one_date: date of the event to check (not the same that in the event browse if recurrent) + # event: Event browse record + # event_maxdelta: biggest duration from alarms for this event + # in_the_next_X_seconds: looking in the future (in seconds) + # after: if not False: will return alert if after this date (date as string - todo: change in master) + # missing: if not False: will return alert even if we are too late + # notif: Looking for type notification + # mail: looking for type email + res = [] + + # TODO: replace notif and email in master by alarm_type + remove event_maxdelta and if using it + alarm_type = [] if notif: alarm_type.append('notification') if mail: alarm_type.append('email') - if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date + if one_date - timedelta(minutes=(missing and 0 or event_maxdelta)) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date for alarm in event.alarm_ids: if alarm.type in alarm_type and \ - one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \ - (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)): + one_date - timedelta(minutes=(missing and 0 or alarm.duration_minutes)) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \ + (not after or one_date - timedelta(minutes=alarm.duration_minutes) > openerp.fields.Datetime.from_string(after)): alert = { 'alarm_id': alarm.id, 'event_id': event.id, @@ -419,55 +429,53 @@ class calendar_alarm_manager(osv.AbstractModel): return res def get_next_mail(self, cr, uid, context=None): + now = openerp.fields.Datetime.to_string(datetime.now()) + + icp = self.pool['ir.config_parameter'] + last_notif_mail = icp.get_param(cr, SUPERUSER_ID, 'calendar.last_notif_mail', default=False) or now + try: - cron = self.pool['ir.model.data'].get_object( - cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context) + cron = self.pool['ir.model.data'].get_object(cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context) except ValueError: _logger.error("Cron for " + self._name + " can not be identified !") return False - 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": - cron_interval = cron.interval_number * 60 * 60 - elif cron.interval_type == "minutes": - cron_interval = cron.interval_number * 60 - elif cron.interval_type == "seconds": - cron_interval = cron.interval_number - else: - cron_interval = False + interval_to_second = { + "weeks": 7 * 24 * 60 * 60, + "days": 24 * 60 * 60, + "hours": 60 * 60, + "minutes": 60, + "seconds": 1 + } - if not cron_interval: + if cron.interval_type not in interval_to_second.keys(): _logger.error("Cron delay can not be computed !") return False + cron_interval = cron.interval_number * interval_to_second[cron.interval_type] + 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 curEvent in self.pool.get('calendar.event').browse(cr, uid, all_events.keys(), context=context): + max_delta = all_events[curEvent.id]['max_duration'] + if curEvent.recurrency: - bFound = False - LastFound = False + at_least_one = False + last_found = False for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context): in_date_format = one_date.replace(tzinfo=None) - 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) - - 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 + last_found = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, 0, after=last_notif_mail, notif=False, missing=True, context=context) + for alert in last_found: + self.do_mail_reminder(cr, uid, alert, context=context) + at_least_one = True # if it's the first alarm for this recurrent event + if at_least_one and not last_found: # 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.start, DEFAULT_SERVER_DATETIME_FORMAT) - 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) + last_found = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, 0, after=last_notif_mail, notif=False, missing=True, context=context) + for alert in last_found: + self.do_mail_reminder(cr, uid, alert, context=context) + icp.set_param(cr, SUPERUSER_ID, 'calendar.last_notif_mail', now) def get_next_notif(self, cr, uid, context=None): ajax_check_every_seconds = 300 @@ -497,7 +505,7 @@ class calendar_alarm_manager(osv.AbstractModel): break else: in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT) - 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) + 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)) @@ -516,6 +524,7 @@ class calendar_alarm_manager(osv.AbstractModel): cr, uid, [att.id for att in event.attendee_ids], + email_from=event.user_id.partner_id.email, template_xmlid='calendar_template_meeting_reminder', force=True, context=context @@ -885,12 +894,12 @@ class calendar_event(osv.Model): stop_date = values.get('stop_date') or event.stop_date start_date = values.get('start_date') or event.start_date if stop_date and start_date: - diff = datetime.strptime(stop_date.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) - datetime.strptime(start_date.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) + diff = openerp.fields.Date.from_string(stop_date) - openerp.fields.Date.from_string(start_date) elif values.get('stop_datetime') or values.get('start_datetime'): stop_datetime = values.get('stop_datetime') or event.stop_datetime start_datetime = values.get('start_datetime') or event.start_datetime if stop_datetime and start_datetime: - diff = datetime.strptime(stop_datetime.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime(start_datetime.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) + diff = openerp.fields.Datetime.from_string(stop_datetime) - openerp.fields.Datetime.from_string(start_datetime) if diff: duration = float(diff.days) * 24 + (float(diff.seconds) / 3600) values['duration'] = round(duration, 2) @@ -1012,7 +1021,7 @@ class calendar_event(osv.Model): tz = pytz.timezone(user.tz) if user.tz else pytz.utc if starttime: - start = datetime.strptime(starttime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) + start = openerp.fields.Datetime.from_string(starttime) startdate = tz.localize(start) # Add "+hh:mm" timezone startdate = startdate.replace(hour=8) # Set 8 AM in localtime startdate = startdate.astimezone(pytz.utc) # Convert to UTC @@ -1433,7 +1442,7 @@ class calendar_event(osv.Model): def get_interval(self, cr, uid, ids, date, interval, tz=None, context=None): #Function used only in calendar_event_data.xml for email template - date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) + date = openerp.fields.Datetime.from_string(date) if tz: timezone = pytz.timezone(tz or 'UTC') diff --git a/addons/calendar/calendar_data.xml b/addons/calendar/calendar_data.xml index 71c4e77e289..d23713efca4 100644 --- a/addons/calendar/calendar_data.xml +++ b/addons/calendar/calendar_data.xml @@ -38,39 +38,19 @@ days notification - - - + - 15 min mail - - minutes + 2~3 hours mail + + hours email - 30 min mail - - minutes - email - - - 1 hour mail - + 5~6 hours mail + hours email - - 2 hours mail - - hours - email - - - 1 day mail - - days - email - Customer Meeting diff --git a/addons/calendar/calendar_view.xml b/addons/calendar/calendar_view.xml index 155292c3e4e..725a7b7f48e 100644 --- a/addons/calendar/calendar_view.xml +++ b/addons/calendar/calendar_view.xml @@ -21,6 +21,27 @@ form + + + + Calendar Alarm Tree + calendar.alarm + + + + + + + + + + + + Calendar Alarm + calendar.alarm + form + + @@ -226,7 +247,6 @@ - @@ -311,10 +331,13 @@ + + - + + Meetings diff --git a/addons/event_sale/event_sale_data.xml b/addons/event_sale/event_sale_data.xml index 9c5dae6042b..38e1f2434fb 100644 --- a/addons/event_sale/event_sale_data.xml +++ b/addons/event_sale/event_sale_data.xml @@ -1,6 +1,6 @@ - + Generic Events diff --git a/addons/stock/stock.py b/addons/stock/stock.py index b10a118406a..dae2f0d82ce 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -324,6 +324,11 @@ class stock_quant(osv.osv): 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.quant', context=c), } + def init(self, cr): + cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('stock_quant_product_location_index',)) + if not cr.fetchone(): + cr.execute('CREATE INDEX stock_quant_product_location_index ON stock_quant (product_id, location_id, company_id, qty, in_date, reservation_id)') + def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True): ''' Overwrite the read_group in order to sum the function field 'inventory_value' in group by''' res = super(stock_quant, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy) @@ -415,7 +420,12 @@ class stock_quant(osv.osv): self.move_quants_write(cr, uid, to_move_quants, move, location_to, dest_package_id, context=context) self.pool.get('stock.move').recalculate_move_state(cr, uid, to_recompute_move_ids, context=context) if location_to.usage == 'internal': - if self.search(cr, uid, [('product_id', '=', move.product_id.id), ('qty','<', 0)], limit=1, context=context): + # Do manual search for quant to avoid full table scan (order by id) + cr.execute(""" + SELECT 0 FROM stock_quant, stock_location WHERE product_id = %s AND stock_location.id = stock_quant.location_id AND + ((stock_location.parent_left >= %s AND stock_location.parent_left < %s) OR stock_location.id = %s) AND qty < 0.0 LIMIT 1 + """, (move.product_id.id, location_to.parent_left, location_to.parent_right, location_to.id)) + if cr.fetchone(): for quant in quants_reconcile: self._quant_reconcile_negative(cr, uid, quant, move, context=context) @@ -529,7 +539,10 @@ class stock_quant(osv.osv): return False qty_round = float_round(qty, precision_rounding=rounding) new_qty_round = float_round(quant.qty - qty, precision_rounding=rounding) - new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': new_qty_round, 'history_ids': [(4, x.id) for x in quant.history_ids]}, context=context) + # Fetch the history_ids manually as it will not do a join with the stock moves then (=> a lot faster) + cr.execute("""SELECT move_id FROM stock_quant_move_rel WHERE quant_id = %s""", (quant.id,)) + res = cr.fetchall() + new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': new_qty_round, 'history_ids': [(4, x[0]) for x in res]}, context=context) self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty_round}, context=context) return self.browse(cr, uid, new_quant, context=context) @@ -1858,6 +1871,10 @@ class stock_move(osv.osv): 'You try to move a product using a UoM that is not compatible with the UoM of the product moved. Please use an UoM in the same UoM category.', ['product_uom']), ] + def init(self, cr): + cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('stock_move_product_location_index',)) + if not cr.fetchone(): + cr.execute('CREATE INDEX stock_move_product_location_index ON stock_move (product_id, location_id, location_dest_id, company_id, state)') @api.cr_uid_ids_context def do_unreserve(self, cr, uid, move_ids, context=None): diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 755d11061cd..62d7d0c745d 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -1250,7 +1250,7 @@ instance.web.Client = instance.web.Widget.extend({ this.crashmanager = new instance.web.CrashManager(); instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error); self.notification = new instance.web.Notification(this); - self.notification.appendTo(self.$el); + self.notification.appendTo(self.$el.find('.openerp')); self.loading = new instance.web.Loading(self); self.loading.appendTo(self.$('.openerp_webclient_container')); self.action_manager = new instance.web.ActionManager(self); diff --git a/addons/website/tests/test_crawl.py b/addons/website/tests/test_crawl.py index 7078a8c79d0..52865e23a2a 100644 --- a/addons/website/tests/test_crawl.py +++ b/addons/website/tests/test_crawl.py @@ -1,15 +1,12 @@ # -*- coding: utf-8 -*- import logging import urlparse -import unittest2 -import urllib2 import time -import werkzeug.urls import lxml.html import openerp -from openerp import tools +import re _logger = logging.getLogger(__name__) @@ -25,12 +22,15 @@ class Crawler(openerp.tests.HttpCase): post_install = True def crawl(self, url, seen=None, msg=''): - if seen == None: + if seen == None: seen = set() - if url in seen: + + url_slug = re.sub(r"[/](([^/=?&]+-)?[0-9]+)([/]|$)", '//', url) + url_slug = re.sub(r"([^/=?&]+)=[^/=?&]+", '\g<1>=param', url_slug) + if url_slug in seen: return seen else: - seen.add(url) + seen.add(url_slug) _logger.info("%s %s", msg, url) r = self.url_open(url) diff --git a/addons/website_membership/controllers/main.py b/addons/website_membership/controllers/main.py index c88fb43da9d..942c3f13267 100644 --- a/addons/website_membership/controllers/main.py +++ b/addons/website_membership/controllers/main.py @@ -14,18 +14,18 @@ class WebsiteMembership(http.Controller): @http.route([ '/members', '/members/page/', - '/members/association/', - '/members/association//page/', + '/members/association/', + '/members/association//page/', '/members/country/', '/members/country/-', '/members/country//page/', '/members/country/-/page/', - '/members/association//country/-', - '/members/association//country/', - '/members/association//country/-/page/', - '/members/association//country//page/', + '/members/association//country/-', + '/members/association//country/', + '/members/association//country/-/page/', + '/members/association//country//page/', ], type='http', auth="public", website=True) def members(self, membership_id=None, country_name=None, country_id=0, page=0, **post): cr, uid, context = request.cr, request.uid, request.context @@ -38,7 +38,8 @@ class WebsiteMembership(http.Controller): # base domain for groupby / searches base_line_domain = [("partner.website_published", "=", True), ('state', 'in', ['free', 'paid'])] - if membership_id: + if membership_id and membership_id != 'free': + membership_id = int(membership_id) base_line_domain.append(('membership_id', '=', membership_id)) membership = product_obj.browse(cr, uid, membership_id, context=context) else: @@ -48,9 +49,14 @@ class WebsiteMembership(http.Controller): ('partner.website_description', 'ilike', post_name)] # group by country, based on all customers (base domain) - membership_line_ids = membership_line_obj.search(cr, SUPERUSER_ID, base_line_domain, context=context) + if membership_id != 'free': + membership_line_ids = membership_line_obj.search(cr, SUPERUSER_ID, base_line_domain, context=context) + country_domain = ('member_lines', 'in', membership_line_ids) + else: + membership_line_ids = [] + country_domain = ('free_member', '=', True) countries = partner_obj.read_group( - cr, uid, [('member_lines', 'in', membership_line_ids), ("website_published", "=", True)], ["id", "country_id"], + cr, uid, [country_domain, ("website_published", "=", True)], ["id", "country_id"], groupby="country_id", orderby="country_id", context=request.context) countries_total = sum(country_dict['country_id_count'] for country_dict in countries) @@ -77,22 +83,38 @@ class WebsiteMembership(http.Controller): line_domain.append(('membership_id', 'in', membership_ids)) # displayed membership lines - membership_line_ids = membership_line_obj.search(cr, SUPERUSER_ID, line_domain, context=context) + if membership_id != 'free': + membership_line_ids = membership_line_obj.search(cr, SUPERUSER_ID, line_domain, context=context) membership_lines = membership_line_obj.browse(cr, uid, membership_line_ids, context=context) + # TODO: Following line can be deleted in master. Kept for retrocompatibility. membership_lines = sorted(membership_lines, key=lambda x: x.membership_id.website_sequence) partner_ids = [m.partner.id for m in membership_lines] + free_partner_ids = partner_obj.search(cr, uid, [('free_member', '=', True), ('website_published', '=', True)], context=context) google_map_partner_ids = ",".join(map(str, partner_ids)) + memberships_data = [] + for membership_record in memberships: + memberships_data.append({'id': membership_record.id, 'name': membership_record.name}) + memberships_partner_ids = {} + for line in membership_lines: + memberships_partner_ids.setdefault(line.membership_id.id, []).append(line.partner.id) + if free_partner_ids: + memberships_data.append({'id': 'free', 'name': _('Free Members')}) + if not membership_id or membership_id == 'free': + memberships_partner_ids['free'] = free_partner_ids - partners = dict((p.id, p) for p in partner_obj.browse(request.cr, SUPERUSER_ID, partner_ids, request.context)) + partners = dict((p.id, p) for p in partner_obj.browse(request.cr, SUPERUSER_ID, list(set(partner_ids + free_partner_ids)), request.context)) # request pager for lines pager = request.website.pager(url="/members", total=len(membership_line_ids), page=page, step=self._references_per_page, scope=7, url_args=post) values = { 'partners': partners, - 'membership_lines': membership_lines, - 'memberships': memberships, - 'membership': membership, + 'membership_lines': membership_lines, # TODO: This line can be deleted in master. Kept for retrocompatibility. + 'memberships': memberships, # TODO: This line too. + 'membership': membership, # TODO: This line too. + 'memberships_data': memberships_data, + 'memberships_partner_ids': memberships_partner_ids, + 'membership_id': membership_id, 'countries': countries, 'current_country': current_country and [current_country['id'], current_country['name']] or None, 'current_country_id': current_country and current_country['id'] or 0, diff --git a/addons/website_membership/views/website_membership.xml b/addons/website_membership/views/website_membership.xml index 4f5e98faaf4..a5b5b4af062 100644 --- a/addons/website_membership/views/website_membership.xml +++ b/addons/website_membership/views/website_membership.xml @@ -31,10 +31,10 @@
@@ -51,27 +51,28 @@
- +

No result found.

- - - -

+ + +

+ + +
+ +
+ + + +
+
+
+ - -
- -
- - - -
-
-