Forward port of branch saas-3 up to fc9fc3e
authorMartin Trigaux <mat@openerp.com>
Mon, 6 Oct 2014 13:52:23 +0000 (15:52 +0200)
committerMartin Trigaux <mat@openerp.com>
Mon, 6 Oct 2014 13:52:23 +0000 (15:52 +0200)
1  2 
addons/account/account_move_line.py
addons/account_anglo_saxon/invoice.py
addons/mail/static/src/js/mail_followers.js
addons/mail/static/src/xml/mail_followers.xml
addons/point_of_sale/static/src/js/widget_base.js
addons/project/project.py
openerp/addons/base/ir/ir_translation.py

  #
  ##############################################################################
  
 -import sys
  import time
  from datetime import datetime
 -from operator import itemgetter
 -
 -from lxml import etree
  
  from openerp import workflow
 -from openerp.osv import fields, osv, orm
 +from openerp.osv import fields, osv
  from openerp.tools.translate import _
  import openerp.addons.decimal_precision as dp
  from openerp import tools
 +from openerp.report import report_sxw
 +import openerp
  
  class account_move_line(osv.osv):
      _name = "account.move.line"
@@@ -39,7 -41,8 +39,7 @@@
          fiscalperiod_obj = self.pool.get('account.period')
          account_obj = self.pool.get('account.account')
          fiscalyear_ids = []
 -        if context is None:
 -            context = {}
 +        context = dict(context or {})
          initial_bal = context.get('initial_bal', False)
          company_clause = " "
          if context.get('company_id', False):
  
              if move_line.reconcile_id:
                  continue
 -            if not move_line.account_id.type in ('payable', 'receivable'):
 -                #this function does not suport to be used on move lines not related to payable or receivable accounts
 +            if not move_line.account_id.reconcile:
 +                #this function does not suport to be used on move lines not related to a reconcilable account
                  continue
  
              if move_line.currency_id:
              period_id = context.get('period_id')
              if type(period_id) == str:
                  ids = period_obj.search(cr, uid, [('name', 'ilike', period_id)])
 -                context.update({
 -                    'period_id': ids and ids[0] or False
 -                })
 +                context = dict(context, period_id=ids and ids[0] or False)
          return context
  
      def _default_get(self, cr, uid, fields, context=None):
          #default_get should only do the following:
          #   -propose the next amount in debit/credit in order to balance the move
          #   -propose the next account from the journal (default debit/credit account) accordingly
 -        if context is None:
 -            context = {}
 +        context = dict(context or {})
          account_obj = self.pool.get('account.account')
          period_obj = self.pool.get('account.period')
          journal_obj = self.pool.get('account.journal')
          for line_id, invoice_id in cursor.fetchall():
              res[line_id] = invoice_id
              invoice_ids.append(invoice_id)
-         invoice_names = {False: ''}
+         invoice_names = {}
          for invoice_id, name in invoice_obj.name_get(cursor, user, invoice_ids, context=context):
              invoice_names[invoice_id] = name
          for line_id in res.keys():
              invoice_id = res[line_id]
