[MERGE] Forward-port latest saas-3 bugfixes, up to 30f43da
authorOlivier Dony <odo@openerp.com>
Tue, 27 May 2014 15:18:43 +0000 (17:18 +0200)
committerOlivier Dony <odo@openerp.com>
Tue, 27 May 2014 15:18:43 +0000 (17:18 +0200)
12 files changed:
1  2 
addons/account/account_invoice_view.xml
addons/account/report/account_aged_partner_balance.py
addons/account_analytic_analysis/account_analytic_analysis.py
addons/hr_recruitment/hr_recruitment.py
addons/mail/static/src/js/mail.js
addons/purchase/purchase.py
addons/web/static/src/js/view_form.js
addons/website/models/ir_http.py
addons/website_sale/controllers/main.py
addons/website_sale/models/product.py
openerp/http.py
openerp/report/render/rml2pdf/trml2pdf.py

@@@ -60,7 -60,7 +60,7 @@@
                          </group>
                          <group>
                              <field domain="[('company_id', '=', parent.company_id), ('journal_id', '=', parent.journal_id), ('type', '&lt;&gt;', 'view')]" name="account_id" on_change="onchange_account_id(product_id, parent.partner_id, parent.type, parent.fiscal_position,account_id)" groups="account.group_account_user"/>
-                             <field name="invoice_line_tax_id" context="{'type':parent.type}" domain="[('parent_id','=',False),('company_id', '=', parent.company_id)]" widget="many2many_tags"/>
+                             <field name="invoice_line_tax_id" context="{'type':parent.get('type')}" domain="[('parent_id','=',False),('company_id', '=', parent.company_id)]" widget="many2many_tags"/>
                              <field domain="[('type','&lt;&gt;','view'), ('company_id', '=', parent.company_id)]" name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
                              <field name="company_id" groups="base.group_multi_company" readonly="1"/>
                          </group>
                      <field name="journal_id" invisible="1"/>
                      <field name="period_id" invisible="1" groups="account.group_account_user"/>
                      <field name="company_id" groups="base.group_multi_company" widget="selection"/>
 -                    <field name="user_id"/>
 +                    <field name="user_id" string="Responsible"/>
                      <field name="date_due"/>
                      <field name="origin"/>
                      <field name="currency_id" groups="base.group_multi_currency"/>
                              <group>
                                  <group>
                                      <field domain="[('partner_id', '=', partner_id)]" name="partner_bank_id" on_change="onchange_partner_bank(partner_bank_id)"/>
 -                                    <field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_invoice']}"/>
 +                                    <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" options="{'no_create': True}"/>
                                  </group>
                                  </group>
                                  <group>
                                      <field name="origin" groups="base.group_user"/>
 -                                    <field name="name" string="Customer Reference"/>
 +                                    <field name="name"/>
                                      <field name="move_id" groups="account.group_account_user"/>
                                  </group>
                              </group>
  ##############################################################################
  
  import time
 +from openerp.osv import osv
  from openerp.report import report_sxw
  from common_report_header import common_report_header
  
 +
  class aged_trial_report(report_sxw.rml_parse, common_report_header):
  
      def __init__(self, cr, uid, name, context):
                  dates_query += ' < %s)'
                  args_list += (form[str(i)]['stop'],)
              args_list += (self.date_from,)
-             self.cr.execute('''SELECT l.partner_id, SUM(l.debit-l.credit)
+             self.cr.execute('''SELECT l.partner_id, SUM(l.debit-l.credit), l.reconcile_partial_id
                      FROM account_move_line AS l, account_account, account_move am 
                      WHERE (l.account_id = account_account.id) AND (l.move_id=am.id)
                          AND (am.state IN %s)
                          AND account_account.active
                          AND ''' + dates_query + '''
                      AND (l.date <= %s)
-                     GROUP BY l.partner_id''', args_list)
-             t = self.cr.fetchall()
-             d = {}
-             for i in t:
-                 d[i[0]] = i[1]
-             history.append(d)
+                     GROUP BY l.partner_id, l.reconcile_partial_id''', args_list)
+             partners_partial = self.cr.fetchall()
+             partners_amount = dict((i[0],0) for i in partners_partial)
+             for partner_info in partners_partial:
+                 if partner_info[2]:
+                     # in case of partial reconciliation, we want to keep the left amount in the oldest period
+                     self.cr.execute('''SELECT MIN(COALESCE(date_maturity,date)) FROM account_move_line WHERE reconcile_partial_id = %s''', (partner_info[2],))
+                     date = self.cr.fetchall()
+                     if date and args_list[-3] <= date[0][0] <= args_list[-2]:
+                         # partial reconcilation
+                         self.cr.execute('''SELECT SUM(l.debit-l.credit)
+                                            FROM account_move_line AS l
+                                            WHERE l.reconcile_partial_id = %s''', (partner_info[2],))
+                         unreconciled_amount = self.cr.fetchall()
+                         partners_amount[partner_info[0]] += unreconciled_amount[0][0]
+                 else:
+                     partners_amount[partner_info[0]] += partner_info[1]
+             history.append(partners_amount)
  
          for partner in partners:
              values = {}
              return self._translate('Receivable and Payable Accounts')
          return ''
  
 -report_sxw.report_sxw('report.account.aged_trial_balance', 'res.partner',
 -        'addons/account/report/account_aged_partner_balance.rml',parser=aged_trial_report, header="internal landscape")
  
 +class report_agedpartnerbalance(osv.AbstractModel):
 +    _name = 'report.account.report_agedpartnerbalance'
 +    _inherit = 'report.abstract_report'
 +    _template = 'account.report_agedpartnerbalance'
 +    _wrapped_report_class = aged_trial_report
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@@ -82,7 -82,7 +82,7 @@@ class account_analytic_invoice_line(osv
          if not name:
              name = self.pool.get('product.product').name_get(cr, uid, [res.id], context=local_context)[0][1]
              if res.description_sale:
-                 result['name'] += '\n'+res.description_sale
+                 name += '\n'+res.description_sale
  
          result.update({'name': name or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': price})
  
@@@ -553,39 -553,33 +553,39 @@@ class account_analytic_account(osv.osv)
              'nodestroy': True,
          }
  
 -    def on_change_template(self, cr, uid, ids, template_id, 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, context=context)
 +        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)
 -        invoice_line_ids = []
 -        for x in template.recurring_invoice_line_ids:
 -            invoice_line_ids.append((0, 0, {
 -                'product_id': x.product_id.id,
 -                'uom_id': x.uom_id.id,
 -                'name': x.name,
 -                'quantity': x.quantity,
 -                'price_unit': x.price_unit,
 -                'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False,
 -            }))
 -        res['value']['fix_price_invoices'] = template.fix_price_invoices
 -        res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets
 -        res['value']['hours_qtt_est'] = template.hours_qtt_est
 -        res['value']['amount_max'] = template.amount_max
 -        res['value']['to_invoice'] = template.to_invoice.id
 -        res['value']['pricelist_id'] = template.pricelist_id.id
 -        res['value']['recurring_invoices'] = template.recurring_invoices
 -        res['value']['recurring_interval'] = template.recurring_interval
 -        res['value']['recurring_rule_type'] = template.recurring_rule_type
 -        res['value']['recurring_invoice_line_ids'] = invoice_line_ids
 +        
 +        if not ids:
 +            res['value']['fix_price_invoices'] = template.fix_price_invoices
 +            res['value']['amount_max'] = template.amount_max
 +        if not ids:
 +            res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets
 +            res['value']['hours_qtt_est'] = template.hours_qtt_est
 +        
 +        if template.to_invoice.id:
 +            res['value']['to_invoice'] = template.to_invoice.id
 +        if template.pricelist_id.id:
 +            res['value']['pricelist_id'] = template.pricelist_id.id
 +        if not ids:
 +            invoice_line_ids = []
 +            for x in template.recurring_invoice_line_ids:
 +                invoice_line_ids.append((0, 0, {
 +                    'product_id': x.product_id.id,
 +                    'uom_id': x.uom_id.id,
 +                    'name': x.name,
 +                    'quantity': x.quantity,
 +                    'price_unit': x.price_unit,
 +                    'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False,
 +                }))
 +            res['value']['recurring_invoices'] = template.recurring_invoices
 +            res['value']['recurring_interval'] = template.recurring_interval
 +            res['value']['recurring_rule_type'] = template.recurring_rule_type
 +            res['value']['recurring_invoice_line_ids'] = invoice_line_ids
          return res
  
      def onchange_recurring_invoices(self, cr, uid, ids, recurring_invoices, date_start=False, context=None):
  
      def onchange_invoice_on_timesheets(self, cr, uid, ids, invoice_on_timesheets, context=None):
          if not invoice_on_timesheets:
 -            return {}
 +            return {'value': {'to_invoice': False}}
          result = {'value': {'use_timesheets': True}}
          try:
              to_invoice = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'hr_timesheet_invoice', 'timesheet_invoice_factor1')
                      new_date = next_date+relativedelta(days=+interval)
                  elif contract.recurring_rule_type == 'weekly':
                      new_date = next_date+relativedelta(weeks=+interval)
