@@ -475,7 +496,9 @@
-
+
-
+
+
+
+
@@ -531,6 +557,13 @@
+
@@ -643,6 +676,7 @@
+
@@ -726,6 +760,8 @@
event.mail
-
+
diff --git a/addons/event_sale/data/event_demo.xml b/addons/event_sale/data/event_demo.xml
index 6ce4d91dfd6..f11a1dfdeec 100644
--- a/addons/event_sale/data/event_demo.xml
+++ b/addons/event_sale/data/event_demo.xml
@@ -1,48 +1,31 @@
-
- Standard
-
+
+
+ 0
+
+
-
1000.0
- 50
-
-
- VIP
-
+
-
1500.0
- 10
-
- General Admission
-
+
+
+ 1000.0
+
+
+
+ 1500.0
+
+
+
-
99.0
- 100
-
- Standard
-
-
-
- 1000.0
- 50
-
-
-
- VIP
-
-
-
- 1500.0
- 5
-
diff --git a/addons/event_sale/data/event_sale_data.xml b/addons/event_sale/data/event_sale_data.xml
index ce5c21e136e..bf8bc78249e 100644
--- a/addons/event_sale/data/event_sale_data.xml
+++ b/addons/event_sale/data/event_sale_data.xml
@@ -13,17 +13,11 @@
Event Registration
+
EVENT_REG
service
-
-
- Ticketing
-
-
-
-
diff --git a/addons/event_sale/models/event_event.py b/addons/event_sale/models/event_event.py
index 1d7bc163a36..b4e45656a2d 100644
--- a/addons/event_sale/models/event_event.py
+++ b/addons/event_sale/models/event_event.py
@@ -1,42 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models, _
-
-
-class EventType(models.Model):
- _inherit = 'event.type'
-
- @api.model
- def _get_default_event_ticket_ids(self):
- product = self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
- if not product:
- return False
- return [(0, 0, {
- 'name': _('Registration'),
- 'product_id': product.id,
- 'price': 0,
- })]
-
- use_ticketing = fields.Boolean('Ticketing')
- event_ticket_ids = fields.One2many(
- 'event.event.ticket', 'event_type_id',
- string='Tickets', default=_get_default_event_ticket_ids)
-
- @api.onchange('name')
- def _onchange_name(self):
- if self.name:
- self.event_ticket_ids.filtered(lambda ticket: ticket.name == _('Registration')).update({
- 'name': _('Registration for %s') % self.name
- })
+from odoo import api, fields, models
class Event(models.Model):
_inherit = 'event.event'
- event_ticket_ids = fields.One2many(
- 'event.event.ticket', 'event_id', string='Event Ticket',
- copy=True)
sale_order_lines_ids = fields.One2many(
'sale.order.line', 'event_id',
string='All sale order lines pointing to this event')
@@ -44,25 +14,6 @@ class Event(models.Model):
currency_id = fields.Many2one(
'res.currency', string='Currency',
related='company_id.currency_id', readonly=True)
- start_sale_date = fields.Date('Start sale date', compute='_compute_start_sale_date')
-
- @api.onchange('event_type_id')
- def _onchange_type(self):
- super(Event, self)._onchange_type()
- if self.event_type_id.use_ticketing:
- self.event_ticket_ids = [(5, 0, 0)] + [
- (0, 0, {
- 'name': self.name and _('Registration for %s') % self.name or ticket.name,
- 'product_id': ticket.product_id.id,
- 'price': ticket.price,
- })
- for ticket in self.event_type_id.event_ticket_ids]
-
- @api.depends('event_ticket_ids.start_sale_date')
- def _compute_start_sale_date(self):
- for event in self:
- start_dates = [ticket.start_sale_date for ticket in event.event_ticket_ids if ticket.start_sale_date]
- event.start_sale_date = min(start_dates) if start_dates else False
@api.depends('company_id.currency_id',
'sale_order_lines_ids.price_subtotal', 'sale_order_lines_ids.currency_id',
@@ -78,12 +29,6 @@ class Event(models.Model):
for sale_order_line_id in event.sale_order_lines_ids
])
- @api.depends('event_ticket_ids.sale_available')
- def _compute_event_registrations_open(self):
- non_open_events = self.filtered(lambda event: not any(event.event_ticket_ids.mapped('sale_available')))
- non_open_events.event_registrations_open = False
- super(Event, self - non_open_events)._compute_event_registrations_open()
-
def action_view_linked_orders(self):
""" Redirects to the orders linked to the current events """
sale_order_action = self.env.ref('sale.action_orders').read()[0]
diff --git a/addons/event_sale/models/event_registration.py b/addons/event_sale/models/event_registration.py
index bc9c2ae4917..be96547f10e 100644
--- a/addons/event_sale/models/event_registration.py
+++ b/addons/event_sale/models/event_registration.py
@@ -2,14 +2,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
-from odoo.exceptions import ValidationError
from odoo.tools import float_is_zero
class EventRegistration(models.Model):
_inherit = 'event.registration'
- event_ticket_id = fields.Many2one('event.event.ticket', string='Event Ticket', readonly=True, states={'draft': [('readonly', False)]})
# in addition to origin generic fields, add real relational fields to correctly
# handle attendees linked to sales orders and their lines
# TDE FIXME: maybe add an onchange on sale_order_id + origin
@@ -19,18 +17,6 @@ class EventRegistration(models.Model):
source_id = fields.Many2one('utm.source', 'Source', related="sale_order_id.source_id", store=True)
medium_id = fields.Many2one('utm.medium', 'Medium', related="sale_order_id.medium_id", store=True)
- @api.onchange('event_id')
- def _onchange_event_id(self):
- # We reset the ticket when keeping it would lead to an inconstitent state.
- if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id):
- self.event_ticket_id = None
-
- @api.constrains('event_ticket_id', 'state')
- def _check_ticket_seats_limit(self):
- for record in self:
- if record.event_ticket_id.seats_max and record.event_ticket_id.seats_available < 0:
- raise ValidationError(_('No more available seats for this ticket'))
-
def _check_auto_confirmation(self):
res = super(EventRegistration, self)._check_auto_confirmation()
if res:
@@ -41,29 +27,38 @@ class EventRegistration(models.Model):
@api.model
def create(self, vals):
+ if vals.get('sale_order_line_id'):
+ so_line_vals = self._synchronize_so_line_values(
+ self.env['sale.order.line'].browse(vals['sale_order_line_id'])
+ )
+ vals.update(so_line_vals)
res = super(EventRegistration, self).create(vals)
if res.origin or res.sale_order_id:
- res.message_post_with_view('mail.message_origin_link',
+ res.message_post_with_view(
+ 'mail.message_origin_link',
values={'self': res, 'origin': res.sale_order_id},
subtype_id=self.env.ref('mail.mt_note').id)
return res
- @api.model
- def _prepare_attendee_values(self, registration):
- """ Override to add sale related stuff """
- line_id = registration.get('sale_order_line_id')
- if line_id:
- registration.setdefault('partner_id', line_id.order_id.partner_id)
- att_data = super(EventRegistration, self)._prepare_attendee_values(registration)
- if line_id and line_id.event_ticket_id.sale_available:
- att_data.update({
- 'event_id': line_id.event_id.id,
- 'event_ticket_id': line_id.event_ticket_id.id,
- 'origin': line_id.order_id.name,
- 'sale_order_id': line_id.order_id.id,
- 'sale_order_line_id': line_id.id,
- })
- return att_data
+ def write(self, vals):
+ if vals.get('sale_order_line_id'):
+ so_line_vals = self._synchronize_so_line_values(
+ self.env['sale.order.line'].browse(vals['sale_order_line_id'])
+ )
+ vals.update(so_line_vals)
+ return super(EventRegistration, self).write(vals)
+
+ def _synchronize_so_line_values(self, so_line):
+ if so_line:
+ return {
+ 'partner_id': so_line.order_id.partner_id.id,
+ 'event_id': so_line.event_id.id,
+ 'event_ticket_id': so_line.event_ticket_id.id,
+ 'origin': so_line.order_id.name,
+ 'sale_order_id': so_line.order_id.id,
+ 'sale_order_line_id': so_line.id,
+ }
+ return {}
def summary(self):
res = super(EventRegistration, self).summary()
diff --git a/addons/event_sale/models/event_ticket.py b/addons/event_sale/models/event_ticket.py
index 06711ddd2a1..e3e666d5db3 100644
--- a/addons/event_sale/models/event_ticket.py
+++ b/addons/event_sale/models/event_ticket.py
@@ -1,145 +1,105 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models, _
-from odoo.exceptions import ValidationError, UserError
+import logging
+
+from odoo import api, fields, models
+
+_logger = logging.getLogger(__name__)
-class EventTicket(models.Model):
- _name = 'event.event.ticket'
- _description = 'Event Ticket'
+class EventTemplateTicket(models.Model):
+ _inherit = 'event.type.ticket'
def _default_product_id(self):
return self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
- name = fields.Char(string='Name', required=True, translate=True)
- event_type_id = fields.Many2one('event.type', string='Event Category', ondelete='cascade')
- event_id = fields.Many2one('event.event', string="Event", ondelete='cascade')
- company_id = fields.Many2one('res.company', related='event_id.company_id')
# product
- product_id = fields.Many2one('product.product', string='Product',
- required=True, domain=[("event_ok", "=", True)],
+ product_id = fields.Many2one(
+ 'product.product', string='Product', required=True, domain=[("event_ok", "=", True)],
default=_default_product_id)
price = fields.Float(string='Price', digits='Product Price')
price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits='Product Price')
- price_reduce_taxinc = fields.Float(compute='_get_price_reduce_tax', string='Price Reduce Tax inc')
- # sale
- start_sale_date = fields.Date(string="Sales Start")
- end_sale_date = fields.Date(string="Sales End")
- is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired')
- sale_available = fields.Boolean(string='Is Available', compute='_compute_sale_available')
- registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
- # seats fields
- seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')],
- string='Available Seat', required=True, store=True, compute='_compute_seats', default="limited")
- seats_max = fields.Integer(string='Maximum Available Seats',
- help="Define the number of available tickets. If you have too much registrations you will "
- "not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited.")
- 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)
-
- def _compute_is_expired(self):
- for ticket in self:
- if ticket.end_sale_date:
- current_date = fields.Date.context_today(ticket.with_context(tz=ticket.event_id.date_tz))
- ticket.is_expired = ticket.end_sale_date < current_date
- else:
- ticket.is_expired = False
-
- @api.depends('product_id.active', 'start_sale_date', 'end_sale_date')
- def _compute_sale_available(self):
- for ticket in self:
- current_date = fields.Date.context_today(ticket.with_context(tz=ticket.event_id.date_tz))
- if not ticket.product_id.active:
- ticket.sale_available = False
- elif ticket.start_sale_date and ticket.start_sale_date > current_date:
- ticket.sale_available = False
- elif ticket.end_sale_date and ticket.end_sale_date < current_date:
- ticket.sale_available = False
- else:
- ticket.sale_available = True
+ @api.depends('product_id')
def _compute_price_reduce(self):
for record in self:
product = record.product_id
discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0
record.price_reduce = (1.0 - discount) * record.price
- def _get_price_reduce_tax(self):
+ @api.onchange('product_id')
+ def _onchange_product_id(self):
+ self.price = self.product_id.list_price or 0
+ if not self.description and self.product_id.description_sale:
+ self.description = self.product_id.description_sale
+
+ def _init_column(self, column_name):
+ if column_name != "product_id":
+ return super(EventTemplateTicket, self)._init_column(column_name)
+
+ # fetch void columns
+ self.env.cr.execute("SELECT id FROM %s WHERE product_id IS NULL" % self._table)
+ ticket_type_ids = self.env.cr.fetchall()
+ if not ticket_type_ids:
+ return
+
+ # update existing columns
+ _logger.debug("Table '%s': setting default value of new column %s to unique values for each row",
+ self._table, column_name)
+ default_event_product = self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
+ if default_event_product:
+ product_id = default_event_product.id
+ else:
+ product_id = self.env['product.product'].create({
+ 'name': 'Generic Registration Product',
+ 'list_price': 0,
+ 'standard_price': 0,
+ 'type': 'service',
+ 'default_code': 'EVENT_REG',
+ 'type': 'service',
+ }).id
+ self.env['ir.model.data'].create({
+ 'name': 'product_product_event',
+ 'module': 'event_sale',
+ 'model': 'product.product',
+ 'res_id': product_id,
+ })
+ self.env.cr._obj.execute(
+ f'UPDATE {self._table} SET product_id = %s WHERE id IN %s;',
+ (product_id, tuple(ticket_type_ids))
+ )
+
+ @api.model
+ def _get_event_ticket_fields_whitelist(self):
+ """ Add sale specific fields to copy from template to ticket """
+ return super(EventTemplateTicket, self)._get_event_ticket_fields_whitelist() + ['product_id', 'price']
+
+
+class EventTicket(models.Model):
+ _inherit = 'event.event.ticket'
+
+ # product
+ price_reduce_taxinc = fields.Float(string='Price Reduce Tax inc', compute='_compute_price_reduce_taxinc')
+
+ def _compute_price_reduce_taxinc(self):
for record in self:
# sudo necessary here since the field is most probably accessed through the website
tax_ids = record.sudo().product_id.taxes_id.filtered(lambda r: r.company_id == record.event_id.company_id)
taxes = tax_ids.compute_all(record.price_reduce, record.event_id.company_id.currency_id, 1.0, product=record.product_id)
record.price_reduce_taxinc = taxes['total_included']
- @api.depends('seats_max', 'registration_ids.state')
- def _compute_seats(self):
- """ Determine reserved, available, reserved but unconfirmed and used seats. """
- # initialize fields to 0 + compute seats availability
- for ticket in self:
- ticket.seats_availability = 'unlimited' if ticket.seats_max == 0 else 'limited'
- 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.env['event.registration'].flush(['event_id', 'event_ticket_id', 'state'])
- self.env.cr.execute(query, (tuple(self.ids),))
- for event_ticket_id, state, num in self.env.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.depends('product_id.active')
+ def _compute_sale_available(self):
+ inactive_product_tickets = self.filtered(lambda ticket: not ticket.product_id.active)
+ for ticket in inactive_product_tickets:
+ ticket.sale_available = False
+ super(EventTicket, self - inactive_product_tickets)._compute_sale_available()
- @api.constrains('registration_ids', 'seats_max')
- def _check_seats_limit(self):
- for record in self:
- if record.seats_max and record.seats_available < 0:
- raise ValidationError(_('No more available seats for this ticket type.'))
-
- @api.constrains('event_type_id', 'event_id')
- def _constrains_event(self):
- if any(ticket.event_type_id and ticket.event_id for ticket in self):
- raise UserError(_('Ticket cannot belong to both the event category and the event itself.'))
-
- @api.onchange('product_id')
- def _onchange_product_id(self):
- self.price = self.product_id.list_price or 0
-
- def _get_ticket_multiline_description_sale(self):
- """ Compute a multiline description of this ticket, in the context of sales.
- It will often be used as the default description of a sales order line referencing this ticket.
-
- 1. the first line is the ticket name
- 2. the second line is the event name (if it exists, which should be the case with a normal workflow) or the product name (if it exists)
-
- We decided to ignore entirely the product name and the product description_sale because they are considered to be replaced by the ticket name and event name.
- -> the workflow of creating a new event also does not lead to filling them correctly, as the product is created through the event interface
- """
-
- name = self.display_name
-
- if self.event_id:
- name += '\n' + self.event_id.display_name
- elif self.product_id:
- name += '\n' + self.product_id.display_name
-
- return name
-
- @api.constrains('start_sale_date', 'end_sale_date')
- def _check_start_sale_date_and_end_sale_date(self):
- for ticket in self:
- if ticket.start_sale_date and ticket.end_sale_date and ticket.start_sale_date > ticket.end_sale_date:
- raise UserError(_('The stop date cannot be earlier than the start date.'))
+ def _get_ticket_multiline_description(self):
+ """ If people set a description on their product it has more priority
+ than the ticket name itself for the SO description. """
+ self.ensure_one()
+ if self.product_id.description_sale:
+ return '%s\n%s' % (self.product_id.description_sale, self.event_id.display_name)
+ return super(EventTicket, self)._get_ticket_multiline_description()
diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py
index 10a8e0a5e2c..a46eeee1503 100644
--- a/addons/event_sale/models/sale_order.py
+++ b/addons/event_sale/models/sale_order.py
@@ -60,13 +60,12 @@ class SaleOrderLine(models.Model):
existing_registrations.filtered(lambda self: self.state == 'cancel').action_set_draft()
for count in range(int(so_line.product_uom_qty) - len(existing_registrations)):
- registration = {}
+ registration_vals = {}
if registration_data:
- registration = registration_data.pop()
+ registration_vals = registration_data.pop()
# TDE CHECK: auto confirmation
- registration['sale_order_line_id'] = so_line
- Registration.with_context(registration_force_draft=True).create(
- Registration._prepare_attendee_values(registration))
+ registration_vals['sale_order_line_id'] = so_line.id
+ Registration.with_context(registration_force_draft=True).create(registration_vals)
return True
@api.onchange('product_id')
@@ -103,7 +102,7 @@ class SaleOrderLine(models.Model):
lang=self.order_id.partner_id.lang,
)
- return ticket._get_ticket_multiline_description_sale() + self._get_sale_order_line_multiline_description_variants()
+ return ticket._get_ticket_multiline_description() + self._get_sale_order_line_multiline_description_variants()
else:
return super(SaleOrderLine, self).get_sale_order_line_multiline_description_sale(product)
diff --git a/addons/event_sale/security/ir.model.access.csv b/addons/event_sale/security/ir.model.access.csv
index c8ea33e6655..fbbe3fbeb29 100644
--- a/addons/event_sale/security/ir.model.access.csv
+++ b/addons/event_sale/security/ir.model.access.csv
@@ -1,5 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_event_event_ticket_user,event.event.ticket.user,event_sale.model_event_event_ticket,event.group_event_user,1,0,0,0
-access_event_event_ticket_admin,event.event.ticket.admin,event_sale.model_event_event_ticket,event.group_event_manager,1,1,1,1
access_product_template_event_manager,product.template.event.manager,product.model_product_template,event.group_event_manager,1,1,1,1
access_product_product_event_manager,product.product.event.manager,product.model_product_product,event.group_event_manager,1,1,1,1
diff --git a/addons/event_sale/tests/common.py b/addons/event_sale/tests/common.py
index 30f437fd760..698a52c9805 100644
--- a/addons/event_sale/tests/common.py
+++ b/addons/event_sale/tests/common.py
@@ -12,6 +12,7 @@ class TestEventSaleCommon(TestEventCommon):
cls.event_product = cls.env['product.product'].create({
'name': 'Test Registration Product',
+ 'description_sale': 'Mighty Description',
'list_price': 10,
'event_ok': True,
'standard_price': 30.0,
diff --git a/addons/event_sale/tests/test_event_internals.py b/addons/event_sale/tests/test_event_internals.py
index 9f8539ec6bd..d70911b1524 100644
--- a/addons/event_sale/tests/test_event_internals.py
+++ b/addons/event_sale/tests/test_event_internals.py
@@ -2,13 +2,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime, timedelta
+from unittest.mock import patch
from odoo.addons.event_sale.tests.common import TestEventSaleCommon
-from odoo.fields import Datetime as FieldsDatetime
+from odoo.fields import Datetime as FieldsDatetime, Date as FieldsDate
from odoo.tests.common import users
-class EventSaleTest(TestEventSaleCommon):
+class TestEventData(TestEventSaleCommon):
@users('user_eventmanager')
def test_event_configuration_from_type(self):
@@ -17,11 +18,9 @@ class EventSaleTest(TestEventSaleCommon):
event_type = self.event_type_complex.with_user(self.env.user)
event_type.write({
'use_mail_schedule': False,
- 'use_ticketing': False,
+ 'use_ticket': False,
})
- # Event type does not use tickets but data is kept for compatibility and avoid recreating them
- self.assertEqual(len(event_type.event_ticket_ids), 1)
- self.assertEqual(event_type.event_ticket_ids.name, 'Registration')
+ self.assertEqual(event_type.event_type_ticket_ids, self.env['event.type.ticket'])
event = self.env['event.event'].create({
'name': 'Event Update Type',
@@ -33,53 +32,119 @@ class EventSaleTest(TestEventSaleCommon):
self.assertEqual(event.event_ticket_ids, self.env['event.event.ticket'])
event_type.write({
- 'use_ticketing': True,
- 'event_ticket_ids': [(5, 0), (0, 0, {
+ 'use_ticket': True,
+ 'event_type_ticket_ids': [(5, 0), (0, 0, {
'name': 'First Ticket',
'product_id': self.event_product.id,
+ 'seats_max': 5,
})]
})
- event_type.event_ticket_ids._onchange_product_id()
+ event_type.event_type_ticket_ids._onchange_product_id()
+ self.assertEqual(event_type.event_type_ticket_ids.description, self.event_product.description_sale)
+
+ # synchronize event
event._onchange_type()
- self.assertEqual(event.event_ticket_ids.name, 'Registration for %s' % event.name)
+ self.assertEqual(event.event_ticket_ids.name, event.event_type_id.event_type_ticket_ids.name)
+ self.assertEqual(event.event_ticket_ids.seats_availability, 'limited')
+ self.assertEqual(event.event_ticket_ids.seats_max, 5)
self.assertEqual(event.event_ticket_ids.product_id, self.event_product)
self.assertEqual(event.event_ticket_ids.price, self.event_product.list_price)
+ self.assertEqual(event.event_ticket_ids.description, self.event_product.description_sale)
def test_event_registrable(self):
- """Test if `_compute_event_registrations_open` works properly."""
- test_event = self.env['event.event'].create({
- 'name': 'TestEvent',
- 'date_begin': datetime.now() - timedelta(days=5),
- 'date_end': datetime.now() + timedelta(days=5),
- })
- test_event_ticket = self.env['event.event.ticket'].create({
+ """Test if `_compute_event_registrations_open` works properly with additional
+ product active conditions compared to base tests (see event) """
+ event = self.event_0.with_user(self.env.user)
+ self.assertTrue(event.event_registrations_open)
+
+ # ticket without dates boundaries -> ok
+ ticket = self.env['event.event.ticket'].create({
'name': 'TestTicket',
- 'event_id': test_event.id,
- 'product_id': self.env['product.product'].search([], limit=1).id,
+ 'event_id': event.id,
+ 'product_id': self.event_product.id,
})
+ self.assertTrue(event.event_registrations_open)
- self.assertEqual(test_event.event_registrations_open, True)
+ # ticket has inactive product -> ko
+ ticket.product_id.action_archive()
+ self.assertFalse(event.event_registrations_open)
- test_event.write({
- 'date_begin': datetime.now() - timedelta(days=5),
- 'date_end': datetime.now() - timedelta(days=1),
+ # at least one valid ticket -> ok is back
+ event_product = self.env['product.product'].create({'name': 'Test Registration Product New',})
+ new_ticket = self.env['event.event.ticket'].create({
+ 'name': 'TestTicket 2',
+ 'event_id': event.id,
+ 'product_id': event_product.id,
+ 'end_sale_date': datetime.now() + timedelta(days=2),
})
+ self.assertTrue(new_ticket.sale_available)
+ self.assertTrue(event.event_registrations_open)
- test_event.date_end = datetime.now() - timedelta(days=1)
- self.assertEqual(test_event.event_registrations_open, False)
- test_event.write({
- 'date_begin': datetime.now() - timedelta(days=1),
- 'date_end': datetime.now() + timedelta(days=5),
+class TestEventTicketData(TestEventSaleCommon):
+
+ def setUp(self):
+ super(TestEventTicketData, self).setUp()
+ self.ticket_date_patcher = patch('odoo.addons.event.models.event_ticket.fields.Date', wraps=FieldsDate)
+ self.ticket_date_patcher_mock = self.ticket_date_patcher.start()
+ self.ticket_date_patcher_mock.context_today.return_value = date(2020, 1, 31)
+
+ def tearDown(self):
+ super(TestEventTicketData, self).tearDown()
+ self.ticket_date_patcher.stop()
+
+ @users('user_eventmanager')
+ def test_event_ticket_fields(self):
+ """ Test event ticket fields synchronization """
+ event = self.event_0.with_user(self.env.user)
+ event.write({
+ 'event_ticket_ids': [
+ (5, 0),
+ (0, 0, {
+ 'name': 'First Ticket',
+ 'product_id': self.event_product.id,
+ 'seats_max': 30,
+ }), (0, 0, { # limited in time, available (01/10 (start) < 01/31 (today) < 02/10 (end))
+ 'name': 'Second Ticket',
+ 'product_id': self.event_product.id,
+ 'start_sale_date': date(2020, 1, 10),
+ 'end_sale_date': date(2020, 2, 10),
+ })
+ ],
})
+ first_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'First Ticket')
+ first_ticket._onchange_product_id()
+ second_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'Second Ticket')
+ second_ticket._onchange_product_id()
+ # force second ticket price, after calling the onchange
+ second_ticket.write({'price': 8.0})
- test_event.write({'event_ticket_ids': [(5, 0)]})
- self.assertEqual(test_event.event_registrations_open, False, 'cannot register if no tickets')
+ # price coming from product
+ self.assertEqual(first_ticket.price, self.event_product.list_price)
+ self.assertEqual(second_ticket.price, 8.0)
- test_event_ticket = self.env['event.event.ticket'].create({
- 'name': 'TestTicket',
- 'event_id': test_event.id,
- 'product_id': self.env['product.product'].search([], limit=1).id,
+ # default availability
+ self.assertEqual(first_ticket.seats_availability, 'limited')
+ self.assertTrue(first_ticket.sale_available)
+ self.assertFalse(first_ticket.is_expired)
+ self.assertEqual(second_ticket.seats_availability, 'unlimited')
+ self.assertTrue(second_ticket.sale_available)
+ self.assertFalse(second_ticket.is_expired)
+
+ # product archived
+ self.event_product.action_archive()
+ self.assertFalse(first_ticket.sale_available)
+ self.assertFalse(second_ticket.sale_available)
+
+ # sale is ended
+ self.event_product.action_unarchive()
+ second_ticket.write({'end_sale_date': date(2020, 1, 20)})
+ self.assertFalse(second_ticket.sale_available)
+ self.assertTrue(second_ticket.is_expired)
+ # sale has not started
+ second_ticket.write({
+ 'start_sale_date': date(2020, 2, 10),
+ 'end_sale_date': date(2020, 2, 20),
})
- test_event_ticket.product_id.active = False
- self.assertEqual(test_event.event_registrations_open, False, 'cannot register if product linked to the tickets are all archived')
+ self.assertFalse(second_ticket.sale_available)
+ self.assertFalse(second_ticket.is_expired)
diff --git a/addons/event_sale/views/event_registration_views.xml b/addons/event_sale/views/event_registration_views.xml
index 79394c37acf..c66daf53e02 100644
--- a/addons/event_sale/views/event_registration_views.xml
+++ b/addons/event_sale/views/event_registration_views.xml
@@ -1,24 +1,12 @@
-
- event.registration.search.inherit
- event.registration
-
-
-
-
-
-
-
-
event.registration.tree.inherit
event.registration
-
@@ -41,17 +29,6 @@
event.registration
-
-
-
diff --git a/addons/event_sale/views/event_ticket_views.xml b/addons/event_sale/views/event_ticket_views.xml
index 80e96a5cc5d..04547b31f3b 100644
--- a/addons/event_sale/views/event_ticket_views.xml
+++ b/addons/event_sale/views/event_ticket_views.xml
@@ -1,106 +1,95 @@
-
+
+
+
+
+ event.type.ticket.view.tree.inherit.sale
+ event.type.ticket
+
+
+
+
+
+
+
+
+
+
+
+
+ event.type.ticket.view.form.inherit.sale
+ event.type.ticket
+
+
+
+
+
+
+
+
+
+
+
- event.event.ticket.view.tree.from_event
+ event.event.ticket.view.tree.from.event.inherit.sale
event.event.ticket
-
+
-
-
-
-
-
+
+
+
+
-
-
-
-
+
- event.event.ticket.view.form.from.event
+ event.event.ticket.view.form.from.event.inherit.sale
event.event.ticket
-
+
-
+
+
+
+
+
+
event.event.ticket.view.kanban.from.event
event.event.ticket
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- reserved + unconfirmed
-
-
-
-
-
+
+
+
+
+
+
+
-
- event.event.ticket.view.form
+
+ event.event.ticket.view.form.inherit.sale
event.event.ticket
+
-
+
+
+
+
+
+
+
-
+
+
diff --git a/addons/event_sale/views/event_views.xml b/addons/event_sale/views/event_views.xml
index e18cbb1ae3b..5d846504792 100644
--- a/addons/event_sale/views/event_views.xml
+++ b/addons/event_sale/views/event_views.xml
@@ -1,33 +1,5 @@
-
- event.type.view.form.inherit.sale
- event.type
-
-
-
-
-
event.form.inherit
@@ -49,15 +21,7 @@
+
diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py
index 15c83004bb2..dfb66f7f834 100644
--- a/addons/website_event/controllers/main.py
+++ b/addons/website_event/controllers/main.py
@@ -254,25 +254,62 @@ class WebsiteEventController(http.Controller):
"url": event.website_url})
return request.env['ir.ui.view'].render_template("website_event.country_events_list", result)
- def _process_tickets_details(self, data):
- nb_register = int(data.get('nb_register-0', 0))
- if nb_register:
- return [{'id': 0, 'name': 'Registration', 'quantity': nb_register, 'price': 0}]
- return []
+ def _process_tickets_form(self, event, form_details):
+ """ Process posted data about ticket order. Generic ticket are supported
+ for event without tickets (generic registration).
+
+ :return: list of order per ticket: [{
+ 'id': if of ticket if any (0 if no ticket),
+ 'ticket': browse record of ticket if any (None if no ticket),
+ 'name': ticket name (or generic 'Registration' name if no ticket),
+ 'quantity': number of registrations for that ticket,
+ }, {...}]
+ """
+ ticket_order = {}
+ for key, value in form_details.items():
+ registration_items = key.split('nb_register-')
+ if len(registration_items) != 2:
+ continue
+ ticket_order[int(registration_items[1])] = int(value)
+
+ ticket_dict = dict((ticket.id, ticket) for ticket in request.env['event.event.ticket'].search([
+ ('id', 'in', [tid for tid in ticket_order.keys() if tid]),
+ ('event_id', '=', event.id)
+ ]))
+
+ return [{
+ 'id': tid if ticket_dict.get(tid) else 0,
+ 'ticket': ticket_dict.get(tid),
+ 'name': ticket_dict[tid]['name'] if ticket_dict.get(tid) else _('Registration'),
+ 'quantity': count,
+ } for tid, count in ticket_order.items() if count]
@http.route(['/event/