-             res[line_id] = (invoice_id, invoice_names[invoice_id])
+             res[line_id] = invoice_id and (invoice_id, invoice_names[invoice_id]) or False
          return res
  
      def name_get(self, cr, uid, ids, context=None):
                  res[line.id] = str(line.reconcile_partial_id.name)
          return res
  
 +    def _get_move_from_reconcile(self, cr, uid, ids, context=None):
 +        move = {}
 +        for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context):
 +            for line in r.line_partial_ids:
 +                move[line.move_id.id] = True
 +            for line in r.line_id:
 +                move[line.move_id.id] = True
 +        move_line_ids = []
 +        if move:
 +            move_line_ids = self.pool.get('account.move.line').search(cr, uid, [('journal_id','in',move.keys())], context=context)
 +        return move_line_ids
 +
 +
      _columns = {
 -        'name': fields.char('Name', size=64, required=True),
 +        'name': fields.char('Name', required=True),
          'quantity': fields.float('Quantity', digits=(16,2), help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports."),
          'product_uom_id': fields.many2one('product.uom', 'Unit of Measure'),
          'product_id': fields.many2one('product.product', 'Product'),
          'account_id': fields.many2one('account.account', 'Account', required=True, ondelete="cascade", domain=[('type','<>','view'), ('type', '<>', 'closed')], select=2),
          'move_id': fields.many2one('account.move', 'Journal Entry', ondelete="cascade", help="The move of this entry line.", select=2, required=True),
          'narration': fields.related('move_id','narration', type='text', relation='account.move', string='Internal Note'),
 -        'ref': fields.related('move_id', 'ref', string='Reference', type='char', size=64, store=True),
 -        'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=1),
 -        'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=2),
 -        'reconcile_partial_id': fields.many2one('account.move.reconcile', 'Partial Reconcile', readonly=True, ondelete='set null', select=2),
 -        'reconcile': fields.function(_get_reconcile, type='char', string='Reconcile Ref'),
 +        'ref': fields.related('move_id', 'ref', string='Reference', type='char', store=True),
 +        'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=1, copy=False),
 +        'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=2, copy=False),
 +        'reconcile_partial_id': fields.many2one('account.move.reconcile', 'Partial Reconcile', readonly=True, ondelete='set null', select=2, copy=False),
 +        'reconcile_ref': fields.function(_get_reconcile, type='char', string='Reconcile Ref', oldname='reconcile', store={
 +                    'account.move.line': (lambda self, cr, uid, ids, c={}: ids, ['reconcile_id','reconcile_partial_id'], 50),'account.move.reconcile': (_get_move_from_reconcile, None, 50)}),
          'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')),
          'amount_residual_currency': fields.function(_amount_residual, string='Residual Amount in Currency', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in its currency (maybe different of the company currency)."),
          'amount_residual': fields.function(_amount_residual, string='Residual Amount', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in the company currency."),
          'analytic_lines': fields.one2many('account.analytic.line', 'move_id', 'Analytic lines'),
          'centralisation': fields.selection([('normal','Normal'),('credit','Credit Centralisation'),('debit','Debit Centralisation'),('currency','Currency Adjustment')], 'Centralisation', size=8),
          'balance': fields.function(_balance, fnct_search=_balance_search, string='Balance'),
 -        'state': fields.selection([('draft','Unbalanced'), ('valid','Balanced')], 'Status', readonly=True),
 +        'state': fields.selection([('draft','Unbalanced'), ('valid','Balanced')], 'Status', readonly=True, copy=False),
          'tax_code_id': fields.many2one('account.tax.code', 'Tax Account', help="The Account can either be a base tax code or a tax code account."),
          'tax_amount': fields.float('Tax/Base Amount', digits_compute=dp.get_precision('Account'), select=True, help="If the Tax account is a tax code account, this field will contain the taxed amount.If the tax account is base tax code, "\
                      "this field will contain the basic amount(without tax)."),
          'invoice': fields.function(_invoice, string='Invoice',
              type='many2one', relation='account.invoice', fnct_search=_invoice_search),
 -        'account_tax_id':fields.many2one('account.tax', 'Tax'),
 +        'account_tax_id':fields.many2one('account.tax', 'Tax', copy=False),
          'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'),
 -        'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', 
 +        'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company',
                              string='Company', store=True, readonly=True)
      }
  
          if context.get('journal_type', False):
              jids = journal_pool.search(cr, uid, [('type','=', context.get('journal_type'))])
              if not jids:
 -                raise osv.except_osv(_('Configuration Error!'), _('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration/Journals/Journals.') % context.get('journal_type'))
 +                model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_journal_form')
 +                msg = _("""Cannot find any account journal of "%s" type for this company, You should create one.\n Please go to Journal Configuration""") % context.get('journal_type').replace('_', ' ').title()
 +                raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel'))
              journal_id = jids[0]
          return journal_id
  
          (_check_date, 'The date of your Journal Entry is not in the defined period! You should change the date or remove this constraint from the journal.', ['date']),
          (_check_currency, 'The selected account of your Journal Entry forces to provide a secondary currency. You should remove the secondary currency on the account or select a multi-currency view on the journal.', ['currency_id']),
          (_check_currency_and_amount, "You cannot create journal items with a secondary currency without recording both 'currency' and 'amount currency' field.", ['currency_id','amount_currency']),
 -        (_check_currency_amount, 'The amount expressed in the secondary currency must be positive when the journal item is a debit and negative when if it is a credit.', ['amount_currency']),
 +        (_check_currency_amount, 'The amount expressed in the secondary currency must be positive when account is debited and negative when account is credited.', ['amount_currency']),
          (_check_currency_company, "You cannot provide a secondary currency if it is the same than the company one." , ['currency_id']),
      ]
  
          if (amount>0) and journal:
              x = journal_obj.browse(cr, uid, journal).default_credit_account_id
              if x: acc = x
 +        context = dict(context)
          context.update({
                  'date': date,
                  'res.currency.compute.account': acc,
      def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
          if context is None:
              context = {}
 +        if context.get('fiscalyear'):
 +            args.append(('period_id.fiscalyear_id', '=', context.get('fiscalyear', False)))
          if context and context.get('next_partner_only', False):
              if not context.get('partner_id', False):
                  partner = self.list_partners_to_reconcile(cr, uid, context=context)
              args.append(('partner_id', '=', partner[0]))
          return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count)
  
 +    def prepare_move_lines_for_reconciliation_widget(self, cr, uid, lines, target_currency=False, target_date=False, context=None):
 +        """ Returns move lines formatted for the manual/bank reconciliation widget
 +
 +            :param target_currency: curreny you want the move line debit/credit converted into
 +            :param target_date: date to use for the monetary conversion
 +        """
 +        if not lines:
 +            return []
 +        if context is None:
 +            context = {}
 +        ctx = context.copy()
 +        currency_obj = self.pool.get('res.currency')
 +        company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
 +        rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_aml', context=context)
 +        ret = []
 +
 +        for line in lines:
 +            partial_reconciliation_siblings_ids = []
 +            if line.reconcile_partial_id:
 +                partial_reconciliation_siblings_ids = self.search(cr, uid, [('reconcile_partial_id', '=', line.reconcile_partial_id.id)], context=context)
 +                partial_reconciliation_siblings_ids.remove(line.id)
 +
 +            ret_line = {
 +                'id': line.id,
 +                'name': line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
 +                'ref': line.move_id.ref,
 +                'account_code': line.account_id.code,
 +                'account_name': line.account_id.name,
 +                'account_type': line.account_id.type,
 +                'date_maturity': line.date_maturity,
 +                'date': line.date,
 +                'period_name': line.period_id.name,
 +                'journal_name': line.journal_id.name,
 +                'partner_id': line.partner_id.id,
 +                'partner_name': line.partner_id.name,
 +                'is_partially_reconciled': bool(line.reconcile_partial_id),
 +                'partial_reconciliation_siblings_ids': partial_reconciliation_siblings_ids,
 +            }
 +
 +            # Amount residual can be negative
 +            debit = line.debit
 +            credit = line.credit
 +            total_amount = abs(debit - credit)
 +            total_amount_currency = line.amount_currency
 +            amount_residual = line.amount_residual
 +            amount_residual_currency = line.amount_residual_currency
 +            if line.amount_residual < 0:
 +                debit, credit = credit, debit
 +                amount_residual = -amount_residual
 +                amount_residual_currency = -amount_residual_currency
 +
 +            # Get right debit / credit:
 +            line_currency = line.currency_id or company_currency
 +            amount_currency_str = ""
 +            total_amount_currency_str = ""
 +            if line.currency_id and line.amount_currency:
 +                amount_currency_str = rml_parser.formatLang(amount_residual_currency, currency_obj=line.currency_id)
 +                total_amount_currency_str = rml_parser.formatLang(total_amount_currency, currency_obj=line.currency_id)
 +            if target_currency and line_currency == target_currency and target_currency != company_currency:
 +                debit = debit > 0 and amount_residual_currency or 0.0
 +                credit = credit > 0 and amount_residual_currency or 0.0
 +                amount_currency_str = rml_parser.formatLang(amount_residual, currency_obj=company_currency)
 +                total_amount_currency_str = rml_parser.formatLang(total_amount, currency_obj=company_currency)
 +                amount_str = rml_parser.formatLang(debit or credit, currency_obj=target_currency)
 +                total_amount_str = rml_parser.formatLang(total_amount_currency, currency_obj=target_currency)
 +            else:
 +                debit = debit > 0 and amount_residual or 0.0
 +                credit = credit > 0 and amount_residual or 0.0
 +                amount_str = rml_parser.formatLang(debit or credit, currency_obj=company_currency)
 +                total_amount_str = rml_parser.formatLang(total_amount, currency_obj=company_currency)
 +                if target_currency and target_currency != company_currency:
 +                    amount_currency_str = rml_parser.formatLang(debit or credit, currency_obj=line_currency)
 +                    total_amount_currency_str = rml_parser.formatLang(total_amount, currency_obj=line_currency)
 +                    ctx = context.copy()
 +                    if target_date:
 +                        ctx.update({'date': target_date})
 +                    debit = currency_obj.compute(cr, uid, company_currency.id, target_currency.id, debit, context=ctx)
 +                    credit = currency_obj.compute(cr, uid, company_currency.id, target_currency.id, credit, context=ctx)
 +                    amount_str = rml_parser.formatLang(debit or credit, currency_obj=target_currency)
 +                    total_amount = currency_obj.compute(cr, uid, company_currency.id, target_currency.id, total_amount, context=ctx)
 +                    total_amount_str = rml_parser.formatLang(total_amount, currency_obj=target_currency)
 +
 +            ret_line['credit'] = credit
 +            ret_line['debit'] = debit
 +            ret_line['amount_str'] = amount_str
 +            ret_line['amount_currency_str'] = amount_currency_str
 +            ret_line['total_amount_str'] = total_amount_str # For partial reconciliations
 +            ret_line['total_amount_currency_str'] = total_amount_currency_str
 +            ret.append(ret_line)
 +        return ret
 +
      def list_partners_to_reconcile(self, cr, uid, context=None):
          cr.execute(
               """SELECT partner_id FROM (
                  WHERE debit > 0 AND credit > 0 AND (last_reconciliation_date IS NULL OR max_date > last_reconciliation_date)
                  ORDER BY last_reconciliation_date""")
          ids = [x[0] for x in cr.fetchall()]
 -        if not ids: 
 +        if not ids:
              return []
  
          # To apply the ir_rules
              else:
                  currency_id = line.company_id.currency_id
              if line.reconcile_id:
 -                raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name)) 
 +                raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name))
              if line.reconcile_partial_id:
                  for line2 in line.reconcile_partial_id.line_partial_ids:
 +                    if line2.state != 'valid':
 +                        raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s) cannot be used in a reconciliation as it is not balanced!") % (line2.name, line2.id))
                      if not line2.reconcile_id:
                          if line2.id not in merges:
                              merges.append(line2.id)
              'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
          }, context=reconcile_context)
          move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=reconcile_context)
 -        return True
 +        return r_id
  
      def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
          account_obj = self.pool.get('account.account')
          period_pool = self.pool.get('account.period')
          pids = period_pool.find(cr, user, date, context=context)
          if pids:
 -            res.update({
 -                'period_id':pids[0]
 -            })
 -            context.update({
 -                'period_id':pids[0]
 -            })
 +            res.update({'period_id':pids[0]})
 +            context = dict(context, period_id=pids[0])
          return {
              'value':res,
              'context':context,
          period = period_obj.browse(cr, uid, period_id, context=context)
          for (state,) in result:
              if state == 'done':
 -                raise osv.except_osv(_('Error!'), _('You can not add/modify entries in a closed period %s of journal %s.' % (period.name,journal.name)))                
 +                raise osv.except_osv(_('Error!'), _('You can not add/modify entries in a closed period %s of journal %s.' % (period.name,journal.name)))
          if not result:
              jour_period_obj.create(cr, uid, {
                  'name': (journal.code or journal.name)+':'+(period.name or ''),
          move_obj = self.pool.get('account.move')
          cur_obj = self.pool.get('res.currency')
          journal_obj = self.pool.get('account.journal')
 -        if context is None:
 -            context = {}
 +        context = dict(context or {})
          if vals.get('move_id', False):
              move = self.pool.get('account.move').browse(cr, uid, vals['move_id'], context=context)
              if move.company_id:
                  vals['company_id'] = move.company_id.id
              if move.date and not vals.get('date'):
                  vals['date'] = move.date
 -        if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
 +        if ('account_id' in vals) and not account_obj.read(cr, uid, [vals['account_id']], ['active'])[0]['active']:
              raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
          if 'journal_id' in vals and vals['journal_id']:
              context['journal_id'] = vals['journal_id']
          if vals.get('account_tax_id', False):
              tax_id = tax_obj.browse(cr, uid, vals['account_tax_id'])
              total = vals['debit'] - vals['credit']
 -            if journal.type in ('purchase_refund', 'sale_refund'):
 +            base_code = 'base_code_id'
 +            tax_code = 'tax_code_id'
 +            account_id = 'account_collected_id'
 +            base_sign = 'base_sign'
 +            tax_sign = 'tax_sign'
 +            if journal.type in ('purchase_refund', 'sale_refund') or (journal.type in ('cash', 'bank') and total < 0):
                  base_code = 'ref_base_code_id'
                  tax_code = 'ref_tax_code_id'
                  account_id = 'account_paid_id'
                  base_sign = 'ref_base_sign'
                  tax_sign = 'ref_tax_sign'
 -            else:
 -                base_code = 'base_code_id'
 -                tax_code = 'tax_code_id'
 -                account_id = 'account_collected_id'
 -                base_sign = 'base_sign'
 -                tax_sign = 'tax_sign'
              tmp_cnt = 0
 -            for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=True).get('taxes'):
 +            for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=False).get('taxes'):
                  #create the base movement
                  if tmp_cnt == 0:
                      if tax[base_code]:
                          tmp_cnt += 1
 -                        self.write(cr, uid,[result], {
 +                        if tax_id.price_include:
 +                            total = tax['price_unit']
 +                        newvals = {
                              'tax_code_id': tax[base_code],
 -                            'tax_amount': tax[base_sign] * abs(total)
 -                        })
 +                            'tax_amount': tax[base_sign] * abs(total),
 +                        }
 +                        if tax_id.price_include:
 +                            if tax['price_unit'] < 0:
 +                                newvals['credit'] = abs(tax['price_unit'])
 +                            else:
 +                                newvals['debit'] = tax['price_unit']
 +                        self.write(cr, uid, [result], newvals, context=context)
                  else:
                      data = {
                          'move_id': vals['move_id'],
                          'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
                          'date': vals['date'],
 -                        'partner_id': vals.get('partner_id',False),
 -                        'ref': vals.get('ref',False),
 +                        'partner_id': vals.get('partner_id', False),
 +                        'ref': vals.get('ref', False),
 +                        'statement_id': vals.get('statement_id', False),
                          'account_tax_id': False,
                          'tax_code_id': tax[base_code],
                          'tax_amount': tax[base_sign] * abs(total),
                      'date': vals['date'],
                      'partner_id': vals.get('partner_id',False),
                      'ref': vals.get('ref',False),
 +                    'statement_id': vals.get('statement_id', False),
                      'account_tax_id': False,
                      'tax_code_id': tax[tax_code],
                      'tax_amount': tax[tax_sign] * abs(tax['amount']),
                      self.create(cr, uid, data, context)
              del vals['account_tax_id']
  
 -        if check and not context.get('novalidate') and ((not context.get('no_store_function')) or journal.entry_posted):
 +        if check and not context.get('novalidate') and (context.get('recompute', True) or journal.entry_posted):
              tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
              if journal.entry_posted and tmp:
                  move_obj.button_validate(cr,uid, [vals['move_id']], context)
@@@ -121,14 -121,18 +121,18 @@@ class account_invoice_line(osv.osv)
                                  if inv.currency_id.id != company_currency:
                                      valuation_price_unit = self.pool.get('res.currency').compute(cr, uid, company_currency, inv.currency_id.id, valuation_price_unit, context={'date': inv.date_invoice})
                                  if valuation_price_unit != i_line.price_unit and line['price_unit'] == i_line.price_unit and acc:
-                                     price_diff = round(i_line.price_unit - valuation_price_unit, account_prec)
-                                     line.update({'price': round(valuation_price_unit * line['quantity'], account_prec)})
+                                     # price with discount and without tax included
+                                     price_unit = self.pool['account.tax'].compute_all(cr, uid, line['taxes'],
+                                         i_line.price_unit * (1-(i_line.discount or 0.0)/100.0), line['quantity'])['total']
+                                     price_line = round(valuation_price_unit * line['quantity'], account_prec)
+                                     price_diff = round(price_unit - price_line, account_prec)
+                                     line.update({'price': price_line})
                                      diff_res.append({
                                          'type': 'src',
                                          'name': i_line.name[:64],
-                                         'price_unit': price_diff,
+                                         'price_unit': round(price_diff / line['quantity'], account_prec),
                                          'quantity': line['quantity'],
-                                         'price': round(price_diff * line['quantity'], account_prec),
+                                         'price': price_diff,
                                          'account_id': acc,
                                          'product_id': line['product_id'],
                                          'uos_id': line['uos_id'],
                          res += diff_res
          return res
  
 -    def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
 +    def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, company_id=None, context=None):
          fiscal_pool = self.pool.get('account.fiscal.position')
 -        res = super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom_id, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context, company_id)
 +        res = super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom_id, qty, name, type, partner_id, fposition_id, price_unit, currency_id, company_id, context)
          if not product:
              return res
          if type in ('in_invoice','in_refund'):
@@@ -70,23 -70,14 +70,24 @@@ openerp_mail_followers = function(sessi
                      self.do_unfollow();
              });
              // event: click on a subtype, that (un)subscribe for this subtype
 -            this.$el.on('click', '.oe_subtype_list input', self.do_update_subscription);
 +            this.$el.on('click', '.oe_subtype_list input', function(event) {
 +                self.do_update_subscription(event);
 +                var $list = self.$('.oe_subtype_list');
 +                if(!$list.hasClass('open')) {
 +                    $list.addClass('open');
 +                }
 +                if(self.$('.oe_subtype_list ul')[0].children.length < 1) {
 +                    $list.removeClass('open');
 +                }
 +                event.stopPropagation();
 +            });
              // event: click on 'invite' button, that opens the invite wizard
              this.$('.oe_invite').on('click', self.on_invite_follower);
              // event: click on 'edit_subtype(pencil)' button to edit subscription
              this.$el.on('click', '.oe_edit_subtype', self.on_edit_subtype);
              this.$el.on('click', '.oe_remove_follower', self.on_remove_follower);
