diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index 42e55b83e1b..dc6ea19677d 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -135,7 +135,7 @@ class EventEvent(models.Model): string='Maximum Attendees Number', 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')], + [('unlimited', 'Unlimited'), ('limited', 'Limited')], 'Maximum Attendees', required=True, default='unlimited') seats_min = fields.Integer( string='Minimum Attendees', @@ -170,12 +170,13 @@ class EventEvent(models.Model): date_end = fields.Datetime(string='End Date', required=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_ongoing = fields.Boolean('Is Ongoing', compute='_compute_is_ongoing', search='_search_is_ongoing') is_one_day = fields.Boolean(compute='_compute_field_is_one_day') start_sale_date = fields.Date('Start sale date', compute='_compute_start_sale_date') # Location and communication is_online = fields.Boolean('Online Event') address_id = fields.Many2one( - 'res.partner', string='Location', tracking=True, + 'res.partner', string='Venue', tracking=True, default=lambda self: self.env.company.partner_id, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True, readonly=False) @@ -219,7 +220,7 @@ class EventEvent(models.Model): @api.depends('date_end', 'seats_available', 'seats_availability', 'event_ticket_ids.sale_available') def _compute_event_registrations_open(self): for event in self: - event.event_registrations_open = (event.date_end > fields.Datetime.now()) and \ + event.event_registrations_open = event.date_end and (event.date_end > fields.Datetime.now()) and \ (event.seats_available or event.seats_availability == 'unlimited') and \ (not event.event_ticket_ids or any(ticket.sale_available for ticket in event.event_ticket_ids)) @@ -251,6 +252,25 @@ class EventEvent(models.Model): else: event.date_end_located = False + @api.depends('date_begin', 'date_end') + def _compute_is_ongoing(self): + now = fields.Datetime.now() + for event in self: + event.is_ongoing = event.date_begin <= now < event.date_end + + def _search_is_ongoing(self, operator, value): + if operator not in ['=', '!=']: + raise ValueError(_('This operator is not supported')) + if not isinstance(value, bool): + raise ValueError(_('Value should be True or False (not %s)'), value) + now = fields.Datetime.now() + if (operator == '=' and value) or (operator == '!=' and not value): + domain = [('date_begin', '<=', now), ('date_end', '>', now)] + else: + domain = ['|', ('date_begin', '>', now), ('date_end', '<=', now)] + event_ids = self.env['event.event']._search(domain) + return [('id', 'in', event_ids)] + @api.depends('date_begin', 'date_end', 'date_tz') def _compute_field_is_one_day(self): for event in self: diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 575e3442786..5f6c32a620b 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -29,7 +29,7 @@ class EventRegistration(models.Model): partner_id = fields.Many2one( 'res.partner', string='Contact', states={'done': [('readonly', True)]}) - name = fields.Char(string='Attendee Name', index=True) + name = fields.Char(string='Attendee Name', index=True, required=True, tracking=True) email = fields.Char(string='Email') phone = fields.Char(string='Phone') mobile = fields.Char(string='Mobile') @@ -133,8 +133,6 @@ class EventRegistration(models.Model): return ret_list 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 @@ -249,6 +247,12 @@ class EventRegistration(models.Model): else: return _('on ') + format_datetime(self.env, self.event_begin_date, tz=self.event_id.date_tz, dt_format='medium') - def summary(self): + def _get_registration_summary(self): self.ensure_one() - return {'information': []} + return { + 'id': self.id, + 'partner_id': self.partner_id.id, + 'ticket_name': self.event_ticket_id.name or _('None'), + 'name': self.name, + 'event_name': self.event_id.name, + } diff --git a/addons/event/tests/test_event_internals.py b/addons/event/tests/test_event_internals.py index f19090f791c..ad07e4b0b1c 100644 --- a/addons/event/tests/test_event_internals.py +++ b/addons/event/tests/test_event_internals.py @@ -21,7 +21,7 @@ class TestEventData(TestEventCommon): event = self.event_0.with_user(self.env.user) event.write({ - 'registration_ids': [(0, 0, {'partner_id': self.customer.id})], + 'registration_ids': [(0, 0, {'partner_id': self.customer.id, 'name': 'test_reg'})], 'date_begin': datetime(2020, 1, 31, 15, 0, 0), 'date_end': datetime(2020, 4, 5, 18, 0, 0), }) @@ -79,15 +79,18 @@ class TestEventData(TestEventCommon): self.assertTrue(event.auto_confirm) for x in range(5): reg = self.env['event.registration'].create({ - 'event_id': event.id + 'event_id': event.id, + 'name': 'reg_open', }) self.assertEqual(reg.state, 'open') reg_draft = self.env['event.registration'].create({ - 'event_id': event.id + 'event_id': event.id, + 'name': 'reg_draft', }) reg_draft.write({'state': 'draft'}) reg_done = self.env['event.registration'].create({ - 'event_id': event.id + 'event_id': event.id, + 'name': 'reg_done', }) reg_done.write({'state': 'done'}) self.assertEqual(event.seats_available, event.event_type_id.default_registration_max - 6) diff --git a/addons/event/tests/test_event_security.py b/addons/event/tests/test_event_security.py index ef056c438b5..d2ac6d275a3 100644 --- a/addons/event/tests/test_event_security.py +++ b/addons/event/tests/test_event_security.py @@ -83,8 +83,8 @@ class TestEventSecurity(TestEventCommon): event._onchange_type() event.flush() - registration = self.env['event.registration'].create({'event_id': event.id}) - registration.write({'name': 'Myself'}) + registration = self.env['event.registration'].create({'event_id': event.id, 'name': 'Myself'}) + registration.write({'name': 'Myself2'}) stage = self.env.ref('event.event_stage_done') stage.write({'name': 'ManagerEnded'}) diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index be2bdacf90d..c355af5473f 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -227,8 +227,8 @@
-
@@ -485,11 +485,22 @@
+ + event.event.view.graph + event.event + + + + + + + + Events ir.actions.act_window event.event - kanban,calendar,tree,form,pivot + kanban,calendar,tree,form,pivot,graph

