summaryrefslogtreecommitdiff
path: root/addons/event_sale/models
diff options
context:
space:
mode:
Diffstat (limited to 'addons/event_sale/models')
-rw-r--r--addons/event_sale/models/__init__.py8
-rw-r--r--addons/event_sale/models/account_move.py16
-rw-r--r--addons/event_sale/models/event_event.py85
-rw-r--r--addons/event_sale/models/event_registration.py133
-rw-r--r--addons/event_sale/models/event_ticket.py122
-rw-r--r--addons/event_sale/models/product.py27
-rw-r--r--addons/event_sale/models/sale_order.py160
7 files changed, 551 insertions, 0 deletions
diff --git a/addons/event_sale/models/__init__.py b/addons/event_sale/models/__init__.py
new file mode 100644
index 00000000..7228262e
--- /dev/null
+++ b/addons/event_sale/models/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+from . import account_move
+from . import event_event
+from . import event_registration
+from . import event_ticket
+from . import sale_order
+from . import product
diff --git a/addons/event_sale/models/account_move.py b/addons/event_sale/models/account_move.py
new file mode 100644
index 00000000..8c5b3853
--- /dev/null
+++ b/addons/event_sale/models/account_move.py
@@ -0,0 +1,16 @@
+# -*- 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 action_invoice_paid(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).action_invoice_paid()
+ 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_event.py b/addons/event_sale/models/event_event.py
new file mode 100644
index 00000000..5af548e1
--- /dev/null
+++ b/addons/event_sale/models/event_event.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class Event(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',
+ 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.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
+ 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'.
+ Meaning we have a number that can change over time, but using the conversion rates
+ at the time of the related sale.order would mean thousands of extra requests as we would
+ 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)],
+ ['event_id', 'currency_id', 'price_subtotal:sum'],
+ ['event_id', 'currency_id'],
+ lazy=False
+ )
+
+ 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],
+ 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)
+
+ def action_view_linked_orders(self):
+ """ Redirects to the 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)],
+ 'context': {'create': 0},
+ })
+ return sale_order_action
diff --git a/addons/event_sale/models/event_registration.py b/addons/event_sale/models/event_registration.py
new file mode 100644
index 00000000..1acb6126
--- /dev/null
+++ b/addons/event_sale/models/event_registration.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.tools import 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)
+ utm_campaign_id = fields.Many2one(compute='_compute_utm_campaign_id', readonly=False, store=True)
+ utm_source_id = fields.Many2one(compute='_compute_utm_source_id', readonly=False, store=True)
+ utm_medium_id = fields.Many2one(compute='_compute_utm_medium_id', readonly=False, store=True)
+
+ @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_digits=so.currency_id.rounding):
+ record.payment_status = 'free'
+ elif record.is_paid:
+ record.payment_status = 'paid'
+ else:
+ record.payment_status = 'to_pay'
+
+ @api.depends('sale_order_id')
+ def _compute_utm_campaign_id(self):
+ for registration in self:
+ if registration.sale_order_id.campaign_id:
+ registration.utm_campaign_id = registration.sale_order_id.campaign_id
+ elif not registration.utm_campaign_id:
+ registration.utm_campaign_id = False
+
+ @api.depends('sale_order_id')
+ def _compute_utm_source_id(self):
+ for registration in self:
+ if registration.sale_order_id.source_id:
+ registration.utm_source_id = registration.sale_order_id.source_id
+ elif not registration.utm_source_id:
+ registration.utm_source_id = False
+
+ @api.depends('sale_order_id')
+ def _compute_utm_medium_id(self):
+ for registration in self:
+ if registration.sale_order_id.medium_id:
+ registration.utm_medium_id = registration.sale_order_id.medium_id
+ elif not registration.utm_medium_id:
+ registration.utm_medium_id = False
+
+ def action_view_sale_order(self):
+ action = self.env["ir.actions.actions"]._for_xml_id("sale.action_orders")
+ action['views'] = [(False, 'form')]
+ action['res_id'] = self.sale_order_id.id
+ return action
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if vals.get('sale_order_line_id'):
+ so_line_vals = self._synchronize_so_line_values(
+ self.env['sale.order.line'].browse(vals['sale_order_line_id'])
+ )
+ vals.update(so_line_vals)
+ registrations = super(EventRegistration, self).create(vals_list)
+ for registration in registrations:
+ if registration.sale_order_id:
+ registration.message_post_with_view(
+ 'mail.message_origin_link',
+ values={'self': registration, 'origin': registration.sale_order_id},
+ subtype_id=self.env.ref('mail.mt_note').id)
+ return registrations
+
+ def write(self, vals):
+ if vals.get('sale_order_line_id'):
+ so_line_vals = self._synchronize_so_line_values(
+ 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):
+ if so_line:
+ return {
+ 'partner_id': so_line.order_id.partner_id.id,
+ 'event_id': so_line.event_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):
+ 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()
+ 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': not self.is_paid,
+ })
+ return res
diff --git a/addons/event_sale/models/event_ticket.py b/addons/event_sale/models/event_ticket.py
new file mode 100644
index 00000000..da142f97
--- /dev/null
+++ b/addons/event_sale/models/event_ticket.py
@@ -0,0 +1,122 @@
+# -*- 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__)
+
+
+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=[("event_ok", "=", True)], 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
+
+ @api.depends('product_id', 'price')
+ def _compute_price_reduce(self):
+ for ticket in self:
+ product = ticket.product_id
+ discount = (product.lst_price - product.price) / product.lst_price if product.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):
+ _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()
+
+ def _get_ticket_multiline_description(self):
+ """ If people set a description on their product it has more priority
+ than the ticket name itself for the SO description. """
+ 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()
diff --git a/addons/event_sale/models/product.py b/addons/event_sale/models/product.py
new file mode 100644
index 00000000..a8980466
--- /dev/null
+++ b/addons/event_sale/models/product.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+from odoo import api, fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ event_ok = fields.Boolean(string='Is an Event Ticket', help="If checked this product automatically "
+ "creates an event registration at the sales order confirmation.")
+
+ @api.onchange('event_ok')
+ def _onchange_event_ok(self):
+ if self.event_ok:
+ self.type = 'service'
+
+
+class Product(models.Model):
+ _inherit = 'product.product'
+
+ event_ticket_ids = fields.One2many('event.event.ticket', 'product_id', string='Event Tickets')
+
+ @api.onchange('event_ok')
+ def _onchange_event_ok(self):
+ """ Redirection, inheritance mechanism hides the method on the model """
+ if self.event_ok:
+ self.type = 'service'
diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py
new file mode 100644
index 00000000..bf3aca6c
--- /dev/null
+++ b/addons/event_sale/models/sale_order.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+
+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
+ only. """
+ result = super(SaleOrder, self).write(vals)
+ if vals.get('partner_id'):
+ registrations_toupdate = self.env['event.registration'].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:
+ # 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)
+ if any(line.event_id for line in so.order_line):
+ 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(SaleOrder, self).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)]
+ 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)
+
+ def unlink(self):
+ self.mapped('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',
+ 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',
+ 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)
+
+ @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.filtered('event_id'):
+ 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.onchange('product_id')
+ def _onchange_product_id(self):
+ # We reset the event when keeping it would lead to an inconstitent state.
+ # We need to do it this way because the only relation between the product and the event is through the corresponding tickets.
+ if self.event_id and (not self.product_id or self.product_id.id not in self.event_id.mapped('event_ticket_ids.product_id.id')):
+ self.event_id = None
+
+ @api.onchange('event_id')
+ def _onchange_event_id(self):
+ # We reset the ticket when keeping it would lead to an inconstitent state.
+ if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id):
+ self.event_ticket_id = None
+
+ @api.onchange('product_uom', 'product_uom_qty')
+ def product_uom_change(self):
+ if not self.event_ticket_id:
+ super(SaleOrderLine, self).product_uom_change()
+
+ @api.onchange('event_ticket_id')
+ def _onchange_event_ticket_id(self):
+ # we call this to force update the default name
+ self.product_id_change()
+
+ 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, product):
+ """ 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:
+ ticket = self.event_ticket_id.with_context(
+ lang=self.order_id.partner_id.lang,
+ )
+
+ return ticket._get_ticket_multiline_description() + self._get_sale_order_line_multiline_description_variants()
+ else:
+ return super(SaleOrderLine, self).get_sale_order_line_multiline_description_sale(product)
+
+ def _get_display_price(self, product):
+ if self.event_ticket_id and self.event_id:
+ return self.event_ticket_id.with_context(pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id).price_reduce
+ else:
+ return super()._get_display_price(product)