[MERGE][IMP] event[_sale]: improve event views (frontend/backend) and ticket sale flow
PURPOSE Improve global event usability by adding a graph view as well as various changes on the ticket sale flow. We also improved the track proposal layout, added a handy button to invite partners to your event and prepared some data for the event_barcode module. SPECIFICATIONS - Add a "Invite" button on the event, that allows sending invitations though the mass_mailing / mass_sms features - Add a graph view for the event.event model - Allow styling the description and the biography when sending a track proposal with basic plaintext2html usage - Improve event and sale views integration in the event_sale module (stat buttons and next activity generation) - General improvements to the "auto confirm" flow of registrations - Prepare data for the event_barcode module, to make it "event agnostic" More details in underlying commits LINKS Task ID : 2093336 Enterprise PR: odoo/enterprise#7760 closes odoo/odoo#43420 Related: odoo/enterprise#7760 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -227,8 +227,8 @@
|
||||
<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>
|
||||
<label for="name" class="oe_edit_only" string="Event Name"/>
|
||||
<h1><field name="name" placeholder="e.g. Conference for Architects"/></h1>
|
||||
<field name="is_online" groups="base.group_no_one"/>
|
||||
<label for="is_online" string="Online" groups="base.group_no_one"/>
|
||||
</div>
|
||||
@@ -485,11 +485,22 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_view_graph" model="ir.ui.view" >
|
||||
<field name="name">event.event.view.graph</field>
|
||||
<field name="model">event.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Events">
|
||||
<field name="name" type="row"/>
|
||||
<field name="seats_available" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_event_view">
|
||||
<field name="name">Events</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">event.event</field>
|
||||
<field name="view_mode">kanban,calendar,tree,form,pivot</field>
|
||||
<field name="view_mode">kanban,calendar,tree,form,pivot,graph</field>
|
||||
<field name="search_view_id" ref="view_event_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
@@ -525,7 +536,7 @@
|
||||
<field name="phone" optional="hide"/>
|
||||
<field name="mobile" optional="hide"/>
|
||||
<field name="event_id"/>
|
||||
<field name="event_ticket_id"/>
|
||||
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]"/>
|
||||
<field name="origin" optional="hide"/>
|
||||
<field name="state" readonly="0"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
@@ -658,6 +669,8 @@
|
||||
<search string="Event Registration">
|
||||
<field name="id" string="Registration ID"/>
|
||||
<field name="name" string="Participant" filter_domain="['|', '|', ('name', 'ilike', self), ('email', 'ilike', self), ('origin', 'ilike', self)]"/>
|
||||
<filter string="Ongoing Events" name="filter_is_ongoing" domain="[('event_id.is_ongoing', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
|
||||
<filter string="Expected" name="expected" domain="[('state', 'in', ['draft', 'open', 'done'])]"/>
|
||||
<separator/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="event_ticket_id_change_exception" name="Message: Alert on event ticket id change">
|
||||
<div>
|
||||
<p>
|
||||
<span>Registration modification for attendee:</span>
|
||||
<a href="#" data-oe-model="event.registration" t-att-data-oe-id="registration.id"><t t-esc="registration.name"/></a>.
|
||||
<span>Manual actions may be needed.</span>
|
||||
</p>
|
||||
<div class="mt16">
|
||||
<p>Exception:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" data-oe-model="event.registration" t-att-data-oe-id="registration.id"><t t-esc="registration.name"/></a>:
|
||||
<span>Ticket changed from <strong><t t-esc="old_ticket_name"/></strong> to <strong><t t-esc="new_ticket_name"/></strong></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -29,8 +29,17 @@
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_event_registration_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_sale_order" type="object"
|
||||
class="oe_stat_button" icon="fa-usd" string="Sale Order">
|
||||
</button>
|
||||
</div>
|
||||
<field name="is_paid" invisible="1"/>
|
||||
<widget name="web_ribbon" title="Paid" bg_color="bg-success" attrs="{'invisible': [('is_paid', '=', False)]}"/>
|
||||
</xpath>
|
||||
<group name="event" position="after">
|
||||
<group string="Origin">
|
||||
<group string="Origin" groups="base.group_no_one">
|
||||
<field name="sale_order_id"/>
|
||||
<field name="origin" attrs="{'invisible': [('sale_order_id', '!=', False)]}"/>
|
||||
<field name="sale_order_line_id" readonly="1" attrs="{'invisible': [('sale_order_id', '=', False)]}"/>
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_attendee_list" type="object"
|
||||
class="oe_stat_button" icon="fa-users" attrs="{'invisible': [('attendee_count', '=', 0)]}">
|
||||
<field name="attendee_count" widget="statinfo" string="Attendees"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']//form//field[@name='product_id']" position="after">
|
||||
<field
|
||||
name="event_id"
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo import models, _
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
_inherit = "event.event"
|
||||
|
||||
def action_mass_mailing_attendees(self):
|
||||
mass_mailing_action = dict(
|
||||
name='Mass Mail Attendees',
|
||||
type='ir.actions.act_window',
|
||||
res_model='mailing.mailing',
|
||||
view_mode='form',
|
||||
target='current',
|
||||
context=dict(
|
||||
default_mailing_model_id=self.env.ref('event.model_event_registration').id,
|
||||
default_mailing_domain=repr([('event_id', 'in', self.ids)])
|
||||
),
|
||||
)
|
||||
return mass_mailing_action
|
||||
return {
|
||||
'name': 'Mass Mail Attendees',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mailing.mailing',
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
'context': {
|
||||
'default_mailing_model_id': self.env.ref('event.model_event_registration').id,
|
||||
'default_mailing_domain': repr([('event_id', 'in', self.ids)])
|
||||
},
|
||||
}
|
||||
|
||||
def action_invite_contacts(self):
|
||||
return {
|
||||
'name': 'Mass Mail Invitation',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mailing.mailing',
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
'context': {'default_mailing_model_id': self.env.ref('base.model_res_partner').id},
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
<record id="event_event_view_form_inherit_mass_mailing" model="ir.ui.view">
|
||||
<field name="name">event.event.view.form.inherit.mass.mailing</field>
|
||||
<field name="model">event.event</field>
|
||||
<field name="priority" eval="15"/>
|
||||
<field name="priority" eval="4"/>
|
||||
<field name="inherit_id" ref="event.view_event_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='stage_id']" position="before">
|
||||
<field name="event_registrations_open" invisible="1"/>
|
||||
<button name="action_invite_contacts" type="object" string="Invite"
|
||||
class="btn btn-primary" attrs="{'invisible': [('event_registrations_open', '=', False)]}"/>
|
||||
<button name="action_invite_contacts" type="object" string="Invite"
|
||||
class="btn btn-secondary" attrs="{'invisible': [('event_registrations_open', '=', True)]}"/>
|
||||
<button name="action_mass_mailing_attendees" string="Contact Attendees" type="object" attrs="{'invisible': [('seats_expected', '=', 0)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -12,3 +12,9 @@ class Event(models.Model):
|
||||
action = super(Event, self).action_mass_mailing_attendees()
|
||||
action['view_id'] = self.env.ref('mass_mailing_sms.mailing_mailing_view_form_mixed').id
|
||||
return action
|
||||
|
||||
def action_invite_contacts(self):
|
||||
# Minimal override: set form view being the one mixing sms and mail (not prioritized one)
|
||||
action = super(Event, self).action_invite_contacts()
|
||||
action['view_id'] = self.env.ref('mass_mailing_sms.mailing_mailing_view_form_mixed').id
|
||||
return action
|
||||
|
||||
@@ -27,6 +27,7 @@ class TestEventWebsite(TestEventWebsiteHelper):
|
||||
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
|
||||
'registration_ids': [(0, 0, {
|
||||
'partner_id': self.user_eventuser.partner_id.id,
|
||||
'name': 'test_reg',
|
||||
})],
|
||||
'website_menu': True,
|
||||
})
|
||||
|
||||
@@ -6,11 +6,12 @@ import babel.dates
|
||||
import collections
|
||||
import datetime
|
||||
import pytz
|
||||
import base64
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import fields, http
|
||||
from odoo.http import request
|
||||
from odoo.tools import html_escape as escape, html2plaintext
|
||||
from odoo.tools import plaintext2html, html2plaintext
|
||||
|
||||
|
||||
class WebsiteEventTrackController(http.Controller):
|
||||
@@ -93,6 +94,7 @@ class WebsiteEventTrackController(http.Controller):
|
||||
|
||||
return request.render("website_event_track.agenda", {
|
||||
'event': event,
|
||||
'main_object': event,
|
||||
'days': days,
|
||||
'tracks_by_days': tracks_by_days,
|
||||
'tag': tag
|
||||
@@ -129,7 +131,7 @@ class WebsiteEventTrackController(http.Controller):
|
||||
if not event.can_access_from_current_website():
|
||||
raise NotFound()
|
||||
|
||||
return request.render("website_event_track.event_track_proposal", {'event': event})
|
||||
return request.render("website_event_track.event_track_proposal", {'event': event, 'main_object': event})
|
||||
|
||||
@http.route(['''/event/<model("event.event"):event>/track_proposal/post'''], type='http', auth="public", methods=['POST'], website=True)
|
||||
def event_track_proposal_post(self, event, **post):
|
||||
@@ -146,11 +148,12 @@ class WebsiteEventTrackController(http.Controller):
|
||||
'partner_name': post['partner_name'],
|
||||
'partner_email': post['email_from'],
|
||||
'partner_phone': post['phone'],
|
||||
'partner_biography': escape(post['biography']),
|
||||
'partner_biography': plaintext2html(post['biography']),
|
||||
'event_id': event.id,
|
||||
'tag_ids': [(6, 0, tags)],
|
||||
'user_id': False,
|
||||
'description': escape(post['description'])
|
||||
'description': plaintext2html(post['description']),
|
||||
'image': base64.b64encode(post['image'].read()) if post.get('image') else False
|
||||
})
|
||||
if request.env.user != request.website.user_id:
|
||||
track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids)
|
||||
|
||||
@@ -66,14 +66,14 @@ class Track(models.Model):
|
||||
date = fields.Datetime('Track Date')
|
||||
date_end = fields.Datetime('Track End Date', compute='_compute_end_date', store=True)
|
||||
duration = fields.Float('Duration', default=1.5, help="Track duration in hours.")
|
||||
location_id = fields.Many2one('event.track.location', 'Room')
|
||||
location_id = fields.Many2one('event.track.location', 'Location')
|
||||
event_id = fields.Many2one('event.event', 'Event', required=True)
|
||||
color = fields.Integer('Color', related="stage_id.color")
|
||||
priority = fields.Selection([
|
||||
('0', 'Low'), ('1', 'Medium'),
|
||||
('2', 'High'), ('3', 'Highest')],
|
||||
'Priority', required=True, default='1')
|
||||
image = fields.Image("Image", related='partner_id.image_128', store=True, readonly=False)
|
||||
image = fields.Image("Image", max_width=128, max_height=128)
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_website_url(self):
|
||||
@@ -88,7 +88,6 @@ class Track(models.Model):
|
||||
self.partner_name = self.partner_id.name
|
||||
self.partner_email = self.partner_id.email
|
||||
self.partner_phone = self.partner_id.phone
|
||||
self.partner_biography = self.partner_id.website_description
|
||||
|
||||
@api.depends('date', 'duration')
|
||||
def _compute_end_date(self):
|
||||
|
||||
@@ -16,6 +16,7 @@ class TestEventWebsiteTrack(TestEventWebsiteHelper):
|
||||
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
|
||||
'registration_ids': [(0, 0, {
|
||||
'partner_id': self.user_eventuser.partner_id.id,
|
||||
'name': 'test_reg',
|
||||
})],
|
||||
'website_menu': True,
|
||||
'website_track': True,
|
||||
|
||||
@@ -222,7 +222,8 @@
|
||||
<div t-field="track.partner_biography"/>
|
||||
<div t-if="track.partner_id" class="row mt32">
|
||||
<div class="col-md-2">
|
||||
<span t-field="track.partner_id.sudo().image_128" t-options='{"widget": "image", "class": "rounded-circle"}'/>
|
||||
<span t-if="track.image" t-field="track.image" t-options="{'widget': 'image', 'class': 'rounded-circle'}"/>
|
||||
<span t-else="" t-field="track.partner_id.sudo().image_128" t-options='{"widget": "image", "class": "rounded-circle"}'/>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<h4 t-field="track.partner_id.name" class="mb4"/>
|
||||
@@ -338,7 +339,7 @@
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="description">Talk Introduction</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<textarea class="form-control" name="description" style="min-height: 120px"/>
|
||||
<textarea class="form-control" name="description"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" t-if="len(event.allowed_track_tag_ids)">
|
||||
@@ -376,6 +377,12 @@
|
||||
<textarea class="form-control" name="biography" rows="5"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="image">Speaker(s) Picture</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<input name="image" type="file" accept="image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row o_form_buttons">
|
||||
<div class="offset-lg-3 offset-md-4 col-md-8 col-lg-7">
|
||||
<button type="submit" class="btn btn-primary">Submit Proposal</button>
|
||||
@@ -441,8 +448,8 @@
|
||||
<li><b>Proposed By</b>: <t t-esc="track.partner_name or track.partner_email"/></li>
|
||||
<li><b>Mail</b>: <a t-attf-href="mailto:#{track.partner_email}"><t t-esc="track.partner_email"/></a></li>
|
||||
<li><b>Phone</b>: <t t-esc="track.partner_phone"/></li>
|
||||
<li><b>Speakers Biography</b>: <t t-esc="track.partner_biography"/></li>
|
||||
<li><b>Talk Introduction</b>: <t t-esc="track.description"/></li>
|
||||
<li><b>Speakers Biography</b>: <div t-field="track.partner_biography"/></li>
|
||||
<li><b>Talk Introduction</b>: <div t-field="track.description"/></li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
|
||||
<filter string="Date" name="date" context="{'group_by': 'date'}"/>
|
||||
<filter string="Event" name="event" context="{'group_by': 'event_id'}"/>
|
||||
<filter string="Room" name="location" context="{'group_by': 'location_id'}"/>
|
||||
<filter string="Location" name="location" context="{'group_by': 'location_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
@@ -118,8 +118,9 @@
|
||||
<field name="website_url" invisible="1"/>
|
||||
<field name="is_published" widget="website_redirect_button"/>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<field name="kanban_state" widget="state_selection"/>
|
||||
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<field name="kanban_state" widget="state_selection" class="ml-3"/>
|
||||
<field name="image" widget="image" class="oe_avatar"/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
@@ -201,7 +202,7 @@
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Add a new track
|
||||
</p><p>
|
||||
Tracks define the schedule of your event. These can be a talk, a round table, a meeting, etc.
|
||||
Tracks define the schedule of your event.<br/>These can be a talk, a round table, a meeting, etc.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
@@ -215,14 +216,14 @@
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Add a new track
|
||||
</p><p>
|
||||
Tracks define the schedule of your event. These can be a talk, a round table, a meeting, etc.
|
||||
Tracks define the schedule of your event.<br/>These can be a talk, a round table, a meeting, etc.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_event_track"
|
||||
name="Event Tracks"
|
||||
name="Tracks"
|
||||
action="action_event_track"
|
||||
parent="event.event_main_menu"
|
||||
groups="base.group_no_one"/>
|
||||
@@ -232,7 +233,7 @@
|
||||
<field name="name">Event Locations</field>
|
||||
<field name="model">event.track.location</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Event Location">
|
||||
<form string="Event Locations">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
|
||||
Reference in New Issue
Block a user