-             this.$el.on('click', '.oe_show_more', self.on_show_more_followers)
+             this.$el.on('click', '.oe_show_more', self.on_show_more_followers);
+             this.$el.on('click', 'a[data-partner]', self.on_follower_clicked);
          },
  
          on_edit_subtype: function(event) {
              var $currentTarget = $(event.currentTarget);
              var user_pid = $currentTarget.data('id');
              $('div.oe_edit_actions').remove();
 -            self.$dialog = new session.web.dialog($('<div class="oe_edit_actions">'), {
 -                            modal: true,
 -                            width: 'auto',
 -                            height: 'auto',
 +            self.$dialog = new session.web.Dialog(this, {
 +                            size: 'small',
                              title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
                              buttons: [
                                      { text: _t("Apply"), click: function() { 
                                          self.do_update_subscription(event, user_pid);
 -                                        $(this).dialog("close");
 +                                        this.parents('.modal').modal('hide');
                                      }},
 -                                    { text: _t("Cancel"), click: function() { $(this).dialog("close"); }}
 +                                    { text: _t("Cancel"), click: function() { this.parents('.modal').modal('hide'); }}
                                  ],
 -                    });
 +                    }, "<div class='oe_edit_actions'>").open();
              return self.fetch_subtypes(user_pid);
          },
  
              }
          },
  