@@ -525,7 +536,7 @@ - + @@ -658,6 +669,8 @@ + + diff --git a/addons/event_sale/__manifest__.py b/addons/event_sale/__manifest__.py index 16890176e67..a889c3765df 100644 --- a/addons/event_sale/__manifest__.py +++ b/addons/event_sale/__manifest__.py @@ -27,6 +27,7 @@ this event. 'views/product_views.xml', 'views/sale_order_views.xml', 'data/event_sale_data.xml', + 'data/mail_data.xml', 'report/event_event_templates.xml', 'security/ir.model.access.csv', 'security/event_security.xml', diff --git a/addons/event_sale/data/mail_data.xml b/addons/event_sale/data/mail_data.xml new file mode 100644 index 00000000000..79ad7a392a0 --- /dev/null +++ b/addons/event_sale/data/mail_data.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/addons/event_sale/models/account_move.py b/addons/event_sale/models/account_move.py index 0f5b46f2748..8c5b38532ea 100644 --- a/addons/event_sale/models/account_move.py +++ b/addons/event_sale/models/account_move.py @@ -12,5 +12,5 @@ class AccountMove(models.Model): paid confirm attendees. Attendees should indeed not be confirmed before full payment. """ res = super(AccountMove, self).action_invoice_paid() - self.mapped('line_ids.sale_line_ids')._update_registrations(confirm=True) + self.mapped('line_ids.sale_line_ids')._update_registrations(confirm=True, mark_as_paid=True) return res diff --git a/addons/event_sale/models/event_registration.py b/addons/event_sale/models/event_registration.py index be96547f10e..3c8b4c780a9 100644 --- a/addons/event_sale/models/event_registration.py +++ b/addons/event_sale/models/event_registration.py @@ -8,6 +8,7 @@ from odoo.tools import float_is_zero class EventRegistration(models.Model): _inherit = 'event.registration' + is_paid = fields.Boolean('Is Paid') # 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 @@ -17,13 +18,11 @@ class EventRegistration(models.Model): source_id = fields.Many2one('utm.source', 'Source', related="sale_order_id.source_id", store=True) medium_id = fields.Many2one('utm.medium', 'Medium', related="sale_order_id.medium_id", store=True) - 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 + def action_view_sale_order(self): + action = self.env.ref('sale.action_orders').read()[0] + action['views'] = [(False, 'form')] + action['res_id'] = self.sale_order_id.id + return action @api.model def create(self, vals): @@ -46,6 +45,12 @@ class EventRegistration(models.Model): self.env['sale.order.line'].browse(vals['sale_order_line_id']) ) vals.update(so_line_vals) + + if vals.get('event_ticket_id'): + self.filtered( + lambda registration: registration.event_ticket_id and registration.event_ticket_id.id != vals['event_ticket_id'] + )._sale_order_ticket_type_change_notify(self.env['event.event.ticket'].browse(vals['event_ticket_id'])) + return super(EventRegistration, self).write(vals) def _synchronize_so_line_values(self, so_line): @@ -60,21 +65,39 @@ class EventRegistration(models.Model): } return {} - 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'))) + def _sale_order_ticket_type_change_notify(self, new_event_ticket): + fallback_user_id = self.env.user.id if not self.env.user._is_public() else self.env.ref("base.user_admin").id + for registration in self: + render_context = { + 'registration': registration, + 'old_ticket_name': registration.event_ticket_id.name, + 'new_ticket_name': new_event_ticket.name + } + user_id = registration.event_id.user_id.id or \ + registration.sale_order_id.user_id.id or \ + fallback_user_id + registration.sale_order_id.activity_schedule_with_view('mail.mail_activity_data_warning', + user_id=user_id, + views_or_xmlid='event_sale.event_ticket_id_change_exception', + render_context=render_context) + + def _action_set_paid(self): + self.write({'is_paid': True}) + + def _get_registration_summary(self): + res = super(EventRegistration, self)._get_registration_summary() order = self.sale_order_id.sudo() order_line = self.sale_order_line_id.sudo() + has_to_pay = False 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.payment_state != 'paid' for invoice in order.invoice_ids): payment_status = _('To pay') - res['alert'] = _('The registration must be paid') + has_to_pay = True else: payment_status = _('Paid') - information.append((_('Payment'), payment_status)) + res.update({ + 'payment_status': payment_status, + 'has_to_pay': has_to_pay + }) return res diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index a46eeee1503..47f91ce8ac9 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- -from odoo import api, fields, models +from odoo import api, fields, models, _ class SaleOrder(models.Model): _inherit = "sale.order" + attendee_count = fields.Integer('Attendee Count', compute='_compute_attendee_count') + def write(self, vals): """ Synchronize partner from SO to registrations. This is done notably in website_sale controller shop/address that updates customer, but not @@ -32,6 +34,24 @@ class SaleOrder(models.Model): .for_xml_id('event_sale', 'action_sale_order_event_registration') return res + def action_view_attendee_list(self): + action = self.env.ref('event.event_registration_action_tree').read()[0] + action['domain'] = [('sale_order_id', 'in', self.ids)] + return action + + def _compute_attendee_count(self): + sale_orders_data = self.env['event.registration'].read_group( + [('sale_order_id', 'in', self.ids)], + ['sale_order_id'], ['sale_order_id'] + ) + attendee_count_data = { + sale_order_data['sale_order_id'][0]: + sale_order_data['sale_order_id_count'] + for sale_order_data in sale_orders_data + } + for sale_order in self: + sale_order.attendee_count = attendee_count_data.get(sale_order.id, 0) + class SaleOrderLine(models.Model): @@ -45,7 +65,7 @@ class SaleOrderLine(models.Model): help="Choose an event ticket and it will automatically create a registration for this event ticket.") event_ok = fields.Boolean(related='product_id.event_ok', readonly=True) - def _update_registrations(self, confirm=True, cancel_to_draft=False, registration_data=None): + def _update_registrations(self, confirm=True, cancel_to_draft=False, registration_data=None, mark_as_paid=False): """ Create or update registrations linked to a sales order line. A sale order line has a product_uom_qty attribute that will be the number of registrations linked to this line. This method update existing registrations @@ -56,6 +76,8 @@ class SaleOrderLine(models.Model): existing_registrations = registrations.filtered(lambda self: self.sale_order_line_id.id == so_line.id) if confirm: existing_registrations.filtered(lambda self: self.state not in ['open', 'cancel']).action_confirm() + if mark_as_paid: + existing_registrations.filtered(lambda self: not self.is_paid)._action_set_paid() if cancel_to_draft: existing_registrations.filtered(lambda self: self.state == 'cancel').action_set_draft() @@ -65,7 +87,7 @@ class SaleOrderLine(models.Model): registration_vals = registration_data.pop() # TDE CHECK: auto confirmation registration_vals['sale_order_line_id'] = so_line.id - Registration.with_context(registration_force_draft=True).create(registration_vals) + Registration.create(registration_vals) return True @api.onchange('product_id') diff --git a/addons/event_sale/views/event_registration_views.xml b/addons/event_sale/views/event_registration_views.xml index c66daf53e02..e44a1f8d436 100644 --- a/addons/event_sale/views/event_registration_views.xml +++ b/addons/event_sale/views/event_registration_views.xml @@ -29,8 +29,17 @@ event.registration + +

+ +
+ + + - + diff --git a/addons/event_sale/views/sale_order_views.xml b/addons/event_sale/views/sale_order_views.xml index aca5b637696..c050a48dc4a 100644 --- a/addons/event_sale/views/sale_order_views.xml +++ b/addons/event_sale/views/sale_order_views.xml @@ -5,6 +5,12 @@ sale.order + + + event.event.view.form.inherit.mass.mailing event.event - + + +