[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:
Thibault Delavallée
2020-01-10 16:06:40 +00:00
parent 59e5c8630f
commit abcd6c15cf
25 changed files with 658 additions and 621 deletions
+2 -1
View File
@@ -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
View File
@@ -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
+202
View File
@@ -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': []}
-1
View File
@@ -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.')
+1 -1
View File
@@ -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)
+4 -2
View File
@@ -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
-321
View File
@@ -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
+98
View File
@@ -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
+144
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
]
@@ -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')