mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 22:12:03 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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'
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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])]")
|
||||
|
|
@ -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])]")
|
||||
Loading…
Add table
Add a link
Reference in a new issue