[MERGE] forward port of branch saas-5 up to 0739bc4
authorDenis Ledoux <dle@odoo.com>
Mon, 11 Aug 2014 13:52:15 +0000 (15:52 +0200)
committerDenis Ledoux <dle@odoo.com>
Mon, 11 Aug 2014 13:58:02 +0000 (15:58 +0200)
28 files changed:
1  2 
addons/account/account_invoice.py
addons/account/report/account_invoice_report_view.xml
addons/account/wizard/account_fiscalyear_close.py
addons/account_analytic_plans/account_analytic_plans.py
addons/account_voucher/account_voucher.py
addons/analytic/analytic.py
addons/calendar/calendar.py
addons/calendar/calendar_view.xml
addons/crm/base_partner_merge.py
addons/crm_claim/crm_claim_view.xml
addons/email_template/email_template.py
addons/mail/mail_followers.py
addons/mail/static/src/xml/mail.xml
addons/point_of_sale/point_of_sale_view.xml
addons/point_of_sale/static/src/xml/pos.xml
addons/portal_sale/portal_sale_data.xml
addons/stock/stock.py
addons/web/static/src/css/base.css
addons/web/static/src/css/base.sass
addons/web/static/src/js/view_form.js
addons/web_kanban/static/src/js/kanban.js
addons/website/controllers/main.py
addons/website/models/website.py
addons/website/views/website_templates.xml
addons/website_quote/models/order.py
openerp/addons/base/ir/ir_cron.py
openerp/report/report_sxw.py
openerp/tools/yaml_import.py

