19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,10 +1,6 @@
# -*- coding: utf-8 -*-
from . import account_move
from . import event_event
from . import event_registration
from . import event_ticket
from . import product_template
from . import sale_order
from . import product
from . import sale_order_template_line
from . import sale_order_template_option
from . import sale_order_line

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class AccountMove(models.Model):
_inherit = 'account.move'
def _invoice_paid_hook(self):
""" When an invoice linked to a sales order selling registrations is
paid confirm attendees. Attendees should indeed not be confirmed before
full payment. """
res = super(AccountMove, self)._invoice_paid_hook()
self.mapped('line_ids.sale_line_ids')._update_registrations(confirm=True, mark_as_paid=True)
return res

View file

@ -4,25 +4,22 @@
from odoo import api, fields, models
class Event(models.Model):
class EventEvent(models.Model):
_inherit = 'event.event'
sale_order_lines_ids = fields.One2many(
'sale.order.line', 'event_id',
groups='sales_team.group_sale_salesman',
string='All sale order lines pointing to this event')
sale_price_subtotal = fields.Monetary(
string='Sales (Tax Excluded)', compute='_compute_sale_price_subtotal',
sale_price_total = fields.Monetary(
string='Sales (Tax Included)', compute='_compute_sale_price_total',
groups='sales_team.group_sale_salesman')
currency_id = fields.Many2one(
'res.currency', string='Currency',
related='company_id.currency_id', readonly=True)
@api.depends('company_id.currency_id',
'sale_order_lines_ids.price_subtotal', 'sale_order_lines_ids.currency_id',
'sale_order_lines_ids.price_total', 'sale_order_lines_ids.currency_id',
'sale_order_lines_ids.company_id', 'sale_order_lines_ids.order_id.date_order')
def _compute_sale_price_subtotal(self):
""" Takes all the sale.order.lines related to this event and converts amounts
def _compute_sale_price_total(self):
""" Takes only confirmed the sale.order.lines related to this event and converts amounts
from the currency of the sale order to the currency of the event company.
To avoid extra overhead, we use conversion rates as of 'today'.
@ -31,56 +28,28 @@ class Event(models.Model):
have to do one conversion per sale.order (and a sale.order is created every time
we sell a single event ticket). """
date_now = fields.Datetime.now()
sale_price_by_event = {}
if self.ids:
event_subtotals = self.env['sale.order.line']._read_group(
[('event_id', 'in', self.ids),
('price_subtotal', '!=', 0),
('state', '!=', 'cancel')],
['event_id', 'currency_id', 'price_subtotal:sum'],
['event_id', 'currency_id'],
lazy=False
event_subtotals = self.env['sale.order.line']._read_group(
[('event_id', 'in', self.ids), ('price_total', '!=', 0), ('state', '=', 'sale')],
['event_id', 'currency_id'],
['price_total:sum'],
)
event_subtotals_mapping = dict.fromkeys(self._origin, 0)
for event, currency, sum_price_total in event_subtotals:
event_subtotals_mapping[event] += event.currency_id._convert(
sum_price_total,
currency,
event.company_id or self.env.company,
date_now,
)
company_by_event = {
event._origin.id or event.id: event.company_id
for event in self
}
currency_by_event = {
event._origin.id or event.id: event.currency_id
for event in self
}
currency_by_id = {
currency.id: currency
for currency in self.env['res.currency'].browse(
[event_subtotal['currency_id'][0] for event_subtotal in event_subtotals]
)
}
for event_subtotal in event_subtotals:
price_subtotal = event_subtotal['price_subtotal']
event_id = event_subtotal['event_id'][0]
currency_id = event_subtotal['currency_id'][0]
sale_price = currency_by_event[event_id]._convert(
price_subtotal,
currency_by_id[currency_id],
company_by_event[event_id] or self.env.company,
date_now)
if event_id in sale_price_by_event:
sale_price_by_event[event_id] += sale_price
else:
sale_price_by_event[event_id] = sale_price
for event in self:
event.sale_price_subtotal = sale_price_by_event.get(event._origin.id or event.id, 0)
event.sale_price_total = event_subtotals_mapping.get(event._origin, 0)
def action_view_linked_orders(self):
""" Redirects to the orders linked to the current events """
""" Redirects to only the confirmed orders linked to the current events """
sale_order_action = self.env["ir.actions.actions"]._for_xml_id("sale.action_orders")
sale_order_action.update({
'domain': [('state', '!=', 'cancel'), ('order_line.event_id', 'in', self.ids)],
'domain': [('state', '=', 'sale'), ('order_line.event_id', 'in', self.ids)],
'context': {'create': 0},
})
return sale_order_action

View file

@ -1,22 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.tools import float_is_zero
class EventRegistration(models.Model):
_inherit = 'event.registration'
is_paid = fields.Boolean('Is Paid')
# TDE FIXME: maybe add an onchange on sale_order_id
sale_order_id = fields.Many2one('sale.order', string='Sales Order', ondelete='cascade', copy=False)
sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade', copy=False)
payment_status = fields.Selection(string="Payment Status", selection=[
('to_pay', 'Not Paid'),
('paid', 'Paid'),
('free', 'Free'),
], compute="_compute_payment_status", compute_sudo=True)
sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade', copy=False, index='btree_not_null')
state = fields.Selection(default=None, compute="_compute_registration_status", store=True, readonly=False, precompute=True)
utm_campaign_id = fields.Many2one(compute='_compute_utm_campaign_id', readonly=False,
store=True, ondelete="set null")
utm_source_id = fields.Many2one(compute='_compute_utm_source_id', readonly=False,
@ -24,17 +19,32 @@ class EventRegistration(models.Model):
utm_medium_id = fields.Many2one(compute='_compute_utm_medium_id', readonly=False,
store=True, ondelete="set null")
@api.depends('is_paid', 'sale_order_id.currency_id', 'sale_order_line_id.price_total')
def _compute_payment_status(self):
for record in self:
so = record.sale_order_id
so_line = record.sale_order_line_id
if not so or float_is_zero(so_line.price_total, precision_rounding=so.currency_id.rounding):
record.payment_status = 'free'
elif record.is_paid:
record.payment_status = 'paid'
def _has_order(self):
return super()._has_order() or self.sale_order_id
@api.depends('sale_order_id.state', 'sale_order_id.currency_id', 'sale_order_id.amount_total')
def _compute_registration_status(self):
for sale_order, registrations in self.filtered('sale_order_id').grouped('sale_order_id').items():
cancelled_so_registrations = registrations.filtered(lambda reg: reg.sale_order_id.state == 'cancel')
cancelled_so_registrations.state = 'cancel'
cancelled_registrations = cancelled_so_registrations | registrations.filtered(lambda reg: reg.state == 'cancel')
if float_is_zero(sale_order.amount_total, precision_rounding=sale_order.currency_id.rounding):
registrations.sale_status = 'free'
registrations.filtered(lambda reg: not reg.state or reg.state == 'draft').state = "open"
else:
record.payment_status = 'to_pay'
sold_registrations = registrations.filtered(lambda reg: reg.sale_order_id.state == 'sale') - cancelled_registrations
sold_registrations.sale_status = 'sold'
(registrations - sold_registrations).sale_status = 'to_pay'
sold_registrations.filtered(lambda reg: not reg.state or reg.state in {'draft', 'cancel'}).state = "open"
(registrations - sold_registrations - cancelled_registrations).state = 'draft'
super()._compute_registration_status()
# set default value to free and open if none was set yet
for registration in self:
if not registration.sale_status:
registration.sale_status = 'free'
if not registration.state:
registration.state = 'open'
@api.depends('sale_order_id')
def _compute_utm_campaign_id(self):
@ -77,10 +87,11 @@ class EventRegistration(models.Model):
registrations = super(EventRegistration, self).create(vals_list)
for registration in registrations:
if registration.sale_order_id:
registration.message_post_with_view(
registration.message_post_with_source(
'mail.message_origin_link',
values={'self': registration, 'origin': registration.sale_order_id},
subtype_id=self.env.ref('mail.mt_note').id)
render_values={'self': registration, 'origin': registration.sale_order_id},
subtype_xmlid='mail.mt_note',
)
return registrations
def write(self, vals):
@ -90,10 +101,15 @@ class EventRegistration(models.Model):
)
vals.update(so_line_vals)
updated_fields_to_notify = []
if vals.get('event_slot_id'):
updated_fields_to_notify.append(('event.slot', 'event_slot_id'))
if vals.get('event_ticket_id'):
updated_fields_to_notify.append(('event.event.ticket', 'event_ticket_id'))
for model, field in updated_fields_to_notify:
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']))
lambda registration: registration[field] and registration[field].id != vals[field]
)._sale_order_registration_data_change_notify(field, self.env[model].browse(vals[field]))
return super(EventRegistration, self).write(vals)
@ -103,35 +119,51 @@ class EventRegistration(models.Model):
# Avoid registering public users but respect the portal workflows
'partner_id': False if self.env.user._is_public() and self.env.user.partner_id == so_line.order_id.partner_id else so_line.order_id.partner_id.id,
'event_id': so_line.event_id.id,
'event_slot_id': so_line.event_slot_id.id,
'event_ticket_id': so_line.event_ticket_id.id,
'sale_order_id': so_line.order_id.id,
'sale_order_line_id': so_line.id,
}
return {}
def _sale_order_ticket_type_change_notify(self, new_event_ticket):
def _sale_order_registration_data_change_notify(self, new_record_field, new_record):
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
'record_type': _('Ticket') if new_record_field == 'event_ticket_id' else _('Slot'),
'old_name': registration[new_record_field].display_name,
'new_name': new_record.display_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',
views_or_xmlid='event_sale.event_registration_change_exception',
render_context=render_context)
def _action_set_paid(self):
self.write({'is_paid': True})
def _compute_field_value(self, field):
if field.name != 'state':
return super()._compute_field_value(field)
unconfirmed = self.filtered(lambda reg: reg.ids and reg.state in {'draft', 'cancel'})
res = super()._compute_field_value(field)
confirmed = unconfirmed.filtered(lambda reg: reg.state == 'open')
if confirmed:
confirmed._update_mail_schedulers()
return res
def _get_registration_summary(self):
res = super(EventRegistration, self)._get_registration_summary()
res.update({
'payment_status': self.payment_status,
'payment_status_value': dict(self._fields['payment_status']._description_selection(self.env))[self.payment_status],
'has_to_pay': self.payment_status == 'to_pay',
'sale_status': self.sale_status,
'sale_status_value': self.sale_status and dict(self._fields['sale_status']._description_selection(self.env))[self.sale_status],
'has_to_pay': self.sale_status == 'to_pay',
})
return res
def _get_event_registration_ids_from_order(self):
self.ensure_one()
return self.sale_order_id.order_line.filtered(
lambda line: line.event_id == self.event_id
).registration_ids.ids

View file

@ -1,131 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
from odoo import models
class EventTemplateTicket(models.Model):
_inherit = 'event.type.ticket'
def _default_product_id(self):
return self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
description = fields.Text(compute='_compute_description', readonly=False, store=True)
# product
product_id = fields.Many2one(
'product.product', string='Product', required=True,
domain=[("detailed_type", "=", "event")], default=_default_product_id)
price = fields.Float(
string='Price', compute='_compute_price',
digits='Product Price', readonly=False, store=True)
price_reduce = fields.Float(
string="Price Reduce", compute="_compute_price_reduce",
compute_sudo=True, digits='Product Price')
@api.depends('product_id')
def _compute_price(self):
for ticket in self:
if ticket.product_id and ticket.product_id.lst_price:
ticket.price = ticket.product_id.lst_price or 0
elif not ticket.price:
ticket.price = 0
@api.depends('product_id')
def _compute_description(self):
for ticket in self:
if ticket.product_id and ticket.product_id.description_sale:
ticket.description = ticket.product_id.description_sale
# initialize, i.e for embedded tree views
if not ticket.description:
ticket.description = False
# TODO clean this feature in master
# Feature broken by design, depending on the hacky `_get_contextual_price` field on products
# context_dependent, core part of the pricelist mess
# This field usage should be restricted to the UX, and any use in effective
# price computation should be replaced by clear calls to the pricelist API
@api.depends_context('uom', 'qty', 'pricelist') # Cf product.price context dependencies
@api.depends('product_id', 'price')
def _compute_price_reduce(self):
for ticket in self:
product = ticket.product_id
pricelist = product.product_tmpl_id._get_contextual_pricelist()
lst_price = product.currency_id._convert(
product.lst_price,
pricelist.currency_id,
self.env.company,
fields.Datetime.now(),
round=False,
)
discount = (lst_price - product._get_contextual_price()) / lst_price if lst_price else 0.0
ticket.price_reduce = (1.0 - discount) * ticket.price
def _init_column(self, column_name):
if column_name != "product_id":
return super(EventTemplateTicket, self)._init_column(column_name)
# fetch void columns
self.env.cr.execute("SELECT id FROM %s WHERE product_id IS NULL" % self._table)
ticket_type_ids = self.env.cr.fetchall()
if not ticket_type_ids:
return
# update existing columns
_logger.debug("Table '%s': setting default value of new column %s to unique values for each row",
self._table, column_name)
default_event_product = self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
if default_event_product:
product_id = default_event_product.id
else:
product_id = self.env['product.product'].create({
'name': 'Generic Registration Product',
'list_price': 0,
'standard_price': 0,
'type': 'service',
}).id
self.env['ir.model.data'].create({
'name': 'product_product_event',
'module': 'event_sale',
'model': 'product.product',
'res_id': product_id,
})
self.env.cr._obj.execute(
f'UPDATE {self._table} SET product_id = %s WHERE id IN %s;',
(product_id, tuple(ticket_type_ids))
)
@api.model
def _get_event_ticket_fields_whitelist(self):
""" Add sale specific fields to copy from template to ticket """
return super(EventTemplateTicket, self)._get_event_ticket_fields_whitelist() + ['product_id', 'price']
class EventTicket(models.Model):
class EventEventTicket(models.Model):
_inherit = 'event.event.ticket'
_order = "event_id, price"
# product
price_reduce_taxinc = fields.Float(
string='Price Reduce Tax inc', compute='_compute_price_reduce_taxinc',
compute_sudo=True)
def _compute_price_reduce_taxinc(self):
for event in self:
# sudo necessary here since the field is most probably accessed through the website
tax_ids = event.product_id.taxes_id.filtered(lambda r: r.company_id == event.event_id.company_id)
taxes = tax_ids.compute_all(event.price_reduce, event.event_id.company_id.currency_id, 1.0, product=event.product_id)
event.price_reduce_taxinc = taxes['total_included']
@api.depends('product_id.active')
def _compute_sale_available(self):
inactive_product_tickets = self.filtered(lambda ticket: not ticket.product_id.active)
for ticket in inactive_product_tickets:
ticket.sale_available = False
super(EventTicket, self - inactive_product_tickets)._compute_sale_available()
_order = "event_id, sequence, price, name, id"
def _get_ticket_multiline_description(self):
""" If people set a description on their product it has more priority
@ -133,4 +11,4 @@ class EventTicket(models.Model):
self.ensure_one()
if self.product_id.description_sale:
return '%s\n%s' % (self.product_id.description_sale, self.event_id.display_name)
return super(EventTicket, self)._get_ticket_multiline_description()
return super()._get_ticket_multiline_description()

View file

@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
detailed_type = fields.Selection(selection_add=[
('event', 'Event Ticket'),
], ondelete={'event': 'set service'})
@api.onchange('detailed_type')
def _onchange_type_event(self):
if self.detailed_type == 'event':
self.invoice_policy = 'order'
def _detailed_type_mapping(self):
type_mapping = super()._detailed_type_mapping()
type_mapping['event'] = 'service'
return type_mapping
class Product(models.Model):
_inherit = 'product.product'
event_ticket_ids = fields.One2many('event.event.ticket', 'product_id', string='Event Tickets')

View file

@ -0,0 +1,15 @@
from odoo import _, api, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
def _prepare_service_tracking_tooltip(self):
if self.service_tracking == 'event':
return _("Create an Attendee for the selected Event.")
return super()._prepare_service_tracking_tooltip()
@api.onchange('service_tracking')
def _onchange_type_event(self):
if self.service_tracking == 'event':
self.invoice_policy = 'order'

View file

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import fields, models, _
from odoo.exceptions import ValidationError
from odoo.fields import Domain
class SaleOrder(models.Model):
@ -13,28 +15,29 @@ class SaleOrder(models.Model):
in website_sale controller shop/address that updates customer, but not
only. """
result = super(SaleOrder, self).write(vals)
if vals.get('partner_id'):
registrations_toupdate = self.sudo().env['event.registration'].search([('sale_order_id', 'in', self.ids)])
if any(line.service_tracking == 'event' for line in self.order_line) and vals.get('partner_id'):
registrations_toupdate = self.env['event.registration'].sudo().search([('sale_order_id', 'in', self.ids)])
registrations_toupdate.write({'partner_id': vals['partner_id']})
return result
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
for so in self:
if not any(line.product_type == 'event' for line in so.order_line):
if not any(line.service_tracking == 'event' for line in so.order_line):
continue
# confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
so.order_line._update_registrations(confirm=so.amount_total == 0, cancel_to_draft=False)
so_lines_missing_events = so.order_line.filtered(lambda line: line.service_tracking == 'event' and not line.event_id)
if so_lines_missing_events:
so_lines_descriptions = "".join(f"\n- {so_line_description.name}" for so_line_description in so_lines_missing_events)
raise ValidationError(_("Please make sure all your event related lines are configured before confirming this order:%s", so_lines_descriptions))
# Initialize registrations
so.order_line._init_registrations()
if len(self) == 1:
return self.env['ir.actions.act_window'].with_context(
default_sale_order_id=so.id
)._for_xml_id('event_sale.action_sale_order_event_registration')
return res
def _action_cancel(self):
self.order_line._cancel_associated_registrations()
return super()._action_cancel()
def action_view_attendee_list(self):
action = self.env["ir.actions.actions"]._for_xml_id("event.event_registration_action_tree")
action['domain'] = [('sale_order_id', 'in', self.ids)]
@ -44,143 +47,13 @@ class SaleOrder(models.Model):
sale_orders_data = self.env['event.registration']._read_group(
[('sale_order_id', 'in', self.ids),
('state', '!=', 'cancel')],
['sale_order_id'], ['sale_order_id']
['sale_order_id'], ['__count'],
)
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
sale_order.id: count for sale_order, count in sale_orders_data
}
for sale_order in self:
sale_order.attendee_count = attendee_count_data.get(sale_order.id, 0)
def unlink(self):
self.order_line._unlink_associated_registrations()
return super(SaleOrder, self).unlink()
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
event_id = fields.Many2one(
'event.event', string='Event',
compute="_compute_event_id", store=True, readonly=False, precompute=True,
help="Choose an event and it will automatically create a registration for this event.")
event_ticket_id = fields.Many2one(
'event.event.ticket', string='Event Ticket',
compute="_compute_event_ticket_id", store=True, readonly=False, precompute=True,
help="Choose an event ticket and it will automatically create a registration for this event ticket.")
# TODO in master: remove this field, unused anymore
event_ok = fields.Boolean(compute='_compute_event_ok')
@api.depends('product_id.detailed_type')
def _compute_event_ok(self):
for record in self:
record.event_ok = record.product_id.detailed_type == 'event'
@api.depends('state', 'event_id')
def _compute_product_uom_readonly(self):
event_lines = self.filtered(lambda line: line.event_id)
event_lines.update({'product_uom_readonly': True})
super(SaleOrderLine, self - event_lines)._compute_product_uom_readonly()
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
and create new one for missing one. """
RegistrationSudo = self.env['event.registration'].sudo()
registrations = RegistrationSudo.search([('sale_order_line_id', 'in', self.ids)])
registrations_vals = []
for so_line in self:
if not so_line.product_type == 'event':
continue
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()
for count in range(int(so_line.product_uom_qty) - len(existing_registrations)):
values = {
'sale_order_line_id': so_line.id,
'sale_order_id': so_line.order_id.id
}
# TDE CHECK: auto confirmation
if registration_data:
values.update(registration_data.pop())
registrations_vals.append(values)
if registrations_vals:
RegistrationSudo.create(registrations_vals)
return True
@api.depends('product_id')
def _compute_event_id(self):
event_lines = self.filtered(lambda line: line.product_id and line.product_id.detailed_type == 'event')
(self - event_lines).event_id = False
for line in event_lines:
if line.product_id not in line.event_id.event_ticket_ids.product_id:
line.event_id = False
@api.depends('event_id')
def _compute_event_ticket_id(self):
event_lines = self.filtered('event_id')
(self - event_lines).event_ticket_id = False
for line in event_lines:
if line.event_id != line.event_ticket_id.event_id:
line.event_ticket_id = False
@api.depends('event_ticket_id')
def _compute_price_unit(self):
super()._compute_price_unit()
@api.depends('event_ticket_id')
def _compute_name(self):
"""Override to add the compute dependency.
The custom name logic can be found below in _get_sale_order_line_multiline_description_sale.
"""
super()._compute_name()
def unlink(self):
self._unlink_associated_registrations()
return super(SaleOrderLine, self).unlink()
def _cancel_associated_registrations(self):
self.env['event.registration'].search([('sale_order_line_id', 'in', self.ids)]).action_cancel()
def _unlink_associated_registrations(self):
self.env['event.registration'].search([('sale_order_line_id', 'in', self.ids)]).unlink()
def _get_sale_order_line_multiline_description_sale(self):
""" We override this method because we decided that:
The default description of a sales order line containing a ticket must be different than the default description when no ticket is present.
So in that case we use the description computed from the ticket, instead of the description computed from the product.
We need this override to be defined here in sales order line (and not in product) because here is the only place where the event_ticket_id is referenced.
"""
if self.event_ticket_id:
return self.event_ticket_id._get_ticket_multiline_description() + self._get_sale_order_line_multiline_description_variants()
else:
return super()._get_sale_order_line_multiline_description_sale()
def _get_display_price(self):
if self.event_ticket_id and self.event_id:
event_ticket = self.event_ticket_id.with_context(
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id
)
company = event_ticket.company_id or self.env.company
currency = company.currency_id
pricelist = self.order_id.pricelist_id
if pricelist.discount_policy == "with_discount":
price = event_ticket.price_reduce
else:
price = event_ticket.price
return currency._convert(
price, self.order_id.currency_id,
self.order_id.company_id or self.env.company.id,
self.order_id.date_order or fields.Date.today())
return super()._get_display_price()
def _get_product_catalog_domain(self):
return super()._get_product_catalog_domain() & Domain('service_tracking', '!=', 'event')

View file

@ -0,0 +1,127 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
event_id = fields.Many2one(
'event.event', string='Event',
compute="_compute_event_id", store=True, readonly=False, precompute=True, index='btree_not_null',
help="Choose an event and it will automatically create a registration for this event.")
event_slot_id = fields.Many2one(
'event.slot', string='Slot',
compute="_compute_event_related", store=True, readonly=False, precompute=True,
help="Choose an event slot and it will automatically create a registration for this event slot.")
event_ticket_id = fields.Many2one(
'event.event.ticket', string='Ticket Type',
compute="_compute_event_related", store=True, readonly=False, precompute=True,
help="Choose an event ticket and it will automatically create a registration for this event ticket.")
is_multi_slots = fields.Boolean(related="event_id.is_multi_slots")
registration_ids = fields.One2many('event.registration', 'sale_order_line_id', string="Registrations")
@api.constrains('event_id', 'event_slot_id', 'event_ticket_id', 'product_id')
def _check_event_registration_ticket(self):
for so_line in self:
if so_line.product_id.service_tracking == "event" and (
not so_line.event_id or
not so_line.event_ticket_id or
(so_line.is_multi_slots and not so_line.event_slot_id)
):
raise ValidationError(_(
"The sale order line with the product %(product_name)s needs an event,"
" a ticket and a slot in case the event has multiple time slots.",
product_name=so_line.product_id.name))
@api.depends('state', 'event_id')
def _compute_product_uom_readonly(self):
event_lines = self.filtered(lambda line: line.event_id)
event_lines.update({'product_uom_readonly': True})
super(SaleOrderLine, self - event_lines)._compute_product_uom_readonly()
def _init_registrations(self):
""" Create 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. """
registrations_vals = []
for so_line in self:
if so_line.service_tracking != 'event':
continue
for _count in range(int(so_line.product_uom_qty) - len(so_line.registration_ids)):
values = {
'sale_order_line_id': so_line.id,
'sale_order_id': so_line.order_id.id,
}
# When confirming in backend a single order, keep paid registrations in draft
# so attendee details can be filled before confirmation; free ones stay open for seat checks.
if len(self.order_id) == 1 and not so_line.currency_id.is_zero(so_line.price_total):
values['state'] = 'draft'
registrations_vals.append(values)
if registrations_vals:
self.env['event.registration'].sudo().create(registrations_vals)
return True
@api.depends('product_id')
def _compute_event_id(self):
event_lines = self.filtered(lambda line: line.product_id and line.product_id.service_tracking == 'event')
(self - event_lines).event_id = False
for line in event_lines:
if line.product_id not in line.event_id.event_ticket_ids.product_id:
line.event_id = False
@api.depends('event_id')
def _compute_event_related(self):
event_lines = self.filtered('event_id')
(self - event_lines).event_slot_id = False
(self - event_lines).event_ticket_id = False
for line in event_lines:
if line.event_id != line.event_slot_id.event_id:
line.event_slot_id = False
if line.event_id != line.event_ticket_id.event_id:
line.event_ticket_id = False
@api.depends('event_ticket_id')
def _compute_price_unit(self):
super()._compute_price_unit()
@api.depends('event_slot_id', 'event_ticket_id')
def _compute_name(self):
"""Override to add the compute dependency.
The custom name logic can be found below in _get_sale_order_line_multiline_description_sale.
"""
super()._compute_name()
def _get_sale_order_line_multiline_description_sale(self):
""" We override this method because we decided that:
The default description of a sales order line containing a ticket must be different than the default description when no ticket is present.
So in that case we use the description computed from the ticket, instead of the description computed from the product.
We need this override to be defined here in sales order line (and not in product) because here is the only place where the event_ticket_id is referenced.
"""
if self.event_ticket_id:
return self.event_ticket_id._get_ticket_multiline_description() + \
('\n%s' % self.event_slot_id.display_name if self.event_slot_id else '') + \
self._get_sale_order_line_multiline_description_variants()
else:
return super()._get_sale_order_line_multiline_description_sale()
def _use_template_name(self):
""" We do not want configured description to get rewritten by template default"""
if self.event_ticket_id:
return False
return super()._use_template_name()
def _get_display_price(self):
if self.event_ticket_id and self.event_id:
event_ticket = self.event_ticket_id
company = event_ticket.company_id or self.env.company
if not self.pricelist_item_id._show_discount():
price = event_ticket.with_context(**self._get_pricelist_price_context()).price_reduce
else:
price = event_ticket.price
return self._convert_to_sol_currency(price, company.currency_id)
return super()._get_display_price()

View file

@ -1,6 +0,0 @@
from odoo import fields, models
class SaleOrderTemplateLine(models.Model):
_inherit = "sale.order.template.line"
product_id = fields.Many2one(domain="[('sale_ok', '=', True), ('detailed_type', '!=', 'event'), ('company_id', 'in', [company_id, False])]")

View file

@ -1,6 +0,0 @@
from odoo import fields, models
class SaleOrderTemplateOption(models.Model):
_inherit = "sale.order.template.option"
product_id = fields.Many2one(domain="[('sale_ok', '=', True), ('detailed_type', '!=', 'event'), ('company_id', 'in', [company_id, False])]")