+         on_follower_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);
+         },
          read_value: function () {
              var self = this;
              this.displayed_nb = this.displayed_limit;
          fetch_generic: function (error, event) {
              var self = this;
              event.preventDefault();
 -            return this.ds_users.call('read', [this.session.uid, ['partner_id']]).then(function (results) {
 -                var pid = results['partner_id'][0];
 +            return this.ds_users.call('read', [[this.session.uid], ['partner_id']]).then(function (results) {
 +                var pid = results[0]['partner_id'][0];
                  self.message_is_follower = (_.indexOf(self.value, pid) != -1);
              }).then(self.proxy('display_generic'));
          },
              if (user_pid) {
                  dialog = true;
              } else {
 -                var subtype_list_ul = this.$('.oe_subtype_list').empty();
 -                if (! this.message_is_follower) return;
 +                var subtype_list_ul = this.$('.oe_subtype_list ul').empty();
 +                if (! this.message_is_follower) {
 +                    this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true);
 +                    return;
 +                }
 +                else {
 +                    this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false);
 +                }
              }
              var id = this.view.datarecord.id;
              this.ds_model.call('message_get_subscription_data', [[id], user_pid, new session.web.CompoundContext(this.build_context(), {})])
          display_subtypes:function (data, id, dialog) {
              var self = this;
              if (dialog) {
 -                var $list = self.$dialog;
 +                var $list = self.$dialog.$el;
              }
              else {
 -                var $list = this.$('.oe_subtype_list');
 +                var $list = this.$('.oe_subtype_list ul');
              }
              var records = data[id].message_subtype_data;
              this.records_length = $.map(records, function(value, index) { return index; }).length;
              if (this.records_length > 1) { self.display_followers(); }
 +            var old_model = '';
              _(records).each(function (record, record_name) {
 +                if (old_model != record.parent_model){
 +                    if (old_model != ''){
 +                        var index = $($list).find('.oe_subtype').length;
 +                        $($($list).find('.oe_subtype')[index-1]).addClass('subtype-border');
 +                    }
 +                    old_model = record.parent_model;
 +                }
                  record.name = record_name;
                  record.followed = record.followed || undefined;
 -                $(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo($list);
 +                $(session.web.qweb.render('mail.followers.subtype', {'record': record, 'dialog': dialog})).appendTo($list);
              });
 -            if (_.size(records) > 1) {
 -                $list.show();
 -            }
          },
  
          do_follow: function () {
              var context = new session.web.CompoundContext(this.build_context(), {});
 +            this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false);
              this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], undefined, context])
                  .then(this.proxy('read_value'));
  
          },
          
          do_unfollow: function (user_pid) {
 +            var self = this;
              if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) {
                  _(this.$('.oe_msg_subtype_check')).each(function (record) {
                      $(record).attr('checked',false);
                  });
              var action_unsubscribe = 'message_unsubscribe_users';
 +            this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true);
              var follower_ids = [this.session.uid];
              if (user_pid) {
                  action_unsubscribe = 'message_unsubscribe';
              if (!checklist.length) {
                  if (!this.do_unfollow(user_pid)) {
                      $(event.target).attr("checked", "checked");
 +                } else {
 +                      self.$('.oe_subtype_list ul').empty(); 
                  }
              } else {
                  var context = new session.web.CompoundContext(this.build_context(), {});
@@@ -5,23 -5,17 +5,23 @@@
          followers main template
          Template used to display the followers, the actions and the subtypes in a record.
          -->
 -    <div t-name="mail.followers" class="oe_followers">        
 +    <div t-name="mail.followers" class="oe_followers">
          <div class="oe_actions">
 -            <button type="button" class="oe_follower oe_notfollow">
 -                <span class="oe_follow">Follow</span>
 -                <span class="oe_unfollow">Unfollow</span>
 -                <span class="oe_following">Following</span>
 -            </button>
 +            <div t-attf-class="btn-group oe_subtype_list">
 +                <button class="btn oe_follower oe_notfollow">
 +                    <span class="oe_follow">Follow</span>
 +                    <span class="oe_unfollow">Unfollow</span>
 +                    <span class="oe_following">Following</span>
 +                </button>
 +                <button type="button" t-attf-class="btn btn-default dropdown-toggle" data-toggle="dropdown">
 +                    <span class="caret"></span>
 +                </button>
 +                <ul class="dropdown-menu" role="menu"></ul>
 +            </div>
 +
              <t t-if="widget.comment">
                  <h5 class="oe_comment"><t t-raw="widget.comment"/></h5>
              </t>
 -            <div class="oe_subtype_list"></div>
          </div>
          <div class='oe_follower_title_box'>
              <h4 class='oe_follower_title'>Followers</h4>
@@@ -36,7 -30,7 +36,7 @@@
          -->
      <div t-name="mail.followers.partner" class='oe_partner'>
          <img class="oe_mail_thumbnail oe_mail_frame" t-attf-src="{record.avatar_url}"/>
-         <a t-attf-href="#model=res.partner&amp;id=#{record.id}" t-att-title="record.name"><t t-esc="record.name"/></a>
+         <a t-attf-href="#model=res.partner&amp;id=#{record.id}" t-att-title="record.name" t-att-data-partner="record.id"><t t-esc="record.name"/></a>
          <span t-if="record.is_editable and (widget.records_length &gt; 1)" class="oe_edit_subtype oe_e oe_hidden" title="Edit subscription" t-att-data-id="record.id">&amp;</span>
          <span t-if="widget.view_is_editable" class="oe_remove_follower oe_e" title="Remove this follower" t-att-data-id="record.id">X</span>
      </div>
      <t t-name="mail.followers.subtype">
          <table class='oe_subtype'>
              <tr>
 -                <td width="10%"><input type="checkbox" t-att-checked="record.followed" t-att-id="'input_mail_followers_subtype_'+record.id" t-att-data-id="record.id" t-att-name="record.name"  class="oe_msg_subtype_check"/></td>
 -                <td><label t-att-for="'input_mail_followers_subtype_'+record.id"><t t-esc="record.name"/></label></td>
 +                <td width="10%">
 +                    <input type="checkbox" t-att-checked="record.followed" t-att-id="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')" t-att-data-id="record.id" t-att-name="record.name"  class="oe_msg_subtype_check"/>
 +                </td>
 +                <td>
 +                    <label t-att-for="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')"><t t-raw="record.name"/></label>
 +                </td>
              </tr>
          </table>
      </t>
@@@ -33,9 -33,9 +33,9 @@@ function openerp_pos_basewidget(instanc
                      amount = amount.toFixed(decimals);
                  }
                  if(this.currency.position === 'after'){
-                     return amount + ' ' + this.currency.symbol;
+                     return amount + ' ' + (this.currency.symbol || '');
                  }else{
-                     return this.currency.symbol + ' ' + amount;
+                     return (this.currency.symbol || '') + ' ' + amount;
                  }
              }
  
          hide: function(){
              this.$el.addClass('oe_hidden');
          },
 +        format_pr: function(value,precision){
 +            var decimals = precision > 0 ? Math.max(0,Math.ceil(Math.log(1.0/precision) / Math.log(10))) : 0;
 +            return value.toFixed(decimals);
 +        },
      });
  
  }
