diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py index 640ac90249b..be41b06e346 100644 --- a/addons/account/account_move_line.py +++ b/addons/account/account_move_line.py @@ -1026,7 +1026,7 @@ class account_move_line(osv.osv): if opening_reconciliation: obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False}) obj_move_rec.unlink(cr, uid, unlink_ids) - if all_moves: + if len(all_moves) >= 2: obj_move_line.reconcile_partial(cr, uid, all_moves, 'auto',context=context) return True diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index d69cd04b6be..785d7cd6db0 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -542,18 +542,17 @@ class account_analytic_account(osv.osv): 'nodestroy': True, } - def on_change_template(self, cr, uid, ids, template_id, date_start=False, fix_price_invoices=False, invoice_on_timesheets=False, recurring_invoices=False, context=None): + def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None): if not template_id: return {} - obj_analytic_line = self.pool.get('account.analytic.invoice.line') res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context) template = self.browse(cr, uid, template_id, context=context) - if not fix_price_invoices: + if not ids: res['value']['fix_price_invoices'] = template.fix_price_invoices res['value']['amount_max'] = template.amount_max - if not invoice_on_timesheets: + if not ids: res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets res['value']['hours_qtt_est'] = template.hours_qtt_est @@ -561,7 +560,7 @@ class account_analytic_account(osv.osv): res['value']['to_invoice'] = template.to_invoice.id if template.pricelist_id.id: res['value']['pricelist_id'] = template.pricelist_id.id - if not recurring_invoices: + if not ids: invoice_line_ids = [] for x in template.recurring_invoice_line_ids: invoice_line_ids.append((0, 0, { @@ -690,8 +689,9 @@ class account_analytic_account(osv.osv): } return invoice - def _prepare_invoice_lines(self, cr, uid, contract, fiscal_position, context=None): + def _prepare_invoice_lines(self, cr, uid, contract, fiscal_position_id, context=None): fpos_obj = self.pool.get('account.fiscal.position') + fiscal_position = fpos_obj.browse(cr, uid, fiscal_position_id, context=context) invoice_lines = [] for line in contract.recurring_invoice_line_ids: diff --git a/addons/account_analytic_analysis/account_analytic_analysis_view.xml b/addons/account_analytic_analysis/account_analytic_analysis_view.xml index 97fb15e9290..a6fe828926a 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis_view.xml +++ b/addons/account_analytic_analysis/account_analytic_analysis_view.xml @@ -38,9 +38,6 @@ {'required': [('type','=','contract'),'|','|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True), ('recurring_invoices', '=', True)]} - - on_change_template(template_id, date_start, fix_price_invoices, invoice_on_timesheets, recurring_invoices) - diff --git a/addons/auth_oauth/auth_oauth_view.xml b/addons/auth_oauth/auth_oauth_view.xml index ffc39752ec6..5c889d3a686 100644 --- a/addons/auth_oauth/auth_oauth_view.xml +++ b/addons/auth_oauth/auth_oauth_view.xml @@ -11,6 +11,7 @@ + diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml index 1abcdac9712..fef80bb8a53 100644 --- a/addons/crm/crm_lead_data.xml +++ b/addons/crm/crm_lead_data.xml @@ -14,6 +14,7 @@ Dead 1 + 1 0 1 30 diff --git a/addons/crm/static/description/index.html b/addons/crm/static/description/index.html index 70f52d0ca87..d1fe80f0275 100644 --- a/addons/crm/static/description/index.html +++ b/addons/crm/static/description/index.html @@ -88,7 +88,7 @@ Find duplicates, merge leads and assign them to the right salesperson in one ope

Get your opportunities organized to stay focused on the best deals. Manage all your customer interactions from the opportunity like emails, phone calls, internal notes, meetings and quotations.

-Follow opportunities that interrests you to get notified upon specific events: deal won or lost, stage changed, new customer demand, etc. +Follow opportunities that interest you to get notified upon specific events: deal won or lost, stage changed, new customer demand, etc.

diff --git a/addons/event/email_template.xml b/addons/event/email_template.xml index d99774acd3c..d4479bcfc87 100644 --- a/addons/event/email_template.xml +++ b/addons/event/email_template.xml @@ -9,7 +9,7 @@ Your registration at ${object.event_id.name} Hello ${object.name},

-

The event ${object.event_id.name} that you registered for is confirmed and will be held from ${object.event_id.date_begin} to ${object.event_id.date_end}. +

The event ${object.event_id.name} that you registered for is confirmed and will be held from ${object.event_id.date_begin_located.strftime('%Y-%m-%d %H:%M:%S (%Z)')} to ${object.event_id.date_end_located.strftime('%Y-%m-%d %H:%M:%S (%Z)')}. For any further information please contact our event department.

Thank you for your participation!

Best regards

]]>
diff --git a/addons/event/event.py b/addons/event/event.py index 37efedf2549..8021cce19ae 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -18,7 +18,8 @@ # along with this program. If not, see . # ############################################################################## - +import pytz +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from datetime import datetime, timedelta from openerp.osv import fields, osv from openerp.tools.translate import _ @@ -150,6 +151,22 @@ class event_event(osv.osv): continue return res + def _compute_date_tz(self, cr, uid, ids, fld, arg, context=None): + if context is None: + context = {} + res = {} + for event in self.browse(cr, uid, ids, context=context): + ctx = dict(context, tz=(event.date_tz or 'UTC')) + if fld == 'date_begin_located': + date_to_convert = event.date_begin + elif fld == 'date_end_located': + date_to_convert = event.date_end + res[event.id] = fields.datetime.context_timestamp(cr, uid, datetime.strptime(date_to_convert, DEFAULT_SERVER_DATETIME_FORMAT), context=ctx) + return res + + def _tz_get(self, cr, uid, context=None): + return [(x, x) for x in pytz.all_timezones] + _columns = { 'name': fields.char('Event Name', size=64, required=True, translate=True, readonly=False, states={'done': [('readonly', True)]}), 'user_id': fields.many2one('res.users', 'Responsible User', readonly=False, states={'done': [('readonly', True)]}), @@ -169,8 +186,11 @@ class event_event(osv.osv): store={'event.registration': (_get_events_from_registrations, ['state'], 10), 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max', 'registration_ids'], 20)}), 'registration_ids': fields.one2many('event.registration', 'event_id', 'Registrations', readonly=False, states={'done': [('readonly', True)]}), + 'date_tz': fields.selection(_tz_get, string='Timezone'), 'date_begin': fields.datetime('Start Date', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'date_end': fields.datetime('End Date', required=True, readonly=True, states={'draft': [('readonly', False)]}), + 'date_begin_located': fields.function(_compute_date_tz, string='Start Date Located', type="datetime"), + 'date_end_located': fields.function(_compute_date_tz, string='End Date Located', type="datetime"), 'state': fields.selection([ ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), @@ -197,7 +217,8 @@ class event_event(osv.osv): 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'event.event', context=c), 'user_id': lambda obj, cr, uid, context: uid, 'organizer_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, context=c).company_id.partner_id.id, - 'address_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, context=c).company_id.partner_id.id + 'address_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, context=c).company_id.partner_id.id, + 'date_tz': lambda self, cr, uid, ctx: ctx.get('tz', "UTC"), } def _check_seats_limit(self, cr, uid, ids, context=None): diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml index 2cdb74bea5b..f608724c9a2 100644 --- a/addons/event/event_view.xml +++ b/addons/event/event_view.xml @@ -83,6 +83,7 @@ +
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index ecb4c495d94..63c23f30cff 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -217,15 +217,7 @@ class gamification_challenge(osv.Model): vals['user_ids'] = [] vals['user_ids'] += [(4, user_id) for user_id in user_ids] - create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context) - - # subscribe new users to the challenge - if vals.get('user_ids'): - # done with browse after super to be sure catch all after orm process - challenge = self.browse(cr, uid, create_res, context=context) - self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context) - - return create_res + return super(gamification_challenge, self).create(cr, uid, vals, context=context) def write(self, cr, uid, ids, vals, context=None): if isinstance(ids, (int,long)): @@ -240,6 +232,11 @@ class gamification_challenge(osv.Model): write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context) + if vals.get('report_message_frequency', 'never') != 'never': + # _recompute_challenge_users do not set users for challenges with no reports, subscribing them now + for challenge in self.browse(cr, uid, ids, context=context): + self.message_subscribe(cr, uid, [challenge.id], [user.partner_id.id for user in challenge.user_ids], context=context) + if vals.get('state') == 'inprogress': self._recompute_challenge_users(cr, uid, ids, context=context) self._generate_goals_from_challenge(cr, uid, ids, context=context) @@ -264,6 +261,9 @@ class gamification_challenge(osv.Model): - Create the missing goals (eg: modified the challenge to add lines) - Update every running challenge """ + if context is None: + context = {} + # start scheduled challenges planned_challenge_ids = self.search(cr, uid, [ ('state', '=', 'draft'), @@ -281,6 +281,9 @@ class gamification_challenge(osv.Model): if not ids: ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context) + # in cron mode, will do intermediate commits + # TODO in trunk: replace by parameter + context.update({'commit_gamification': True}) return self._update_all(cr, uid, ids, context=context) def _update_all(self, cr, uid, ids, context=None): @@ -355,11 +358,6 @@ class gamification_challenge(osv.Model): if write_op: self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context) - if to_remove_ids: - self.message_unsubscribe_users(cr, uid, [challenge.id], to_remove_ids, context=None) - if to_add_ids: - self.message_subscribe_users(cr, uid, [challenge.id], to_add_ids, context=context) - return True @@ -393,9 +391,9 @@ class gamification_challenge(osv.Model): :param list(int) ids: the list of challenge concerned""" goal_obj = self.pool.get('gamification.goal') - to_update = [] for challenge in self.browse(cr, uid, ids, context=context): (start_date, end_date) = start_end_date_for_period(challenge.period) + to_update = [] # if no periodicity, use challenge dates if not start_date and challenge.start_date: @@ -426,7 +424,15 @@ class gamification_challenge(osv.Model): cr.execute(query, query_params) user_with_goal_ids = cr.dictfetchall() - user_without_goal_ids = list(set([user.id for user in challenge.user_ids]) - set([user['user_id'] for user in user_with_goal_ids])) + + participant_user_ids = [user.id for user in challenge.user_ids] + user_without_goal_ids = list(set(participant_user_ids) - set([user['user_id'] for user in user_with_goal_ids])) + user_squating_challenge_ids = list(set([user['user_id'] for user in user_with_goal_ids]) - set(participant_user_ids)) + if user_squating_challenge_ids: + # users that used to match the challenge + goal_to_remove_ids = goal_obj.search(cr, uid, [('challenge_id', '=', challenge.id), ('user_id', 'in', user_squating_challenge_ids)], context=context) + goal_obj.unlink(cr, uid, goal_to_remove_ids, context=context) + values = { 'definition_id': line.definition_id.id, @@ -614,9 +620,10 @@ class gamification_challenge(osv.Model): ctx.update({'challenge_lines': lines_boards}) body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx) - # send to every follower of the challenge + # send to every follower and participant of the challenge self.message_post(cr, uid, challenge.id, body=body_html, + partner_ids=[user.partner_id.id for user in challenge.user_ids], context=context, subtype='mail.mt_comment') if challenge.report_message_group_id: @@ -698,34 +705,35 @@ class gamification_challenge(osv.Model): rewarded_users = [] challenge_ended = end_date == yesterday.strftime(DF) or force if challenge.reward_id and challenge_ended or challenge.reward_realtime: - for user in challenge.user_ids: - reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [ - ('challenge_id', '=', challenge.id), - ('user_id', '=', user.id), - ('start_date', '=', start_date), - ('end_date', '=', end_date), - ('state', '=', 'reached') - ], context=context) - if len(reached_goal_ids) == len(challenge.line_ids): + # not using start_date as intemportal goals have a start date but no end_date + reached_goals = self.pool.get('gamification.goal').read_group(cr, uid, [ + ('challenge_id', '=', challenge.id), + ('end_date', '=', end_date), + ('state', '=', 'reached') + ], fields=['user_id'], groupby=['user_id'], context=context) + for reach_goals_user in reached_goals: + if reach_goals_user['user_id_count'] == len(challenge.line_ids): # the user has succeeded every assigned goal + user_id = reach_goals_user['user_id'][0] if challenge.reward_realtime: badges = self.pool['gamification.badge.user'].search(cr, uid, [ ('challenge_id', '=', challenge.id), ('badge_id', '=', challenge.reward_id.id), - ('user_id', '=', user.id), + ('user_id', '=', user_id), ], count=True, context=context) if badges > 0: # has already recieved the badge for this challenge continue - self.reward_user(cr, uid, user.id, challenge.reward_id.id, challenge.id, context=context) - rewarded_users.append(user) + self.reward_user(cr, uid, user_id, challenge.reward_id.id, challenge.id, context=context) + rewarded_users.append(user_id) if challenge_ended: # open chatter message message_body = _("The challenge %s is finished." % challenge.name) if rewarded_users: - message_body += _("
Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([user.name for user in rewarded_users]))) + user_names = self.pool['res.users'].name_get(cr, uid, rewarded_users, context=context) + message_body += _("
Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([name for (user_id, name) in user_names]))) else: message_body += _("
Nobody has succeeded to reach every goal, no badge is rewared for this challenge.") @@ -746,7 +754,10 @@ class gamification_challenge(osv.Model): self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, challenge.id, context=context) message_body += "
3. %s - %s" % (third_user.name, challenge.reward_third_id.name) - self.message_post(cr, uid, challenge.id, body=message_body, context=context) + self.message_post(cr, uid, challenge.id, + partner_ids=[user.partner_id.id for user in challenge.user_ids], + body=message_body, + context=context) return True diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index af9a763236d..b4a47a0d07a 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -138,7 +138,6 @@ class gamification_goal(osv.Model): _name = 'gamification.goal' _description = 'Gamification goal instance' - _inherit = 'mail.thread' def _get_completion(self, cr, uid, ids, field_name, arg, context=None): """Return the percentage of completeness of the goal, between 0 and 100""" @@ -226,8 +225,7 @@ class gamification_goal(osv.Model): temp_obj = self.pool.get('email.template') template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context) body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context) - - self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment') + self.pool['mail.thread'].message_post(cr, uid, 0, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment') return {'to_update': True} return {} @@ -241,9 +239,9 @@ class gamification_goal(osv.Model): the target value being reached, the goal is set as failed.""" if context is None: context = {} + commit = context.get('commit_gamification', False) goals_by_definition = {} - goals_to_write = {} all_goals = {} for goal in self.browse(cr, uid, ids, context=context): if goal.state in ('draft', 'canceled'): @@ -251,10 +249,10 @@ class gamification_goal(osv.Model): continue goals_by_definition.setdefault(goal.definition_id, []).append(goal) - goals_to_write[goal.id] = {} all_goals[goal.id] = goal for definition, goals in goals_by_definition.items(): + goals_to_write = dict((goal.id, {}) for goal in goals) if definition.computation_mode == 'manually': for goal in goals: goals_to_write[goal.id].update(self._check_remind_delay(cr, uid, goal, context)) @@ -345,22 +343,24 @@ class gamification_goal(osv.Model): if new_value != goal.current: goals_to_write[goal.id]['current'] = new_value - for goal_id, value in goals_to_write.items(): - if not value: - continue - goal = all_goals[goal_id] + for goal_id, value in goals_to_write.items(): + if not value: + continue + goal = all_goals[goal_id] - # check goal target reached - if (goal.definition_condition == 'higher' and value.get('current', goal.current) >= goal.target_goal) \ - or (goal.definition_condition == 'lower' and value.get('current', goal.current) <= goal.target_goal): - value['state'] = 'reached' + # check goal target reached + if (goal.definition_condition == 'higher' and value.get('current', goal.current) >= goal.target_goal) \ + or (goal.definition_condition == 'lower' and value.get('current', goal.current) <= goal.target_goal): + value['state'] = 'reached' - # check goal failure - elif goal.end_date and fields.date.today() > goal.end_date: - value['state'] = 'failed' - value['closed'] = True - if value: - self.write(cr, uid, [goal.id], value, context=context) + # check goal failure + elif goal.end_date and fields.date.today() > goal.end_date: + value['state'] = 'failed' + value['closed'] = True + if value: + self.write(cr, uid, [goal.id], value, context=context) + if commit: + cr.commit() return True def action_start(self, cr, uid, ids, context=None): diff --git a/addons/gamification/views/goal.xml b/addons/gamification/views/goal.xml index 528d5db0511..884467404ce 100644 --- a/addons/gamification/views/goal.xml +++ b/addons/gamification/views/goal.xml @@ -86,10 +86,6 @@ -
- - -
diff --git a/addons/hr/hr.py b/addons/hr/hr.py index a13a1866493..5bb172eb082 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -248,7 +248,13 @@ class hr_employee(osv.osv): 'image': _get_default_image, 'color': 0, } - + + def copy_data(self, cr, uid, ids, default=None, context=None): + if default is None: + default = {} + default.update({'child_ids': False}) + return super(hr_employee, self).copy_data(cr, uid, ids, default, context=context) + def _broadcast_welcome(self, cr, uid, employee_id, context=None): """ Broadcast the welcome message to all users in the employee company. """ employee = self.browse(cr, uid, employee_id, context=context) @@ -421,18 +427,23 @@ class hr_department(osv.osv): res.append((record['id'], name)) return res - def copy(self, cr, uid, ids, default=None, context=None): + def copy_data(self, cr, uid, ids, default=None, context=None): if default is None: default = {} - default = default.copy() default['member_ids'] = [] - return super(hr_department, self).copy(cr, uid, ids, default, context=context) + return super(hr_department, self).copy_data(cr, uid, ids, default, context=context) class res_users(osv.osv): _name = 'res.users' _inherit = 'res.users' + def copy_data(self, cr, uid, ids, default=None, context=None): + if default is None: + default = {} + default.update({'employee_ids': False}) + return super(res_users, self).copy_data(cr, uid, ids, default, context=context) + _columns = { 'employee_ids': fields.one2many('hr.employee', 'user_id', 'Related employees'), } diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py index a4674fc2adc..40362096554 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py @@ -22,9 +22,11 @@ import time from datetime import datetime from dateutil.relativedelta import relativedelta +from pytz import timezone +import pytz from openerp.osv import fields, osv -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.translate import _ class hr_timesheet_sheet(osv.osv): @@ -397,22 +399,56 @@ class hr_attendance(osv.osv): attendance_ids.extend([row[0] for row in cr.fetchall()]) return attendance_ids - def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None): + def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None): + """ Simulate timesheet in employee timezone + + Return the attendance date in string format in the employee + tz converted from utc timezone as we consider date of employee + timesheet is in employee timezone + """ + employee_obj = self.pool['hr.employee'] + + tz = False + if employee_id: + employee = employee_obj.browse(cr, uid, employee_id, context=context) + tz = employee.user_id.partner_id.tz + if not date: date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - # ending date with no time to avoid timesheet with early date_to - date_to = date[0:10]+' 00:00:00' - # limit=1 because only one sheet possible for an employee between 2 dates - sheet_ids = self.pool.get('hr_timesheet_sheet.sheet').search(cr, uid, [ - ('date_to', '>=', date_to), ('date_from', '<=', date), - ('employee_id', '=', employee_id) - ], limit=1, context=context) + + att_tz = timezone(tz or 'utc') + + attendance_dt = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT) + att_tz_dt = pytz.utc.localize(attendance_dt) + att_tz_dt = att_tz_dt.astimezone(att_tz) + # We take only the date omiting the hours as we compare with timesheet + # date_from which is a date format thus using hours would lead to + # be out of scope of timesheet + att_tz_date_str = datetime.strftime(att_tz_dt, DEFAULT_SERVER_DATE_FORMAT) + return att_tz_date_str + + def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None): + + sheet_obj = self.pool['hr_timesheet_sheet.sheet'] + if not date: + date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + + att_tz_date_str = self._get_attendance_employee_tz( + cr, uid, employee_id, + date=date, context=context) + sheet_ids = sheet_obj.search(cr, uid, + [('date_from', '<=', att_tz_date_str), + ('date_to', '>=', att_tz_date_str), + ('employee_id', '=', employee_id)], + limit=1, context=context) return sheet_ids and sheet_ids[0] or False def _sheet(self, cursor, user, ids, name, args, context=None): res = {}.fromkeys(ids, False) for attendance in self.browse(cursor, user, ids, context=context): - res[attendance.id] = self._get_current_sheet(cursor, user, attendance.employee_id.id, attendance.name, context=context) + res[attendance.id] = self._get_current_sheet( + cursor, user, attendance.employee_id.id, attendance.name, + context=context) return res _columns = { @@ -434,10 +470,13 @@ class hr_attendance(osv.osv): sheet_id = context.get('sheet_id') or self._get_current_sheet(cr, uid, vals.get('employee_id'), vals.get('name'), context=context) if sheet_id: + att_tz_date_str = self._get_attendance_employee_tz( + cr, uid, vals.get('employee_id'), + date=vals.get('name'), context=context) ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, sheet_id, context=context) if ts.state not in ('draft', 'new'): raise osv.except_osv(_('Error!'), _('You can not enter an attendance in a submitted timesheet. Ask your manager to reset it before adding attendance.')) - elif ts.date_from > vals.get('name') or ts.date_to < vals.get('name'): + elif ts.date_from > att_tz_date_str or ts.date_to < att_tz_date_str: raise osv.except_osv(_('User Error!'), _('You can not enter an attendance date outside the current timesheet dates.')) return super(hr_attendance,self).create(cr, uid, vals, context=context) @@ -589,4 +628,3 @@ class res_company(osv.osv): # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index a913d3c067d..4a069cb5428 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -6,6 +6,7 @@ from openerp import SUPERUSER_ID from openerp import http from openerp.http import request from openerp.addons.web.controllers.main import content_disposition +import mimetypes class MailController(http.Controller): @@ -19,10 +20,11 @@ class MailController(http.Controller): if res: filecontent = base64.b64decode(res.get('base64')) filename = res.get('filename') + content_type = mimetypes.guess_type(filename) if filecontent and filename: return request.make_response( filecontent, - headers=[('Content-Type', 'application/octet-stream'), + headers=[('Content-Type', content_type[0] or 'application/octet-stream'), ('Content-Disposition', content_disposition(filename))]) return request.not_found() diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index a32d82e221e..9811fcd6aef 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -234,13 +234,17 @@ class mail_mail(osv.Model): :return: True """ ir_mail_server = self.pool.get('ir.mail_server') + ir_attachment = self.pool['ir.attachment'] for mail in self.browse(cr, SUPERUSER_ID, ids, context=context): try: - # handle attachments - attachments = [] - for attach in mail.attachment_ids: - attachments.append((attach.datas_fname, base64.b64decode(attach.datas))) + # load attachment binary data with a separate read(), as prefetching all + # `datas` (binary field) could bloat the browse cache, triggerring + # soft/hard mem limits with temporary data. + attachment_ids = [a.id for a in mail.attachment_ids] + attachments = [(a['datas_fname'], base64.b64decode(a['datas'])) + for a in ir_attachment.read(cr, SUPERUSER_ID, attachment_ids, + ['datas_fname', 'datas'])] # specific behavior to customize the send email for notified partners email_list = [] if mail.email_to: @@ -289,6 +293,14 @@ class mail_mail(osv.Model): # /!\ can't use mail.state here, as mail.refresh() will cause an error # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1 self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent) + _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id) + except MemoryError: + # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job + # instead of marking the mail as failed + _logger.exception('MemoryError while processing mail with ID %r and Msg-Id %r. '\ + 'Consider raising the --limit-memory-hard startup option', + mail.id, mail.message_id) + raise except Exception as e: _logger.exception('failed sending mail.mail %s', mail.id) mail.write({'state': 'exception'}) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 112dff6bac6..d2dcef4d85e 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -434,14 +434,14 @@ class mail_thread(osv.AbstractModel): fol_obj.unlink(cr, SUPERUSER_ID, fol_ids, context=context) return res - def copy(self, cr, uid, id, default=None, context=None): + def copy_data(self, cr, uid, id, default=None, context=None): # avoid tracking multiple temporary changes during copy context = dict(context or {}, mail_notrack=True) default = default or {} default['message_ids'] = [] default['message_follower_ids'] = [] - return super(mail_thread, self).copy(cr, uid, id, default=default, context=context) + return super(mail_thread, self).copy_data(cr, uid, id, default=default, context=context) #------------------------------------------------------ # Automatically log tracked fields diff --git a/addons/mail/res_config.py b/addons/mail/res_config.py index c19eb4a38ee..2c94b432dc3 100644 --- a/addons/mail/res_config.py +++ b/addons/mail/res_config.py @@ -34,14 +34,14 @@ class project_configuration(osv.TransientModel): } def get_default_alias_domain(self, cr, uid, ids, context=None): - alias_domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.domain", context=context) - if not alias_domain: + alias_domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.domain", default=None, context=context) + if alias_domain is None: domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "web.base.url", context=context) try: alias_domain = urlparse.urlsplit(domain).netloc.split(':')[0] except Exception: pass - return {'alias_domain': alias_domain} + return {'alias_domain': alias_domain or False} def set_alias_domain(self, cr, uid, ids, context=None): config_parameters = self.pool.get("ir.config_parameter") diff --git a/addons/multi_company/multi_company_demo.xml b/addons/multi_company/multi_company_demo.xml index 44a021c1434..d7d1af24f07 100644 --- a/addons/multi_company/multi_company_demo.xml +++ b/addons/multi_company/multi_company_demo.xml @@ -59,7 +59,7 @@ - + OpenERP US @@ -400,7 +400,7 @@ - + diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index f18edf39e85..f188af8aa82 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -1167,8 +1167,8 @@ class pos_order(osv.osv): return self.write(cr, uid, ids, {'state': 'payment'}, context=context) def action_paid(self, cr, uid, ids, context=None): - self.create_picking(cr, uid, ids, context=context) self.write(cr, uid, ids, {'state': 'paid'}, context=context) + self.create_picking(cr, uid, ids, context=context) return True def action_cancel(self, cr, uid, ids, context=None): diff --git a/addons/stock/product.py b/addons/stock/product.py index 3eddf0a1c72..1cd93225d45 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -105,7 +105,9 @@ class product_product(osv.osv): 'compute_child': False }) - qty = product.qty_available + # qty_available depends of the location in the context + qty = self.read(cr, uid, [product.id], ['qty_available'], context=c)[0]['qty_available'] + diff = product.standard_price - new_price if not diff: raise osv.except_osv(_('Error!'), _("No difference between standard price and new price!")) if qty: diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 5276e7f21e5..efb8582e9cb 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2307,7 +2307,9 @@ class stock_move(osv.osv): # if product is set to average price and a specific value was entered in the picking wizard, # we use it - if move.product_id.cost_method == 'average' and move.price_unit: + if move.location_dest_id.usage != 'internal' and move.product_id.cost_method == 'average': + reference_amount = qty * move.product_id.standard_price + elif move.product_id.cost_method == 'average' and move.price_unit: reference_amount = qty * move.price_unit reference_currency_id = move.price_currency_id.id or reference_currency_id diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index 4707da2bbb0..dba3225e760 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -792,7 +792,7 @@ - + @@ -926,7 +926,7 @@
- +
@@ -1053,7 +1053,7 @@ - +
@@ -1340,7 +1340,7 @@ - + diff --git a/addons/warning/warning.py b/addons/warning/warning.py index d582478113a..539316482d2 100644 --- a/addons/warning/warning.py +++ b/addons/warning/warning.py @@ -62,14 +62,14 @@ class sale_order(osv.osv): message = False partner = self.pool.get('res.partner').browse(cr, uid, part, context=context) if partner.sale_warn != 'no-message': - if partner.sale_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.sale_warn_msg) title = _("Warning for %s") % partner.name message = partner.sale_warn_msg warning = { 'title': title, 'message': message, } + if partner.sale_warn == 'block': + return {'value': {'partner_id': False}, 'warning': warning} result = super(sale_order, self).onchange_partner_id(cr, uid, ids, part, context=context) @@ -90,14 +90,15 @@ class purchase_order(osv.osv): message = False partner = self.pool.get('res.partner').browse(cr, uid, part) if partner.purchase_warn != 'no-message': - if partner.purchase_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.purchase_warn_msg) title = _("Warning for %s") % partner.name message = partner.purchase_warn_msg warning = { 'title': title, 'message': message } + if partner.purchase_warn == 'block': + return {'value': {'partner_id': False}, 'warning': warning} + result = super(purchase_order, self).onchange_partner_id(cr, uid, ids, part) if result.get('warning',False): @@ -123,15 +124,16 @@ class account_invoice(osv.osv): message = False partner = self.pool.get('res.partner').browse(cr, uid, partner_id) if partner.invoice_warn != 'no-message': - if partner.invoice_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.invoice_warn_msg) - title = _("Warning for %s") % partner.name message = partner.invoice_warn_msg warning = { 'title': title, 'message': message } + + if partner.invoice_warn == 'block': + return {'value': {'partner_id': False}, 'warning': warning} + result = super(account_invoice, self).onchange_partner_id(cr, uid, ids, type, partner_id, date_invoice=date_invoice, payment_term=payment_term, partner_bank_id=partner_bank_id, company_id=company_id) @@ -154,14 +156,15 @@ class stock_picking(osv.osv): title = False message = False if partner.picking_warn != 'no-message': - if partner.picking_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.picking_warn_msg) title = _("Warning for %s") % partner.name message = partner.picking_warn_msg warning = { 'title': title, 'message': message } + if partner.picking_warn == 'block': + return {'value': {'partner_id': False}, 'warning': warning} + result = super(stock_picking, self).onchange_partner_in(cr, uid, ids, partner_id, context) if result.get('warning',False): warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title'] @@ -183,14 +186,15 @@ class stock_picking_in(osv.osv): title = False message = False if partner.picking_warn != 'no-message': - if partner.picking_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.picking_warn_msg) title = _("Warning for %s") % partner.name message = partner.picking_warn_msg warning = { 'title': title, 'message': message } + if partner.picking_warn == 'block': + return {'value': {'partner_id': False}, 'warning': warning} + result = super(stock_picking_in, self).onchange_partner_in(cr, uid, ids, partner_id, context) if result.get('warning',False): warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title'] @@ -209,14 +213,15 @@ class stock_picking_out(osv.osv): title = False message = False if partner.picking_warn != 'no-message': - if partner.picking_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.picking_warn_msg) title = _("Warning for %s") % partner.name message = partner.picking_warn_msg warning = { 'title': title, 'message': message } + if partner.picking_warn == 'block': + return {'value': {'partner_id': False}, 'warning': warning} + result = super(stock_picking_out, self).onchange_partner_in(cr, uid, ids, partner_id, context) if result.get('warning',False): warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title'] @@ -256,12 +261,12 @@ class sale_order_line(osv.osv): message = False if product_info.sale_line_warn != 'no-message': - if product_info.sale_line_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (product_info.name), product_info.sale_line_warn_msg) title = _("Warning for %s") % product_info.name message = product_info.sale_line_warn_msg warning['title'] = title warning['message'] = message + if product_info.sale_line_warn == 'block': + return {'value': {'product_id': False}, 'warning': warning} result = super(sale_order_line, self).product_id_change( cr, uid, ids, pricelist, product, qty, uom, qty_uos, uos, name, partner_id, @@ -288,12 +293,12 @@ class purchase_order_line(osv.osv): message = False if product_info.purchase_line_warn != 'no-message': - if product_info.purchase_line_warn == 'block': - raise osv.except_osv(_('Alert for %s!') % (product_info.name), product_info.purchase_line_warn_msg) title = _("Warning for %s") % product_info.name message = product_info.purchase_line_warn_msg warning['title'] = title warning['message'] = message + if product_info.purchase_line_warn == 'block': + return {'value': {'product_id': False}, 'warning': warning} result = super(purchase_order_line, self).onchange_product_id(cr, uid, ids, pricelist, product, qty, uom, partner_id, date_order, fiscal_position_id) diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index 1a9ccccba5c..8682a349f70 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -2,6 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_website_public,website,website.model_website,,1,0,0,0 access_website,website,website.model_website,base.group_website_designer,1,1,1,1 access_website_menu,access_website_menu,model_website_menu,,1,0,0,0 +access_website_menu_designer,Web Menu Manager,model_website_menu,base.group_website_designer,1,1,1,1 access_website,web menu manager,website.model_website,base.group_website_designer,1,1,1,1 access_website_converter_test,access_website_converter_test,model_website_converter_test,,1,1,1,1 access_website_converter_test_sub,access_website_converter_test_sub,model_website_converter_test_sub,,1,1,1,1 diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 365221f9574..cd36a802422 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -124,6 +124,7 @@ class WebsiteForum(http.Controller): values.update({ 'main_object': tag or forum, 'question_ids': question_ids, + 'question_count': question_count, 'pager': pager, 'tag': tag, 'filters': filters, @@ -134,7 +135,7 @@ class WebsiteForum(http.Controller): @http.route(['/forum//faq'], type='http', auth="public", website=True, multilang=True) def forum_faq(self, forum, **post): - values = self._prepare_forum_values(forum=forum, searches=dict(), **post) + values = self._prepare_forum_values(forum=forum, searches=dict(), header={'is_guidelines': True}, **post) return request.website.render("website_forum.faq", values) @http.route('/forum/get_tags', type='http', auth="public", multilang=True, methods=['GET'], website=True) @@ -147,7 +148,7 @@ class WebsiteForum(http.Controller): def tags(self, forum, page=1, **post): cr, uid, context = request.cr, request.uid, request.context Tag = request.registry['forum.tag'] - obj_ids = Tag.search(cr, uid, [('forum_id', '=', forum.id)], limit=None, context=context) + obj_ids = Tag.search(cr, uid, [('forum_id', '=', forum.id), ('posts_count', '>', 0)], limit=None, order='posts_count DESC', context=context) tags = Tag.browse(cr, uid, obj_ids, context=context) values = self._prepare_forum_values(forum=forum, searches={'tags': True}, **post) values.update({ @@ -426,7 +427,7 @@ class WebsiteForum(http.Controller): tag_count = User.search(cr, SUPERUSER_ID, [('karma', '>', 1)], count=True, context=context) pager = request.website.pager(url="/forum/users", total=tag_count, page=page, step=step, scope=30) - obj_ids = User.search(cr, SUPERUSER_ID, [('karma', '>', 1)], limit=step, offset=pager['offset'], context=context) + obj_ids = User.search(cr, SUPERUSER_ID, [('karma', '>', 1)], limit=step, offset=pager['offset'], order='karma DESC', context=context) users = User.browse(cr, SUPERUSER_ID, obj_ids, context=context) searches['users'] = 'True' @@ -590,12 +591,13 @@ class WebsiteForum(http.Controller): @http.route('/forum//post//comment//convert_to_answer', type='http', auth="public", multilang=True, website=True) def convert_comment_to_answer(self, forum, post, comment, **kwarg): - values = { - 'content': comment.body, - } + body = comment.body request.registry['mail.message'].unlink(request.cr, request.uid, [comment.id], context=request.context) question = post.parent_id if post.parent_id else post - return self.post_new(forum, question, **values) + for answer in question.child_ids: + if answer.create_uid.id == request.uid: + return self.post_comment(forum, answer, comment=html2plaintext(body)) + return self.post_new(forum, question, content=body) @http.route('/forum//post//convert_to_comment', type='http', auth="user", multilang=True, website=True) def convert_answer_to_comment(self, forum, post, **kwarg): diff --git a/addons/website_forum/data/forum_data.xml b/addons/website_forum/data/forum_data.xml index 245169c23e9..a387a8de7ae 100644 --- a/addons/website_forum/data/forum_data.xml +++ b/addons/website_forum/data/forum_data.xml @@ -13,10 +13,7 @@ 35 - - auth_signup.allow_uninvited - - + diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 7a8385d1a82..061b9e55eb8 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -70,6 +70,7 @@ class Post(osv.Model): _name = 'forum.post' _description = 'Forum Post' _inherit = ['mail.thread', 'website.seo.metadata'] + _order = "is_correct DESC, vote_count DESC" def _get_user_vote(self, cr, uid, ids, field_name, arg, context): res = dict.fromkeys(ids, 0) diff --git a/addons/website_forum/static/src/css/website_forum.css b/addons/website_forum/static/src/css/website_forum.css index ab8ea684aae..b90c1786e24 100644 --- a/addons/website_forum/static/src/css/website_forum.css +++ b/addons/website_forum/static/src/css/website_forum.css @@ -21,6 +21,9 @@ background-color: #cccccc; margin-left: 4px; } +.question .badge-active { + background-color: #428bca; +} .oe_grey { background-color: #eeeeee; diff --git a/addons/website_forum/static/src/css/website_forum.sass b/addons/website_forum/static/src/css/website_forum.sass index da2df1460aa..f8e46f06b79 100644 --- a/addons/website_forum/static/src/css/website_forum.sass +++ b/addons/website_forum/static/src/css/website_forum.sass @@ -17,6 +17,8 @@ .badge background-color: #ccc margin-left: 4px + .badge-active + background-color: #428bca .oe_grey background-color: #eeeeee diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index bd8b47f0817..c73486d7d39 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -92,7 +92,8 @@
@@ -144,8 +145,11 @@ [Deleted] [Closed]
- - + + + +
by

- Questions + Questions All Unanswered Followed - Tag + by activity date by most answered by most voted @@ -332,12 +336,12 @@ users having a high karma can see closed questions to moderate them.

-
+
- +
@@ -451,7 +455,7 @@ Close
  • - reopen + Reopen
  • Edit @@ -467,7 +471,7 @@