[MERGE] forward port of branch 8.0 up to 92183e5
authorChristophe Simonis <chs@odoo.com>
Fri, 24 Oct 2014 16:23:46 +0000 (18:23 +0200)
committerChristophe Simonis <chs@odoo.com>
Fri, 24 Oct 2014 16:23:46 +0000 (18:23 +0200)
1  2 
addons/event_sale/models/event.py
addons/event_sale/models/sale_order.py
addons/mail/static/src/js/mail.js
addons/sale/sale.py
addons/web/controllers/main.py
addons/website_event_sale/controllers/main.py
addons/website_event_sale/views/website_event_sale.xml
addons/website_sale/controllers/main.py
addons/website_sale/views/templates.xml

index 5b66d89,0000000..01f622c
mode 100644,000000..100644
--- /dev/null
@@@ -1,150 -1,0 +1,160 @@@
 +# -*- coding: utf-8 -*-
 +
 +from openerp import models, fields, api, _
++import openerp.addons.decimal_precision as dp
 +from openerp.exceptions import Warning
 +
 +
 +class event_event(models.Model):
 +    _inherit = 'event.event'
 +
 +    event_ticket_ids = fields.One2many(
 +        'event.event.ticket', 'event_id', string='Event Ticket',
-         default=lambda rec: rec._default_tickets())
++            default=lambda rec: rec._default_tickets(), copy=True)
 +    seats_max = fields.Integer(
 +        string='Maximum Available Seats',
 +        help="The maximum registration level is equal to the sum of the maximum registration of event ticket. " +
 +             "If you have too much registrations you are not able to confirm your event. (0 to ignore this rule )",
 +        store=True, readonly=True, compute='_compute_seats_max')
 +
 +    badge_back = fields.Html('Badge Back', translate=True, states={'done': [('readonly', True)]})
 +    badge_innerleft = fields.Html('Badge Innner Left', translate=True, states={'done': [('readonly', True)]})
 +    badge_innerright = fields.Html('Badge Inner Right', translate=True, states={'done': [('readonly', True)]})
 +
 +    @api.model
 +    def _default_tickets(self):
 +        try:
 +            product = self.env.ref('event_sale.product_product_event')
 +            return [{
 +                'name': _('Subscription'),
 +                'product_id': product.id,
 +                'price': 0,
 +            }]
 +        except ValueError:
 +            return self.env['event.event.ticket']
 +
 +    @api.one
 +    @api.depends('event_ticket_ids.seats_max')
 +    def _compute_seats_max(self):
 +        self.seats_max = sum(ticket.seats_max for ticket in self.event_ticket_ids)
 +
 +
 +class event_ticket(models.Model):
 +    _name = 'event.event.ticket'
 +    _description = 'Event Ticket'
 +
 +    name = fields.Char('Name', required=True, translate=True)
 +    event_id = fields.Many2one('event.event', "Event", required=True, ondelete='cascade')
 +    product_id = fields.Many2one(
 +        'product.product', 'Product',
 +        required=True, domain=[("event_type_id", "!=", False)],
 +        default=lambda self: self._default_product_id())
 +    registration_ids = fields.One2many('event.registration', 'event_ticket_id', 'Registrations')
-     price = fields.Float('Price')
++    price = fields.Float('Price', digits=dp.get_precision('Product Price'))
++    price_reduce = fields.Float("Price Reduce", compute="_get_price_compute", store=False,
++                                digits=dp.get_precision('Product Price'))
 +    deadline = fields.Date("Sales End")
 +    is_expired = fields.Boolean('Is Expired', compute='_is_expired', store=True)
 +
 +    @api.model
 +    def _default_product_id(self):
 +        try:
 +            product = self.env['ir.model.data'].get_object('event_sale', 'product_product_event')
 +            return product.id
 +        except ValueError:
 +            return False
 +
 +    @api.one
 +    @api.depends('deadline')
 +    def _is_expired(self):
 +        # FIXME: A ticket is considered expired when the deadline is passed. The deadline should
 +        #        be considered in the timezone of the event, not the timezone of the user!
 +        #        Until we add a TZ on the event we'll use the context's current date, more accurate
 +        #        than using UTC all the time.
 +        current_date = fields.Date.context_today(self.with_context({'tz': self.event_id.date_tz}))
 +        self.is_expired = self.deadline < current_date
 +
