[MOV] (website_)event(_sale/_question/_track): split main model files
PURPOSE As event will soon evolve (onchange -> compute, code improvements, addition of new features) cleaning and improving tests is necessary to help avoid issues. SPECIFICATIONS As event model grow in complexity and features, it is easier to find its way through the application with having registration model lying in its own file to separate it from event-specific models (event.type, event.event). Ticket (event_sale) and sponsor (website_event_track) models are also extracted in their own file. LINKS LINKS Side effect of Task ID 2089156 (event onchange to compute) Community PR odoo/odoo#43127 Enterprise PR odoo/enterprise#7656
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event
|
||||
from . import event_event
|
||||
from . import event_mail
|
||||
from . import event_registration
|
||||
from . import event_stage
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import pytz
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import format_datetime
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.translate import html_translate
|
||||
from odoo.tools.misc import _format_time_ago
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
@@ -395,196 +394,3 @@ class EventEvent(models.Model):
|
||||
|
||||
result[event.id] = cal.serialize().encode('utf-8')
|
||||
return result
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
_name = 'event.registration'
|
||||
_description = 'Event Registration'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'id desc'
|
||||
|
||||
# event
|
||||
origin = fields.Char(
|
||||
string='Source Document', readonly=True,
|
||||
help="Reference of the document that created the registration, for example a sales order")
|
||||
event_id = fields.Many2one(
|
||||
'event.event', string='Event', required=True,
|
||||
readonly=True, states={'draft': [('readonly', False)]})
|
||||
# attendee
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Contact',
|
||||
states={'done': [('readonly', True)]})
|
||||
name = fields.Char(string='Attendee Name', index=True)
|
||||
email = fields.Char(string='Email')
|
||||
phone = fields.Char(string='Phone')
|
||||
mobile = fields.Char(string='Mobile')
|
||||
# organization
|
||||
date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now()) # weird crash is directly now
|
||||
date_closed = fields.Datetime(string='Attended Date', readonly=True)
|
||||
event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True)
|
||||
event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', related='event_id.company_id',
|
||||
store=True, readonly=True, states={'draft': [('readonly', False)]})
|
||||
state = fields.Selection([
|
||||
('draft', 'Unconfirmed'), ('cancel', 'Cancelled'),
|
||||
('open', 'Confirmed'), ('done', 'Attended')],
|
||||
string='Status', default='draft', readonly=True, copy=False, tracking=True)
|
||||
|
||||
@api.constrains('event_id', 'state')
|
||||
def _check_seats_limit(self):
|
||||
for registration in self:
|
||||
if registration.event_id.seats_availability == 'limited' and registration.event_id.seats_max and registration.event_id.seats_available < (1 if registration.state == 'draft' else 0):
|
||||
raise ValidationError(_('No more seats available for this event.'))
|
||||
|
||||
def _check_auto_confirmation(self):
|
||||
if self._context.get('registration_force_draft'):
|
||||
return False
|
||||
if any(not registration.event_id.auto_confirm or
|
||||
(not registration.event_id.seats_available and registration.event_id.seats_availability == 'limited') for registration in self):
|
||||
return False
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
registration = super(EventRegistration, self).create(vals)
|
||||
if registration._check_auto_confirmation():
|
||||
registration.sudo().confirm_registration()
|
||||
return registration
|
||||
|
||||
@api.model
|
||||
def _prepare_attendee_values(self, registration):
|
||||
""" Method preparing the values to create new attendees based on a
|
||||
sales order line. It takes some registration data (dict-based) that are
|
||||
optional values coming from an external input like a web page. This method
|
||||
is meant to be inherited in various addons that sell events. """
|
||||
partner_id = registration.pop('partner_id', self.env.user.partner_id)
|
||||
event_id = registration.pop('event_id', False)
|
||||
data = {
|
||||
'name': registration.get('name', partner_id.name),
|
||||
'phone': registration.get('phone', partner_id.phone),
|
||||
'mobile': registration.get('mobile', partner_id.mobile),
|
||||
'email': registration.get('email', partner_id.email),
|
||||
'partner_id': partner_id.id,
|
||||
'event_id': event_id and event_id.id or False,
|
||||
}
|
||||
data.update({key: value for key, value in registration.items() if key in self._fields})
|
||||
return data
|
||||
|
||||
def do_draft(self):
|
||||
self.write({'state': 'draft'})
|
||||
|
||||
def confirm_registration(self):
|
||||
self.write({'state': 'open'})
|
||||
|
||||
# auto-trigger after_sub (on subscribe) mail schedulers, if needed
|
||||
onsubscribe_schedulers = self.event_id.event_mail_ids.filtered(
|
||||
lambda s: s.interval_type == 'after_sub')
|
||||
onsubscribe_schedulers.execute()
|
||||
|
||||
def button_reg_close(self):
|
||||
""" Close Registration """
|
||||
for registration in self:
|
||||
registration.write({'state': 'done', 'date_closed': fields.Datetime.now()})
|
||||
|
||||
def button_reg_cancel(self):
|
||||
self.write({'state': 'cancel'})
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner(self):
|
||||
if self.partner_id:
|
||||
contact_id = self.partner_id.address_get().get('contact', False)
|
||||
if contact_id:
|
||||
contact = self.env['res.partner'].browse(contact_id)
|
||||
self.name = contact.name or self.name
|
||||
self.email = contact.email or self.email
|
||||
self.phone = contact.phone or self.phone
|
||||
self.mobile = contact.mobile or self.mobile
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(EventRegistration, self)._message_get_suggested_recipients()
|
||||
public_users = self.env['res.users'].sudo()
|
||||
public_groups = self.env.ref("base.group_public", raise_if_not_found=False)
|
||||
if public_groups:
|
||||
public_users = public_groups.sudo().with_context(active_test=False).mapped("users")
|
||||
try:
|
||||
for attendee in self:
|
||||
is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False
|
||||
if attendee.partner_id and not is_public:
|
||||
attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer'))
|
||||
elif attendee.email:
|
||||
attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email'))
|
||||
except AccessError: # no read access rights -> ignore suggested recipients
|
||||
pass
|
||||
return recipients
|
||||
|
||||
def _message_get_default_recipients(self):
|
||||
# Prioritize registration email over partner_id, which may be shared when a single
|
||||
# partner booked multiple seats
|
||||
return {r.id: {
|
||||
'partner_ids': [],
|
||||
'email_to': r.email,
|
||||
'email_cc': False}
|
||||
for r in self}
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if self.email and not self.partner_id:
|
||||
# we consider that posting a message with a specified recipient (not a follower, a specific one)
|
||||
# on a document without customer means that it was created through the chatter using
|
||||
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
|
||||
new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email)
|
||||
if new_partner:
|
||||
self.search([
|
||||
('partner_id', '=', False),
|
||||
('email', '=', new_partner.email),
|
||||
('state', 'not in', ['cancel']),
|
||||
]).write({'partner_id': new_partner.id})
|
||||
return super(EventRegistration, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def action_send_badge_email(self):
|
||||
""" Open a window to compose an email, with the template - 'event_badge'
|
||||
message loaded by default
|
||||
"""
|
||||
self.ensure_one()
|
||||
template = self.env.ref('event.event_registration_mail_template_badge')
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
|
||||
ctx = dict(
|
||||
default_model='event.registration',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template.id,
|
||||
default_composition_mode='comment',
|
||||
custom_layout="mail.mail_notification_light",
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
'views': [(compose_form.id, 'form')],
|
||||
'view_id': compose_form.id,
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def get_date_range_str(self):
|
||||
self.ensure_one()
|
||||
today = fields.Datetime.now()
|
||||
event_date = self.event_begin_date
|
||||
diff = (event_date.date() - today.date())
|
||||
if diff.days <= 0:
|
||||
return _('today')
|
||||
elif diff.days == 1:
|
||||
return _('tomorrow')
|
||||
elif (diff.days < 7):
|
||||
return _('in %d days') % (diff.days, )
|
||||
elif (diff.days < 14):
|
||||
return _('next week')
|
||||
elif event_date.month == (today + relativedelta(months=+1)).month:
|
||||
return _('next month')
|
||||
else:
|
||||
return _('on ') + format_datetime(self.env, self.event_begin_date, tz=self.event_id.date_tz, dt_format='medium')
|
||||
|
||||
def summary(self):
|
||||
self.ensure_one()
|
||||
return {'information': []}
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import format_datetime
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
_name = 'event.registration'
|
||||
_description = 'Event Registration'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'id desc'
|
||||
|
||||
# event
|
||||
origin = fields.Char(
|
||||
string='Source Document', readonly=True,
|
||||
help="Reference of the document that created the registration, for example a sales order")
|
||||
event_id = fields.Many2one(
|
||||
'event.event', string='Event', required=True,
|
||||
readonly=True, states={'draft': [('readonly', False)]})
|
||||
# attendee
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Contact',
|
||||
states={'done': [('readonly', True)]})
|
||||
name = fields.Char(string='Attendee Name', index=True)
|
||||
email = fields.Char(string='Email')
|
||||
phone = fields.Char(string='Phone')
|
||||
mobile = fields.Char(string='Mobile')
|
||||
# organization
|
||||
date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now()) # weird crash is directly now
|
||||
date_closed = fields.Datetime(string='Attended Date', readonly=True)
|
||||
event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True)
|
||||
event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', related='event_id.company_id',
|
||||
store=True, readonly=True, states={'draft': [('readonly', False)]})
|
||||
state = fields.Selection([
|
||||
('draft', 'Unconfirmed'), ('cancel', 'Cancelled'),
|
||||
('open', 'Confirmed'), ('done', 'Attended')],
|
||||
string='Status', default='draft', readonly=True, copy=False, tracking=True)
|
||||
|
||||
@api.constrains('event_id', 'state')
|
||||
def _check_seats_limit(self):
|
||||
for registration in self:
|
||||
if registration.event_id.seats_availability == 'limited' and registration.event_id.seats_max and registration.event_id.seats_available < (1 if registration.state == 'draft' else 0):
|
||||
raise ValidationError(_('No more seats available for this event.'))
|
||||
|
||||
def _check_auto_confirmation(self):
|
||||
if self._context.get('registration_force_draft'):
|
||||
return False
|
||||
if any(not registration.event_id.auto_confirm or
|
||||
(not registration.event_id.seats_available and registration.event_id.seats_availability == 'limited') for registration in self):
|
||||
return False
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
registration = super(EventRegistration, self).create(vals)
|
||||
if registration._check_auto_confirmation():
|
||||
registration.sudo().confirm_registration()
|
||||
return registration
|
||||
|
||||
@api.model
|
||||
def _prepare_attendee_values(self, registration):
|
||||
""" Method preparing the values to create new attendees based on a
|
||||
sales order line. It takes some registration data (dict-based) that are
|
||||
optional values coming from an external input like a web page. This method
|
||||
is meant to be inherited in various addons that sell events. """
|
||||
partner_id = registration.pop('partner_id', self.env.user.partner_id)
|
||||
event_id = registration.pop('event_id', False)
|
||||
data = {
|
||||
'name': registration.get('name', partner_id.name),
|
||||
'phone': registration.get('phone', partner_id.phone),
|
||||
'mobile': registration.get('mobile', partner_id.mobile),
|
||||
'email': registration.get('email', partner_id.email),
|
||||
'partner_id': partner_id.id,
|
||||
'event_id': event_id and event_id.id or False,
|
||||
}
|
||||
data.update({key: value for key, value in registration.items() if key in self._fields})
|
||||
return data
|
||||
|
||||
def do_draft(self):
|
||||
self.write({'state': 'draft'})
|
||||
|
||||
def confirm_registration(self):
|
||||
self.write({'state': 'open'})
|
||||
|
||||
# auto-trigger after_sub (on subscribe) mail schedulers, if needed
|
||||
onsubscribe_schedulers = self.event_id.event_mail_ids.filtered(
|
||||
lambda s: s.interval_type == 'after_sub')
|
||||
onsubscribe_schedulers.execute()
|
||||
|
||||
def button_reg_close(self):
|
||||
""" Close Registration """
|
||||
for registration in self:
|
||||
registration.write({'state': 'done', 'date_closed': fields.Datetime.now()})
|
||||
|
||||
def button_reg_cancel(self):
|
||||
self.write({'state': 'cancel'})
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner(self):
|
||||
if self.partner_id:
|
||||
contact_id = self.partner_id.address_get().get('contact', False)
|
||||
if contact_id:
|
||||
contact = self.env['res.partner'].browse(contact_id)
|
||||
self.name = contact.name or self.name
|
||||
self.email = contact.email or self.email
|
||||
self.phone = contact.phone or self.phone
|
||||
self.mobile = contact.mobile or self.mobile
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(EventRegistration, self)._message_get_suggested_recipients()
|
||||
public_users = self.env['res.users'].sudo()
|
||||
public_groups = self.env.ref("base.group_public", raise_if_not_found=False)
|
||||
if public_groups:
|
||||
public_users = public_groups.sudo().with_context(active_test=False).mapped("users")
|
||||
try:
|
||||
for attendee in self:
|
||||
is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False
|
||||
if attendee.partner_id and not is_public:
|
||||
attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer'))
|
||||
elif attendee.email:
|
||||
attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email'))
|
||||
except AccessError: # no read access rights -> ignore suggested recipients
|
||||
pass
|
||||
return recipients
|
||||
|
||||
def _message_get_default_recipients(self):
|
||||
# Prioritize registration email over partner_id, which may be shared when a single
|
||||
# partner booked multiple seats
|
||||
return {r.id: {
|
||||
'partner_ids': [],
|
||||
'email_to': r.email,
|
||||
'email_cc': False}
|
||||
for r in self}
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if self.email and not self.partner_id:
|
||||
# we consider that posting a message with a specified recipient (not a follower, a specific one)
|
||||
# on a document without customer means that it was created through the chatter using
|
||||
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
|
||||
new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email)
|
||||
if new_partner:
|
||||
self.search([
|
||||
('partner_id', '=', False),
|
||||
('email', '=', new_partner.email),
|
||||
('state', 'not in', ['cancel']),
|
||||
]).write({'partner_id': new_partner.id})
|
||||
return super(EventRegistration, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def action_send_badge_email(self):
|
||||
""" Open a window to compose an email, with the template - 'event_badge'
|
||||
message loaded by default
|
||||
"""
|
||||
self.ensure_one()
|
||||
template = self.env.ref('event.event_registration_mail_template_badge')
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
|
||||
ctx = dict(
|
||||
default_model='event.registration',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template.id,
|
||||
default_composition_mode='comment',
|
||||
custom_layout="mail.mail_notification_light",
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
'views': [(compose_form.id, 'form')],
|
||||
'view_id': compose_form.id,
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def get_date_range_str(self):
|
||||
self.ensure_one()
|
||||
today = fields.Datetime.now()
|
||||
event_date = self.event_begin_date
|
||||
diff = (event_date.date() - today.date())
|
||||
if diff.days <= 0:
|
||||
return _('today')
|
||||
elif diff.days == 1:
|
||||
return _('tomorrow')
|
||||
elif (diff.days < 7):
|
||||
return _('in %d days') % (diff.days, )
|
||||
elif (diff.days < 14):
|
||||
return _('next week')
|
||||
elif event_date.month == (today + relativedelta(months=+1)).month:
|
||||
return _('next month')
|
||||
else:
|
||||
return _('on ') + format_datetime(self.env, self.event_begin_date, tz=self.event_id.date_tz, dt_format='medium')
|
||||
|
||||
def summary(self):
|
||||
self.ensure_one()
|
||||
return {'information': []}
|
||||
@@ -16,7 +16,6 @@ class EventStage(models.Model):
|
||||
pipe_end = fields.Boolean(
|
||||
string='End Stage', default=False,
|
||||
help='Events will be automatically be moved into this stage their are passed and event moved in the stage will be automatically greened')
|
||||
|
||||
legend_blocked = fields.Char(
|
||||
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, required=True,
|
||||
help='Override the default value displayed for the blocked state for kanban selection.')
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestEventData(TestEventCommon):
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_date_computation(self):
|
||||
self.patcher = patch('odoo.addons.event.models.event.fields.Datetime', wraps=FieldsDatetime)
|
||||
self.patcher = patch('odoo.addons.event.models.event_event.fields.Datetime', wraps=FieldsDatetime)
|
||||
self.mock_datetime = self.patcher.start()
|
||||
self.mock_datetime.now.return_value = datetime(2020, 1, 31, 8, 0, 0)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import account_invoice
|
||||
from . import account_move
|
||||
from . import event_event
|
||||
from . import event_registration
|
||||
from . import event_ticket
|
||||
from . import sale_order
|
||||
from . import product
|
||||
from . import event
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
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')
|
||||
|
||||
sale_total_price = fields.Monetary(compute='_compute_sale_total_price')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', string='Currency',
|
||||
default=lambda self: self.env.company.currency_id.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('sale_order_lines_ids')
|
||||
def _compute_sale_total_price(self):
|
||||
for event in self:
|
||||
event.sale_total_price = sum([
|
||||
event.currency_id._convert(
|
||||
sale_order_line_id.price_reduce_taxexcl,
|
||||
sale_order_line_id.currency_id,
|
||||
sale_order_line_id.company_id,
|
||||
sale_order_line_id.order_id.date_order)
|
||||
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()
|
||||
|
||||
@api.depends('sale_order_lines_ids')
|
||||
def _compute_sale_total_price(self):
|
||||
for event in self:
|
||||
event.sale_total_price = sum([
|
||||
event.currency_id._convert(
|
||||
sale_order_line_id.price_reduce_taxexcl,
|
||||
sale_order_line_id.currency_id,
|
||||
sale_order_line_id.company_id,
|
||||
sale_order_line_id.order_id.date_order)
|
||||
for sale_order_line_id in event.sale_order_lines_ids
|
||||
])
|
||||
|
||||
|
||||
class EventTicket(models.Model):
|
||||
_name = 'event.event.ticket'
|
||||
_description = 'Event 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_id = fields.Many2one('product.product', string='Product',
|
||||
required=True, domain=[("event_ok", "=", True)],
|
||||
default=_default_product_id)
|
||||
registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
|
||||
price = fields.Float(string='Price', digits='Product Price')
|
||||
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')
|
||||
|
||||
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')
|
||||
# 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
|
||||
|
||||
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):
|
||||
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.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.'))
|
||||
|
||||
|
||||
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
|
||||
sale_order_id = fields.Many2one('sale.order', string='Source Sales Order', ondelete='cascade')
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade')
|
||||
campaign_id = fields.Many2one('utm.campaign', 'Campaign', related="sale_order_id.campaign_id", store=True)
|
||||
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:
|
||||
orders = self.env['sale.order'].search([('state', '=', 'draft'), ('id', 'in', self.mapped('sale_order_id').ids)], limit=1)
|
||||
if orders:
|
||||
res = False
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(EventRegistration, self).create(vals)
|
||||
if res.origin or res.sale_order_id:
|
||||
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 summary(self):
|
||||
res = super(EventRegistration, self).summary()
|
||||
if self.event_ticket_id.product_id.image_128:
|
||||
res['image'] = '/web/image/product.product/%s/image_128' % self.event_ticket_id.product_id.id
|
||||
information = res.setdefault('information', {})
|
||||
information.append((_('Name'), self.name))
|
||||
information.append((_('Ticket'), self.event_ticket_id.name or _('None')))
|
||||
order = self.sale_order_id.sudo()
|
||||
order_line = self.sale_order_line_id.sudo()
|
||||
if not order or float_is_zero(order_line.price_total, precision_digits=order.currency_id.rounding):
|
||||
payment_status = _('Free')
|
||||
elif not order.invoice_ids or any(invoice.state != 'paid' for invoice in order.invoice_ids):
|
||||
payment_status = _('To pay')
|
||||
res['alert'] = _('The registration must be paid')
|
||||
else:
|
||||
payment_status = _('Paid')
|
||||
information.append((_('Payment'), payment_status))
|
||||
return res
|
||||
@@ -0,0 +1,98 @@
|
||||
# -*- 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
|
||||
})
|
||||
|
||||
|
||||
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')
|
||||
|
||||
sale_total_price = fields.Monetary(compute='_compute_sale_total_price')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', string='Currency',
|
||||
default=lambda self: self.env.company.currency_id.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('sale_order_lines_ids')
|
||||
def _compute_sale_total_price(self):
|
||||
for event in self:
|
||||
event.sale_total_price = sum([
|
||||
event.currency_id._convert(
|
||||
sale_order_line_id.price_reduce_taxexcl,
|
||||
sale_order_line_id.currency_id,
|
||||
sale_order_line_id.company_id,
|
||||
sale_order_line_id.order_id.date_order)
|
||||
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()
|
||||
|
||||
@api.depends('sale_order_lines_ids')
|
||||
def _compute_sale_total_price(self):
|
||||
for event in self:
|
||||
event.sale_total_price = sum([
|
||||
event.currency_id._convert(
|
||||
sale_order_line_id.price_reduce_taxexcl,
|
||||
sale_order_line_id.currency_id,
|
||||
sale_order_line_id.company_id,
|
||||
sale_order_line_id.order_id.date_order)
|
||||
for sale_order_line_id in event.sale_order_lines_ids
|
||||
])
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- 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
|
||||
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
|
||||
sale_order_id = fields.Many2one('sale.order', string='Source Sales Order', ondelete='cascade')
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade')
|
||||
campaign_id = fields.Many2one('utm.campaign', 'Campaign', related="sale_order_id.campaign_id", store=True)
|
||||
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:
|
||||
orders = self.env['sale.order'].search([('state', '=', 'draft'), ('id', 'in', self.mapped('sale_order_id').ids)], limit=1)
|
||||
if orders:
|
||||
res = False
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(EventRegistration, self).create(vals)
|
||||
if res.origin or res.sale_order_id:
|
||||
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 summary(self):
|
||||
res = super(EventRegistration, self).summary()
|
||||
if self.event_ticket_id.product_id.image_128:
|
||||
res['image'] = '/web/image/product.product/%s/image_128' % self.event_ticket_id.product_id.id
|
||||
information = res.setdefault('information', {})
|
||||
information.append((_('Name'), self.name))
|
||||
information.append((_('Ticket'), self.event_ticket_id.name or _('None')))
|
||||
order = self.sale_order_id.sudo()
|
||||
order_line = self.sale_order_line_id.sudo()
|
||||
if not order or float_is_zero(order_line.price_total, precision_digits=order.currency_id.rounding):
|
||||
payment_status = _('Free')
|
||||
elif not order.invoice_ids or any(invoice.state != 'paid' for invoice in order.invoice_ids):
|
||||
payment_status = _('To pay')
|
||||
res['alert'] = _('The registration must be paid')
|
||||
else:
|
||||
payment_status = _('Paid')
|
||||
information.append((_('Payment'), payment_status))
|
||||
return res
|
||||
@@ -0,0 +1,144 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class EventTicket(models.Model):
|
||||
_name = 'event.event.ticket'
|
||||
_description = 'Event 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_id = fields.Many2one('product.product', string='Product',
|
||||
required=True, domain=[("event_ok", "=", True)],
|
||||
default=_default_product_id)
|
||||
registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
|
||||
price = fields.Float(string='Price', digits='Product Price')
|
||||
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')
|
||||
|
||||
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')
|
||||
# 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
|
||||
|
||||
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):
|
||||
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.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.'))
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event
|
||||
from . import event_mail
|
||||
from . import event_registration
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import event
|
||||
from . import event_event
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import event
|
||||
from . import event_event
|
||||
from . import event_question
|
||||
from . import event_registration
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- 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'
|
||||
|
||||
use_questions = fields.Boolean('Questions to Attendees')
|
||||
question_ids = fields.One2many(
|
||||
'event.question', 'event_type_id',
|
||||
string='Questions', copy=True)
|
||||
|
||||
|
||||
class EventEvent(models.Model):
|
||||
""" Override Event model to add optional questions when buying tickets. """
|
||||
_inherit = 'event.event'
|
||||
|
||||
question_ids = fields.One2many('event.question', 'event_id', 'Questions', copy=True)
|
||||
general_question_ids = fields.One2many('event.question', 'event_id', 'General Questions',
|
||||
domain=[('is_individual', '=', False)])
|
||||
specific_question_ids = fields.One2many('event.question', 'event_id', 'Specific Questions',
|
||||
domain=[('is_individual', '=', True)])
|
||||
|
||||
@api.onchange('event_type_id')
|
||||
def _onchange_type(self):
|
||||
super(EventEvent, self)._onchange_type()
|
||||
if self.event_type_id.use_questions and self.event_type_id.question_ids:
|
||||
self.question_ids = [(5, 0, 0)] + [
|
||||
(0, 0, {
|
||||
'title': question.title,
|
||||
'sequence': question.sequence,
|
||||
'is_individual': question.is_individual,
|
||||
})
|
||||
for question in self.event_type_id.question_ids
|
||||
]
|
||||
-54
@@ -5,60 +5,6 @@ from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class EventType(models.Model):
|
||||
_inherit = 'event.type'
|
||||
|
||||
use_questions = fields.Boolean('Questions to Attendees')
|
||||
question_ids = fields.One2many(
|
||||
'event.question', 'event_type_id',
|
||||
string='Questions', copy=True)
|
||||
|
||||
|
||||
class EventEvent(models.Model):
|
||||
""" Override Event model to add optional questions when buying tickets. """
|
||||
_inherit = 'event.event'
|
||||
|
||||
question_ids = fields.One2many('event.question', 'event_id', 'Questions', copy=True)
|
||||
general_question_ids = fields.One2many('event.question', 'event_id', 'General Questions',
|
||||
domain=[('is_individual', '=', False)])
|
||||
specific_question_ids = fields.One2many('event.question', 'event_id', 'Specific Questions',
|
||||
domain=[('is_individual', '=', True)])
|
||||
|
||||
@api.onchange('event_type_id')
|
||||
def _onchange_type(self):
|
||||
super(EventEvent, self)._onchange_type()
|
||||
if self.event_type_id.use_questions and self.event_type_id.question_ids:
|
||||
self.question_ids = [(5, 0, 0)] + [
|
||||
(0, 0, {
|
||||
'title': question.title,
|
||||
'sequence': question.sequence,
|
||||
'is_individual': question.is_individual,
|
||||
})
|
||||
for question in self.event_type_id.question_ids
|
||||
]
|
||||
|
||||
|
||||
class EventRegistrationAnswer(models.Model):
|
||||
''' This m2m table has to be explicitly instanciated as we need unique ids
|
||||
in the reporting view event.question.report.
|
||||
|
||||
This model is purely technical. '''
|
||||
|
||||
_name = 'event.registration.answer'
|
||||
_table = 'event_registration_answer'
|
||||
_description = 'Event Registration Answer'
|
||||
|
||||
event_answer_id = fields.Many2one('event.answer', required=True, ondelete='cascade')
|
||||
event_registration_id = fields.Many2one('event.registration', required=True, ondelete='cascade')
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
""" Store answers on attendees. """
|
||||
_inherit = 'event.registration'
|
||||
|
||||
answer_ids = fields.Many2many('event.answer', 'event_registration_answer', string='Answers')
|
||||
|
||||
|
||||
class EventQuestion(models.Model):
|
||||
_name = 'event.question'
|
||||
_rec_name = 'title'
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class EventRegistrationAnswer(models.Model):
|
||||
''' This m2m table has to be explicitly instanciated as we need unique ids
|
||||
in the reporting view event.question.report.
|
||||
|
||||
This model is purely technical. '''
|
||||
|
||||
_name = 'event.registration.answer'
|
||||
_table = 'event_registration_answer'
|
||||
_description = 'Event Registration Answer'
|
||||
|
||||
event_answer_id = fields.Many2one('event.answer', required=True, ondelete='cascade')
|
||||
event_registration_id = fields.Many2one('event.registration', required=True, ondelete='cascade')
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
""" Store answers on attendees. """
|
||||
_inherit = 'event.registration'
|
||||
|
||||
answer_ids = fields.Many2many('event.answer', 'event_registration_answer', string='Answers')
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event_event
|
||||
from . import event_sponsor
|
||||
from . import event_track
|
||||
from . import event
|
||||
from . import event_track_stage
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SponsorType(models.Model):
|
||||
_name = "event.sponsor.type"
|
||||
_description = 'Event Sponsor Type'
|
||||
_order = "sequence"
|
||||
|
||||
name = fields.Char('Sponsor Type', required=True, translate=True)
|
||||
sequence = fields.Integer('Sequence')
|
||||
|
||||
|
||||
class Sponsor(models.Model):
|
||||
_name = "event.sponsor"
|
||||
_description = 'Event Sponsor'
|
||||
_order = "sequence"
|
||||
|
||||
event_id = fields.Many2one('event.event', 'Event', required=True)
|
||||
sponsor_type_id = fields.Many2one('event.sponsor.type', 'Sponsoring Type', required=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Sponsor/Customer', required=True)
|
||||
url = fields.Char('Sponsor Website')
|
||||
sequence = fields.Integer('Sequence', store=True, related='sponsor_type_id.sequence', readonly=False)
|
||||
image_128 = fields.Image(string="Logo", related='partner_id.image_128', store=True, readonly=False)
|
||||
@@ -28,25 +28,6 @@ class TrackLocation(models.Model):
|
||||
name = fields.Char('Location', required=True)
|
||||
|
||||
|
||||
class TrackStage(models.Model):
|
||||
_name = 'event.track.stage'
|
||||
_description = 'Event Track Stage'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(string='Stage Name', required=True, translate=True)
|
||||
sequence = fields.Integer(string='Sequence', default=1)
|
||||
mail_template_id = fields.Many2one(
|
||||
'mail.template', string='Email Template',
|
||||
domain=[('model', '=', 'event.track')],
|
||||
help="If set an email will be sent to the customer when the track reaches this step.")
|
||||
fold = fields.Boolean(
|
||||
string='Folded in Kanban',
|
||||
help='This stage is folded in the kanban view when there are no records in that stage to display.')
|
||||
is_done = fields.Boolean(string='Accepted Stage')
|
||||
is_cancel = fields.Boolean(string='Canceled Stage')
|
||||
color = fields.Integer(string='Color')
|
||||
|
||||
|
||||
class Track(models.Model):
|
||||
_name = "event.track"
|
||||
_description = 'Event Track'
|
||||
@@ -194,25 +175,3 @@ class Track(models.Model):
|
||||
'view_id': False,
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
|
||||
|
||||
class SponsorType(models.Model):
|
||||
_name = "event.sponsor.type"
|
||||
_description = 'Event Sponsor Type'
|
||||
_order = "sequence"
|
||||
|
||||
name = fields.Char('Sponsor Type', required=True, translate=True)
|
||||
sequence = fields.Integer('Sequence')
|
||||
|
||||
|
||||
class Sponsor(models.Model):
|
||||
_name = "event.sponsor"
|
||||
_description = 'Event Sponsor'
|
||||
_order = "sequence"
|
||||
|
||||
event_id = fields.Many2one('event.event', 'Event', required=True)
|
||||
sponsor_type_id = fields.Many2one('event.sponsor.type', 'Sponsoring Type', required=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Sponsor/Customer', required=True)
|
||||
url = fields.Char('Sponsor Website')
|
||||
sequence = fields.Integer('Sequence', store=True, related='sponsor_type_id.sequence', readonly=False)
|
||||
image_128 = fields.Image(string="Logo", related='partner_id.image_128', store=True, readonly=False)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class TrackStage(models.Model):
|
||||
_name = 'event.track.stage'
|
||||
_description = 'Event Track Stage'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(string='Stage Name', required=True, translate=True)
|
||||
sequence = fields.Integer(string='Sequence', default=1)
|
||||
mail_template_id = fields.Many2one(
|
||||
'mail.template', string='Email Template',
|
||||
domain=[('model', '=', 'event.track')],
|
||||
help="If set an email will be sent to the customer when the track reaches this step.")
|
||||
fold = fields.Boolean(
|
||||
string='Folded in Kanban',
|
||||
help='This stage is folded in the kanban view when there are no records in that stage to display.')
|
||||
is_done = fields.Boolean(string='Accepted Stage')
|
||||
is_cancel = fields.Boolean(string='Canceled Stage')
|
||||
color = fields.Integer(string='Color')
|
||||
Reference in New Issue
Block a user