--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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}}
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) > 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) > 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"
+ }'/>&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)>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)>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}&view_type=form&model=event.event"> <em>Configure Event Registration</em></a></i>
+ </t>
+ </div>
</form>
</xpath>
</template>