++    @api.one
++    @api.depends('price', 'product_id.lst_price', 'product_id.price')
++    def _get_price_compute(self):
++        product = self.product_id
++        discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0
++        self.price_reduce = (1.0 - discount) * self.price
++
 +    seats_max = fields.Integer('Maximum Available Seats', help="You can for each event define a maximum registration level. If you have too much registrations you are not able to confirm your event. (put 0 to ignore this rule )")
 +    seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=True)
 +    seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=True)
 +    seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations', compute='_compute_seats', store=True)
 +    seats_used = fields.Integer(compute='_compute_seats', store=True)
 +
 +    @api.multi
 +    @api.depends('seats_max', 'registration_ids.state')
 +    def _compute_seats(self):
 +        """ Determine reserved, available, reserved but unconfirmed and used seats. """
 +        # initialize fields to 0
 +        for ticket in self:
 +            ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
 +        # aggregate registrations by ticket and by state
 +        if self.ids:
 +            state_field = {
 +                'draft': 'seats_unconfirmed',
 +                'open': 'seats_reserved',
 +                'done': 'seats_used',
 +            }
 +            query = """ SELECT event_ticket_id, state, count(event_id)
 +                        FROM event_registration
 +                        WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done')
 +                        GROUP BY event_ticket_id, state
 +                    """
 +            self._cr.execute(query, (tuple(self.ids),))
 +            for event_ticket_id, state, num in self._cr.fetchall():
 +                ticket = self.browse(event_ticket_id)
 +                ticket[state_field[state]] += num
 +        # compute seats_available
 +        for ticket in self:
 +            if ticket.seats_max > 0:
 +                ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used)
 +
 +    @api.one
 +    @api.constrains('registration_ids', 'seats_max')
 +    def _check_seats_limit(self):
 +        if self.seats_max and self.seats_available < 0:
 +            raise Warning('No more available seats for the ticket')
 +
 +    @api.onchange('product_id')
 +    def onchange_product_id(self):
 +        price = self.product_id.list_price if self.product_id else 0
 +        return {'value': {'price': price}}
 +
 +
 +class event_registration(models.Model):
 +    _inherit = 'event.registration'
 +
 +    event_ticket_id = fields.Many2one('event.event.ticket', 'Event Ticket')
 +    # sale_order_line_id = fields.Many2one('sale.order.line', 'Sale Order Line', ondelete='cascade')
 +
 +    @api.one
 +    @api.constrains('event_ticket_id', 'state')
 +    def _check_ticket_seats_limit(self):
 +        if self.event_ticket_id.seats_max and self.event_ticket_id.seats_available < 0:
 +            raise Warning('No more available seats for this ticket')
 +
 +    @api.one
 +    def _check_auto_confirmation(self):
 +        res = super(event_registration, self)._check_auto_confirmation()[0]
 +        if res and self.origin:
 +            orders = self.env['sale.order'].search([('name', '=', self.origin)], limit=1)
 +            if orders and orders[0].state == 'draft':
 +                res = False
 +        return res
 +
 +    @api.model
 +    def create(self, vals):
 +        res = super(event_registration, self).create(vals)
 +        if res.origin:
 +            message = _("The registration has been created for event %(event_name)s%(ticket)s from sale order %(order)s") % ({
 +                'event_name': '<i>%s</i>' % res.event_id.name,
 +                'ticket': res.event_ticket_id and _(' with ticket %s') % (('<i>%s</i>') % res.event_ticket_id.name) or '',
 +                'order': res.origin})
 +            res.message_post(body=message)
 +        return res
index e5d26b7,0000000..1b5f5ac
mode 100644,000000..100644
--- /dev/null
@@@ -1,85 -1,0 +1,92 @@@
 +# -*- coding: utf-8 -*-
 +
 +from openerp import api
 +from openerp.osv import fields, osv
 +
 +
 +class sale_order(osv.osv):
 +    _inherit = "sale.order"
 +
 +    def action_button_confirm(self, cr, uid, ids, context=None):
 +        # TDE note: This method works on a list of one id (see sale/sale.py) so working on ids[0] seems safe.
 +        res = super(sale_order, self).action_button_confirm(cr, uid, ids, context=context)
 +        redirect_to_event_registration = any(line.event_id for order in self.browse(cr, uid, ids, context=context) for line in order.order_line)
 +        if redirect_to_event_registration:
 +            event_ctx = dict(context, default_sale_order_id=ids[0])
 +            return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'event_sale', 'action_sale_order_event_registration', event_ctx)
 +        else:
 +            return res
 +
 +
 +class sale_order_line(osv.osv):
 +    _inherit = 'sale.order.line'
 +    _columns = {
 +        'event_id': fields.many2one(
 +            'event.event', 'Event',
 +            help="Choose an event and it will automatically create a registration for this event."),
 +        'event_ticket_id': fields.many2one(
 +            'event.event.ticket', 'Event Ticket',
 +            help="Choose an event ticket and it will automatically create a registration for this event ticket."),
 +        # those 2 fields are used for dynamic domains and filled by onchange
 +        # TDE: really necessary ? ...
 +        'event_type_id': fields.related('product_id', 'event_type_id', type='many2one', relation="event.type", string="Event Type"),
 +        'event_ok': fields.related('product_id', 'event_ok', string='event_ok', type='boolean'),
 +    }
 +
