From 412ff994f1120457c912aad487265e3de8a9f35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Debauche=20St=C3=A9phane?= <std@odoo.com> Date: Wed, 23 Oct 2019 11:04:15 +0000 Subject: [PATCH] [IMP] event: replace state by kanban stage Purpose ======= Remove the state on the event and add a stage. So, we can have more control on the event flow, and we will have less constrains. Website event ============= Before ------ People can register for an event if "seats are available" and if the state is "confirmed". After ----- People can register for an event if `event_registrations_open` is True, - Event: seats are available and the event is not finished - Event sale: One or more ticket has `sale_available` set to True Task #2088538 --- addons/event/__manifest__.py | 1 + addons/event/controllers/main.py | 2 +- addons/event/data/event_data.xml | 31 ++++++ addons/event/data/event_demo.xml | 14 +-- addons/event/models/__init__.py | 1 + addons/event/models/event.py | 100 ++++++++++-------- addons/event/models/event_mail.py | 18 ++-- addons/event/models/event_stage.py | 28 +++++ addons/event/security/ir.model.access.csv | 2 + addons/event/static/src/scss/event.scss | 12 +++ addons/event/tests/test_event_flow.py | 11 -- addons/event/views/event_stage_views.xml | 61 +++++++++++ addons/event/views/event_views.xml | 34 +++--- addons/event_sale/data/event_demo.xml | 10 +- addons/event_sale/models/event.py | 78 +++++++++++--- addons/event_sale/tests/test_event_sale.py | 38 ++++--- addons/event_sale/views/event_views.xml | 7 +- .../mass_mailing_event/views/event_views.xml | 2 +- .../views/event_views.xml | 2 +- addons/website_event/controllers/main.py | 8 +- .../website_event/views/event_templates.xml | 35 ++---- addons/website_event/views/event_views.xml | 8 +- addons/website_event_sale/controllers/main.py | 2 +- addons/website_event_sale/tests/test_ui.py | 5 +- .../views/event_templates.xml | 40 ++++--- 25 files changed, 365 insertions(+), 185 deletions(-) create mode 100644 addons/event/models/event_stage.py create mode 100644 addons/event/views/event_stage_views.xml diff --git a/addons/event/__manifest__.py b/addons/event/__manifest__.py index ad7ff430dcc0..07f59b562703 100644 --- a/addons/event/__manifest__.py +++ b/addons/event/__manifest__.py @@ -23,6 +23,7 @@ Key Features 'security/ir.model.access.csv', 'wizard/event_confirm_view.xml', 'views/event_views.xml', + 'views/event_stage_views.xml', 'report/event_event_templates.xml', 'report/event_event_reports.xml', 'data/email_template_data.xml', diff --git a/addons/event/controllers/main.py b/addons/event/controllers/main.py index 723d30aaa510..509d486bcadf 100644 --- a/addons/event/controllers/main.py +++ b/addons/event/controllers/main.py @@ -8,7 +8,7 @@ from odoo.http import Controller, request, route, content_disposition class EventController(Controller): - @route(['''/event/<model("event.event", "[('state', 'in', ('confirm', 'done'))]"):event>/ics'''], type='http', auth="public") + @route(['''/event/<model("event.event"):event>/ics'''], type='http', auth="public") def event_ics_file(self, event, **kwargs): files = event._get_ics_file() if not event.id in files: diff --git a/addons/event/data/event_data.xml b/addons/event/data/event_data.xml index d93512bea690..19b153f2621d 100644 --- a/addons/event/data/event_data.xml +++ b/addons/event/data/event_data.xml @@ -27,5 +27,36 @@ <field name="auto_confirm" eval="True"/> <field name="use_mail_schedule" eval="False"/> </record> + + <!-- Event stages --> + <record id="event_stage_new" model="event.stage"> + <field name="name">New</field> + <field name="description">Freshly created</field> + <field name="sequence">1</field> + </record> + <record id="event_stage_booked" model="event.stage"> + <field name="name">Booked</field> + <field name="description">The place has been reserved</field> + <field name="sequence">2</field> + </record> + <record id="event_stage_announced" model="event.stage"> + <field name="name">Announced</field> + <field name="description">The event has been publicly announced</field> + <field name="sequence">3</field> + </record> + <record id="event_stage_done" model="event.stage"> + <field name="name">Ended</field> + <field name="description">Fully ended</field> + <field name="sequence">5</field> + <field name="pipe_end" eval="True"/> + <field name="fold" eval="True"/> + </record> + <record id="event_stage_cancelled" model="event.stage"> + <field name="name">Cancelled</field> + <field name="description">The event has been cancelled</field> + <field name="sequence">6</field> + <field name="pipe_end" eval="True"/> + <field name="fold" eval="True"/> + </record> </data> </odoo> diff --git a/addons/event/data/event_demo.xml b/addons/event/data/event_demo.xml index f9f877e1e9a6..c05a1db0771a 100644 --- a/addons/event/data/event_demo.xml +++ b/addons/event/data/event_demo.xml @@ -40,6 +40,7 @@ <field name="seats_max">500</field> <field name="address_id" ref="base.res_partner_1"/> <field name="event_type_id" ref="event_type_1"/> + <field name="stage_id" ref="event_stage_booked"/> </record> <record id="event.event_1" model="event.event"> @@ -50,6 +51,7 @@ <field name="event_type_id" ref="event_type_1"/> <field name="address_id" ref="base.res_partner_1"/> <field name="seats_availability">unlimited</field> + <field name="stage_id" ref="event_stage_booked"/> </record> <record id="event_2" model="event.event"> @@ -61,6 +63,7 @@ <field name="address_id" ref="base.res_partner_4"/> <field name="seats_availability">limited</field> <field name="seats_max">200</field> + <field name="stage_id" ref="event_stage_booked"/> </record> <record id="event.event_3" model="event.event"> @@ -71,6 +74,7 @@ <field name="event_type_id" ref="event_type_3"/> <field name="address_id" ref="base.res_partner_3"/> <field name="seats_availability">unlimited</field> + <field name="stage_id" ref="event_stage_announced"/> </record> <record id="event.event_4" model="event.event"> @@ -80,6 +84,7 @@ <field eval="(DateTime.today()+ timedelta(days=50)).strftime('%Y-%m-%d 22:30:00')" name="date_end"/> <field name="event_type_id" ref="event_type_4"/> <field name="address_id" ref="base.res_partner_4"/> + <field name="stage_id" ref="event_stage_announced"/> </record> <record id="event.event_5" model="event.event"> @@ -100,14 +105,6 @@ <field name="address_id" ref="base.res_partner_1"/> </record> - <function model="event.event" name="button_confirm" eval="[ref('event_0')]"/> - <function model="event.event" name="button_confirm" eval="[ref('event_1')]"/> - <function model="event.event" name="button_confirm" eval="[ref('event_2')]"/> - <function model="event.event" name="button_confirm" eval="[ref('event_3')]"/> - <function model="event.event" name="button_confirm" eval="[ref('event_4')]"/> - <function model="event.event" name="button_confirm" eval="[ref('event_5')]"/> - <function model="event.event" name="button_confirm" eval="[ref('event_6')]"/> - <!-- Attendee --> <record id="reg_2_0" model="event.registration"> <field name="name">Camptocamp</field> @@ -116,6 +113,5 @@ <field name="event_id" ref="event_2"/> <field name="partner_id" ref="base.res_partner_12"/> </record> - </data> </odoo> diff --git a/addons/event/models/__init__.py b/addons/event/models/__init__.py index eed59489b268..4d521dddd92d 100644 --- a/addons/event/models/__init__.py +++ b/addons/event/models/__init__.py @@ -3,5 +3,6 @@ from . import event from . import event_mail +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.py index 2fa05466c0ec..42a0c7f4eb99 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -95,19 +95,23 @@ class EventEvent(models.Model): _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_begin' + def _get_default_stage_id(self): + event_stages = self.env['event.stage'].search([]) + return event_stages[0] if event_stages else False + name = fields.Char( string='Event', translate=True, required=True, - readonly=False, states={'done': [('readonly', True)]}) + readonly=False) active = fields.Boolean(default=True) user_id = fields.Many2one( 'res.users', string='Responsible', default=lambda self: self.env.user, tracking=True, - readonly=False, states={'done': [('readonly', True)]}) + readonly=False) company_id = fields.Many2one( 'res.company', string='Company', change_default=True, default=lambda self: self.env.company, - required=False, readonly=False, states={'done': [('readonly', True)]}) + required=False, readonly=False) organizer_id = fields.Many2one( 'res.partner', string='Organizer', tracking=True, @@ -115,14 +119,22 @@ class EventEvent(models.Model): domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") event_type_id = fields.Many2one( 'event.type', string='Category', - readonly=False, states={'done': [('readonly', True)]}) + readonly=False) color = fields.Integer('Kanban Color Index') event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', copy=True) - + # Kanban fields + kanban_state = fields.Selection([('normal', 'In Progress'), ('done', 'Done'), ('blocked', 'Blocked')]) + kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True, store=True) + stage_id = fields.Many2one( + 'event.stage', ondelete='restrict', default=_get_default_stage_id, + group_expand='_read_group_stage_ids', tracking=True) + legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True) + legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True) + legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True) # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', - readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, + readonly=True, help="For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted.") seats_availability = fields.Selection( [('limited', 'Limited'), ('unlimited', 'Unlimited')], @@ -145,41 +157,35 @@ class EventEvent(models.Model): seats_expected = fields.Integer( string='Number of Expected Attendees', compute_sudo=True, readonly=True, compute='_compute_seats') - # Registration fields registration_ids = fields.One2many( 'event.registration', 'event_id', string='Attendees', - readonly=False, states={'done': [('readonly', True)]}) + readonly=False) + event_registrations_open = fields.Boolean('Registration open', compute='_compute_event_registrations_open') # Date fields date_tz = fields.Selection('_tz_get', string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC') date_begin = fields.Datetime( string='Start Date', required=True, - tracking=True, states={'done': [('readonly', True)]}) + tracking=True) date_end = fields.Datetime( string='End Date', required=True, - tracking=True, states={'done': [('readonly', True)]}) + tracking=True) date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') is_one_day = fields.Boolean(compute='_compute_field_is_one_day') - - state = fields.Selection([ - ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), - ('confirm', 'Confirmed'), ('done', 'Done')], - string='Status', default='draft', readonly=True, required=True, copy=False, - help="If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'.") auto_confirm = fields.Boolean(string='Autoconfirm Registrations') is_online = fields.Boolean('Online Event') address_id = fields.Many2one( 'res.partner', string='Location', default=lambda self: self.env.company.partner_id, - readonly=False, states={'done': [('readonly', True)]}, + readonly=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True, readonly=False) twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html( string='Description', translate=html_translate, sanitize_attributes=False, - readonly=False, states={'done': [('readonly', True)]}) + readonly=False) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') @@ -216,6 +222,21 @@ class EventEvent(models.Model): event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used) event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used + @api.depends('date_end', 'seats_available', 'seats_availability') + def _compute_event_registrations_open(self): + for event in self: + event.event_registrations_open = event.date_end > fields.Datetime.now() and (event.seats_available or event.seats_availability == 'unlimited') + + @api.depends('stage_id', 'kanban_state') + def _compute_kanban_state_label(self): + for event in self: + if event.kanban_state == 'normal': + event.kanban_state_label = event.stage_id.legend_normal + elif event.kanban_state == 'blocked': + event.kanban_state_label = event.stage_id.legend_blocked + else: + event.kanban_state_label = event.stage_id.legend_done + @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones] @@ -307,13 +328,15 @@ class EventEvent(models.Model): result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates)))) return result + @api.model + def _read_group_stage_ids(self, stages, domain, order): + return self.env['event.stage'].search([]) + @api.model def create(self, vals): res = super(EventEvent, self).create(vals) if res.organizer_id: res.message_subscribe([res.organizer_id.id]) - if res.auto_confirm: - res.button_confirm() return res def write(self, vals): @@ -328,29 +351,21 @@ class EventEvent(models.Model): default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) - def button_draft(self): - self.write({'state': 'draft'}) - - def button_cancel(self): - if any('done' in event.mapped('registration_ids.state') for event in self): - raise UserError(_("There are already attendees who attended this event. Please reset it to draft if you want to cancel this event.")) - self.registration_ids.write({'state': 'cancel'}) - self.state = 'cancel' - - def button_done(self): - self.write({'state': 'done'}) - - def button_confirm(self): - self.write({'state': 'confirm'}) + def action_set_done(self): + """ + Action which will move the events + into the first next (by sequence) stage defined as "Ended" + (if they are not already in an ended stage) + """ + first_ended_stage = self.env['event.stage'].search([('pipe_end', '=', True)], order='sequence') + if first_ended_stage: + self.write({'stage_id': first_ended_stage[0].id}) def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for event in self: for attendee in event.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) - def _is_event_registrable(self): - return self.date_end > fields.Datetime.now() - def _get_ics_file(self): """ Returns iCalendar file for the event invitation. :returns a dict of .ics file content for each event @@ -417,8 +432,7 @@ class EventRegistration(models.Model): def _check_auto_confirmation(self): if self._context.get('registration_force_draft'): return False - if any(registration.event_id.state != 'confirm' or - not registration.event_id.auto_confirm or + 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 @@ -463,13 +477,7 @@ class EventRegistration(models.Model): def button_reg_close(self): """ Close Registration """ for registration in self: - today = fields.Datetime.now() - if registration.event_id.date_begin <= today and registration.event_id.state == 'confirm': - registration.write({'state': 'done', 'date_closed': today}) - elif registration.event_id.state == 'draft': - raise UserError(_("You must wait the event confirmation before doing this action.")) - else: - raise UserError(_("You must wait the event starting day before doing this action.")) + registration.write({'state': 'done', 'date_closed': fields.Datetime.now()}) def button_reg_cancel(self): self.write({'state': 'cancel'}) diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 43ae07976aff..fd53adbb0bc2 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -92,19 +92,17 @@ class EventMailScheduler(models.Model): else: mail.done = len(mail.mail_registration_ids) == len(mail.event_id.registration_ids) and all(mail.mail_sent for mail in mail.mail_registration_ids) - @api.depends('event_id.state', 'event_id.date_begin', 'interval_type', 'interval_unit', 'interval_nbr') + @api.depends('event_id.date_begin', 'interval_type', 'interval_unit', 'interval_nbr') def _compute_scheduled_date(self): for mail in self: - if mail.event_id.state not in ['confirm', 'done']: - mail.scheduled_date = False + if mail.interval_type == 'after_sub': + date, sign = mail.event_id.create_date, 1 + elif mail.interval_type == 'before_event': + date, sign = mail.event_id.date_begin, -1 else: - if mail.interval_type == 'after_sub': - date, sign = mail.event_id.create_date, 1 - elif mail.interval_type == 'before_event': - date, sign = mail.event_id.date_begin, -1 - else: - date, sign = mail.event_id.date_end, 1 - mail.scheduled_date = date + _INTERVALS[mail.interval_unit](sign * mail.interval_nbr) + date, sign = mail.event_id.date_end, 1 + + mail.scheduled_date = date + _INTERVALS[mail.interval_unit](sign * mail.interval_nbr) if date else False def execute(self): for mail in self: diff --git a/addons/event/models/event_stage.py b/addons/event/models/event_stage.py new file mode 100644 index 000000000000..d4064e3f9d4a --- /dev/null +++ b/addons/event/models/event_stage.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, fields, models + + +class EventStage(models.Model): + _name = 'event.stage' + _description = 'Event Stage' + _order = 'sequence, name' + + name = fields.Char(string='Stage Name', required=True, translate=True) + description = fields.Text(string='Stage description', translate=True) + sequence = fields.Integer('Sequence', default=1) + fold = fields.Boolean(string='Folded in Kanban', default=False) + 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.') + legend_done = fields.Char( + 'Green Kanban Label', default=lambda s: _('Ready for Next Stage'), translate=True, required=True, + help='Override the default value displayed for the done state for kanban selection.') + legend_normal = fields.Char( + 'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, required=True, + help='Override the default value displayed for the normal state for kanban selection.') diff --git a/addons/event/security/ir.model.access.csv b/addons/event/security/ir.model.access.csv index 8a49f01f1911..d00f94350cc2 100644 --- a/addons/event/security/ir.model.access.csv +++ b/addons/event/security/ir.model.access.csv @@ -13,3 +13,5 @@ access_event_mail_registration,event.mail.registration,model_event_mail_registra access_event_mail_registration_manager,event.mail.registration.manager,model_event_mail_registration,event.group_event_manager,1,1,1,1 access_event_type_mail_event_user,event.type.mail.event.user,model_event_type_mail,event.group_event_user,1,0,0,0 access_event_type_mail_event_manager,event.type.mail.event.manager,model_event_type_mail,event.group_event_manager,1,1,1,1 +access_event_stage_user,event.stage.user,model_event_stage,event.group_event_user,1,1,1,1 +access_event_stage_manager,event.stage.manager,model_event_stage,event.group_event_manager,1,1,1,1 diff --git a/addons/event/static/src/scss/event.scss b/addons/event/static/src/scss/event.scss index 563f70b6e211..c1ad84c10d89 100644 --- a/addons/event/static/src/scss/event.scss +++ b/addons/event/static/src/scss/event.scss @@ -23,5 +23,17 @@ padding: 8px; margin-left: 8px; } + + .o_event_bottom_right { + position: absolute; + right: 8px; + bottom: 8px; + + img { + width: 20px; + height: 20px; + object-fit: cover; + } + } } } diff --git a/addons/event/tests/test_event_flow.py b/addons/event/tests/test_event_flow.py index 8121528596b2..d57dd62189d2 100644 --- a/addons/event/tests/test_event_flow.py +++ b/addons/event/tests/test_event_flow.py @@ -27,7 +27,6 @@ class TestEventFlow(TestEventCommon): 'seats_max': 2, 'seats_availability': 'limited', }) - self.assertEqual(test_event.state, 'confirm', 'Event: auto_confirmation of event failed') # EventUser create registrations for this event test_reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({ @@ -58,13 +57,6 @@ class TestEventFlow(TestEventCommon): self.assertEqual(test_reg1.state, 'done', 'Event: wrong state of attended registration') self.assertEqual(test_event.seats_used, 2, 'Event: incorrect number of attendees after closing registration') - # EventUser closes the event - test_event.button_done() - - # EventUser cancels -> not possible when having attendees - with self.assertRaises(UserError): - test_event.button_cancel() - @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_10_advanced_event_flow(self): """ Avanced event flow: no auto confirmation, manage minimum / maximum @@ -76,9 +68,6 @@ class TestEventFlow(TestEventCommon): 'date_end': datetime.datetime.now() + relativedelta(days=1), 'seats_max': 10, }) - self.assertEqual( - test_event.state, 'draft', - 'Event: new event should be in draft state, no auto confirmation') # EventUser create registrations for this event -> no auto confirmation test_reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({ diff --git a/addons/event/views/event_stage_views.xml b/addons/event/views/event_stage_views.xml new file mode 100644 index 000000000000..bc28b5d74ad0 --- /dev/null +++ b/addons/event/views/event_stage_views.xml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<odoo> +<data> + <record id="event_stage_view_form" model="ir.ui.view"> + <field name="name">event.stage.view.form</field> + <field name="model">event.stage</field> + <field name="arch" type="xml"> + <form string="Events Stage"> + <sheet> + <group> + <group> + <field name="name"/> + <field name="pipe_end"/> + </group> + <group> + <field name="fold"/> + <field name="sequence"/> + </group> + </group> + <group string="Stage Description and Tooltips"> + <p class="text-muted" colspan="2"> + You can define here labels that will be displayed for the state instead + of the default labels in the kanban view. + </p> + <label for="legend_normal" string=" " class="o_status " title="Task in progress. Click to block or set as done." aria-label="Task in progress. Click to block or set as done." role="img"/> + <field name="legend_normal" nolabel="1"/> + <label for="legend_blocked" string=" " class="o_status o_status_red" title="Task is blocked. Click to unblock or set as done." aria-label="Task is blocked. Click to unblock or set as done." role="img"/> + <field name="legend_blocked" nolabel="1"/> + <label for="legend_done" string=" " class="o_status o_status_green" title="This step is done. Click to block or set in progress." aria-label="This step is done. Click to block or set in progress." role="img"/> + <field name="legend_done" nolabel="1"/> + + <p class="text-muted" colspan="2"> + You can also add a description to help your coworkers understand the meaning and purpose of the stage. + </p> + <field name="description" placeholder="Add a description..." nolabel="1" colspan="2"/> + </group> + </sheet> + </form> + </field> + </record> + + <record id="event_stage_view_tree" model="ir.ui.view"> + <field name="name">event.stage.view.tree</field> + <field name="model">event.stage</field> + <field name="arch" type="xml"> + <tree string="Events Stage"> + <field name="sequence" widget="handle"/> + <field name="name"/> + </tree> + </field> + </record> + + <record id="event_stage_action" model="ir.actions.act_window"> + <field name="name">Event Stages</field> + <field name="res_model">event.stage</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem id="event_stage_menu" name="Event Stages" action="event_stage_action" parent="menu_event_configuration"/> +</data> +</odoo> diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index 29be162d7b2c..8d10bc382654 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -188,12 +188,7 @@ <field name="arch" type="xml"> <form string="Events"> <header> - <button string="Confirm Event" name="button_confirm" states="draft" type="object" class="oe_highlight" groups="base.group_user"/> - <button string="Finish Event" name="button_done" states="confirm" type="object" class="oe_highlight" groups="base.group_user"/> - <button string="Set To Draft" name="button_draft" states="cancel,done" type="object" groups="base.group_user"/> - <button string="Cancel Event" name="button_cancel" states="draft,confirm" type="object" groups="base.group_user" attrs="{'invisible': ['|', ('seats_expected', '!=', 0)]}"/> - <button string="Cancel Event" name="button_cancel" states="draft,confirm" type="object" groups="base.group_user" confirm="Are you sure you want to cancel this event? All the linked attendees will be cancelled as well." attrs="{'invisible': ['|', ('seats_expected', '=', 0)]}"/> - <field name="state" widget="statusbar" statusbar_visible="draft,confirm,done"/> + <field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/> </header> <sheet> <div class="oe_button_box" name="button_box" groups="base.group_user"> @@ -205,7 +200,11 @@ <field name="seats_expected" widget="statinfo" string="Attendees"/> </button> </div> - <widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/> + <field name="legend_blocked" invisible="1"/> + <field name="legend_normal" invisible="1"/> + <field name="legend_done" invisible="1"/> + <widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/> + <field name="kanban_state" widget="state_selection" class="ml-auto"/> <div class="oe_title"> <label for="name" class="oe_edit_only"/> <h1><field name="name" placeholder="Event Name"/></h1> @@ -282,7 +281,7 @@ <field name="name">event.event.tree</field> <field name="model">event.event</field> <field name="arch" type="xml"> - <tree string="Events" decoration-bf="message_needaction==True" decoration-danger="(seats_min and seats_min>seats_reserved) or (seats_max and seats_max<seats_reserved)" decoration-muted="state=='cancel'"> + <tree string="Events" decoration-bf="message_needaction==True" decoration-danger="(seats_min and seats_min>seats_reserved) or (seats_max and seats_max<seats_reserved)"> <field name="name"/> <field name="event_type_id"/> <field name="date_begin"/> @@ -291,7 +290,6 @@ <field name="seats_min"/> <field name="seats_max" invisible="1"/> <field name="user_id"/> - <field name="state"/> <field name="message_needaction" invisible="1"/> <field name="company_id" groups="base.group_multi_company"/> <field name="activity_exception_decoration" widget="activity_exception"/> @@ -303,8 +301,9 @@ <field name="name">event.event.kanban</field> <field name="model">event.event</field> <field name="arch" type="xml"> - <kanban class="o_event_kanban_view"> + <kanban class="o_event_kanban_view" default_group_by="stage_id"> <field name="user_id"/> + <field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/> <field name="country_id"/> <field name="date_begin"/> <field name="date_end"/> @@ -313,6 +312,9 @@ <field name="seats_used"/> <field name="seats_expected"/> <field name="color"/> + <field name="legend_blocked"/> + <field name="legend_normal"/> + <field name="legend_done"/> <templates> <t t-name="kanban-box"> <div t-attf-class="{{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click"> @@ -354,6 +356,10 @@ </t> </h4> </div> + <div class="o_event_bottom_right"> + <field name="kanban_state" widget="state_selection" groups="base.group_user"/> + <img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value"/> + </div> </div> </t> </templates> @@ -368,7 +374,6 @@ <field name="arch" type="xml"> <calendar date_start="date_begin" date_stop="date_end" string="Event Organization" mode="month" color="event_type_id" event_limit="5"> <field name="user_id" avatar_field="image_128"/> - <field name="state"/> <field name="seats_expected"/> <field name="seats_reserved"/> <field name="seats_used"/> @@ -385,14 +390,13 @@ <field name="name" string="Event"/> <field name="event_type_id"/> <field name="user_id"/> + <field name="stage_id"/> <filter string="My Events" name="myevents" help="My Events" domain="[('user_id', '=', uid)]"/> <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/> <separator/> - <filter string="Confirmed" name="confirm" domain="[('state', '=', 'confirm')]" help="Confirmed events"/> - <filter string="Unconfirmed" name="draft" domain="[('state', '=', 'draft')]" help="Events in New state"/> <separator/> <filter string="Upcoming/Running" name="upcoming" - domain="['&', ('state', '!=', 'cancel'), ('date_end', '>=', datetime.datetime.combine(context_today(), datetime.time(0,0,0)))]" help="Upcoming events from today" /> + domain="[('date_end', '>=', datetime.datetime.combine(context_today(), datetime.time(0,0,0)))]" help="Upcoming events from today" /> <filter string="Online Events" name="online_events" help="Online Events" domain="[('is_online', '=', True)]"/> <separator/> <filter string="Start Date" name="start_date" date="date_begin"/> @@ -409,7 +413,7 @@ <group expand="0" string="Group By"> <filter string="Responsible" name="responsible" context="{'group_by': 'user_id'}"/> <filter string="Event Category" name="event_type_id" context="{'group_by': 'event_type_id'}"/> - <filter string="Status" name="status" context="{'group_by': 'state'}"/> + <filter string="Stage" name="stage_id" context="{'group_by': 'stage_id'}"/> <filter string="Start Date" name="date_begin" domain="[]" context="{'group_by': 'date_begin'}"/> </group> </search> diff --git a/addons/event_sale/data/event_demo.xml b/addons/event_sale/data/event_demo.xml index 28ef85b1bd8d..6ce4d91dfd61 100644 --- a/addons/event_sale/data/event_demo.xml +++ b/addons/event_sale/data/event_demo.xml @@ -5,7 +5,7 @@ <field name="name">Standard</field> <field name="event_id" ref="event.event_0"/> <field name="product_id" ref="product_product_event"/> - <field name="deadline" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d')"/> + <field name="end_sale_date" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d')"/> <field name="price">1000.0</field> <field name="seats_max">50</field> </record> @@ -14,7 +14,7 @@ <field name="name">VIP</field> <field name="event_id" ref="event.event_0"/> <field name="product_id" ref="product_product_event"/> - <field name="deadline" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d')"/> + <field name="end_sale_date" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d')"/> <field name="price">1500.0</field> <field name="seats_max">10</field> </record> @@ -23,7 +23,7 @@ <field name="name">General Admission</field> <field name="event_id" ref="event.event_4"/> <field name="product_id" ref="product_product_event"/> - <field name="deadline" eval="(DateTime.today() + timedelta(30)).strftime('%Y-%m-%d')"/> + <field name="end_sale_date" eval="(DateTime.today() + timedelta(30)).strftime('%Y-%m-%d')"/> <field name="price">99.0</field> <field name="seats_max">100</field> </record> @@ -32,7 +32,7 @@ <field name="name">Standard</field> <field name="event_id" ref="event.event_2"/> <field name="product_id" ref="product_product_event"/> - <field name="deadline" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d')"/> + <field name="end_sale_date" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d')"/> <field name="price">1000.0</field> <field name="seats_max">50</field> </record> @@ -41,7 +41,7 @@ <field name="name">VIP</field> <field name="event_id" ref="event.event_2"/> <field name="product_id" ref="product_product_event"/> - <field name="deadline" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d')"/> + <field name="end_sale_date" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d')"/> <field name="price">1500.0</field> <field name="seats_max">5</field> </record> diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index ad629a8f7e6c..79643ad7e503 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -1,6 +1,8 @@ # -*- 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 @@ -41,6 +43,17 @@ class Event(models.Model): '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') + + sales_total_price = fields.Monetary(compute='_compute_sales_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() @@ -53,12 +66,29 @@ class Event(models.Model): }) for ticket in self.event_type_id.event_ticket_ids] - def _is_event_registrable(self): - if super(Event, self)._is_event_registrable(): - self.ensure_one() - return all(self.event_ticket_ids.with_context(active_test=False).mapped(lambda t: t.product_id.active)) - else: - return False + @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_sales_total_price(self): + for event in self: + event.sales_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() class EventTicket(models.Model): @@ -77,8 +107,10 @@ class EventTicket(models.Model): default=_default_product_id) registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations') price = fields.Float(string='Price', digits='Product Price') - deadline = fields.Date(string="Sales End") + 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') @@ -94,12 +126,25 @@ class EventTicket(models.Model): seats_used = fields.Integer(compute='_compute_seats', store=True) def _compute_is_expired(self): - for record in self: - if record.deadline: - current_date = fields.Date.context_today(record.with_context(tz=record.event_id.date_tz)) - record.is_expired = record.deadline < current_date + 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: - record.is_expired = False + 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: @@ -178,6 +223,12 @@ class EventTicket(models.Model): 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' @@ -228,9 +279,8 @@ class EventRegistration(models.Model): 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: + if line_id and line_id.event_ticket_id.sale_available: att_data.update({ - 'event_id': line_id.event_id.id, 'event_id': line_id.event_id.id, 'event_ticket_id': line_id.event_ticket_id.id, 'origin': line_id.order_id.name, diff --git a/addons/event_sale/tests/test_event_sale.py b/addons/event_sale/tests/test_event_sale.py index 87a0e0cbfdca..b42ae3063194 100644 --- a/addons/event_sale/tests/test_event_sale.py +++ b/addons/event_sale/tests/test_event_sale.py @@ -71,32 +71,40 @@ class EventSaleTest(common.TransactionCase): self.assertTrue(registrations, "The registration is not created.") def test_event_is_registrable(self): - self.patcher = patch('odoo.addons.event.models.event.fields.Datetime', wraps=Datetime) - self.mock_datetime = self.patcher.start() - + """Test if `_compute_event_registrations_open` works properly.""" test_event = self.env['event.event'].create({ 'name': 'TestEvent', - 'date_begin': datetime.datetime(2019, 6, 8, 12, 0), - 'date_end': datetime.datetime(2019, 6, 12, 12, 0), + 'date_begin': datetime.datetime.now() - datetime.timedelta(days=5), + 'date_end': datetime.datetime.now() + datetime.timedelta(days=5), + }) + test_event_ticket = self.env['event.event.ticket'].create({ + 'name': 'TestTicket', + 'event_id': test_event.id, + 'product_id': self.env['product.product'].search([], limit=1).id, }) - self.mock_datetime.now.return_value = datetime.datetime(2019, 6, 9, 12, 0) - self.assertEqual(test_event._is_event_registrable(), True) + self.assertEqual(test_event.event_registrations_open, True) - self.mock_datetime.now.return_value = datetime.datetime(2019, 6, 13, 12, 0) - self.assertEqual(test_event._is_event_registrable(), False) + test_event.write({ + 'date_begin': datetime.datetime.now() - datetime.timedelta(days=5), + 'date_end': datetime.datetime.now() - datetime.timedelta(days=1), + }) + + test_event.date_end = datetime.datetime.now() - datetime.timedelta(days=1) + self.assertEqual(test_event.event_registrations_open, False) + + test_event.write({ + 'date_begin': datetime.datetime.now() - datetime.timedelta(days=1), + 'date_end': datetime.datetime.now() + datetime.timedelta(days=5), + }) - self.mock_datetime.now.return_value = datetime.datetime(2019, 6, 10, 12, 0) test_event.write({'event_ticket_ids': [(6, 0, [])]}) - self.assertEqual(test_event._is_event_registrable(), True) + self.assertEqual(test_event.event_registrations_open, False, 'cannot register if no tickets') test_event_ticket = self.env['event.event.ticket'].create({ 'name': 'TestTicket', 'event_id': test_event.id, 'product_id': self.env['product.product'].search([], limit=1).id, }) - test_event_ticket.copy() test_event_ticket.product_id.active = False - self.assertEqual(test_event._is_event_registrable(), False) - - self.patcher.stop() + self.assertEqual(test_event.event_registrations_open, False, 'cannot register if product linked to the tickets are all archived') diff --git a/addons/event_sale/views/event_views.xml b/addons/event_sale/views/event_views.xml index c9227b6ddf22..8cc7b9e2421f 100644 --- a/addons/event_sale/views/event_views.xml +++ b/addons/event_sale/views/event_views.xml @@ -94,7 +94,8 @@ <tree string="Tickets" editable="bottom"> <field name="name"/> <field name="product_id" context="{'default_event_ok':1}"/> - <field name="deadline"/> + <field name="start_sale_date"/> + <field name="end_sale_date"/> <field name="price"/> <field name="seats_max"/> <field name="seats_reserved" readonly="1"/> @@ -129,7 +130,7 @@ <group> <field name="name"/> <field name="product_id" context="{'default_event_ok':1}"/> - <field name="deadline"/> + <field name="end_sale_date"/> <field name="price"/> </group><group> <field name="seats_max"/> @@ -158,7 +159,7 @@ <field name="event_id"/> <field name="seats_availability"/> <field name="seats_available"/> - <field name="deadline"/> + <field name="end_sale_date"/> <field name="price"/> <field name="price_reduce" groups="base.group_no_one"/> </group> diff --git a/addons/mass_mailing_event/views/event_views.xml b/addons/mass_mailing_event/views/event_views.xml index be489bf2ec99..0bc4a121e635 100644 --- a/addons/mass_mailing_event/views/event_views.xml +++ b/addons/mass_mailing_event/views/event_views.xml @@ -5,7 +5,7 @@ <field name="model">event.event</field> <field name="inherit_id" ref="event.view_event_form"/> <field name="arch" type="xml"> - <xpath expr="//button[@name='button_cancel']" position="after"> + <xpath expr="//header" position="inside"> <button name="action_mass_mailing_attendees" string="Contact Attendees" type="object" attrs="{'invisible': [('seats_expected', '=', 0)]}"/> </xpath> </field> diff --git a/addons/mass_mailing_event_track/views/event_views.xml b/addons/mass_mailing_event_track/views/event_views.xml index 73e3c326b9b8..65763e22c007 100644 --- a/addons/mass_mailing_event_track/views/event_views.xml +++ b/addons/mass_mailing_event_track/views/event_views.xml @@ -5,7 +5,7 @@ <field name="model">event.event</field> <field name="inherit_id" ref="website_event_track.view_event_form"/> <field name="arch" type="xml"> - <xpath expr="//button[@name='button_cancel']" position="after"> + <xpath expr="//header" position="inside"> <button name="action_mass_mailing_track_speakers" string="Contact Track Speakers" type="object" attrs="{'invisible': [('track_count', '=', 0)]}"/> </xpath> </field> diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index d5348aa7a4ae..7e22470639e0 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -92,7 +92,7 @@ class WebsiteEventController(http.Controller): domain_search["country"] = [("country_id", "=", False)] def dom_without(without): - domain = [('state', "in", ['draft', 'confirm', 'done'])] + domain = [] for key, search in domain_search.items(): if key != without: domain += search @@ -203,7 +203,7 @@ class WebsiteEventController(http.Controller): 'event': event, 'main_object': event, 'range': range, - 'registrable': event.sudo()._is_event_registrable(), + 'registrable': event.sudo().event_registrations_open, 'google_url': urls.get('google_url'), 'iCal_url': urls.get('iCal_url'), } @@ -242,9 +242,9 @@ class WebsiteEventController(http.Controller): domain = request.website.website_domain() if country_code: country = request.env['res.country'].search([('code', '=', country_code)], limit=1) - events = Event.search(domain + ['|', ('address_id', '=', None), ('country_id.code', '=', country_code), ('date_begin', '>=', '%s 00:00:00' % fields.Date.today()), ('state', '=', 'confirm')], order="date_begin") + events = Event.search(domain + ['|', ('address_id', '=', None), ('country_id.code', '=', country_code), ('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin") if not events: - events = Event.search(domain + [('date_begin', '>=', '%s 00:00:00' % fields.Date.today()), ('state', '=', 'confirm')], order="date_begin") + events = Event.search(domain + [('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin") for event in events: if country_code and event.country_id.code == country_code: result['country'] = country diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml index 5f99a5f337cb..d9676ff6a568 100644 --- a/addons/website_event/views/event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -522,39 +522,26 @@ <!-- Event - Registration --> <template id="registration_template"> - <t t-set="tickets_available" t-value="event.seats_available or event.seats_availability == 'unlimited'"/> - <t t-set="buy" t-value="tickets_available and event.state == 'confirm'"/> - <form t-if="buy" id="registration_form" t-attf-action="/event/#{slug(event)}/registration/new" method="post" itemscope="itemscope" itemprop="offers" itemtype="http://schema.org/AggregateOffer" class="mb-5"> + <form t-if="event.event_registrations_open" id="registration_form" t-attf-action="/event/#{slug(event)}/registration/new" method="post" itemscope="itemscope" itemprop="offers" itemtype="http://schema.org/AggregateOffer" class="mb-5"> <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> <div id="o_wevent_tickets" class="bg-white rounded shadow-sm"> + <t t-set="quantity"> + <select name="nb_register-0" class="custom-select w-auto"> + <t t-foreach="range(0, (event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available+1)" t-as="nb"> + <option t-esc="nb" t-att-selected="nb == 1 and 'selected'"/> + </t> + </select> + </t> <t t-id="tickets" t-call="website_event.ticket"> <t t-set="name">Registration</t> <t t-set="price"> <span content="0" itemprop="price" class="font-weight-bold text-uppercase">Free</span> </t> - <t t-if="event.date_begin"> - <t t-set="registration_end"> - End on <span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'date_only': 'true', 'format': 'medium'}"/> - </t> - </t> - <t t-set="quantity"> - <select name="nb_register-0" class="custom-select w-auto"> - <t t-foreach="range(0, (event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available+1)" t-as="nb"> - <option t-esc="nb" t-att-selected="nb == 1 and 'selected'"/> - </t> - </select> - </t> </t> </div> </form> - <div t-if="not buy" class="alert alert-info mb-5" role="status"> - <span t-if="event.state == 'draft'" itemprop="availability" content="http://schema.org/OutOfStock"> - <b>Event registration not yet started.</b> - </span> - <span t-if="event.state != 'draft'" itemprop="availability" content="http://schema.org/Discontinued"> - <b>Event registration is closed.</b> - </span> - <a t-if="request.env.user.has_group('event.group_event_manager')" t-attf-href="/web#id=#{event.id}&view_type=form&model=event.event" class="float-right"> + <div t-elif="request.env.user.has_group('event.group_event_manager')" class="alert alert-info mb-5 o_website_event_configuration" role="status"> + <a t-attf-href="/web#id=#{event.id}&view_type=form&model=event.event"> <i class="fa fa-pencil mr-2" role="img" aria-label="Create" title="Create"/><em>Configure Event Registration</em> </a> </div> @@ -567,7 +554,7 @@ <div class="border-left border-right px-3"><t t-raw="price"/></div> <small t-raw="registration_end" class="text-muted ml-3" itemprop="availabilityEnds"/> <div class="ml-auto"> - <t t-if="tickets_available"> + <t t-if="event.event_registrations_open"> <span class="font-weight-bold align-middle pr-2">Qty</span> <link itemprop="availability" content="http://schema.org/InStock"/> <t t-raw="quantity"/> diff --git a/addons/website_event/views/event_views.xml b/addons/website_event/views/event_views.xml index 46aa80e2dc8a..bdd70acb68eb 100644 --- a/addons/website_event/views/event_views.xml +++ b/addons/website_event/views/event_views.xml @@ -41,15 +41,9 @@ <field name="website_menu"/> <label for="website_menu" string="Website Menu"/> </label> - <xpath expr="//button[@name='button_done']" position="before"> + <xpath expr="//header" position="inside"> <button name="action_open_badge_editor" type="object" - states="confirm" - string="Preview Badges" - class="oe_highlight"/> - <button name="action_open_badge_editor" - type="object" - states="done" string="Preview Badges"/> </xpath> </field> diff --git a/addons/website_event_sale/controllers/main.py b/addons/website_event_sale/controllers/main.py index dc5370b9926a..9dbb339cde9e 100644 --- a/addons/website_event_sale/controllers/main.py +++ b/addons/website_event_sale/controllers/main.py @@ -62,7 +62,7 @@ class WebsiteEventSaleController(WebsiteEventController): context = dict(context or {}, default_event_ticket_ids=[[0, 0, { 'name': _('Registration'), 'product_id': product.id, - 'deadline': False, + 'end_sale_date': False, 'seats_max': 1000, 'price': 0, }]]) diff --git a/addons/website_event_sale/tests/test_ui.py b/addons/website_event_sale/tests/test_ui.py index c682a36a8610..60092d1bf096 100644 --- a/addons/website_event_sale/tests/test_ui.py +++ b/addons/website_event_sale/tests/test_ui.py @@ -23,13 +23,14 @@ class TestUi(HttpCaseWithUserDemo): 'name': 'Standard', 'event_id': self.event_2.id, 'product_id': self.env.ref('event_sale.product_product_event').id, - 'deadline': (Datetime.today() + timedelta(90)).strftime('%Y-%m-%d'), + 'start_sale_date': (Datetime.today() - timedelta(days=5)).strftime('%Y-%m-%d 07:00:00'), + 'end_sale_date': (Datetime.today() + timedelta(90)).strftime('%Y-%m-%d'), 'price': 1000.0, }, { 'name': 'VIP', 'event_id': self.event_2.id, 'product_id': self.env.ref('event_sale.product_product_event').id, - 'deadline': (Datetime.today() + timedelta(90)).strftime('%Y-%m-%d'), + 'end_sale_date': (Datetime.today() + timedelta(90)).strftime('%Y-%m-%d'), 'price': 1500.0, }]) diff --git a/addons/website_event_sale/views/event_templates.xml b/addons/website_event_sale/views/event_templates.xml index 0632490b4f16..6d7ece5bb121 100644 --- a/addons/website_event_sale/views/event_templates.xml +++ b/addons/website_event_sale/views/event_templates.xml @@ -9,7 +9,7 @@ <template id="index" inherit_id="website_event.events_list" name="Event's Ticket"> <xpath expr="//div[@t-foreach='event_ids']//footer" position="inside"> - <t t-if="event.state in ['draft', 'confirm'] and event.event_ticket_ids"> + <t t-if="event.event_registrations_open and event.event_ticket_ids"> <span t-if="event.seats_availability == 'limited' and not event.seats_available" class="text-danger">Sold Out</span> <span t-if="event.seats_availability == 'limited' and event.seats_available and event.seats_available <= ((event.seats_max or 0) / 4)" class="text-muted"> <em>Only <t t-esc="event.seats_available"/> Remaining</em> @@ -20,10 +20,10 @@ <template id="registration_template" inherit_id="website_event.registration_template"> <xpath expr="//t[@t-id='tickets']" position="replace"> - <t t-set="tickets" t-value="event.event_ticket_ids.filtered(lambda t: not t.is_expired)"/> + <t t-set="tickets" t-value="event.event_ticket_ids.filtered(lambda ticket: not ticket.is_expired)"/> <t t-if="len(event.event_ticket_ids) > 1"> <!-- If some tickets expired and there is only one type left, we keep the same layout --> <div class="d-flex align-items-center py-2 pl-3 pr-2 border-bottom"> - <span t-if="not tickets_available" class="text-danger"> + <span t-if="not event.event_registrations_open" class="text-danger"> <i class="fa fa-ban mr-2"/>Sold Out </span> <div class="ml-auto pr-3"> @@ -40,7 +40,8 @@ <div t-foreach="tickets" t-as="ticket" class="row mx-0 bg-light border-bottom"> <div class="col-md-8 py-3" itemscope="itemscope" itemtype="http://schema.org/Offer"> <h5 itemprop="name" t-field="ticket.name" class="my-0"/> - <small t-if="ticket.deadline" class="text-muted mr-3" itemprop="availabilityEnds">Sales end on <span itemprop="priceValidUntil" t-field="ticket.deadline"/></small> + <small t-if="ticket.end_sale_date and ticket.sale_available and not ticket.is_expired" class="text-muted mr-3" itemprop="availabilityEnds">Sales end on <span itemprop="priceValidUntil" t-field="ticket.end_sale_date"/></small> + <small t-if="ticket.start_sale_date and not ticket.sale_available and not ticket.is_expired" class="text-muted mr-3" itemprop="availabilityEnds">Sales start on <span itemprop="priceValidUntil" t-field="ticket.start_sale_date"/></small> </div> <div class="col-md-4 py-3 pl-md-0"> <div class="d-flex align-items-center"> @@ -54,14 +55,16 @@ <span itemprop="priceCurrency" class="d-none" t-esc="website.pricelist_id.currency_id.name"/> </t> <span t-if="not ticket.price and not editable" class="font-weight-bold text-uppercase">Free</span> - <select t-attf-name="nb_register-#{ticket.id}" class="w-auto ml-auto custom-select"> - <t t-set="seats_max_ticket" t-value="(ticket.seats_availability == 'unlimited' or ticket.seats_available > 9) and 10 or ticket.seats_available + 1"/> - <t t-set="seats_max_event" t-value="(event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available + 1"/> - <t t-set="seats_max" t-value="min(seats_max_ticket, seats_max_event)"/> - <t t-foreach="range(0, seats_max)" t-as="nb"> - <option t-esc="nb" t-att-selected="len(ticket) == 0 and nb == 0 and 'selected'"/> - </t> - </select> + <t t-if="not ticket.is_expired and ticket.sale_available"> + <select t-attf-name="nb_register-#{ticket.id}" class="w-auto ml-auto custom-select"> + <t t-set="seats_max_ticket" t-value="(ticket.seats_availability == 'unlimited' or ticket.seats_available > 9) and 10 or ticket.seats_available + 1"/> + <t t-set="seats_max_event" t-value="(event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available + 1"/> + <t t-set="seats_max" t-value="min(seats_max_ticket, seats_max_event)"/> + <t t-foreach="range(0, seats_max)" t-as="nb"> + <option t-esc="nb" t-att-selected="len(ticket) == 0 and nb == 0 and 'selected'"/> + </t> + </select> + </t> </div> </div> <div t-if="ticket.product_id.description_sale" class="col-12"> @@ -70,7 +73,7 @@ </div> <div class="row no-gutters"> <div class="col-md-4 offset-md-8 py-2 pl-md-0 pr-md-2"> - <button type="submit" class="btn btn-primary o_wait_lazy_js btn-block a-submit" t-att-disabled="not tickets_available or None" t-attf-id="#{event.id}">Register</button> + <button type="submit" class="btn btn-primary o_wait_lazy_js btn-block a-submit" t-att-disabled="not event.event_registrations_open or None" t-attf-id="#{event.id}">Register</button> </div> </div> </div> @@ -94,9 +97,9 @@ <span t-field="tickets.product_id.description_sale"/> </t> <t t-set="registration_end"> - <t t-if="tickets.deadline">Sales end on <span itemprop="priceValidUntil" t-field="tickets.deadline"/></t> + <t t-if="tickets.start_sale_date">Sales end on <span itemprop="priceValidUntil" t-field="tickets.end_sale_date"/></t> </t> - <t t-set="quantity"> + <t t-if="not tickets.is_expired and tickets.sale_available" t-set="quantity"> <select t-attf-name="nb_register-#{tickets.id}" class="w-auto custom-select"> <t t-set="seats_max_ticket" t-value="(tickets.seats_availability == 'unlimited' or tickets.seats_available > 9) and 10 or tickets.seats_available + 1"/> <t t-set="seats_max_event" t-value="(event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available + 1"/> @@ -110,7 +113,12 @@ </t> </xpath> <xpath expr="//form[@id='registration_form']" position="attributes"> - <attribute name="t-if">event.event_ticket_ids and not all([ticket.is_expired for ticket in event.event_ticket_ids])</attribute> + <attribute name="t-if">event.event_registrations_open and not all([ticket.is_expired for ticket in event.event_ticket_ids])</attribute> + </xpath> + <xpath expr="//form[@id='registration_form']" position="before"> + <div t-if="event.start_sale_date and event.start_sale_date > datetime.date.today() and not event.event_registrations_open" class="alert alert-info mb-5" role="status"> + <em >Ticket Sales starting on <t t-esc="event.start_sale_date"/></em> + </div> </xpath> </template> -- GitLab