@@@ -35,7 -35,7 +35,7 @@@ class project_task_type(osv.osv)
      _description = 'Task Stage'
      _order = 'sequence'
      _columns = {
 -        'name': fields.char('Stage Name', required=True, size=64, translate=True),
 +        'name': fields.char('Stage Name', required=True, translate=True),
          'description': fields.text('Description'),
          'sequence': fields.integer('Sequence'),
          'case_default': fields.boolean('Default for New Projects',
@@@ -186,11 -186,20 +186,11 @@@ class project(osv.osv)
              task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True)
              res[id] = (project_attachments or 0) + (task_attachments or 0)
          return res
 -
      def _task_count(self, cr, uid, ids, field_name, arg, context=None):
 -        """ :deprecated: this method will be removed with OpenERP v8. Use task_ids
 -                         fields instead. """
 -        if context is None:
 -            context = {}
 -        res = dict.fromkeys(ids, 0)
 -        ctx = context.copy()
 -        ctx['active_test'] = False
 -        task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)], context=ctx)
 -        for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
 -            res[task.project_id.id] += 1
 +        res={}
 +        for tasks in self.browse(cr, uid, ids, context):
 +            res[tasks.id] = len(tasks.task_ids)
          return res
 -
      def _get_alias_models(self, cr, uid, context=None):
          """ Overriden in project_issue to offer more options """
          return [('project.task', "Tasks")]
              'res_model': 'ir.attachment',
              'type': 'ir.actions.act_window',
              'view_id': False,
 -            'view_mode': 'tree,form',
 +            'view_mode': 'kanban,tree,form',
              'view_type': 'form',
              'limit': 80,
              'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id)
      _columns = {
          'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
          'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
 -        'analytic_account_id': fields.many2one('account.analytic.account', 'Contract/Analytic', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.", ondelete="cascade", required=True),
 -        'priority': fields.integer('Sequence (deprecated)',
 -            deprecated='Will be removed with OpenERP v8; use sequence field instead',
 -            help="Gives the sequence order when displaying the list of projects"),
 +        'analytic_account_id': fields.many2one(
 +            'account.analytic.account', 'Contract/Analytic',
 +            help="Link this project to an analytic account if you need financial management on projects. "
 +                 "It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.",
 +            ondelete="cascade", required=True, auto_join=True),
          'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
              help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
          'tasks': fields.one2many('project.task', 'project_id', "Task Activities"),
              }),
          'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
          'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
 -        'task_count': fields.function(_task_count, type='integer', string="Open Tasks",
 -                                      deprecated="This field will be removed in OpenERP v8. Use task_ids one2many field instead."),
 +        'task_count': fields.function(_task_count, type='integer', string="Tasks",),
          'task_ids': fields.one2many('project.task', 'project_id',
                                      domain=[('stage_id.fold', '=', False)]),
          'color': fields.integer('Color Index'),
                      "- Employees Only: employees see all tasks or issues\n"
                      "- Followers Only: employees see only the followed tasks or issues; if portal\n"
                      "   is activated, portal users see the followed tasks or issues."),
 -        'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','Pending'),('close','Closed')], 'Status', required=True,),
 +        'state': fields.selection([('template', 'Template'),
 +                                   ('draft','New'),
 +                                   ('open','In Progress'),
 +                                   ('cancelled', 'Cancelled'),
 +                                   ('pending','Pending'),
 +                                   ('close','Closed')],
 +                                  'Status', required=True, copy=False),
          'doc_count': fields.function(
              _get_attached_docs, string="Number of documents attached", type='integer'
          )
          task_obj = self.pool.get('project.task')
          proj = self.browse(cr, uid, old_project_id, context=context)
          for task in proj.tasks:
 -            map_task_id[task.id] =  task_obj.copy(cr, uid, task.id, {}, context=context)
 +            # preserve task name and stage, normally altered during copy
 +            defaults = {'stage_id': task.stage_id.id,
 +                        'name': task.name}
 +            map_task_id[task.id] =  task_obj.copy(cr, uid, task.id, defaults, context=context)
          self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
          task_obj.duplicate_task(cr, uid, map_task_id, context=context)
          return True
  
      def copy(self, cr, uid, id, default=None, context=None):
 -        if context is None:
 -            context = {}
          if default is None:
              default = {}
 -
 +        context = dict(context or {})
          context['active_test'] = False
 -        default['state'] = 'open'
 -        default['line_ids'] = []
 -        default['tasks'] = []
 -
 -        # Don't prepare (expensive) data to copy children (analytic accounts),
 -        # they are discarded in analytic.copy(), and handled in duplicate_template() 
 -        default['child_ids'] = []
 -
          proj = self.browse(cr, uid, id, context=context)
 -        if not default.get('name', False):
 +        if not default.get('name'):
              default.update(name=_("%s (copy)") % (proj.name))
          res = super(project, self).copy(cr, uid, id, default, context)
          self.map_tasks(cr, uid, id, res, context=context)
          return res
  
      def duplicate_template(self, cr, uid, ids, context=None):
 -        if context is None:
 -            context = {}
 +        context = dict(context or {})
          data_obj = self.pool.get('ir.model.data')
          result = []
          for proj in self.browse(cr, uid, ids, context=context):