++    def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
++        res = super(sale_order_line, self)._prepare_order_line_invoice_line(cr, uid, line, account_id=account_id, context=context)
++        if line.event_id:
++            event = self.pool['event.event'].read(cr, uid, line.event_id.id, ['name'], context=context)
++            res['name'] = '%s: %s' % (res.get('name', ''), event['name'])
++        return res
++
 +    def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
 +                          qty_uos=0, uos=False, name='', partner_id=False, lang=False,
 +                          update_tax=True, date_order=False, packaging=False,
 +                          fiscal_position=False, flag=False, context=None):
 +        """ check product if event type """
 +        res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id, lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
 +        if product:
 +            product_res = self.pool.get('product.product').browse(cr, uid, product, context=context)
 +            if product_res.event_ok:
 +                res['value'].update(event_type_id=product_res.event_type_id.id,
 +                                    event_ok=product_res.event_ok)
 +            else:
 +                res['value'].update(event_type_id=False,
 +                                    event_ok=False)
 +        return res
 +
 +    @api.multi
 +    def _update_registrations(self):
 +        """ Create or update registrations linked to a sale order line. A sale
 +        order line has a product_uom_qty attribute that will be the number of
 +        registrations linked to this line. This method update existing registrations
 +        and create new one for missing one. """
 +        registrations = self.env['event.registration'].search([('origin', 'in', list(set([so.name for line in self for so in line.order_id if line.event_id])))])
 +        for so_line in [l for l in self if l.event_id]:
 +            existing_registrations = [r for r in registrations if r.event_id == so_line.event_id and r.origin == so_line.order_id.name]
 +            for registration in existing_registrations:
 +                registration.write({'state': 'open'})
 +
 +            for count in range(int(so_line.product_uom_qty) - len(existing_registrations)):
 +                self.env['event.registration'].create({
 +                    'event_id': so_line.event_id.id,
 +                    'event_ticket_id': so_line.event_ticket_id.id,
 +                    'partner_id': so_line.order_id.partner_id.id,
 +                    'origin': so_line.order_id.name,
 +                })
 +        return True
 +
 +    def button_confirm(self, cr, uid, ids, context=None):
 +        """ Override confirmation of the sale order line in order to create
 +        or update the possible event registrations linked to the sale. """
 +        '''
 +        create registration with sales order
 +        '''
 +        res = super(sale_order_line, self).button_confirm(cr, uid, ids, context=context)
 +        self._update_registrations(cr, uid, ids, context=context)
 +        return res
 +
 +    def onchange_event_ticket_id(self, cr, uid, ids, event_ticket_id=False, context=None):
 +        price = event_ticket_id and self.pool["event.event.ticket"].browse(cr, uid, event_ticket_id, context=context).price or False
 +        return {'value': {'price_unit': price}}
Simple merge
Simple merge
Simple merge
@@@ -28,39 -29,34 +29,49 @@@ from openerp.tools.translate import 
  
  class website_event(website_event):
  