+                 elif contract.recurring_rule_type == 'yearly':
+                     new_date = next_date+relativedelta(years=+interval)
                  else:
                      new_date = next_date+relativedelta(months=+interval)
                  self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
  #
  ##############################################################################
  
  from datetime import datetime
  from openerp.osv import fields, osv
  from openerp.tools.translate import _
 -from openerp.tools import html2plaintext
 +
  
  AVAILABLE_PRIORITIES = [
      ('', ''),
@@@ -80,7 -82,6 +80,7 @@@ class hr_applicant(osv.Model)
      _description = "Applicant"
      _order = "id desc"
      _inherit = ['mail.thread', 'ir.needaction_mixin']
 +
      _track = {
          'stage_id': {
              # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
@@@ -88,7 -89,6 +88,7 @@@
              'hr_recruitment.mt_applicant_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1,
          },
      }
 +    _mail_mass_mailing = _('Applicants')
  
      def _get_default_department_id(self, cr, uid, context=None):
          """ Gives default department by checking if present in the context """
                  return int(department_ids[0][0])
          return None
  
+     def _get_default_company_id(self, cr, uid, department_id=None, context=None):
+         company_id = False
+         if department_id:
+             department = self.pool['hr.department'].browse(cr,  uid, department_id, context=context)
+             company_id = department.company_id.id if department and department.company_id else False
+         if not company_id:
+             company_id = self.pool['res.company']._company_default_get(cr, uid, 'hr.applicant', context=context)
+         return company_id            
      def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
          access_rights_uid = access_rights_uid or uid
          stage_obj = self.pool.get('hr.recruitment.stage')
          'partner_mobile': fields.char('Mobile', size=32),
          'type_id': fields.many2one('hr.recruitment.degree', 'Degree'),
          'department_id': fields.many2one('hr.department', 'Department'),
 -        'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
 -        'response': fields.integer("Response"),
 +        'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey.survey', string='Survey'),
 +        'response_id': fields.many2one('survey.user_input', "Response", ondelete='set null', oldname="response"),
          'reference': fields.char('Referred By', size=128),
          'source_id': fields.many2one('hr.recruitment.source', 'Source'),
          'day_open': fields.function(_compute_day, string='Days to Open', \
          'user_id': lambda s, cr, uid, c: uid,
          'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
          'department_id': lambda s, cr, uid, c: s._get_default_department_id(cr, uid, c),
-         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.applicant', context=c),
+         'company_id': lambda s, cr, uid, c: s._get_default_company_id(cr, uid, s._get_default_department_id(cr, uid, c), c),
          'color': 0,
          'date_last_stage_update': fields.datetime.now,
      }
          if job_id:
              job_record = self.pool.get('hr.job').browse(cr, uid, job_id, context=context)
              department_id = job_record and job_record.department_id and job_record.department_id.id or False
 -        return {'value': {'department_id': department_id}}
 +            user_id = job_record and job_record.user_id and job_record.user_id.id or False
 +        return {'value': {'department_id': department_id, 'user_id': user_id}}
  
      def onchange_department_id(self, cr, uid, ids, department_id=False, stage_id=False, context=None):
          if not stage_id:
          }
          return res
  
 +    def action_start_survey(self, cr, uid, ids, context=None):
 +        context = context if context else {}
 +        applicant = self.browse(cr, uid, ids, context=context)[0]
 +        survey_obj = self.pool.get('survey.survey')
 +        response_obj = self.pool.get('survey.user_input')
 +        # create a response and link it to this applicant
 +        if not applicant.response_id:
 +            response_id = response_obj.create(cr, uid, {'survey_id': applicant.survey.id, 'partner_id': applicant.partner_id.id}, context=context)
 +            self.write(cr, uid, ids[0], {'response_id': response_id}, context=context)
 +        else:
 +            response_id = applicant.response_id.id
 +        # grab the token of the response and start surveying
 +        response = response_obj.browse(cr, uid, response_id, context=context)
 +        context.update({'survey_token': response.token})
 +        return survey_obj.action_start_survey(cr, uid, [applicant.survey.id], context=context)
 +
      def action_print_survey(self, cr, uid, ids, context=None):
 -        """
 -        If response is available then print this response otherwise print survey form(print template of the survey).
 +        """ If response is available then print this response otherwise print survey form (print template of the survey) """
 +        context = context if context else {}
 +        applicant = self.browse(cr, uid, ids, context=context)[0]
 +        survey_obj = self.pool.get('survey.survey')
 +        response_obj = self.pool.get('survey.user_input')
 +        if not applicant.response_id:
 +            return survey_obj.action_print_survey(cr, uid, [applicant.survey.id], context=context)
 +        else:
 +            response = response_obj.browse(cr, uid, applicant.response_id.id, context=context)
 +            context.update({'survey_token': response.token})
 +            return survey_obj.action_print_survey(cr, uid, [applicant.survey.id], context=context)
  
 -        @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 Survey IDs
 -        @param context: A standard dictionary for contextual values
 -        @return: Dictionary value for print survey form.
 -        """
 -        if context is None:
 -            context = {}
 -        record = self.browse(cr, uid, ids, context=context)
 -        record = record and record[0]
 -        context.update({'survey_id': record.survey.id, 'response_id': [record.response], 'response_no': 0, })
 -        value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
 -        return value
 -
 -    def action_get_attachment_tree_view(self, cr, uid, ids, context):
 -        domain = ['&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', ids)]
 -        return {
 -            'name': _('Attachments'),
 -            'domain': domain,
 -            'res_model': 'ir.attachment',
 -            'type': 'ir.actions.act_window',
 -            'view_id': False,
 -            'view_mode': 'tree,form',
 -            'view_type': 'form',
 -            'limit': 80,
 -            'context': "{'default_res_model': '%s'}" % (self._name)
 -        }
 +    def action_get_attachment_tree_view(self, cr, uid, ids, context=None):
 +        model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'action_attachment')
 +        action = self.pool.get(model).read(cr, uid, action_id, context=context)
 +        action['context'] = {'default_res_model': self._name, 'default_res_id': ids[0]}
 +        action['domain'] = str(['&', ('res_model', '=', self._name), ('res_id', 'in', ids)])
 +        return action
  
      def message_get_suggested_recipients(self, cr, uid, ids, context=None):
          recipients = super(hr_applicant, self).message_get_suggested_recipients(cr, uid, ids, context=context)
          val = msg.get('from').split('<')[0]
          defaults = {
              'name':  msg.get('subject') or _("No Subject"),
 -            'partner_name':val,
 +            'partner_name': val,
              'email_from': msg.get('from'),
              'email_cc': msg.get('cc'),
              'user_id': False,
      def create(self, cr, uid, vals, context=None):
          if context is None:
              context = {}
 +        context['mail_create_nolog'] = True
          if vals.get('department_id') and not context.get('default_department_id'):
              context['default_department_id'] = vals.get('department_id')
 -
 +        if vals.get('job_id') or context.get('default_job_id'):
 +            job_id = vals.get('job_id') or context.get('default_job_id')
 +            vals.update(self.onchange_job(cr, uid, [], job_id, context=context)['value'])
          obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
          applicant = self.browse(cr, uid, obj_id, context=context)
          if applicant.job_id:
 -            self.pool.get('hr.job').message_post(cr, uid, [applicant.job_id.id], body=_('Applicant <b>created</b>'), subtype="hr_recruitment.mt_job_new_applicant", context=context)
 +            name = applicant.partner_name if applicant.partner_name else applicant.name
 +            self.pool['hr.job'].message_post(
 +                cr, uid, [applicant.job_id.id],
 +                body=_('New application from %s') % name,
 +                subtype="hr_recruitment.mt_job_applicant_new", context=context)
          return obj_id
  
      def write(self, cr, uid, ids, vals, context=None):
              ids = [ids]
          res = True
  
 -        # user_id change: update date_start
 +        # user_id change: update date_open
          if vals.get('user_id'):
 -            vals['date_start'] = fields.datetime.now()
 +            vals['date_open'] = fields.datetime.now()
          # stage_id: track last stage before update
          if 'stage_id' in vals:
              vals['date_last_stage_update'] = fields.datetime.now()
          else:
              res = super(hr_applicant, self).write(cr, uid, ids, vals, context=context)
  
 +        # post processing: if job changed, post a message on the job
 +        if vals.get('job_id'):
 +            for applicant in self.browse(cr, uid, ids, context=None):
 +                name = applicant.partner_name if applicant.partner_name else applicant.name
 +                self.pool['hr.job'].message_post(
 +                    cr, uid, [vals['job_id']],
 +                    body=_('New application from %s') % name,
 +                    subtype="hr_recruitment.mt_job_applicant_new", context=context)
 +
          # post processing: if stage changed, post a message in the chatter
          if vals.get('stage_id'):
              stage = self.pool['hr.recruitment.stage'].browse(cr, uid, vals['stage_id'], context=context)
                  address_id = self.pool.get('res.partner').address_get(cr, uid, [applicant.partner_id.id], ['contact'])['contact']
                  contact_name = self.pool.get('res.partner').name_get(cr, uid, [applicant.partner_id.id])[0][1]
              if applicant.job_id and (applicant.partner_name or contact_name):
 -                applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
 +                applicant.job_id.write({'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1}, context=context)
 +                create_ctx = dict(context, mail_broadcast=True)
                  emp_id = hr_employee.create(cr, uid, {'name': applicant.partner_name or contact_name,
                                                       'job_id': applicant.job_id.id,
                                                       'address_home_id': address_id,
                                                       'address_id': applicant.company_id and applicant.company_id.partner_id and applicant.company_id.partner_id.id or False,
                                                       'work_email': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.email or False,
                                                       'work_phone': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.phone or False,
 -                                                     })
 +                                                     }, context=create_ctx)
                  self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
 +                self.pool['hr.job'].message_post(
 +                    cr, uid, [applicant.job_id.id],
 +                    body=_('New Employee %s Hired') % applicant.partner_name if applicant.partner_name else applicant.name,
 +                    subtype="hr_recruitment.mt_job_applicant_hired", context=context)
              else:
                  raise osv.except_osv(_('Warning!'), _('You must define an Applied Job and a Contact Name for this applicant.'))
  
@@@ -515,37 -499,16 +524,37 @@@ class hr_job(osv.osv)
      _inherit = "hr.job"
      _name = "hr.job"
      _inherits = {'mail.alias': 'alias_id'}
 +
 +    def _get_attached_docs(self, cr, uid, ids, field_name, arg, context=None):
 +        res = {}
 +        attachment_obj = self.pool.get('ir.attachment')
 +        for job_id in ids:
 +            applicant_ids = self.pool.get('hr.applicant').search(cr, uid, [('job_id', '=', job_id)], context=context)
 +            res[job_id] = attachment_obj.search(
 +                cr, uid, [
 +                    '|',
 +                    '&', ('res_model', '=', 'hr.job'), ('res_id', '=', job_id),
 +                    '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', applicant_ids)
 +                ], context=context)
 +        return res
 +
      _columns = {
 -        'survey_id': fields.many2one('survey', 'Interview Form', help="Choose an interview form for this job position and you will be able to print/answer this interview from all applicants who apply for this job"),
 +        'survey_id': fields.many2one('survey.survey', 'Interview Form', help="Choose an interview form for this job position and you will be able to print/answer this interview from all applicants who apply for this job"),
          'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
                                      help="Email alias for this job position. New emails will automatically "
                                           "create new applicants for this job position."),
          'address_id': fields.many2one('res.partner', 'Job Location', help="Address where employees are working"),
 +        'application_ids': fields.one2many('hr.applicant', 'job_id', 'Applications'),
 +        'manager_id': fields.related('department_id', 'manager_id', type='many2one', string='Department Manager', relation='hr.employee', readonly=True, store=True),
 +        'document_ids': fields.function(_get_attached_docs, type='one2many', relation='ir.attachment', string='Applications'),
 +        'user_id': fields.many2one('res.users', 'Recruitment Responsible', track_visibility='onchange'),
 +        'color': fields.integer('Color Index'),
      }
 +
      def _address_get(self, cr, uid, context=None):
          user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
          return user.company_id.partner_id.id
 +
      _defaults = {
          'address_id': _address_get
      }
          return res
  
      def action_print_survey(self, cr, uid, ids, context=None):
 -        if context is None:
 -            context = {}
 -        datas = {}
 -        record = self.browse(cr, uid, ids, context=context)[0]
 -        if record.survey_id:
 -            datas['ids'] = [record.survey_id.id]
 -        datas['model'] = 'survey.print'
 -        context.update({'response_id': [0], 'response_no': 0})
 -        return {
 -            'type': 'ir.actions.report.xml',
 -            'report_name': 'survey.form',
 -            'datas': datas,
 -            'context': context,
 -            'nodestroy': True,
 -        }
 +        job = self.browse(cr, uid, ids, context=context)[0]
 +        survey_id = job.survey_id.id
 +        return self.pool.get('survey.survey').action_print_survey(cr, uid, [survey_id], context=context)
 +
 +    def action_get_attachment_tree_view(self, cr, uid, ids, context=None):
 +        #open attachments of job and related applicantions.
 +        model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'action_attachment')
 +        action = self.pool.get(model).read(cr, uid, action_id, context=context)
 +        applicant_ids = self.pool.get('hr.applicant').search(cr, uid, [('job_id', 'in', ids)], context=context)
 +        action['context'] = {'default_res_model': self._name, 'default_res_id': ids[0]}
 +        action['domain'] = str(['|', '&', ('res_model', '=', 'hr.job'), ('res_id', 'in', ids), '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', applicant_ids)])
 +        return action
 +
 +    def action_set_no_of_recruitment(self, cr, uid, id, value, context=None):
 +        return self.write(cr, uid, [id], {'no_of_recruitment': value}, context=context)
  
  
  class applicant_category(osv.osv):