@@@ -564,7 -575,6 +564,7 @@@ class task(osv.osv)
          },
          'kanban_state': {
              'project.mt_task_blocked': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'blocked',
 +            'project.mt_task_ready': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'done',
          },
      }
  
      def copy_data(self, cr, uid, id, default=None, context=None):
          if default is None:
              default = {}
 -        default = default or {}
 -        default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
 -        if not default.get('remaining_hours', False):
 -            default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
 -        default['active'] = True
 -        if not default.get('name', False):
 -            default['name'] = self.browse(cr, uid, id, context=context).name or ''
 -            if not context.get('copy',False):
 -                new_name = _("%s (copy)") % (default.get('name', ''))
 -                default.update({'name':new_name})
 +        if not default.get('name'):
 +            current = self.browse(cr, uid, id, context=context)
 +            default['name'] = _("%s (copy)") % current.name
          return super(task, self).copy_data(cr, uid, id, default, context)
 -    
 -    def copy(self, cr, uid, id, default=None, context=None):
 -        if context is None:
 -            context = {}
 -        if default is None:
 -            default = {}
 -        if not context.get('copy', False):
 -            stage = self._get_default_stage_id(cr, uid, context=context)
 -            if stage:
 -                default['stage_id'] = stage
 -        return super(task, self).copy(cr, uid, id, default, context)
  
      def _is_template(self, cr, uid, ids, field_name, arg, context=None):
          res = {}
          'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
          'name': fields.char('Task Summary', track_visibility='onchange', size=128, required=True, select=True),
          'description': fields.text('Description'),
 -        'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
 +        'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')], 'Priority', select=True),
          'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
          'stage_id': fields.many2one('project.task.type', 'Stage', track_visibility='onchange', select=True,
 -                        domain="[('project_ids', '=', project_id)]"),
 +                        domain="[('project_ids', '=', project_id)]", copy=False),
          'categ_ids': fields.many2many('project.category', string='Tags'),
 -        'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
 +        'kanban_state': fields.selection([('normal', 'In Progress'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
                                           track_visibility='onchange',
                                           help="A task's kanban state indicates special situations affecting it:\n"
                                                " * Normal is the default situation\n"
                                                " * Blocked indicates something is preventing the progress of this task\n"
                                                " * Ready for next stage indicates the task is ready to be pulled to the next stage",
 -                                         readonly=True, required=False),
 +                                         required=False, copy=False),
          'create_date': fields.datetime('Create Date', readonly=True, select=True),
          'write_date': fields.datetime('Last Modification Date', readonly=True, select=True), #not displayed in the view but it might be useful with base_action_rule module (and it needs to be defined first for that)
 -        'date_start': fields.datetime('Starting Date',select=True),
 -        'date_end': fields.datetime('Ending Date',select=True),
 -        'date_deadline': fields.date('Deadline',select=True),
 -        'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
 +        'date_start': fields.datetime('Starting Date', select=True, copy=False),
 +        'date_end': fields.datetime('Ending Date', select=True, copy=False),
 +        'date_deadline': fields.date('Deadline', select=True, copy=False),
 +        'date_last_stage_update': fields.datetime('Last Stage Update', select=True, copy=False),
          'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select=True, track_visibility='onchange', change_default=True),
          'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
          'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
                  'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
                  'project.task.work': (_get_task, ['hours'], 10),
              }),
 +        'reviewer_id': fields.many2one('res.users', 'Reviewer', select=True, track_visibility='onchange'),
          'user_id': fields.many2one('res.users', 'Assigned to', select=True, track_visibility='onchange'),
          'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
          'partner_id': fields.many2one('res.partner', 'Customer'),
          'project_id': _get_default_project_id,
          'date_last_stage_update': fields.datetime.now,
          'kanban_state': 'normal',
 -        'priority': '2',
 +        'priority': '0',
          'progress': 0,
          'sequence': 10,
          'active': True,
 +        'reviewer_id': lambda obj, cr, uid, ctx=None: uid,
          'user_id': lambda obj, cr, uid, ctx=None: uid,
          'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=ctx),
          'partner_id': lambda self, cr, uid, ctx=None: self._get_default_partner(cr, uid, context=ctx),
      }
 -    _order = "priority, sequence, date_start, name, id"
 -
 -    def set_high_priority(self, cr, uid, ids, *args):
 -        """Set task priority to high
 -        """
 -        return self.write(cr, uid, ids, {'priority' : '0'})
 -
 -    def set_normal_priority(self, cr, uid, ids, *args):
 -        """Set task priority to normal
 -        """
 -        return self.write(cr, uid, ids, {'priority' : '2'})
 +    _order = "priority desc, sequence, date_start, name, id"
  
      def _check_recursion(self, cr, uid, ids, context=None):
          for id in ids:
          return res
  
      def get_empty_list_help(self, cr, uid, help, context=None):
 +        context = dict(context or {})
          context['empty_list_help_id'] = context.get('default_project_id')
          context['empty_list_help_model'] = 'project.project'
          context['empty_list_help_document_name'] = _("tasks")
      def set_remaining_time_10(self, cr, uid, ids, context=None):
          return self.set_remaining_time(cr, uid, ids, 10.0, context)
  
 -    def set_kanban_state_blocked(self, cr, uid, ids, context=None):
 -        return self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
 -
 -    def set_kanban_state_normal(self, cr, uid, ids, context=None):
 -        return self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
 -
 -    def set_kanban_state_done(self, cr, uid, ids, context=None):
 -        self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
 -        return False
 -
      def _store_history(self, cr, uid, ids, context=None):
          for task in self.browse(cr, uid, ids, context=context):
              self.pool.get('project.task.history').create(cr, uid, {
      # ------------------------------------------------
  
      def create(self, cr, uid, vals, context=None):
 -        if context is None:
 -            context = {}
 +        context = dict(context or {})
  
          # for default stage
          if vals.get('project_id') and not context.get('default_project_id'):
              context['default_project_id'] = vals.get('project_id')
          # user_id change: update date_start
-         if vals.get('user_id') and not vals.get('start_date'):
+         if vals.get('user_id') and not vals.get('date_start'):
              vals['date_start'] = fields.datetime.now()
  
          # context: no_log, because subtype already handle this
              new_stage = vals.get('stage_id')
              vals_reset_kstate = dict(vals, kanban_state='normal')
              for t in self.browse(cr, uid, ids, context=context):
 -                write_vals = vals_reset_kstate if t.stage_id != new_stage else vals
 +                write_vals = vals_reset_kstate if t.stage_id.id != new_stage else vals
                  super(task, self).write(cr, uid, [t.id], write_vals, context=context)
              result = True
          else:
      # Mail gateway
      # ---------------------------------------------------
  
 +    def _message_get_auto_subscribe_fields(self, cr, uid, updated_fields, auto_follow_fields=None, context=None):
 +        if auto_follow_fields is None:
 +            auto_follow_fields = ['user_id', 'reviewer_id']
 +        return super(task, self)._message_get_auto_subscribe_fields(cr, uid, updated_fields, auto_follow_fields, context=context)
 +
      def message_get_reply_to(self, cr, uid, ids, context=None):
          """ Override to get the reply_to of the parent project. """
 -        return [task.project_id.message_get_reply_to()[0] if task.project_id else False
 -                    for task in self.browse(cr, uid, ids, context=context)]
 +        tasks = self.browse(cr, SUPERUSER_ID, ids, context=context)
 +        project_ids = set([task.project_id.id for task in tasks if task.project_id])
 +        aliases = self.pool['project.project'].message_get_reply_to(cr, uid, list(project_ids), context=context)
 +        return dict((task.id, aliases.get(task.project_id and task.project_id.id or 0, False)) for task in tasks)
  
      def message_new(self, cr, uid, msg, custom_values=None, context=None):
          """ Override to updates the document according to the email. """
@@@ -1123,7 -1162,7 +1123,7 @@@ class project_work(osv.osv)
      _name = "project.task.work"
      _description = "Project Task Work"
      _columns = {
 -        'name': fields.char('Work summary', size=128),
 +        'name': fields.char('Work summary'),
          'date': fields.datetime('Date', select="1"),
          'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
          'hours': fields.float('Time Spent'),
      }
  
      _order = "date desc"
 -    def create(self, cr, uid, vals, *args, **kwargs):
 +    def create(self, cr, uid, vals, context=None):
          if 'hours' in vals and (not vals['hours']):
              vals['hours'] = 0.00
          if 'task_id' in vals:
              cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
 -        return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
 +            self.pool.get('project.task').invalidate_cache(cr, uid, ['remaining_hours'], [vals['task_id']], context=context)
 +        return super(project_work,self).create(cr, uid, vals, context=context)
  
      def write(self, cr, uid, ids, vals, context=None):
          if 'hours' in vals and (not vals['hours']):
              vals['hours'] = 0.00
          if 'hours' in vals:
 +            task_obj = self.pool.get('project.task')
              for work in self.browse(cr, uid, ids, context=context):
                  cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
 +                task_obj.invalidate_cache(cr, uid, ['remaining_hours'], [work.task_id.id], context=context)
          return super(project_work,self).write(cr, uid, ids, vals, context)
  
 -    def unlink(self, cr, uid, ids, *args, **kwargs):
 +    def unlink(self, cr, uid, ids, context=None):
 +        task_obj = self.pool.get('project.task')
          for work in self.browse(cr, uid, ids):
              cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
 -        return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
 +            task_obj.invalidate_cache(cr, uid, ['remaining_hours'], [work.task_id.id], context=context)
 +        return super(project_work,self).unlink(cr, uid, ids, context=context)
  
  
  class account_analytic_account(osv.osv):
          'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
      }
  
 -    def on_change_template(self, cr, uid, ids, template_id, context=None):
 -        res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
 +    def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
 +        res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
          if template_id and 'value' in res:
              template = self.browse(cr, uid, template_id, context=context)
              res['value']['use_tasks'] = template.use_tasks
@@@ -1313,7 -1347,6 +1313,7 @@@ class project_task_history_cumulative(o
  
      _columns = {
          'end_date': fields.date('End Date'),
 +        'nbr_tasks': fields.integer('# of Tasks', readonly=True),
          'project_id': fields.many2one('project.project', 'Project'),
      }
  
                      h.id AS history_id,
                      h.date+generate_series(0, CAST((coalesce(h.end_date, DATE 'tomorrow')::date - h.date) AS integer)-1) AS date,
                      h.task_id, h.type_id, h.user_id, h.kanban_state,
 +                    count(h.task_id) as nbr_tasks,
                      greatest(h.remaining_hours, 1) AS remaining_hours, greatest(h.planned_hours, 1) AS planned_hours,
                      t.project_id
                  FROM
                      project_task_history AS h
                      JOIN project_task AS t ON (h.task_id = t.id)
 +                GROUP BY
 +                  h.id,
 +                  h.task_id,
 +                  t.project_id
  
              ) AS history
          )