+     @http.route(['/event/<model("event.event"):event>/register'], type='http', auth="public", website=True)
+     def event_register(self, event, **post):
+         pricelist_id = int(get_pricelist())
+         values = {
+             'event': event.with_context(pricelist=pricelist_id),
+             'main_object': event.with_context(pricelist=pricelist_id),
+             'range': range,
+         }
+         return request.website.render("website_event.event_description_full", values)
 -    @http.route(['/event/cart/update'], type='http', auth="public", methods=['POST'], website=True)
 -    def cart_update(self, event_id, **post):
 +    def _process_tickets_details(self, data):
 +        ticket_post = {}
 +        for key, value in data.iteritems():
 +            if not key.startswith('nb_register') or not '-' in key:
 +                continue
 +            items = key.split('-')
 +            if len(items) < 2:
 +                continue
 +            ticket_post[int(items[1])] = int(value)
 +        tickets = request.registry['event.event.ticket'].browse(request.cr, request.uid, ticket_post.keys(), request.context)
 +        return [{'id': ticket.id, 'name': ticket.name, 'quantity': ticket_post[ticket.id], 'price': ticket.price} for ticket in tickets if ticket_post[ticket.id]]
 +
 +    @http.route(['/event/<model("event.event"):event>/registration/confirm'], type='http', auth="public", methods=['POST'], website=True)
 +    def registration_confirm(self, event, **post):
          cr, uid, context = request.cr, request.uid, request.context
 -        ticket_obj = request.registry.get('event.event.ticket')
 +        order = request.website.sale_get_order(force_create=1)
  
 -        sale = False
 -        for key, value in post.items():
 -            quantity = int(value or "0")
 -            if not quantity:
 -                continue
 -            sale = True
 -            ticket_id = key.split("-")[0] == 'ticket' and int(key.split("-")[1]) or None
 -            ticket = ticket_obj.browse(cr, SUPERUSER_ID, ticket_id, context=context)
 -            order = request.website.sale_get_order(force_create=1)
 -            order.with_context(event_ticket_id=ticket.id)._cart_update(product_id=ticket.product_id.id, add_qty=quantity)
 +        registrations = self._process_registration_details(post)
 +        registration_ctx = dict(context, registration_force_draft=True)
 +        for registration in registrations:
 +            ticket = request.registry['event.event.ticket'].browse(cr, SUPERUSER_ID, int(registration['ticket_id']), context=context)
 +            order.with_context(event_ticket_id=ticket.id)._cart_update(product_id=ticket.product_id.id, add_qty=1)
 +
 +            request.registry['event.registration'].create(cr, SUPERUSER_ID, {
 +                'name': registration.get('name'),
 +                'phone': registration.get('phone'),
 +                'email': registration.get('email'),
 +                'event_ticket_id': int(registration['ticket_id']),
 +                'partner_id': order.partner_id.id,
 +                'event_id': event.id,
 +                'origin': order.name,
 +            }, context=registration_ctx)
  
 -        if not sale:
 -            return request.redirect("/event/%s" % event_id)
          return request.redirect("/shop/checkout")
  
      def _add_event(self, event_name="New Event", context={}, **kwargs):
      </xpath>
  </template>
  
- <template id="cart" inherit_id="website_sale.cart" name="My Cart Event's Price">
-     <xpath expr="//td[@name='price']/t" position="attributes">
-         <attribute name="t-if">abs(line.product_id.lst_price - line.price_unit) &gt; 0.2 and not line.product_id.event_ok</attribute>
-     </xpath>
- </template>
 -<template id="event_description_full" inherit_id="website_event.event_description_full" customize_show="True" name="Event's Ticket form">
 -    <xpath expr="//div[@t-field='event.description']" position="before">
 -        <form t-attf-action="/event/cart/update?event_id=#{ event.id }" method="post" t-if="event.event_ticket_ids">
 -            <table itemprop="offers" class="table table-striped">
 -                <thead>
 -                    <tr>
 -                        <th>Ticket Type</th>
 -                        <th style="min-width: 100px">Sales End</th>
 -                        <th style="min-width: 100px">Price</th>
 -                        <th></th>
 -                        <th>Quantity</th>
 -                    </tr>
 -                </thead>
 -                <tbody>
 -                    <t t-foreach="event.event_ticket_ids" t-as="ticket">
 -                      <tr itemscope="itemscope" itemtype="http://data-vocabulary.org/Offer" t-if="not ticket.is_expired">
 -                        <td itemscope="itemscope" itemtype="http://data-vocabulary.org/Product">                  
 -                            <div itemprop="name" t-field="ticket.name"/>
 -                            <div><small itemprop="description" t-field="ticket.product_id.description_sale"/></div>
 -                        </td>
 -                        <td><span itemprop="priceValidUntil" t-field="ticket.deadline"/></td>
 -                        <td>
 +<template id="registration_template" inherit_id="website_event.registration_template" customize_show="True" name="Event's Ticket form">
 +    <xpath expr="//tbody" position="replace">
 +        <tbody>
 +            <t t-foreach="event.event_ticket_ids" t-as="ticket">
 +                <tr itemscope="itemscope" itemtype="http://data-vocabulary.org/Offer" t-if="not ticket.is_expired">
 +                    <td itemscope="itemscope" itemtype="http://data-vocabulary.org/Product">
 +                        <div itemprop="name" t-field="ticket.name"/>
 +                        <div><small itemprop="description" t-field="ticket.product_id.description_sale"/></div>
 +                    </td>
 +                    <td>
 +                        <t t-if="ticket.deadline">
 +                            <span itemprop="priceValidUntil" t-field="ticket.deadline"/>
 +                        </t>
 +                        <t t-if="not ticket.deadline">
 +                            <span>Unlimited</span>
 +                        </t>
 +                    </td>
 +                    <td>