@@@ -58,141 -227,11 +58,143 @@@ class account_invoice(models.Model)
              'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj.state == 'open' and obj.type in ('out_invoice', 'out_refund'),
          },
      }
 -    _columns = {
 -        'name': fields.char('Reference/Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
 -        'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
 -        'supplier_invoice_number': fields.char('Supplier Invoice Number', size=64, help="The reference of this invoice as provided by the supplier.", readonly=True, states={'draft':[('readonly',False)]}),
 -        'type': fields.selection([
 +
 +    @api.one
 +    @api.depends('invoice_line.price_subtotal', 'tax_line.amount')
 +    def _compute_amount(self):
 +        self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line)
 +        self.amount_tax = sum(line.amount for line in self.tax_line)
 +        self.amount_total = self.amount_untaxed + self.amount_tax
 +
 +    @api.model
 +    def _default_journal(self):
 +        inv_type = self._context.get('type', 'out_invoice')
 +        inv_types = inv_type if isinstance(inv_type, list) else [inv_type]
 +        company_id = self._context.get('company_id', self.env.user.company_id.id)
 +        domain = [
 +            ('type', 'in', filter(None, map(TYPE2JOURNAL.get, inv_types))),
 +            ('company_id', '=', company_id),
 +        ]
 +        return self.env['account.journal'].search(domain, limit=1)
 +
 +    @api.model
 +    def _default_currency(self):
 +        journal = self._default_journal()
 +        return journal.currency or journal.company_id.currency_id
 +
 +    @api.model
 +    @api.returns('account.analytic.journal')
 +    def _get_journal_analytic(self, inv_type):
 +        """ Return the analytic journal corresponding to the given invoice type. """
 +        journal_type = TYPE2JOURNAL.get(inv_type, 'sale')
 +        journal = self.env['account.analytic.journal'].search([('type', '=', journal_type)], limit=1)
 +        if not journal:
 +            raise except_orm(_('No Analytic Journal!'),
 +                _("You must define an analytic journal of type '%s'!") % (journal_type,))
 +        return journal
 +
 +    @api.one
 +    @api.depends('account_id', 'move_id.line_id.account_id', 'move_id.line_id.reconcile_id')
 +    def _compute_reconciled(self):
 +        self.reconciled = self.test_paid()
 +
 +    @api.model
 +    def _get_reference_type(self):
 +        return [('none', _('Free Reference'))]
 +
 +    @api.one
 +    @api.depends(
 +        'state', 'currency_id', 'invoice_line.price_subtotal',
 +        'move_id.line_id.account_id.type',
 +        'move_id.line_id.amount_residual',
 +        'move_id.line_id.amount_residual_currency',
 +        'move_id.line_id.currency_id',
 +        'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
 +    )
 +    def _compute_residual(self):
 +        nb_inv_in_partial_rec = max_invoice_id = 0
 +        self.residual = 0.0
 +        for line in self.move_id.line_id:
 +            if line.account_id.type in ('receivable', 'payable'):
 +                if line.currency_id == self.currency_id:
 +                    self.residual += line.amount_residual_currency
 +                else:
 +                    # ahem, shouldn't we use line.currency_id here?
 +                    from_currency = line.company_id.currency_id.with_context(date=line.date)
 +                    self.residual += from_currency.compute(line.amount_residual, self.currency_id)
 +                # we check if the invoice is partially reconciled and if there
 +                # are other invoices involved in this partial reconciliation
 +                for pline in line.reconcile_partial_id.line_partial_ids:
 +                    if pline.invoice and self.type == pline.invoice.type:
 +                        nb_inv_in_partial_rec += 1
 +                        # store the max invoice id as for this invoice we will
 +                        # make a balance instead of a simple division
 +                        max_invoice_id = max(max_invoice_id, pline.invoice.id)
 +        if nb_inv_in_partial_rec:
 +            # if there are several invoices in a partial reconciliation, we
 +            # split the residual by the number of invoices to have a sum of
 +            # residual amounts that matches the partner balance
 +            new_value = self.currency_id.round(self.residual / nb_inv_in_partial_rec)
 +            if self.id == max_invoice_id:
 +                # if it's the last the invoice of the bunch of invoices
 +                # partially reconciled together, we make a balance to avoid
 +                # rounding errors
 +                self.residual = self.residual - ((nb_inv_in_partial_rec - 1) * new_value)
 +            else:
 +                self.residual = new_value
 +        # prevent the residual amount on the invoice to be less than 0
 +        self.residual = max(self.residual, 0.0)
 +
 +    @api.one
 +    @api.depends(
 +        'move_id.line_id.account_id',
 +        'move_id.line_id.reconcile_id.line_id',
 +        'move_id.line_id.reconcile_partial_id.line_partial_ids',
 +    )
 +    def _compute_move_lines(self):
 +        # Give Journal Items related to the payment reconciled to this invoice.
 +        # Return partial and total payments related to the selected invoice.
 +        self.move_lines = self.env['account.move.line']
 +        if not self.move_id:
 +            return
 +        data_lines = self.move_id.line_id.filtered(lambda l: l.account_id == self.account_id)
 +        partial_lines = self.env['account.move.line']
 +        for data_line in data_lines:
 +            if data_line.reconcile_id:
 +                lines = data_line.reconcile_id.line_id
 +            elif data_line.reconcile_partial_id:
 +                lines = data_line.reconcile_partial_id.line_partial_ids
 +            else:
 +                lines = self.env['account_move_line']
 +            partial_lines += data_line
 +            self.move_lines = lines - partial_lines
 +
 +    @api.one
 +    @api.depends(
 +        'move_id.line_id.reconcile_id.line_id',
 +        'move_id.line_id.reconcile_partial_id.line_partial_ids',
 +    )
 +    def _compute_payments(self):
 +        partial_lines = lines = self.env['account.move.line']
 +        for line in self.move_id.line_id:
++            if line.account_id != self.account_id:
++                continue
 +            if line.reconcile_id:
 +                lines |= line.reconcile_id.line_id
 +            elif line.reconcile_partial_id:
 +                lines |= line.reconcile_partial_id.line_partial_ids
 +            partial_lines += line
 +        self.payment_ids = (lines - partial_lines).sorted()
 +
 +    name = fields.Char(string='Reference/Description', index=True,
 +        readonly=True, states={'draft': [('readonly', False)]})
 +    origin = fields.Char(string='Source Document',
 +        help="Reference of the document that produced this invoice.",
 +        readonly=True, states={'draft': [('readonly', False)]})
 +    supplier_invoice_number = fields.Char(string='Supplier Invoice Number',
 +        help="The reference of this invoice as provided by the supplier.",
 +        readonly=True, states={'draft': [('readonly', False)]})
 +    type = fields.Selection([
              ('out_invoice','Customer Invoice'),
              ('in_invoice','Supplier Invoice'),
              ('out_refund','Customer Refund'),
          <field name="arch" type="xml">
              <search string="Invoices Analysis">
                  <field name="date"/>
 -                <filter icon="terp-go-year" string="Year" name="year" domain="[('date','&lt;=', time.strftime('%%Y-%%m-%%d')),('date','&gt;=',time.strftime('%%Y-01-01'))]" help="year"/>
 +                <filter string="This Year" name="year" domain="['|', ('date', '=', False), '&amp;',('date','&lt;=', time.strftime('%%Y-12-31')),('date','&gt;=',time.strftime('%%Y-01-01'))]"/>
                  <separator/>
 -                <filter string="Draft" icon="terp-document-new" domain="[('state','=','draft')]" help = "Draft Invoices"/>
 -                <filter string="Pro-forma" icon="terp-gtk-media-pause" domain="['|', ('state','=','proforma'),('state','=','proforma2')]" help = "Pro-forma Invoices"/>
 -                <filter string="Invoiced" name="current" icon="terp-check" domain="[('state','not in', ('draft','cancel','proforma','proforma2'))]" help = "Open and Paid Invoices"/>
 +                <filter string="To Invoice" domain="[('state','=','draft')]" help = "Draft Invoices"/>
 +                <filter string="Pro-forma" domain="['|', ('state','=','proforma'),('state','=','proforma2')]"/>
 +                <filter string="Invoiced" name="current" domain="[('state','not in', ('draft','cancel','proforma','proforma2'))]"/>
                  <separator/>
 -                <filter icon="terp-personal" string="Customer" name="customer" domain="['|', ('type','=','out_invoice'),('type','=','out_refund')]" help="Customer Invoices And Refunds"/>
 -                <filter icon="terp-personal" string="Supplier" domain="['|', ('type','=','in_invoice'),('type','=','in_refund')]" help="Supplier Invoices And Refunds"/>
 +                <filter string="Customer" name="customer" domain="['|', ('type','=','out_invoice'),('type','=','out_refund')]"/>
 +                <filter string="Supplier" domain="['|', ('type','=','in_invoice'),('type','=','in_refund')]"/>
                  <separator/>
 -                <filter icon="terp-dolar" string="Invoice" domain="['|', ('type','=','out_invoice'),('type','=','in_invoice')]" help="Customer And Supplier Invoices"/>
 -                <filter icon="terp-dolar_ok!" string="Refund" domain="['|', ('type','=','out_refund'),('type','=','in_refund')]" help="Customer And Supplier Refunds"/>
 +                <filter string="Invoice" domain="['|', ('type','=','out_invoice'),('type','=','in_invoice')]"/>
 +                <filter string="Refund" domain="['|', ('type','=','out_refund'),('type','=','in_refund')]"/>
-                 <field name="partner_id"/>
+                 <field name="partner_id" operator="child_of"/>
                  <field name="user_id" />
                  <field name="categ_id" filter_domain="[('categ_id', 'child_of', self)]"/>
 -                <group expand="1" string="Group By...">
 +                <group expand="1" string="Group By">
                      <filter string="Partner" name="partner_id" context="{'group_by':'partner_id','residual_visible':True}"/>
                      <filter string="Commercial Partner" name="commercial_partner_id" context="{'group_by':'commercial_partner_id','residual_visible':True}"/>
                      <filter string="Commercial Partner's Country" name="country_id" context="{'group_by':'country_id'}"/>
Simple merge
@@@ -899,8 -901,17 +899,17 @@@ class calendar_event(osv.Model)
          'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
          'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
          'partner_ids': fields.many2many('res.partner', 'calendar_event_res_partner_rel', string='Attendees', states={'done': [('readonly', True)]}),
 -        'alarm_ids': fields.many2many('calendar.alarm', 'calendar_alarm_calendar_event_rel', string='Reminders', ondelete="restrict"),
 +        'alarm_ids': fields.many2many('calendar.alarm', 'calendar_alarm_calendar_event_rel', string='Reminders', ondelete="restrict", copy=False),
      }
+     def _get_default_partners(self, cr, uid, ctx=None):
+         ret = [self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id]
+         active_id = ctx.get('active_id')
+         if ctx.get('active_model') == 'res.partner' and active_id:
+             if active_id not in ret:
+                 ret.append(active_id)
+         return ret
      _defaults = {
          'end_type': 'count',
          'count': 1,
Simple merge
Simple merge
Simple merge
@@@ -48,25 -48,7 +48,26 @@@ class mail_followers(osv.Model)
              help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall."),
      }
  
 +    #
 +    # Modifying followers change access rights to individual documents. As the
 +    # cache may contain accessible/inaccessible data, one has to refresh it.
 +    #
 +    def create(self, cr, uid, vals, context=None):
 +        res = super(mail_followers, self).create(cr, uid, vals, context=context)
 +        self.invalidate_cache(cr, uid, context=context)
 +        return res
 +
 +    def write(self, cr, uid, ids, vals, context=None):
 +        res = super(mail_followers, self).write(cr, uid, ids, vals, context=context)
 +        self.invalidate_cache(cr, uid, context=context)
 +        return res
 +
 +    def unlink(self, cr, uid, ids, context=None):
 +        res = super(mail_followers, self).unlink(cr, uid, ids, context=context)
 +        self.invalidate_cache(cr, uid, context=context)
 +        return res
 +
+     _sql_constraints = [('mail_followers_res_partner_res_model_id_uniq','unique(res_model,res_id,partner_id)','Error, a partner cannot follow twice the same object.')]
  
  class mail_notification(osv.Model):
      """ Class holding notifications pushed to partners. Followers and partners
Simple merge
              <br />
              <t t-esc="widget.pos.company.name"/><br />
              Phone: <t t-esc="widget.pos.company.phone || ''"/><br />
-             User: <t t-esc="widget.pos.user.name"/><br />
+             User: <t t-esc="widget.pos.cashier.name"/><br />
 +            Shop: <t t-esc="widget.pos.shop.name"/><br />
              <br />
              <t t-if="widget.pos.config.receipt_header">
                  <div style='text-align:center'>
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -24,9 -24,10 +24,14 @@@ import tim
  import psycopg2
  from datetime import datetime
  from dateutil.relativedelta import relativedelta
+ import pytz
  
  import openerp
++<<<<<<< HEAD
 +from openerp import SUPERUSER_ID, netsvc, api
++=======
+ from openerp import netsvc, SUPERUSER_ID
++>>>>>>> 0739bc4edabab7e74571087c01a2da68ccadb10e
  from openerp.osv import fields, osv
  from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
  from openerp.tools.safe_eval import safe_eval as eval
@@@ -158,29 -159,27 +163,29 @@@ class ir_cron(osv.osv)
              must not be committed/rolled back!
          """
          try:
 -            now = fields.datetime.context_timestamp(job_cr, SUPERUSER_ID, datetime.now())
 -            nextcall = fields.datetime.context_timestamp(job_cr, SUPERUSER_ID, datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT))
 -            numbercall = job['numbercall']
 -
 -            ok = False
 -            while nextcall < now and numbercall:
 -                if numbercall > 0:
 -                    numbercall -= 1
 -                if not ok or job['doall']:
 -                    self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
 -                if numbercall:
 -                    nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
 -                ok = True
 -            addsql = ''
 -            if not numbercall:
 -                addsql = ', active=False'
 -            cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
 -                       (nextcall.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
 +            with api.Environment.manage():
-                 now = datetime.now() 
-                 nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT)
++                now = fields.datetime.context_timestamp(job_cr, SUPERUSER_ID, datetime.now())
++                nextcall = fields.datetime.context_timestamp(job_cr, SUPERUSER_ID, datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT))
 +                numbercall = job['numbercall']
 +
 +                ok = False
 +                while nextcall < now and numbercall:
 +                    if numbercall > 0:
 +                        numbercall -= 1
 +                    if not ok or job['doall']:
 +                        self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
 +                    if numbercall:
 +                        nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
 +                    ok = True
 +                addsql = ''
 +                if not numbercall:
 +                    addsql = ', active=False'
 +                cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
-                            (nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
++                           (nextcall.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
 +                self.invalidate_cache(cr, SUPERUSER_ID)
  
          finally:
 -            job_cr.commit()
 +            cr.commit()
              cron_cr.commit()
  
      @classmethod
Simple merge
@@@ -431,46 -424,37 +431,45 @@@ class YamlInterpreter(object)
  
                      if not el.attrib.get('on_change', False):
                          continue
 -                    match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
 -                    assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
 -
 -                    # creating the context
 -                    class parent2(object):
 -                        def __init__(self, d):
 -                            self.d = d
 -                        def __getattr__(self, name):
 -                            return self.d.get(name, False)
 -
 -                    ctx = record_dict.copy()
 -                    ctx['context'] = self.context
 -                    ctx['uid'] = SUPERUSER_ID
 -                    ctx['parent'] = parent2(parent)
 -                    for a in fg:
 -                        if a not in ctx:
 -                            ctx[a] = process_val(a, defaults.get(a, False))
 -
 -                    # Evaluation args
 -                    args = map(lambda x: eval(x, ctx), match.group(2).split(','))
 -                    result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
 +
 +                    if el.attrib['on_change'] in ('1', 'true'):
 +                        # New-style on_change
 +                        recs = model.browse(self.cr, SUPERUSER_ID, [], self.context)
 +                        result = recs.onchange(record_dict, field_name, onchange_spec)
 +
 +                    else:
 +                        match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
 +                        assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
 +
 +                        # creating the context
 +                        class parent2(object):
 +                            def __init__(self, d):
 +                                self.d = d
 +                            def __getattr__(self, name):
 +                                return self.d.get(name, False)
 +
 +                        ctx = record_dict.copy()
 +                        ctx['context'] = self.context
 +                        ctx['uid'] = SUPERUSER_ID
 +                        ctx['parent'] = parent2(parent)
 +                        for a in fg:
 +                            if a not in ctx:
 +                                ctx[a] = process_val(a, defaults.get(a, False))
 +
 +                        # Evaluation args
 +                        args = map(lambda x: eval(x, ctx), match.group(2).split(','))
 +                        result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
 +
                      for key, val in (result or {}).get('value', {}).items():
-                         assert key in fg, (
-                             "The field %r returned from the onchange call %r "
-                             "does not exist in the source view %r (of object "
-                             "%r). This field will be ignored (and thus not "
-                             "populated) when clients saves the new record" % (
-                                 key, match.group(1), view_info.get('name', '?'), model._name
-                             ))
-                         if key not in fields:
-                             # do not shadow values explicitly set in yaml.
-                             record_dict[key] = process_val(key, val)
+                         if key in fg:
+                             if key not in fields:
+                                 # do not shadow values explicitly set in yaml.
+                                 record_dict[key] = process_val(key, val)
+                         else:
+                             _logger.debug("The returning field '%s' from your on_change call '%s'"
+                                             " does not exist either on the object '%s', either in"
+                                             " the view '%s'",
+                                             key, match.group(1), model._name, view_info['name'])
                  else:
                      nodes = list(el) + nodes
          else: