diff --git a/addons/event/models/__init__.py b/addons/event/models/__init__.py index 750d7b5de79..727b34645d7 100644 --- a/addons/event/models/__init__.py +++ b/addons/event/models/__init__.py @@ -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 diff --git a/addons/event/models/event.py b/addons/event/models/event_event.py similarity index 66% rename from addons/event/models/event.py rename to addons/event/models/event_event.py index d49b79d369a..4f366871155 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event_event.py @@ -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': []} diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index fd53adbb0bc..4f2407609f9 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -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 diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py new file mode 100644 index 00000000000..8f940a2812e --- /dev/null +++ b/addons/event/models/event_registration.py @@ -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': []} diff --git a/addons/event/models/event_stage.py b/addons/event/models/event_stage.py index d4064e3f9d4..e985df62c68 100644 --- a/addons/event/models/event_stage.py +++ b/addons/event/models/event_stage.py @@ -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.') diff --git a/addons/event/tests/test_event_internals.py b/addons/event/tests/test_event_internals.py index 905222dcc5f..2a79086808a 100644 --- a/addons/event/tests/test_event_internals.py +++ b/addons/event/tests/test_event_internals.py @@ -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) diff --git a/addons/event_sale/models/__init__.py b/addons/event_sale/models/__init__.py index 0eba1dce03b..7228262e083 100644 --- a/addons/event_sale/models/__init__.py +++ b/addons/event_sale/models/__init__.py @@ -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 diff --git a/addons/event_sale/models/account_invoice.py b/addons/event_sale/models/account_move.py similarity index 100% rename from addons/event_sale/models/account_invoice.py rename to addons/event_sale/models/account_move.py diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py deleted file mode 100644 index 0f55cf89e37..00000000000 --- a/addons/event_sale/models/event.py +++ /dev/null @@ -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 diff --git a/addons/event_sale/models/event_event.py b/addons/event_sale/models/event_event.py new file mode 100644 index 00000000000..47e9febedb0 --- /dev/null +++ b/addons/event_sale/models/event_event.py @@ -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 + ]) diff --git a/addons/event_sale/models/event_registration.py b/addons/event_sale/models/event_registration.py new file mode 100644 index 00000000000..c67d4d77788 --- /dev/null +++ b/addons/event_sale/models/event_registration.py @@ -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 diff --git a/addons/event_sale/models/event_ticket.py b/addons/event_sale/models/event_ticket.py new file mode 100644 index 00000000000..f0622873da4 --- /dev/null +++ b/addons/event_sale/models/event_ticket.py @@ -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.')) diff --git a/addons/event_sms/models/__init__.py b/addons/event_sms/models/__init__.py index b8ced32a4d7..95c3bd06c29 100644 --- a/addons/event_sms/models/__init__.py +++ b/addons/event_sms/models/__init__.py @@ -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 diff --git a/addons/event_sms/models/event.py b/addons/event_sms/models/event_registration.py similarity index 100% rename from addons/event_sms/models/event.py rename to addons/event_sms/models/event_registration.py diff --git a/addons/website_event/models/__init__.py b/addons/website_event/models/__init__.py index 5004a8f8411..e972cdb2a6f 100644 --- a/addons/website_event/models/__init__.py +++ b/addons/website_event/models/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import event +from . import event_event diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event_event.py similarity index 100% rename from addons/website_event/models/event.py rename to addons/website_event/models/event_event.py diff --git a/addons/website_event_questions/models/__init__.py b/addons/website_event_questions/models/__init__.py index 5004a8f8411..14919447769 100644 --- a/addons/website_event_questions/models/__init__.py +++ b/addons/website_event_questions/models/__init__.py @@ -1,3 +1,5 @@ # -*- coding: utf-8 -*- -from . import event +from . import event_event +from . import event_question +from . import event_registration diff --git a/addons/website_event_questions/models/event_event.py b/addons/website_event_questions/models/event_event.py new file mode 100644 index 00000000000..c5c04f169f7 --- /dev/null +++ b/addons/website_event_questions/models/event_event.py @@ -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 + ] diff --git a/addons/website_event_questions/models/event.py b/addons/website_event_questions/models/event_question.py similarity index 51% rename from addons/website_event_questions/models/event.py rename to addons/website_event_questions/models/event_question.py index 43b33d1368f..f54fbdcc83b 100644 --- a/addons/website_event_questions/models/event.py +++ b/addons/website_event_questions/models/event_question.py @@ -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' diff --git a/addons/website_event_questions/models/event_registration.py b/addons/website_event_questions/models/event_registration.py new file mode 100644 index 00000000000..b8f23da99d7 --- /dev/null +++ b/addons/website_event_questions/models/event_registration.py @@ -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') diff --git a/addons/website_event_track/models/__init__.py b/addons/website_event_track/models/__init__.py index 48beb002677..1a9f0aa1fe0 100644 --- a/addons/website_event_track/models/__init__.py +++ b/addons/website_event_track/models/__init__.py @@ -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 diff --git a/addons/website_event_track/models/event.py b/addons/website_event_track/models/event_event.py similarity index 100% rename from addons/website_event_track/models/event.py rename to addons/website_event_track/models/event_event.py diff --git a/addons/website_event_track/models/event_sponsor.py b/addons/website_event_track/models/event_sponsor.py new file mode 100644 index 00000000000..53a587e1a2b --- /dev/null +++ b/addons/website_event_track/models/event_sponsor.py @@ -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) diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py index bbfdb93d369..7d7b20a72be 100644 --- a/addons/website_event_track/models/event_track.py +++ b/addons/website_event_track/models/event_track.py @@ -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) diff --git a/addons/website_event_track/models/event_track_stage.py b/addons/website_event_track/models/event_track_stage.py new file mode 100644 index 00000000000..c019c32536d --- /dev/null +++ b/addons/website_event_track/models/event_track_stage.py @@ -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')