-                         <t t-if="ticket.price or editable"><span t-field="ticket.price" t-field-options='{
+                             <t t-if="ticket.price or editable">
+                               <t t-if="(ticket.price-ticket.price_reduce) &gt; 1">
+                                 <del class="text-danger" style="white-space: nowrap;" t-field="ticket.price" t-field-options='{
+                                   "widget": "monetary",
+                                   "from_currency": "website.currency_id",
+                                   "display_currency": "user_id.partner_id.property_product_pricelist.currency_id"
+                                 }'/>&amp;nbsp;
+                               </t>
+                               <span t-field="ticket.price_reduce" t-field-options='{
 -                                   "widget": "monetary",
 -                                   "display_currency": "website.pricelist_id.currency_id"
 -                              }'/>
 -                              <span itemprop="price" style="display:none;" t-esc="ticket.price"/>
 -                              <span itemprop="priceCurrency" style="display:none;" t-esc="website.pricelist_id.currency_id.name"/>
 +                               "widget": "monetary",
 +                               "display_currency": "website.pricelist_id.currency_id"
 +                          }'/>
 +                            <span itemprop="price" style="display:none;" t-esc="ticket.price"/>
 +                            <span itemprop="priceCurrency" style="display:none;" t-esc="website.pricelist_id.currency_id.name"/>
 +                        </t>
 +                        <t t-if="not ticket.price and not editable">
 +                            <span>Free</span>
 +                        </t>
 +                    </td>
 +                    <td>
 +                        <span t-if="ticket.seats_max and ((ticket.seats_reserved or 0)*100 / ticket.seats_max)&gt;75" class="text-muted">
 +                            <t t-esc="ticket.seats_max - ticket.seats_reserved"/> <span>left</span>
 +                        </span>
 +                    </td>
 +                    <td>
 +                        <select t-if="ticket.seats_available" t-attf-name="nb_register-#{ticket.id}" class="form-control">
 +                            <t t-foreach="range(0, ticket.seats_available > 9 and 10 or ticket.seats_available+1 )" t-as="nb">
 +                                <option t-esc="nb"/>
                              </t>
 -                            <t t-if="not ticket.price and not editable">
 -                                <span>Free</span>
 -                            </t>
 -                        </td>
 -                        <td>
 -                            <span t-if="ticket.seats_max and ((ticket.seats_reserved or 0)*100 / ticket.seats_max)&gt;75" class="text-muted">
 -                                <t t-esc="ticket.seats_max - ticket.seats_reserved"/> <span>left</span>
 -                            </span>
 -                        </td>
 -                        <td>
 -                            <select t-if="ticket.seats_available" t-attf-name="ticket-#{ ticket.id }" class="form-control">
 -                                <t t-foreach="range(0, ticket.seats_available > 9 and 10 or ticket.seats_available+1 )" t-as="nb"><option t-esc="nb"/></t>
 -                            </select>
 -                            <span t-if="not ticket.seats_available">Sold Out</span>
 -                        </td>
 -                      </tr>
 -                    </t>
 -                </tbody>
 -            </table>
 -            <button type="submit" class="btn btn-primary btn-lg pull-right" t-if="event.seats_available">Order Now</button>
 -            <div class="clearfix"/>
 -            <hr/>
 +                        </select>
 +                        <span t-if="not ticket.seats_available">Sold Out</span>
 +                    </td>
 +                </tr>
 +            </t>
 +        </tbody>
 +    </xpath>
 +    <xpath expr="//button[@type='submit']" position="replace">
 +        <button type="submit" t-if="event.seats_available" class="btn btn-primary btn-lg pull-right a-submit" t-attf-id="#{event.id}">Order Now</button>
 +        <form t-if="not event.event_ticket_ids">
 +            <div class="alert alert-info">
 +                Event registration not yet started. 
 +                <t t-if="uid">
 +                    <i class="fa fa-plus-circle"><a t-attf-href="/web#id=#{event.id}&amp;view_type=form&amp;model=event.event"> <em>Configure Event Registration</em></a></i>
 +                </t>
 +            </div>
          </form>
      </xpath>
  </template>