@@@ -507,15 -507,18 +507,15 @@@ openerp.mail = function (session) 
              }
              $.when(recipient_done).done(function (partner_ids) {
                  var context = {
 -                    'default_composition_mode': default_composition_mode,
                      'default_parent_id': self.id,
                      'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
                      'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}),
                      'default_partner_ids': partner_ids,
 +                    'default_is_log': self.is_log,
                      'mail_post_autofollow': true,
                      'mail_post_autofollow_partner_ids': partner_ids,
                      'is_private': self.is_private
                  };
 -                if (self.is_log) {
 -                    _.extend(context, {'mail_compose_log': true});
 -                }
                  if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
                      context.default_model = self.context.default_model;
                      context.default_res_id = self.context.default_res_id;
              this.$('.oe_mail_expand').on('click', this.on_expand);
              this.$('.oe_mail_reduce').on('click', this.on_expand);
              this.$('.oe_mail_action_model').on('click', this.on_record_clicked);
+             this.$('.oe_mail_action_author').on('click', this.on_record_author_clicked);
          },
  
          on_record_clicked: function  (event) {
+             event.preventDefault();
+             var self = this;
              var state = {
                  'model': this.model,
                  'id': this.res_id,
                  'title': this.record_name
              };
              session.webclient.action_manager.do_push_state(state);
+             this.context.params = {
+                 model: this.model,
+                 res_id: this.res_id,
+             };
+             this.thread.ds_thread.call("message_redirect_action", {context: this.context}).then(function(action){
+                 self.do_action(action); 
+             });
+         },
+         on_record_author_clicked: function  (event) {
+             event.preventDefault();
+             var partner_id = $(event.target).data('partner');
+             var state = {
+                 'model': 'res.partner',
+                 'id': partner_id,
+                 'title': this.record_name
+             };
+             session.webclient.action_manager.do_push_state(state);
+             var action = {
+                 type:'ir.actions.act_window',
+                 view_type: 'form',
+                 view_mode: 'form',
+                 res_model: 'res.partner',
+                 views: [[false, 'form']],
+                 res_id: partner_id,
+             }
+             this.do_action(action);
          },
  
          /* Call the on_compose_message on the thread of this message. */
@@@ -192,7 -192,7 +192,7 @@@ class purchase_order(osv.osv)
          'picking_ids': fields.one2many('stock.picking.in', 'purchase_id', 'Picking List', readonly=True, help="This is the list of incoming shipments that have been generated for this purchase order."),
          'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
          'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
-         'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', help="It indicates that an invoice has been paid"),
+         'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', help="It indicates that an invoice has been validated"),
          'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
          'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on incoming shipments')], 'Invoicing Control', required=True,
              readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)]},
          '''
          assert len(ids) == 1, 'This option should only be used for a single id at a time'
          self.signal_send_rfq(cr, uid, ids)
 -        datas = {
 -                 'model': 'purchase.order',
 -                 'ids': ids,
 -                 'form': self.read(cr, uid, ids[0], context=context),
 -        }
 -        return {'type': 'ir.actions.report.xml', 'report_name': 'purchase.quotation', 'datas': datas, 'nodestroy': True}
 +        return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
  
      #TODO: implement messages system
      def wkf_confirm_order(self, cr, uid, ids, context=None):
@@@ -1245,10 -1250,10 +1245,10 @@@ class mail_mail(osv.Model)
      _name = 'mail.mail'
      _inherit = 'mail.mail'
  
 -    def _postprocess_sent_message(self, cr, uid, mail, context=None):
 -        if mail.model == 'purchase.order':
 +    def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
 +        if mail_sent and mail.model == 'purchase.order':
              self.pool.get('purchase.order').signal_send_rfq(cr, uid, [mail.res_id])
 -        return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
 +        return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
  
  
  class product_template(osv.Model):
@@@ -586,13 -586,13 +586,13 @@@ instance.web.FormView = instance.web.Vi
              this._internal_set_values(result.value, processed);
          }
          if (!_.isEmpty(result.warning)) {
 -            instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
 +            new instance.web.Dialog(this, {
 +                size: 'medium',
                  title:result.warning.title,
 -                modal: true,
                  buttons: [
 -                    {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
 +                    {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
                  ]
 -            });
 +            }, QWeb.render("CrashManager.warning", result.warning)).open();
          }
  
          return $.Deferred().resolve();
                  };
              })
              .value();
 -
          var d = new instance.web.Dialog(this, {
              title: _t("Set Default"),
              args: {
@@@ -1285,9 -1286,6 +1285,9 @@@ instance.web.form.FormRenderingEngine 
          var defs = [];
          _.each(this.to_replace, function(el) {
              defs.push(el[0].replace(el[1]));
 +            if (el[1].children().length) {
 +                el[0].$el.append(el[1].children());
 +            }
          });
          this.to_replace = [];
          return $.when.apply($, defs);
          var tagname = $tag[0].nodeName.toLowerCase();
          if (this.tags_registry.contains(tagname)) {
              this.tags_to_init.push($tag);
 -            return $tag;
 +            return (tagname === 'button') ? this.process_button($tag) : $tag;
          }
          var fn = self['process_' + tagname];
          if (fn) {
              return $tag;
          }
      },
 +    process_button: function ($button) {
 +        var self = this;
 +        $button.children().each(function() {
 +            self.process($(this));
 +        });
 +        return $button;
 +    },
      process_widget: function($widget) {
          this.widgets_to_init.push($widget);
          return $widget;
@@@ -1924,8 -1915,6 +1924,8 @@@ instance.web.form.WidgetButton = instan
      template: 'WidgetButton',
      init: function(field_manager, node) {
          node.attrs.type = node.attrs['data-button-type'];
 +        this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
 +        this.icon = node.attrs.icon && "<span class=\"fa " + node.attrs.icon + " fa-fw\"></span>";
          this._super(field_manager, node);
          this.force_disabled = false;
          this.string = (this.node.attrs.string || '').replace(/_/g, '');
          var exec_action = function() {
              if (self.node.attrs.confirm) {
                  var def = $.Deferred();
 -                var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
 +                var dialog = new instance.web.Dialog(this, {
                      title: _t('Confirm'),
 -                    modal: true,
                      buttons: [
                          {text: _t("Cancel"), click: function() {
 -                                $(this).dialog("close");
 +                                this.parents('.modal').modal('hide');
                              }
                          },
                          {text: _t("Ok"), click: function() {
                                  var self2 = this;
                                  self.on_confirmed().always(function() {
 -                                    $(self2).dialog("close");
 +                                    self2.parents('.modal').modal('hide');
                                  });
                              }
                          }
                      ],
 -                    beforeClose: function() {
 -                        def.resolve();
 -                    },
 -                });
 +                }, $('<div/>').text(self.node.attrs.confirm)).open();
 +                dialog.on("closing", null, function() {def.resolve();});
                  return def.promise();
              } else {
                  return self.on_confirmed();
          var self = this;
  
          var context = this.build_context();
 -
          return this.view.do_execute_action(
              _.extend({}, this.node.attrs, {context: context}),
              this.view.dataset, this.view.datarecord.id, function (reason) {
@@@ -2441,76 -2434,6 +2441,76 @@@ instance.web.form.FieldFloat = instance
      }
  });
  
 +instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 +    init: function(field_manager, node) {
 +        this._super.apply(this, arguments);
 +    },
 +    start: function() {
 +        var self = this;
 +        this._super.apply(this, arguments);
 +        this.on("change:effective_readonly", this, function () {
 +            this.display_field();
 +            this.render_value();
 +        });
 +        this.display_field();
 +        return this._super();
 +    },
 +    render_value: function() {
 +        this.$('button.select_records').css('visibility', this.get('effective_readonly') ? 'hidden': '');
 +    },
 +    set_value: function(value_) {
 +        var self = this;
 +        this.set('value', value_ || false);
 +        this.display_field();
 +     },
 +    display_field: function() {
 +        var self = this;
 +        this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
 +        if (this.get('value')) {
 +            var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
 +            var domain = instance.web.pyeval.eval('domain', this.get('value'));
 +            var ds = new instance.web.DataSetStatic(self, model, self.build_context());
 +            ds.call('search_count', [domain]).then(function (results) {
 +                $('.oe_domain_count', self.$el).text(results + ' records selected');
 +                $('button span', self.$el).text(' Change selection');
 +            });
 +        } else {
 +            $('.oe_domain_count', this.$el).text('0 record selected');
 +            $('button span', this.$el).text(' Select records');
 +        };
 +        this.$('.select_records').on('click', self.on_click);
 +    },
 +    on_click: function(ev) {
 +        event.preventDefault();
 +        var self = this;
 +        var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
 +        this.pop = new instance.web.form.SelectCreatePopup(this);
 +        this.pop.select_element(
 +            model, {title: 'Select records...'},
 +            [], this.build_context());
 +        this.pop.on("elements_selected", self, function(element_ids) {
 +            if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
 +                var search_data = this.pop.searchview.build_search_data();
 +                var domain_done = instance.web.pyeval.eval_domains_and_contexts({
 +                    domains: search_data.domains,
 +                    contexts: search_data.contexts,
 +                    group_by_seq: search_data.groupbys || []
 +                }).then(function (results) {
 +                    return results.domain;
 +                });
 +            }
 +            else {
 +                var domain = [["id", "in", element_ids]];
 +                var domain_done = $.Deferred().resolve(domain);
 +            }
 +            $.when(domain_done).then(function (domain) {
 +                var domain = self.pop.dataset.domain.concat(domain || []);
 +                self.set_value(domain);
 +            });
 +        });
 +    },
 +});
 +
  instance.web.DateTimeWidget = instance.web.Widget.extend({
      template: "web.datepicker",
      jqueryui_object: 'datetimepicker',
@@@ -2893,87 -2816,6 +2893,87 @@@ instance.web.form.FieldProgressBar = in
      }
  });
  
 +/**
 +    The PercentPie field expect a float from 0 to 100.
 +*/
 +instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
 +    template: 'FieldPercentPie',
 +
 +    render_value: function() {
 +        var value = this.get('value'),
 +            formatted_value = Math.round(value || 0) + '%',
 +            svg = this.$('svg')[0];
 +
 +        svg.innerHTML = "";
 +        nv.addGraph(function() {
 +            var width = 42, height = 42;
 +            var chart = nv.models.pieChart()
 +                .width(width)
 +                .height(height)
 +                .margin({top: 0, right: 0, bottom: 0, left: 0})
 +                .donut(true) 
 +                .showLegend(false)
 +                .showLabels(false)
 +                .tooltips(false)
 +                .color(['#7C7BAD','#DDD'])
 +                .donutRatio(0.62);
 +   
 +            d3.select(svg)
 +                .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
 +                .transition()
 +                .call(chart)
 +                .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
 +
 +            d3.select(svg)
 +                .append("text")
 +                .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
 +                .style({"font-size": "10px", "font-weight": "bold"})
 +                .text(formatted_value);
 +
 +            return chart;
 +        });
 +   
 +    }
 +});
 +
 +/**
 +    The FieldBarChart expectsa list of values (indeed)
 +*/
 +instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
 +    template: 'FieldBarChart',
 +
 +    render_value: function() {
 +        var value = JSON.parse(this.get('value'));
 +        var svg = this.$('svg')[0];
 +        svg.innerHTML = "";
 +        nv.addGraph(function() {
 +            var width = 34, height = 34;
 +            var chart = nv.models.discreteBarChart()
 +                .x(function (d) { return d.tooltip })
 +                .y(function (d) { return d.value })
 +                .width(width)
 +                .height(height)
 +                .margin({top: 0, right: 0, bottom: 0, left: 0})
 +                .tooltips(false)
 +                .showValues(false)
 +                .transitionDuration(350)
 +                .showXAxis(false)
 +                .showYAxis(false);
 +   
 +            d3.select(svg)
 +                .datum([{key: 'values', values: value}])
 +                .transition()
 +                .call(chart)
 +                .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
 +
 +            nv.utils.windowResize(chart.update);
 +
 +            return chart;
 +        });
 +   
 +    }
 +});
 +
  
  instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
      template: 'FieldSelection',
@@@ -3343,7 -3185,7 +3343,7 @@@ instance.web.form.M2ODialog = instance.
      init: function(parent) {
          this._super(parent, {
              title: _.str.sprintf(_t("Add %s"), parent.string),
 -            width: 312,
 +            size: 'medium',
          });
      },
      start: function() {
@@@ -3411,7 -3253,7 +3411,7 @@@ instance.web.form.FieldMany2One = insta
              delete this.$drop_down;
          }
          if (this.$input) {
 -            this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
 +            this.$input.closest(".modal .modal-content").off('scroll');
              this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
                              'focus focusout change keydown');
              delete this.$input;
                  return;
              }
              var pop = new instance.web.form.FormOpenPopup(self);
 -            pop.show_element(
 -                self.field.relation,
 -                self.get("value"),
 -                self.build_context(),
 -                {
 -                    title: _t("Open: ") + self.string
 -                }
 -            );
 -            pop.on('write_completed', self, function(){
 -                self.display_value = {};
 -                self.display_value_backup = {};
 -                self.render_value();
 -                self.focus();
 -                self.trigger('changed_value');
 +            var context = self.build_context().eval();
 +            var model_obj = new instance.web.Model(self.field.relation);
 +            model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
 +                pop.show_element(
 +                    self.field.relation,
 +                    self.get("value"),
 +                    self.build_context(),
 +                    {
 +                        title: _t("Open: ") + self.string,
 +                        view_id: view_id
 +                    }
 +                );
 +                pop.on('write_completed', self, function(){
 +                    self.display_value = {};
 +                    self.display_value_backup = {};
 +                    self.render_value();
 +                    self.focus();
 +                    self.trigger('changed_value');
 +                });
              });
          });
  
                  self.$input.autocomplete("close");
              }
          }, 50);
 -        this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
 +        this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
  
          self.ed_def = $.Deferred();
          self.uned_def = $.Deferred();
                  }
                  self.floating = false;
              }
 -            if (used && self.get("value") === false && ! self.no_ed) {
 +            if (used && self.get("value") === false && ! self.no_ed && (self.options.no_create === false || self.options.no_create === undefined)) {
                  self.ed_def.reject();
                  self.uned_def.reject();
                  self.ed_def = $.Deferred();
                   .html(link);
              if (! this.options.no_open)
                  $link.click(function () {
 -                    self.do_action({
 -                        type: 'ir.actions.act_window',
 -                        res_model: self.field.relation,
 -                        res_id: self.get("value"),
 -                        views: [[false, 'form']],
 -                        target: 'current',
 -                        context: self.build_context().eval(),
 +                    var context = self.build_context().eval();
 +                    var model_obj = new instance.web.Model(self.field.relation);
 +                    model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
 +                        self.do_action(action);
                      });
                      return false;
                   });
@@@ -4337,7 -4177,11 +4337,11 @@@ instance.web.form.One2ManyListView = in
              else
                  return $.when();
          }).done(function () {
-             if (!self.o2m.options.reload_on_button) {
+             var ds = self.o2m.dataset;
+             var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
+                 return value.length;
+             });
+             if (!self.o2m.options.reload_on_button && !cached_records) {
                  self.handle_button(name, id, callback);
              }else {
                  self.handle_button(name, id, function(){
@@@ -5032,12 -4876,13 +5036,12 @@@ instance.web.form.AbstractFormPopup = i
          var self = this;
          this.renderElement();
          var dialog = new instance.web.Dialog(this, {
 -            min_width: '800px',
              dialogClass: 'oe_act_window',
 -            close: function() {
 -                self.check_exit(true);
 -            },
              title: this.options.title || "",
          }, this.$el).open();
 +        dialog.on('closing', this, function (e){
 +            self.check_exit(true);
 +        });
          this.$buttonpane = dialog.$buttons;
          this.start();
      },
      },
      destroy: function () {
          this.trigger('closed');
 -        if (this.$el.is(":data(dialog)")) {
 -            this.$el.dialog('close');
 +        if (this.$el.is(":data(bs.modal)")) {
 +            this.$el.parents('.modal').modal('hide');
          }
          this._super();
      },
@@@ -5567,7 -5412,7 +5571,7 @@@ instance.web.form.FieldBinaryImage = in
   * Options on attribute ; "blockui" {Boolean} block the UI or not
   * during the file is uploading
   */
 -instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 +instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
      template: "FieldBinaryFileUploader",
      init: function(field_manager, node) {
          this._super(field_manager, node);
      start: function() {
          this._super(this);
          this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
 -        this.on("change:effective_readonly", this, function () {
 -            this.render_value();
 -        });
      },
      set_value: function(value_) {
          value_ = value_ || [];
      },
      render_value: function () {
          var self = this;
 -        this.$('.oe_add').css('visibility', this.get('effective_readonly') ? 'hidden': '');
          this.read_name_values().then(function (ids) {
              var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
              render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
@@@ -6026,36 -5875,6 +6030,36 @@@ instance.web.form.X2ManyCounter = insta
  });
  
  /**
 +    This widget is intended to be used on stat button numeric fields.  It will display
 +    the value   many2many and one2many. It is a read-only field that will 
 +    display a simple string "<value of field> <label of the field>"
 +*/
 +instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
 +    is_field_number: true,
 +    init: function() {
 +        this._super.apply(this, arguments);
 +        this.internal_set_value(0);
 +    },
 +    set_value: function(value_) {
 +        if (value_ === false || value_ === undefined) {
 +            value_ = 0;
 +        }
 +        this._super.apply(this, [value_]);
 +    },
 +    render_value: function() {
 +        var options = {
 +            value: this.get("value") || 0,
 +        };
 +        if (! this.node.attrs.nolabel) {
 +            options.text = this.string
 +        }
 +        this.$el.html(QWeb.render("StatInfo", options));
 +    },
 +
 +});
 +
 +
 +/**
   * Registry of form fields, called by :js:`instance.web.FormView`.
   *
   * All referenced classes must implement FieldInterface. Those represent the classes whose instances
@@@ -6068,7 -5887,6 +6072,7 @@@ instance.web.form.widgets = new instanc
      'url' : 'instance.web.form.FieldUrl',
      'text' : 'instance.web.form.FieldText',
      'html' : 'instance.web.form.FieldTextHtml',
 +    'char_domain': 'instance.web.form.FieldCharDomain',
      'date' : 'instance.web.form.FieldDate',
      'datetime' : 'instance.web.form.FieldDatetime',
      'selection' : 'instance.web.form.FieldSelection',
      'reference' : 'instance.web.form.FieldReference',
      'boolean' : 'instance.web.form.FieldBoolean',
      'float' : 'instance.web.form.FieldFloat',
 +    'percentpie': 'instance.web.form.FieldPercentPie',
 +    'barchart': 'instance.web.form.FieldBarChart',
      'integer': 'instance.web.form.FieldFloat',
      'float_time': 'instance.web.form.FieldFloat',
      'progressbar': 'instance.web.form.FieldProgressBar',
      'monetary': 'instance.web.form.FieldMonetary',
      'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
      'x2many_counter': 'instance.web.form.X2ManyCounter',
 +    'statinfo': 'instance.web.form.StatInfo',
  });
  
  /**
@@@ -1,9 -1,8 +1,9 @@@
  # -*- coding: utf-8 -*-
 +import datetime
 +import hashlib
  import logging
  import re
  import traceback
 -
  import werkzeug
  import werkzeug.routing
  
@@@ -79,102 -78,70 +79,102 @@@ class ir_http(orm.AbstractModel)
  
          return self._dispatch()
  
 -    def _postprocess_args(self, arguments):
 -        if hasattr(request, 'rerouting'):
 -            url = request.rerouting[0]
 -        else:
 -            url = request.httprequest.url
 -        original_url = url
 -        for arg in arguments.itervalues():
 -            if isinstance(arg, orm.browse_record) and isinstance(arg._uid, RequestUID):
 -                placeholder = arg._uid
 -                arg._uid = request.uid
 -                try:
 -                    good_slug = slug(arg)
 -                    if str(arg.id) != placeholder.value and placeholder.value != good_slug:
 -                        # TODO: properly recompose the url instead of using replace()
 -                        url = url.replace(placeholder.value, good_slug)
 -                except KeyError:
 -                    return self._handle_exception(werkzeug.exceptions.NotFound())
 -        if url != original_url:
 -            werkzeug.exceptions.abort(werkzeug.utils.redirect(url))
 +    def _postprocess_args(self, arguments, rule):
 +        if not getattr(request, 'website_enabled', False):
 +            return super(ir_http, self)._postprocess_args(arguments, rule)
 +
 +        for arg, val in arguments.items():
 +            # Replace uid placeholder by the current request.uid
 +            if isinstance(val, orm.browse_record) and isinstance(val._uid, RequestUID):
 +                val._uid = request.uid
 +        try:
 +            _, path = rule.build(arguments)
 +            assert path is not None
 +        except Exception:
 +            return self._handle_exception(werkzeug.exceptions.NotFound())
 +
 +        if request.httprequest.method in ('GET', 'HEAD'):
 +            generated_path = werkzeug.url_unquote_plus(path)
 +            current_path = werkzeug.url_unquote_plus(request.httprequest.path)
 +            if generated_path != current_path:
 +                if request.lang != request.website.default_lang_code:
 +                    path = '/' + request.lang + path
 +                return werkzeug.utils.redirect(path)
 +
 +    def _serve_attachment(self):
 +        domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
 +        attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
 +        if attach:
 +            wdate = attach[0]['__last_update']
 +            datas = attach[0]['datas']
 +            response = werkzeug.wrappers.Response()
 +            server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
 +            try:
 +                response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
 +            except ValueError:
 +                # just in case we have a timestamp without microseconds
 +                response.last_modified = datetime.datetime.strptime(wdate, server_format)
 +
 +            response.set_etag(hashlib.sha1(datas).hexdigest())
 +            response.make_conditional(request.httprequest)
 +
 +            if response.status_code == 304:
 +                return response
 +
 +            response.mimetype = attach[0]['mimetype']
 +            response.data = datas.decode('base64')
 +            return response
  
      def _handle_exception(self, exception=None, code=500):
-         res = super(ir_http, self)._handle_exception(exception)
-         if isinstance(exception, werkzeug.exceptions.HTTPException) and hasattr(exception, 'response') and exception.response:
-             return exception.response
+         try:
+             return super(ir_http, self)._handle_exception(exception)
+         except Exception:
 +
 +        attach = self._serve_attachment()
 +        if attach:
 +            return attach
 +
-         if getattr(request, 'website_enabled', False) and request.website:
-             values = dict(
-                 exception=exception,
-                 traceback=traceback.format_exc(exception),
-             )
-             if exception:
-                 code = getattr(exception, 'code', code)
-                 if isinstance(exception, ir_qweb.QWebException):
-                     values.update(qweb_exception=exception)
-                     if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
-                         code = 403
-             if code == 500:
-                 logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
-                 if 'qweb_exception' in values:
-                     view = request.registry.get("ir.ui.view")
-                     views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
-                     to_reset = [v for v in views if v.model_data_id.noupdate is True]
-                     values['views'] = to_reset
-             elif code == 403:
-                 logger.warn("403 Forbidden:\n\n%s", values['traceback'])
-             values.update(
-                 status_message=werkzeug.http.HTTP_STATUS_CODES[code],
-                 status_code=code,
-             )
-             if not request.uid:
-                 self._auth_method_public()
-             try:
-                 html = request.website._render('website.%s' % code, values)
-             except Exception:
-                 html = request.website._render('website.http_error', values)
-             return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
-         return res
+             if getattr(request, 'website_enabled', False) and request.website:
+                 values = dict(
+                     exception=exception,
+                     traceback=traceback.format_exc(exception),
+                 )
+                 if exception:
+                     code = getattr(exception, 'code', code)
+                     if isinstance(exception, ir_qweb.QWebException):
+                         values.update(qweb_exception=exception)
+                         if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
+                             code = 403
+                 if code == 500:
+                     logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
+                     if 'qweb_exception' in values:
+                         view = request.registry.get("ir.ui.view")
+                         views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
+                         to_reset = [v for v in views if v.model_data_id.noupdate is True]
+                         values['views'] = to_reset
+                 elif code == 403:
+                     logger.warn("403 Forbidden:\n\n%s", values['traceback'])
+                 values.update(
+                     status_message=werkzeug.http.HTTP_STATUS_CODES[code],
+                     status_code=code,
+                 )
+                 if not request.uid:
+                     self._auth_method_public()
+                 try:
+                     html = request.website._render('website.%s' % code, values)
+                 except Exception:
+                     html = request.website._render('website.http_error', values)
+                 return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
+             raise
  
  class ModelConverter(ir.ir_http.ModelConverter):
 -    def __init__(self, url_map, model=False):
 +    def __init__(self, url_map, model=False, domain='[]'):
          super(ModelConverter, self).__init__(url_map, model)
 +        self.domain = domain
          self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)'
  
      def to_url(self, value):
          return request.registry[self.model].browse(
              request.cr, _uid, int(m.group(1)), context=request.context)
  
 -    def generate(self, cr, uid, query=None, context=None):
 -        return request.registry[self.model].name_search(
 -            cr, uid, name=query or '', context=context)
 +    def generate(self, cr, uid, query=None, args=None, context=None):
 +        obj = request.registry[self.model]
 +        domain = eval( self.domain, (args or {}).copy())
 +        if query:
 +            domain.append((obj._rec_name, 'ilike', '%'+query+'%'))
 +        for record in obj.search_read(cr, uid, domain=domain, fields=['write_date',obj._rec_name], context=context):
 +            if record.get(obj._rec_name, False):
 +                yield {'loc': (record['id'], record[obj._rec_name])}
  
  class PageConverter(werkzeug.routing.PathConverter):
 -    """ Only point of this converter is to bundle pages enumeration logic
 -
 -    Sads got: no way to get the view's human-readable name even if one exists
 -    """
 -    def generate(self, cr, uid, query=None, context=None):
 +    """ Only point of this converter is to bundle pages enumeration logic """
 +    def generate(self, cr, uid, query=None, args={}, context=None):
          View = request.registry['ir.ui.view']
 -        views = View.search_read(
 -            cr, uid, [['page', '=', True]],
 -            fields=[], order='name', context=context)
 -        xids = View.get_external_id(
 -            cr, uid, [view['id'] for view in views], context=context)
 -
 +        views = View.search_read(cr, uid, [['page', '=', True]],
 +            fields=['xml_id','priority','write_date'], order='name', context=context)
          for view in views:
 -            xid = xids[view['id']]
 -            if xid and (not query or query.lower() in xid.lower()):
 -                yield xid
 +            xid = view['xml_id'].startswith('website.') and view['xml_id'][8:] or view['xml_id']
 +            # the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice
 +            # when we will have an url mapping mechanism, replace this by a rule: page/homepage --> /
 +            if xid=='homepage': continue
 +            if query and query.lower() not in xid.lower():
 +                continue
 +            record = {'loc': xid}
 +            if view['priority'] <> 16:
 +                record['__priority'] = min(round(view['priority'] / 32.0,1), 1)
 +            if view.get('write_date'):
 +                record['__lastmod'] = view['write_date'][:10]
 +            yield record
@@@ -78,8 -78,8 +78,8 @@@ class table_compute(object)
          index = 0
          maxy = 0
          for p in products:
-             x = p.website_size_x
-             y = p.website_size_y
+             x = min(max(p.website_size_x, 1), PPR)
+             y = min(max(p.website_size_y, 1), PPR)
              if index>PPG:
                  x = y = 1
  
@@@ -151,7 -151,7 +151,7 @@@ class Ecommerce(http.Controller)
                  return key_val
          return False
  
 -    @http.route(['/shop/filters/'], type='http', auth="public", methods=['POST'], website=True, multilang=True)
 +    @http.route(['/shop/filters'], type='http', auth="public", methods=['POST'], website=True, multilang=True)
      def filters(self, category=None, **post):
          index = []
          filters = []
                      filters[index.index(cat_id)].append( cat[2] )
              post.pop(key)
  
 -        url = "/shop/"
 +        url = "/shop"
          if category:
              category_obj = request.registry.get('product.public.category')
 -            url = "%scategory/%s/" % (url, slug(category_obj.browse(request.cr, request.uid, int(category), context=request.context)))
 +            url = "%s/category/%s" % (url, slug(category_obj.browse(request.cr, request.uid, int(category), context=request.context)))
          if filters:
              url = "%s?filters=%s" % (url, simplejson.dumps(filters))
          if post.get("search"):
      @http.route(['/shop/pricelist'], type='http', auth="public", website=True, multilang=True)
      def shop_promo(self, promo=None, **post):
          request.registry['website']._ecommerce_change_pricelist(request.cr, request.uid, code=promo, context=request.context)
 -        return request.redirect("/shop/mycart/")
 +        return request.redirect("/shop/mycart")
  
      @http.route([
 -        '/shop/',
 -        '/shop/page/<int:page>/',
 -        '/shop/category/<model("product.public.category"):category>/',
 -        '/shop/category/<model("product.public.category"):category>/page/<int:page>/'
 +        '/shop',
 +        '/shop/page/<int:page>',
 +        '/shop/category/<model("product.public.category"):category>',
 +        '/shop/category/<model("product.public.category"):category>/page/<int:page>'
      ], type='http', auth="public", website=True, multilang=True)
      def shop(self, category=None, page=0, filters='', search='', **post):
          cr, uid, context = request.cr, request.uid, request.context
                  ids = self.attributes_to_ids(cr, uid, filters)
                  domain.append(('id', 'in', ids or [0]))
  
 -        url = "/shop/"
 +        url = "/shop"
          product_count = product_obj.search_count(cr, uid, domain, context=context)
          if search:
              post["search"] = search
          if filters:
              post["filters"] = filters
          if category:
 -            url = "/shop/category/%s/" % slug(category)
 +            url = "/shop/category/%s" % slug(category)
          pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
  
          request.context['pricelist'] = self.get_pricelist()
          }
          return request.website.render("website_sale.products", values)
  
 -    @http.route(['/shop/product/<model("product.template"):product>/'], type='http', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True, multilang=True)
      def product(self, product, search='', category='', filters='', **kwargs):
          if category:
              category_obj = request.registry.get('product.public.category')
                  context=dict(context, mail_create_nosubcribe=True))
          return werkzeug.utils.redirect(request.httprequest.referrer + "#comments")
  
 -    @http.route(['/shop/add_product/'], type='http', auth="user", methods=['POST'], website=True, multilang=True)
 +    @http.route(['/shop/add_product'], type='http', auth="user", methods=['POST'], website=True, multilang=True)
      def add_product(self, name=None, category=0, **post):
          if not name:
              name = _("New Product")
              'name': name, 'public_categ_id': category
          }, context=request.context)
          product = Product.browse(request.cr, request.uid, product_id, context=request.context)
 +        
 +        return request.redirect("/shop/product/%s?enable_editor=1" % slug(product.product_tmpl_id))
  
 -        return request.redirect("/shop/product/%s/?enable_editor=1" % product.product_tmpl_id.id)
 -
 -    @http.route(['/shop/mycart/'], type='http', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/mycart'], type='http', auth="public", website=True, multilang=True)
      def mycart(self, **post):
          cr, uid, context = request.cr, request.uid, request.context
          prod_obj = request.registry.get('product.product')
          order = self.get_order()
          if order and order.state != 'draft':
              request.registry['website'].ecommerce_reset(cr, uid, context=context)
 -            return request.redirect('/shop/')
 +            return request.redirect('/shop')
  
          self.get_pricelist()
  
          }
          return request.website.render("website_sale.mycart", values)
  
 -    @http.route(['/shop/add_cart/'], type='http', auth="public", methods=['POST'], website=True, multilang=True)
 +    @http.route(['/shop/add_cart'], type='http', auth="public", methods=['POST'], website=True, multilang=True)
      def add_cart(self, product_id, remove=None, **kw):
          request.registry['website']._ecommerce_add_product_to_cart(request.cr, request.uid,
              product_id=int(product_id),
              number=float(kw.get('number',1)),
              set_number=float(kw.get('set_number',-1)),
              context=request.context)
 -        return request.redirect("/shop/mycart/")
 +        return request.redirect("/shop/mycart")
  
 -    @http.route(['/shop/change_cart/<int:order_line_id>/'], type='http', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/change_cart/<int:order_line_id>'], type='http', auth="public", website=True, multilang=True)
      def add_cart_order_line(self, order_line_id=None, remove=None, **kw):
          request.registry['website']._ecommerce_add_product_to_cart(request.cr, request.uid,
              order_line_id=order_line_id, number=(remove and -1 or 1),
              context=request.context)
 -        return request.redirect("/shop/mycart/")
 +        return request.redirect("/shop/mycart")
  
 -    @http.route(['/shop/add_cart_json/'], type='json', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/add_cart_json'], type='json', auth="public", website=True, multilang=True)
      def add_cart_json(self, product_id=None, order_line_id=None, remove=None):
          quantity = request.registry['website']._ecommerce_add_product_to_cart(request.cr, request.uid,
              product_id=product_id, order_line_id=order_line_id, number=(remove and -1 or 1),
                  order.amount_total,
                  request.website._render("website_sale.total", {'website_sale_order': order})]
  
 -    @http.route(['/shop/set_cart_json/'], type='json', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/set_cart_json'], type='json', auth="public", website=True, multilang=True)
      def set_cart_json(self, path=None, product_id=None, order_line_id=None, set_number=0, json=None):
          quantity = request.registry['website']._ecommerce_add_product_to_cart(request.cr, request.uid,
              product_id=product_id, order_line_id=order_line_id, set_number=set_number,
                  order.amount_total,
                  request.website._render("website_sale.total", {'website_sale_order': order})]
      
 -    @http.route(['/shop/checkout/'], type='http', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/checkout'], type='http', auth="public", website=True, multilang=True)
      def checkout(self, **post):
          cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
  
          order = self.get_order()
          if not order or order.state != 'draft' or not order.order_line:
              request.registry['website'].ecommerce_reset(cr, uid, context=context)
 -            return request.redirect('/shop/')
 +            return request.redirect('/shop')
          # if transaction pending / done: redirect to confirmation
          tx = context.get('website_sale_transaction')
          if tx and tx.state != 'draft':
  
          return request.website.render("website_sale.checkout", values)
  
 -    @http.route(['/shop/confirm_order/'], type='http', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/confirm_order'], type='http', auth="public", website=True, multilang=True)
      def confirm_order(self, **post):
          cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
          order_line_obj = request.registry.get('sale.order')
          order = self.get_order()
          if not order or order.state != 'draft' or not order.order_line:
              request.registry['website'].ecommerce_reset(cr, uid, context=context)
 -            return request.redirect('/shop/')
 +            return request.redirect('/shop')
          # if transaction pending / done: redirect to confirmation
          tx = context.get('website_sale_transaction')
          if tx and tx.state != 'draft':
  
          order_line_obj.write(cr, SUPERUSER_ID, [order.id], order_info, context=context)
  
 -        return request.redirect("/shop/payment/")
 +        return request.redirect("/shop/payment")
  
 -    @http.route(['/shop/payment/'], type='http', auth="public", website=True, multilang=True)
 +    @http.route(['/shop/payment'], type='http', auth="public", website=True, multilang=True)
      def payment(self, **post):
          """ Payment step. This page proposes several payment means based on available
          payment.acquirer. State at this point :
          order = self.get_order()
          if not order or order.state != 'draft' or not order.order_line:
              request.registry['website'].ecommerce_reset(cr, uid, context=context)
 -            return request.redirect("/shop/")
 +            return request.redirect("/shop")
          # alread a transaction: forward to confirmation
          tx = context.get('website_sale_transaction')
          if tx and tx.state != 'draft':
          order = self.get_order()
  
          if not order or not order.order_line or acquirer_id is None:
 -            return request.redirect("/shop/checkout/")
 +            return request.redirect("/shop/checkout")
  
          # find an already existing transaction
          tx = context.get('website_sale_transaction')
                  'reference': order.name,
                  'sale_order_id': order.id,
              }, context=context)
 -            request.httprequest.session['website_sale_transaction_id'] = tx_id
 +            request.session['website_sale_transaction_id'] = tx_id
          elif tx and tx.state == 'draft':  # button cliked but no more info -> rewrite on tx or create a new one ?
              tx.write({
                  'acquirer_id': acquirer_id,
          cr, uid, context = request.cr, request.uid, request.context
  
          order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
 -        assert order.website_session_id == request.httprequest.session['website_session_id']
 +        assert order.website_session_id == request.session['website_session_id']
  
          if not order:
              return {
                  message = '<p>The payment seems to have been canceled.</p>'
              elif state == 'pending' and tx.acquirer_id.validation == 'manual':
                  message = '<p>Your transaction is waiting confirmation.</p>'
 -                message += tx.acquirer_id.post_msg
 +                if tx.acquirer_id.post_msg:
 +                    message += tx.acquirer_id.post_msg
              else:
                  message = '<p>Your transaction is waiting confirmation.</p>'
              validation = tx.acquirer_id.validation
              'validation': validation
          }
  
 -    @http.route('/shop/payment/validate/', type='http', auth="public", website=True, multilang=True)
 +    @http.route('/shop/payment/validate', type='http', auth="public", website=True, multilang=True)
      def payment_validate(self, transaction_id=None, sale_order_id=None, **post):
          """ Method that should be called by the server when receiving an update
          for a transaction. State at this point :
              order = self.get_order()
          else:
              order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
 -            assert order.website_session_id == request.httprequest.session['website_session_id']
 +            assert order.website_session_id == request.session['website_session_id']
  
          if not order:
 -            return request.redirect('/shop/')
 +            return request.redirect('/shop')
          elif order.amount_total and not tx:
              return request.redirect('/shop/mycart')
  
          cr, uid, context = request.cr, request.uid, request.context
  
          order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
 -        assert order.website_session_id == request.httprequest.session['website_session_id']
 +        assert order.website_session_id == request.session['website_session_id']
  
          request.registry['website']._ecommerce_change_pricelist(cr, uid, None, context=context or {})
  
          return request.website.render("website_sale.confirmation", {'order': order})
  
 -    @http.route(['/shop/change_sequence/'], type='json', auth="public")
 +    @http.route(['/shop/change_sequence'], type='json', auth="public")
      def change_sequence(self, id, sequence):
          product_obj = request.registry.get('product.template')
          if sequence == "top":
          elif sequence == "down":
              product_obj.set_sequence_down(request.cr, request.uid, [id], context=request.context)
  
 -    @http.route(['/shop/change_styles/'], type='json', auth="public")
 +    @http.route(['/shop/change_styles'], type='json', auth="public")
      def change_styles(self, id, style_id):
          product_obj = request.registry.get('product.template')
          product = product_obj.browse(request.cr, request.uid, id, context=request.context)
  
          return not active
  
 -    @http.route(['/shop/change_size/'], type='json', auth="public")
 +    @http.route(['/shop/change_size'], type='json', auth="public")
      def change_size(self, id, x, y):
          product_obj = request.registry.get('product.template')
          product = product_obj.browse(request.cr, request.uid, id, context=request.context)
@@@ -33,12 -33,13 +33,13 @@@ class product_template(osv.Model)
      _inherit = ["product.template", "website.seo.metadata"]
      _order = 'website_published desc, website_sequence desc, name'
      _name = 'product.template'
+     _mail_post_access = 'read'
  
      def _website_url(self, cr, uid, ids, field_name, arg, context=None):
          res = dict.fromkeys(ids, '')
          base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
          for product in self.browse(cr, uid, ids, context=context):
 -            res[product.id] = "%s/shop/product/%s/" % (base_url, product.id)
 +            res[product.id] = "%s/shop/product/%s" % (base_url, product.id)
          return res
  
      _columns = {
@@@ -139,7 -140,7 +140,7 @@@ class product_product(osv.Model)
          res = {}
          base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
          for product in self.browse(cr, uid, ids, context=context):
 -            res[product.id] = "%s/shop/product/%s/" % (base_url, product.product_tmpl_id.id)
 +            res[product.id] = "%s/shop/product/%s" % (base_url, product.product_tmpl_id.id)
          return res
  
      _columns = {
diff --combined openerp/http.py
@@@ -23,7 -23,6 +23,7 @@@ import urlpars
  import warnings
  
  import babel.core
 +import psutil
  import psycopg2
  import simplejson
  import werkzeug.contrib.sessions
@@@ -36,7 -35,7 +36,7 @@@ import werkzeug.wsg
  
  import openerp
  from openerp.service import security, model as service_model
 -import openerp.tools
 +from openerp.tools.func import lazy_property
  
  _logger = logging.getLogger(__name__)
  
@@@ -51,70 -50,6 +51,70 @@@ request = _request_stack(
      A global proxy that always redirect to the current request object.
  """
  
 +def replace_request_password(args):
 +    # password is always 3rd argument in a request, we replace it in RPC logs
 +    # so it's easier to forward logs for diagnostics/debugging purposes...
 +    if len(args) > 2:
 +        args = list(args)
 +        args[2] = '*'
 +    return tuple(args)
 +
 +def dispatch_rpc(service_name, method, params):
 +    """ Handle a RPC call.
 +
 +    This is pure Python code, the actual marshalling (from/to XML-RPC) is done
 +    in a upper layer.
 +    """
 +    try:
 +        rpc_request = logging.getLogger(__name__ + '.rpc.request')
 +        rpc_response = logging.getLogger(__name__ + '.rpc.response')
 +        rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG)
 +        rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG)
 +        if rpc_request_flag or rpc_response_flag:
 +            start_time = time.time()
 +            start_rss, start_vms = 0, 0
 +            start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
 +            if rpc_request and rpc_response_flag:
 +                openerp.netsvc.log(rpc_request, logging.DEBUG, '%s.%s' % (service_name, method), replace_request_password(params))
 +
 +        threading.current_thread().uid = None
 +        threading.current_thread().dbname = None
 +        if service_name == 'common':
 +            dispatch = openerp.service.common.dispatch
 +        elif service_name == 'db':
 +            dispatch = openerp.service.db.dispatch
 +        elif service_name == 'object':
 +            dispatch = openerp.service.model.dispatch
 +        elif service_name == 'report':
 +            dispatch = openerp.service.report.dispatch
 +        else:
 +            dispatch = openerp.service.wsgi_server.rpc_handlers.get(service_name)
 +        result = dispatch(method, params)
 +
 +        if rpc_request_flag or rpc_response_flag:
 +            end_time = time.time()
 +            end_rss, end_vms = 0, 0
 +            end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
 +            logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024)
 +            if rpc_response_flag:
 +                openerp.netsvc.log(rpc_response, logging.DEBUG, logline, result)
 +            else:
 +                openerp.netsvc.log(rpc_request, logging.DEBUG, logline, replace_request_password(params), depth=1)
 +
 +        return result
 +    except (openerp.osv.orm.except_orm, openerp.exceptions.AccessError, \
 +            openerp.exceptions.AccessDenied, openerp.exceptions.Warning, \
 +            openerp.exceptions.RedirectWarning):
 +        raise
 +    except openerp.exceptions.DeferredException, e:
 +        _logger.exception(openerp.tools.exception_to_unicode(e))
 +        openerp.tools.debugger.post_mortem(openerp.tools.config, e.traceback)
 +        raise
 +    except Exception, e:
 +        _logger.exception(openerp.tools.exception_to_unicode(e))
 +        openerp.tools.debugger.post_mortem(openerp.tools.config, sys.exc_info())
 +        raise
 +
  def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
      url = path
      if not query:
@@@ -194,10 -129,12 +194,10 @@@ class WebRequest(object)
          self.session_id = httprequest.session.sid
          self.disable_db = False
          self.uid = None
 -        self.func = None
 -        self.func_arguments = {}
 +        self.endpoint = None
          self.auth_method = None
          self._cr_cm = None
          self._cr = None
 -        self.func_request_type = None
  
          # prevents transaction commit, use when you catch an exception during handling
          self._failed = None
          """
          # some magic to lazy create the cr
          if not self._cr:
 -            self._cr = self.registry.db.cursor()
 +            self._cr = self.registry.cursor()
          return self._cr
  
      def __enter__(self):
  
      def __exit__(self, exc_type, exc_value, traceback):
          _request_stack.pop()
 +
          if self._cr:
              if exc_type is None and not self._failed:
                  self._cr.commit()
 -            else:
 -                # just to be explicit - happens at close() anyway
 -                self._cr.rollback()
              self._cr.close()
          # just to be sure no one tries to re-use the request
          self.disable_db = True
          self.uid = None
  
 -    def set_handler(self, func, arguments, auth):
 +    def set_handler(self, endpoint, arguments, auth):
          # is this needed ?
          arguments = dict((k, v) for k, v in arguments.iteritems()
                           if not k.startswith("_ignored_"))
  
 -        self.func = func
 -        self.func_request_type = func.routing['type']
 -        self.func_arguments = arguments
 +        endpoint.arguments = arguments
 +        self.endpoint = endpoint
          self.auth_method = auth
  
  
             to abitrary responses. Anything returned (except None) will
             be used as response.""" 
          self._failed = exception # prevent tx commit
+         if isinstance(exception, werkzeug.exceptions.HTTPException):
+             return exception
+         raise
  
      def _call_function(self, *args, **kwargs):
          request = self
 -        if self.func_request_type != self._request_type:
 +        if self.endpoint.routing['type'] != self._request_type:
              raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
 -                % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
 +                % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
  
 -        kwargs.update(self.func_arguments)
 +        kwargs.update(self.endpoint.arguments)
  
          # Backward for 7.0
 -        if getattr(self.func.method, '_first_arg_is_req', False):
 +        if self.endpoint.first_arg_is_req:
              args = (request,) + args
  
          # Correct exception handling and concurency retry
              # case, the request cursor is unusable. Rollback transaction to create a new one.
              if self._cr:
                  self._cr.rollback()
 -            return self.func(*a, **kw)
 +            return self.endpoint(*a, **kw)
  
          if self.db:
              return checked_call(self.db, *args, **kwargs)
 -        return self.func(*args, **kwargs)
 +        return self.endpoint(*args, **kwargs)
  
      @property
      def debug(self):
@@@ -332,23 -275,8 +335,23 @@@ def route(route=None, **kw)
              else:
                  routes = [route]
              routing['routes'] = routes
 -        f.routing = routing
 -        return f
 +        @functools.wraps(f)
 +        def response_wrap(*args, **kw):
 +            response = f(*args, **kw)
 +            if isinstance(response, Response) or f.routing_type == 'json':
 +                return response
 +            elif isinstance(response, werkzeug.wrappers.BaseResponse):
 +                response = Response.force_type(response)
 +                response.set_default()
 +                return response
 +            elif isinstance(response, basestring):
 +                return Response(response)
 +            else:
 +                _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
 +            return response
 +        response_wrap.routing = routing
 +        response_wrap.original_func = f
 +        return response_wrap
      return decorator
  
  class JsonRequest(WebRequest):
              mime = 'application/json'
              body = simplejson.dumps(response)
  
 -        return werkzeug.wrappers.Response(
 +        return Response(
                      body, headers=[('Content-Type', mime),
                                     ('Content-Length', len(body))])
  
      def _handle_exception(self, exception):
          """Called within an except block to allow converting exceptions
             to abitrary responses. Anything returned (except None) will
-            be used as response.""" 
-         super(JsonRequest, self)._handle_exception(exception)
-         _logger.exception("Exception during JSON request handling.")
-         error = {
-                 'code': 200,
-                 'message': "OpenERP Server Error",
-                 'data': serialize_exception(exception)
-         }
-         if isinstance(exception, AuthenticationError):
-             error['code'] = 100
-             error['message'] = "OpenERP Session Invalid"
-         return self._json_response(error=error)
+            be used as response."""
+         try:
+             return super(JsonRequest, self)._handle_exception(exception)
+         except Exception:
+             _logger.exception("Exception during JSON request handling.")
+             error = {
+                     'code': 200,
+                     'message': "OpenERP Server Error",
+                     'data': serialize_exception(exception)
+             }
+             if isinstance(exception, AuthenticationError):
+                 error['code'] = 100
+                 error['message'] = "OpenERP Session Invalid"
+             return self._json_response(error=error)
  
      def dispatch(self):
          """ Calls the method asked for by the JSON-RPC2 or JSONP request
@@@ -527,16 -457,23 +532,16 @@@ class HttpRequest(WebRequest)
          self.params = params
  
      def dispatch(self):
 -        # TODO: refactor this correctly. This is a quick fix for pos demo.
 -        if request.httprequest.method == 'OPTIONS' and request.func and request.func.routing.get('cors'):
 -            response = werkzeug.wrappers.Response(status=200)
 -            response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
 -            methods = 'GET, POST'
 -            if request.func_request_type == 'json':
 -                methods = 'POST'
 -            elif request.func.routing.get('methods'):
 -                methods = ', '.join(request.func.routing['methods'])
 -            response.headers.set('Access-Control-Allow-Methods', methods)
 -            response.headers.set('Access-Control-Max-Age',60*60*24)
 -            response.headers.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
 -            return response
 +        if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
 +            headers = {
 +                'Access-Control-Max-Age': 60 * 60 * 24,
 +                'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
 +            }
 +            return Response(status=200, headers=headers)
  
          r = self._call_function(**self.params)
          if not r:
 -            r = werkzeug.wrappers.Response(status=204)  # no content
 +            r = Response(status=204)  # no content
          return r
  
      def make_response(self, data, headers=None, cookies=None):
          :type headers: ``[(name, value)]``
          :param collections.Mapping cookies: cookies to set on the client
          """
 -        response = werkzeug.wrappers.Response(data, headers=headers)
 +        response = Response(data, headers=headers)
          if cookies:
              for k, v in cookies.iteritems():
                  response.set_cookie(k, v)
          return response
  
 +    def render(self, template, qcontext=None, **kw):
 +        """ Lazy render of QWeb template.
 +
 +        The actual rendering of the given template will occur at then end of
 +        the dispatching. Meanwhile, the template and/or qcontext can be
 +        altered or even replaced by a static response.
 +
 +        :param basestring template: template to render
 +        :param dict qcontext: Rendering context to use
 +        """
 +        return Response(template=template, qcontext=qcontext, **kw)
 +
      def not_found(self, description=None):
          """ Helper for 404 response, return its result from the method
          """
@@@ -600,8 -525,8 +605,8 @@@ class ControllerType(type)
  
          # flag old-style methods with req as first argument
          for k, v in attrs.items():
 -            if inspect.isfunction(v):
 -                spec = inspect.getargspec(v)
 +            if inspect.isfunction(v) and hasattr(v, 'original_func'):
 +                spec = inspect.getargspec(v.original_func)
                  first_arg = spec.args[1] if len(spec.args) >= 2 else None
                  if first_arg in ["req", "request"]:
                      v._first_arg_is_req = True
@@@ -625,15 -550,7 +630,15 @@@ class Controller(object)
  class EndPoint(object):
      def __init__(self, method, routing):
          self.method = method
 +        self.original = getattr(method, 'original_func', method)
          self.routing = routing
 +        self.arguments = {}
 +
 +    @property
 +    def first_arg_is_req(self):
 +        # Backward for 7.0
 +        return getattr(self.method, '_first_arg_is_req', False)
 +
      def __call__(self, *args, **kw):
          return self.method(*args, **kw)
  
@@@ -656,19 -573,9 +661,19 @@@ def routing_map(modules, nodb_only, con
                  if inspect.ismethod(mv) and hasattr(mv, 'routing'):
                      routing = dict(type='http', auth='user', methods=None, routes=None)
                      methods_done = list()
 +                    routing_type = None
                      for claz in reversed(mv.im_class.mro()):
                          fn = getattr(claz, mv.func_name, None)
                          if fn and hasattr(fn, 'routing') and fn not in methods_done:
 +                            fn_type = fn.routing.get('type')
 +                            if not routing_type:
 +                                routing_type = fn_type
 +                            else:
 +                                if fn_type and routing_type != fn_type:
 +                                    _logger.warn("Subclass re-defines <function %s.%s> with different type than original."
 +                                                    " Will use original type: %r", fn.__module__, fn.__name__, routing_type)
 +                                fn.routing['type'] = routing_type
 +                            fn.original_func.routing_type = routing_type
                              methods_done.append(fn)
                              routing.update(fn.routing)
                      if not nodb_only or nodb_only == (routing['auth'] == "none"):
@@@ -696,7 -603,7 +701,7 @@@ class SessionExpiredException(Exception
  class Service(object):
      """
          .. deprecated:: 8.0
 -        Use ``openerp.netsvc.dispatch_rpc()`` instead.
 +        Use ``dispatch_rpc()`` instead.
      """
      def __init__(self, session, service_name):
          self.session = session
  
      def __getattr__(self, method):
          def proxy_method(*args):
 -            result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
 +            result = dispatch_rpc(self.service_name, method, args)
              return result
          return proxy_method
  
  class Model(object):
      """
          .. deprecated:: 8.0
 -        Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
 +        Use the resistry and cursor in ``openerp.http.request`` instead.
      """
      def __init__(self, session, model):
          self.session = session
@@@ -777,7 -684,7 +782,7 @@@ class OpenERPSession(werkzeug.contrib.s
                  HTTP_HOST=wsgienv['HTTP_HOST'],
                  REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
              )
 -            uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
 +            uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
          else:
              security.check(db, uid, password)
          self.db = db
      def send(self, service_name, method, *args):
          """
          .. deprecated:: 8.0
 -        Use ``openerp.netsvc.dispatch_rpc()`` instead.
 +        Use ``dispatch_rpc()`` instead.
          """
 -        return openerp.netsvc.dispatch_rpc(service_name, method, args)
 +        return dispatch_rpc(service_name, method, args)
  
      def proxy(self, service):
          """
          .. deprecated:: 8.0
 -        Use ``openerp.netsvc.dispatch_rpc()`` instead.
 +        Use ``dispatch_rpc()`` instead.
          """
          return Service(self, service)
  
@@@ -1005,51 -912,19 +1010,51 @@@ mimetypes.add_type('application/font-wo
  mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
  mimetypes.add_type('application/x-font-ttf', '.ttf')
  
 -class LazyResponse(werkzeug.wrappers.Response):
 -    """ Lazy werkzeug response.
 -    API not yet frozen"""
 +class Response(werkzeug.wrappers.Response):
 +    """ Response object passed through controller route chain.
  
 -    def __init__(self, callback, status_code=None, **kwargs):
 -        super(LazyResponse, self).__init__(mimetype='text/html')
 -        if status_code:
 -            self.status_code = status_code
 -        self.callback = callback
 -        self.params = kwargs
 -    def process(self):
 -        response = self.callback(**self.params)
 -        self.response.append(response)
 +    In addition to the werkzeug.wrappers.Response parameters, this
 +    classe's constructor can take the following additional parameters
 +    for QWeb Lazy Rendering.
 +
 +    :param basestring template: template to render
 +    :param dict qcontext: Rendering context to use
 +    :param int uid: User id to use for the ir.ui.view render call
 +    """
 +    default_mimetype = 'text/html'
 +    def __init__(self, *args, **kw):
 +        template = kw.pop('template', None)
 +        qcontext = kw.pop('qcontext', None)
 +        uid = kw.pop('uid', None)
 +        super(Response, self).__init__(*args, **kw)
 +        self.set_default(template, qcontext, uid)
 +
 +    def set_default(self, template=None, qcontext=None, uid=None):
 +        self.template = template
 +        self.qcontext = qcontext or dict()
 +        self.uid = uid
 +        # Support for Cross-Origin Resource Sharing
 +        if request.endpoint and 'cors' in request.endpoint.routing:
 +            self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
 +            methods = 'GET, POST'
 +            if request.endpoint.routing['type'] == 'json':
 +                methods = 'POST'
 +            elif request.endpoint.routing.get('methods'):
 +                methods = ', '.join(request.endpoint.routing['methods'])
 +            self.headers.set('Access-Control-Allow-Methods', methods)
 +
 +    @property
 +    def is_qweb(self):
 +        return self.template is not None
 +
 +    def render(self):
 +        view_obj = request.registry["ir.ui.view"]
 +        uid = self.uid or request.uid or openerp.SUPERUSER_ID
 +        return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
 +
 +    def flatten(self):
 +        self.response.append(self.render())
 +        self.template = None
  
  class DisableCacheMiddleware(object):
      def __init__(self, app):
              start_response(status, new_headers)
          return self.app(environ, start_wrapped)
  
 -def session_path():
 -    try:
 -        import pwd
 -        username = pwd.getpwuid(os.geteuid()).pw_name
 -    except ImportError:
 -        try:
 -            username = getpass.getuser()
 -        except Exception:
 -            username = "unknown"
 -    path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
 -    try:
 -        os.mkdir(path, 0700)
 -    except OSError as exc:
 -        if exc.errno == errno.EEXIST:
 -            # directory exists: ensure it has the correct permissions
 -            # this will fail if the directory is not owned by the current user
 -            os.chmod(path, 0700)
 -        else:
 -            raise
 -    return path
 -
  class Root(object):
      """Root WSGI application for the OpenERP Web Client.
      """
      def __init__(self):
          # Setup http sessions
 -        path = session_path()
 +        path = openerp.tools.config.session_dir
          _logger.debug('HTTP sessions stored in: %s', path)
          self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
 +        self._loaded = False
  
 -        # TODO should we move this to ir.http so that only configured modules are served ?
 -        _logger.info("HTTP Configuring static files")
 -        self.load_addons()
 -
 +    @lazy_property
 +    def nodb_routing_map(self):
          _logger.info("Generating nondb routing")
 -        self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
 +        return routing_map([''] + openerp.conf.server_wide_modules, True)
  
      def __call__(self, environ, start_response):
          """ Handle a WSGI request
          """
 +        if not self._loaded:
 +            self._loaded = True
 +            self.load_addons()
          return self.dispatch(environ, start_response)
  
      def load_addons(self):
 -        """ Load all addons from addons patch containg static files and
 +        """ Load all addons from addons path containing static files and
          controllers and configure them.  """
 +        # TODO should we move this to ir.http so that only configured modules are served ?
          statics = {}
  
          for addons_path in openerp.modules.module.ad_paths:
                          _logger.debug("Loading %s", module)
                          if 'openerp.addons' in sys.modules:
                              m = __import__('openerp.addons.' + module)
 +                        else:
 +                            m = None
                          addons_module[module] = m
                          addons_manifest[module] = manifest
                          statics['/%s/static' % module] = path_static
  
 -        app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
 -        self.dispatch = DisableCacheMiddleware(app)
 +        if statics:
 +            _logger.info("HTTP Configuring static files")
 +            app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
 +            self.dispatch = DisableCacheMiddleware(app)
  
      def setup_session(self, httprequest):
          # recover or create session
              return HttpRequest(httprequest)
  
      def get_response(self, httprequest, result, explicit_session):
 -        if isinstance(result, LazyResponse):
 +        if isinstance(result, Response) and result.is_qweb:
              try:
 -                result.process()
 +                result.flatten()
              except(Exception), e:
                  if request.db:
                      result = request.registry['ir.http']._handle_exception(e)
                      raise
  
          if isinstance(result, basestring):
 -            response = werkzeug.wrappers.Response(result, mimetype='text/html')
 +            response = Response(result, mimetype='text/html')
          else:
              response = result
  
          if not explicit_session and hasattr(response, 'set_cookie'):
              response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
  
 -        # Support for Cross-Origin Resource Sharing
 -        if request.func and 'cors' in request.func.routing:
 -            response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
 -            methods = 'GET, POST'
 -            if request.func_request_type == 'json':
 -                methods = 'POST'
 -            elif request.func.routing.get('methods'):
 -                methods = ', '.join(request.func.routing['methods'])
 -            response.headers.set('Access-Control-Allow-Methods', methods)
 -
          return response
  
      def dispatch(self, environ, start_response):
                      try:
                          with openerp.tools.mute_logger('openerp.sql_db'):
                              ir_http = request.registry['ir.http']
 -                    except psycopg2.OperationalError:
 -                        # psycopg2 error. At this point, that means the
 -                        # database probably does not exists anymore. Log the
 -                        # user out and fall back to nodb
 +                    except (AttributeError, psycopg2.OperationalError):
 +                        # psycopg2 error or attribute error while constructing
 +                        # the registry. That means the database probably does
 +                        # not exists anymore or the code doesnt match the db.
 +                        # Log the user out and fall back to nodb
                          request.session.logout()
                          result = _dispatch_nodb()
                      else:
          return request.registry['ir.http'].routing_map()
  
  def db_list(force=False, httprequest=None):
 -    dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
 +    dbs = dispatch_rpc("db", "list", [force])
      return db_filter(dbs, httprequest=httprequest)
  
  def db_filter(dbs, httprequest=None):
      httprequest = httprequest or request.httprequest
 -    h = httprequest.environ['HTTP_HOST'].split(':')[0]
 +    h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
      d = h.split('.')[0]
      r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
      dbs = [i for i in dbs if re.match(r, i)]
@@@ -1294,15 -1192,18 +1299,15 @@@ class CommonController(Controller)
      @route('/jsonrpc', type='json', auth="none")
      def jsonrpc(self, service, method, args):
          """ Method used by client APIs to contact OpenERP. """
 -        return openerp.netsvc.dispatch_rpc(service, method, args)
 +        return dispatch_rpc(service, method, args)
  
      @route('/gen_session_id', type='json', auth="none")
      def gen_session_id(self):
          nsession = root.session_store.new()
          return nsession.sid
  
 -root = None
 -
 -def wsgi_postload():
 -    global root
 -    root = Root()
 -    openerp.wsgi.register_wsgi_handler(root)
 +# register main wsgi handler
 +root = Root()
 +openerp.service.wsgi_server.register_wsgi_handler(root)
  
  # vim:et:ts=4:sw=4:
@@@ -965,7 -965,7 +965,7 @@@ class _rml_template(object)
                      }
          pageSize = A4
          if self.localcontext.get('company'):
 -            pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
 +            pageSize = pagesize_map.get(self.localcontext.get('company').rml_paper_format, A4)
          if node.get('pageSize'):
              ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
              pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
              if story_cnt > 0:
                  fis.append(platypus.PageBreak())
              fis += r.render(node_story)
+             # Reset Page Number with new story tag
+             fis.append(PageReset())
              story_cnt += 1
          try:
              if self.localcontext and self.localcontext.get('internal_header',False):