From fd3febf7d7899412d36d1e2263e7a466398cd336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Tue, 14 Aug 2018 16:00:38 +0200 Subject: [PATCH] [IMP] various: add event and ticket name on sale order and invoice line In event_sale it is possible to have one product representing several tickets. But before this commit, on the sale order and invoice the client only saw the name of the product and didn't know which event/ticket they subscribed to. The goal of this commit is to overwrite that behavior whenever a sale order or invoice line is related to an event ticket, to show the ticket name and event name instead of the product name. Technically, the following changes have been done: ---------------------------------------- * various: add a method to compute a default name for a SaleOrderLine, based on its product I added the method because the code to compute the name was duplicated in several places. This will make it more consistent. Moreover this gives the option for modules to override the method to define a custom name as necessary. ---------------------------------------- * event_sale: compute a consistent description for a SaleOrderLine based on an EventTicket The description will be the same whether the sale order comes from the front end or the back end. It takes into account the customer language. The default description will be computed as follow: 1. The ticket name should be the first line of the product description on the SO. 2. The event name should be the second line of the product description on the SO. To compute the ticket multi-line description we decided to ignore entirely the product name and the product description_sale because they are considered to be replaced by the ticket name and event name. We also ignore them because the work-flow of creating a new event does not lead to filling them correctly, as the product is created through the event interface. ---------------------------------------- * event_sale: improve sale order line and event registration views It was possible to select a ticket related to a product before an event was selected. This is typically the case for tickets defined in an event_type but not in an event. The ticket field is now hidden until both the product and the event have been selected. This way we are sure the selected ticket will belong to an event and not an event_type. We reset the ticket on event change because we don't want the user to be able to select a ticket belonging to the wrong event. We also improve here the comments of the related on-change methods. ---------------------------------------- * website_event_sale: update views to show ticket name and event name The standard behavior was to show the product name in several places instead of the sale order line description. However, for event tickets, we don't want to show the product name, but the ticket name and event name instead. ---------------------------------------- * website_event_sale: add ticket on confirmation page The ticket name was missing from the registration confirmation page (shown when the ticket is free and/or the user paid with a direct method). This is now fixed. ---------------------------------------- Task: 1837987 PR: 25819 --- addons/event/views/event_views.xml | 2 +- addons/event_sale/models/event.py | 28 ++++++++++- addons/event_sale/models/sale_order.py | 39 +++++++++++---- addons/event_sale/tests/test_event_sale.py | 9 +++- addons/event_sale/views/event_views.xml | 10 +++- addons/event_sale/views/sale_order_views.xml | 24 +++++++++- addons/product/models/product.py | 10 ++++ addons/sale/models/sale.py | 14 ++++-- addons/sale_management/models/sale_order.py | 4 +- addons/website_event/models/event.py | 7 +-- .../website_event_sale/models/sale_order.py | 17 ++++++- .../views/event_templates.xml | 43 ++++++++++++++++- addons/website_sale/models/sale_order.py | 18 +++++++ addons/website_sale/views/templates.xml | 48 ++++++++++++++----- 14 files changed, 231 insertions(+), 42 deletions(-) diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index ffc04deca47..1977634b90a 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -468,7 +468,7 @@ - + diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index 1f9a7ed53b0..0ab88b3be85 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -160,17 +160,43 @@ class EventTicket(models.Model): def _onchange_product_id(self): self.price = self.product_id.list_price or 0 + def get_ticket_multiline_description_sale(self): + """ Compute a multiline description of this ticket, in the context of sales. + It will often be used as the default description of a sales order line referencing this ticket. + + 1. the first line is the ticket name + 2. the second line is the event name (if it exists, which should be the case with a normal workflow) or the product name (if it exists) + + We decided to ignore entirely the product name and the product description_sale because they are considered to be replaced by the ticket name and event name. + -> the workflow of creating a new event also does not lead to filling them correctly, as the product is created through the event interface + """ + + name = self.display_name + + if self.event_id: + name += '\n' + self.event_id.display_name + elif self.product_id: + name += '\n' + self.product_id.display_name + + return name + class EventRegistration(models.Model): _inherit = 'event.registration' - event_ticket_id = fields.Many2one('event.event.ticket', string='Event Ticket') + event_ticket_id = fields.Many2one('event.event.ticket', string='Event Ticket', readonly=True, states={'draft': [('readonly', False)]}) # in addition to origin generic fields, add real relational fields to correctly # handle attendees linked to sales orders and their lines # TDE FIXME: maybe add an onchange on sale_order_id + origin sale_order_id = fields.Many2one('sale.order', string='Source Sales Order', ondelete='cascade') sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade') + @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.multi @api.constrains('event_ticket_id', 'state') def _check_ticket_seats_limit(self): diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index a4601ac5f76..c41a6218f10 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -27,14 +27,6 @@ class SaleOrderLine(models.Model): "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.multi - def _prepare_invoice_line(self, qty): - self.ensure_one() - res = super(SaleOrderLine, self)._prepare_invoice_line(qty) - if self.event_id: - res['name'] = '%s: %s' % (res.get('name', ''), self.event_id.name) - return res - @api.multi def _update_registrations(self, confirm=True, cancel_to_draft=False, registration_data=None): """ Create or update registrations linked to a sales order line. A sale @@ -60,9 +52,40 @@ class SaleOrderLine(models.Model): Registration._prepare_attendee_values(registration)) 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('event_ticket_id') def _onchange_event_ticket_id(self): company = self.event_id.company_id or self.env.user.company_id currency = company.currency_id self.price_unit = currency._convert( self.event_ticket_id.price, self.order_id.currency_id, self.order_id.company_id, self.order_id.date_order or fields.Date.today()) + + # we call this to force update the default name + self.product_id_change() + + 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_sale() + else: + return super(SaleOrderLine, self).get_sale_order_line_multiline_description_sale(product) diff --git a/addons/event_sale/tests/test_event_sale.py b/addons/event_sale/tests/test_event_sale.py index d73a1cfcb7e..8f17f322780 100644 --- a/addons/event_sale/tests/test_event_sale.py +++ b/addons/event_sale/tests/test_event_sale.py @@ -25,6 +25,12 @@ class EventSaleTest(common.TransactionCase): 'date_begin': '2012-01-01 18:05:15' }) + ticket = self.env['event.event.ticket'].create({ + 'name': 'test_ticket', + 'product_id': product.id, + 'event_id': event.id, + }) + # I create a sales order self.sale_order = self.env['sale.order'].create({ 'partner_id': self.env.ref('base.res_partner_2').id, @@ -40,7 +46,8 @@ class EventSaleTest(common.TransactionCase): 'product_uom_qty': 8.0, 'order_id': self.sale_order.id, 'name': 'sales order line', - 'event_id': event.id + 'event_id': event.id, + 'event_ticket_id': ticket.id, }) # In the event registration I add some attendee detail lines. i choose event product diff --git a/addons/event_sale/views/event_views.xml b/addons/event_sale/views/event_views.xml index e8498e562e1..778bd166cb3 100644 --- a/addons/event_sale/views/event_views.xml +++ b/addons/event_sale/views/event_views.xml @@ -29,7 +29,15 @@ - + diff --git a/addons/event_sale/views/sale_order_views.xml b/addons/event_sale/views/sale_order_views.xml index 3d2b8530947..712da75ffd8 100644 --- a/addons/event_sale/views/sale_order_views.xml +++ b/addons/event_sale/views/sale_order_views.xml @@ -6,8 +6,28 @@ - - + + diff --git a/addons/product/models/product.py b/addons/product/models/product.py index a42b37cf5e7..ccda4b32511 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -572,6 +572,16 @@ class ProductProduct(models.Model): ) return super(ProductProduct, self).get_empty_list_help(help) + def get_product_multiline_description_sale(self): + """ Compute a multiline description of this product, in the context of sales + (do not use for purchases or other display reasons that don't intend to use "description_sale"). + It will often be used as the default description of a sale order line referencing this product. + """ + name = self.display_name + if self.description_sale: + name += '\n' + self.description_sale + return name + class ProductPackaging(models.Model): _name = "product.packaging" diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index cdae56697db..3a66cc1a1f4 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1343,10 +1343,7 @@ class SaleOrderLine(models.Model): self.product_id = False return result - name = product.name_get()[0][1] - if product.description_sale: - name += '\n' + product.description_sale - vals['name'] = name + vals.update(name=self.get_sale_order_line_multiline_description_sale(product)) self._compute_tax_id() @@ -1486,3 +1483,12 @@ class SaleOrderLine(models.Model): def _is_delivery(self): self.ensure_one() return False + + def get_sale_order_line_multiline_description_sale(self, product): + """ Compute a default multiline description for this sales order line. + This method exists so it can be overridden in other modules to change how the default name is computed. + In general only the product is used to compute the name, and this method would not be necessary (we could directly override the method in product). + BUT in event_sale we need to know specifically the sales order line as well as the product to generate the name: + the product is not sufficient because we also need to know the event_id and the event_ticket_id (both which belong to the sale order line). + """ + return product.get_product_multiline_description_sale() diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index f2481bc7fc5..b8a8d3ad8e8 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -238,9 +238,7 @@ class SaleOrderOption(models.Model): return product = self.product_id.with_context(lang=self.order_id.partner_id.lang) self.price_unit = product.list_price - self.name = product.name - if product.description_sale: - self.name += '\n' + product.description_sale + self.name = product.get_product_multiline_description_sale() self.uom_id = self.uom_id or product.uom_id pricelist = self.order_id.pricelist_id if pricelist and product: diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index ae07f798939..e4e10667dfc 100644 --- a/addons/website_event/models/event.py +++ b/addons/website_event/models/event.py @@ -4,12 +4,9 @@ import logging import pytz import werkzeug -from datetime import datetime - from odoo import api, fields, models, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.exceptions import UserError -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT _logger = logging.getLogger(__name__) @@ -162,8 +159,8 @@ class Event(models.Model): return result def _get_event_resource_urls(self, attendees): - url_date_start = datetime.strptime(self.date_begin, DEFAULT_SERVER_DATETIME_FORMAT).strftime('%Y%m%dT%H%M%SZ') - url_date_stop = datetime.strptime(self.date_end, DEFAULT_SERVER_DATETIME_FORMAT).strftime('%Y%m%dT%H%M%SZ') + url_date_start = self.date_begin.strftime('%Y%m%dT%H%M%SZ') + url_date_stop = self.date_end.strftime('%Y%m%dT%H%M%SZ') params = { 'action': 'TEMPLATE', 'text': self.name, diff --git a/addons/website_event_sale/models/sale_order.py b/addons/website_event_sale/models/sale_order.py index 3cf2d253182..097ce0adbaf 100644 --- a/addons/website_event_sale/models/sale_order.py +++ b/addons/website_event_sale/models/sale_order.py @@ -42,7 +42,7 @@ class SaleOrder(models.Model): values['event_id'] = ticket.event_id.id values['event_ticket_id'] = ticket.id values['price_unit'] = ticket.price_reduce or ticket.price - values['name'] = "%s\n%s" % (ticket.event_id.display_name, ticket.name) + values['name'] = ticket.get_ticket_multiline_description_sale() # avoid writing related values that end up locking the product record values.pop('event_ok', None) @@ -96,3 +96,18 @@ class SaleOrder(models.Model): # add in return values the registrations, to display them on website (or not) values['attendee_ids'] = self.env['event.registration'].search([('sale_order_line_id', '=', line.id), ('state', '!=', 'cancel')]).ids return values + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.multi + @api.depends('product_id.display_name', 'event_ticket_id.display_name') + def _compute_name_short(self): + """ If the sale order line concerns a ticket, we don't want the product name, but the ticket name instead. + """ + super(SaleOrderLine, self)._compute_name_short() + + for record in self: + if record.event_ticket_id: + record.name_short = record.event_ticket_id.display_name diff --git a/addons/website_event_sale/views/event_templates.xml b/addons/website_event_sale/views/event_templates.xml index 4be278f9620..bccf05ad8be 100644 --- a/addons/website_event_sale/views/event_templates.xml +++ b/addons/website_event_sale/views/event_templates.xml @@ -84,15 +84,54 @@ - + + + + + + + + +