@@@ -1350,6 -1378,6 +1350,6 @@@ class project_category(osv.osv)
      _name = "project.category"
      _description = "Category of project's task, issue, ..."
      _columns = {
 -        'name': fields.char('Name', size=64, required=True, translate=True),
 +        'name': fields.char('Name', required=True, translate=True),
      }
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@@ -20,6 -20,7 +20,7 @@@
  ##############################################################################
  
  import logging
+ import unicodedata
  
  from openerp import tools
  import openerp.modules
@@@ -78,15 -79,6 +79,15 @@@ class ir_translation_import_cursor(obje
          """Feed a translation, as a dictionary, into the cursor
          """
          params = dict(trans_dict, state="translated" if trans_dict['value'] else "to_translate")
 +
 +        if params['type'] == 'view':
 +            # ugly hack for QWeb views - pending refactoring of translations in master
 +            if params['imd_model'] == 'website':
 +                params['imd_model'] = "ir.ui.view"
 +            # non-QWeb views do not need a matching res_id -> force to 0 to avoid dropping them
 +            elif params['res_id'] is None:
 +                params['res_id'] = 0
 +
          self._cr.execute("""INSERT INTO %s (name, lang, res_id, src, type, imd_model, module, imd_name, value, state, comments)
                              VALUES (%%(name)s, %%(lang)s, %%(res_id)s, %%(src)s, %%(type)s, %%(imd_model)s, %%(module)s,
                                      %%(imd_name)s, %%(value)s, %%(state)s, %%(comments)s)""" % self._table_name,
              FROM ir_model_data AS imd
              WHERE ti.res_id IS NULL
                  AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL
 -
                  AND ti.module = imd.module AND ti.imd_name = imd.name
                  AND ti.imd_model = imd.model; """ % self._table_name)
  
          if self._debug:
 -            cr.execute("SELECT module, imd_model, imd_name FROM %s " \
 +            cr.execute("SELECT module, imd_name, imd_model FROM %s " \
                  "WHERE res_id IS NULL AND module IS NOT NULL" % self._table_name)
              for row in cr.fetchall():
 -                _logger.debug("ir.translation.cursor: missing res_id for %s. %s/%s ", *row)
 +                _logger.info("ir.translation.cursor: missing res_id for %s.%s <%s> ", *row)
  
          # Records w/o res_id must _not_ be inserted into our db, because they are
          # referencing non-existent data.
@@@ -176,11 -169,11 +177,11 @@@ class ir_translation(osv.osv)
              else:
                  model_name, field = record.name.split(',')
                  model = self.pool.get(model_name)
 -                if model and model.exists(cr, uid, record.res_id, context=context):
 +                if model is not None:
                      # Pass context without lang, need to read real stored field, not translation
                      context_no_lang = dict(context, lang=None)
 -                    result = model.read(cr, uid, record.res_id, [field], context=context_no_lang)
 -                    res[record.id] = result[field] if result else False
 +                    result = model.read(cr, uid, [record.res_id], [field], context=context_no_lang)
 +                    res[record.id] = result[0][field] if result else False
          return res
  
      def _set_src(self, cr, uid, id, name, value, args, context=None):
                  })
          return len(ids)
  
 +    def _get_source_query(self, cr, uid, name, types, lang, source, res_id):
 +        if source:
 +            query = """SELECT value
 +                       FROM ir_translation
 +                       WHERE lang=%s
 +                        AND type in %s
 +                        AND src=%s"""
 +            params = (lang or '', types, tools.ustr(source))
 +            if res_id:
 +                query += " AND res_id=%s"
 +                params += (res_id,)
 +            if name:
 +                query += " AND name=%s"
 +                params += (tools.ustr(name),)
 +        else:
 +            query = """SELECT value
 +                       FROM ir_translation
 +                       WHERE lang=%s
 +                        AND type in %s
 +                        AND name=%s"""
 +
 +            params = (lang or '', types, tools.ustr(name))
 +        
 +        return (query, params)
 +
      @tools.ormcache(skiparg=3)
      def _get_source(self, cr, uid, name, types, lang, source=None, res_id=None):
          """
              return tools.ustr(source or '')
          if isinstance(types, basestring):
              types = (types,)
 -        if source:
 -            query = """SELECT value
 -                       FROM ir_translation
 -                       WHERE lang=%s
 -                        AND type in %s
 -                        AND src=%s"""
 -            params = (lang or '', types, tools.ustr(source))
 -            if res_id:
 -                query += "AND res_id=%s"
 -                params += (res_id,)
 -            if name:
 -                query += " AND name=%s"
 -                params += (tools.ustr(name),)
 -            cr.execute(query, params)
 -        else:
 -            cr.execute("""SELECT value
 -                          FROM ir_translation
 -                          WHERE lang=%s
 -                           AND type in %s
 -                           AND name=%s""",
 -                    (lang or '', types, tools.ustr(name)))
 +        
 +        query, params = self._get_source_query(cr, uid, name, types, lang, source, res_id)
 +        
 +        cr.execute(query, params)
          res = cr.fetchone()
          trad = res and res[0] or u''
          if source and not trad:
              return tools.ustr(source)
-         return trad
+         # Remove control characters
+         return filter(lambda c: unicodedata.category(c) != 'Cc', tools.ustr(trad))
  
      def create(self, cr, uid, vals, context=None):
          if context is None: