[ADD] event_booth_sale: add sale capabilities to event_booth

PURPOSE

This module allows to sell booths to partners by setting a product and a price
to the booths categories. When selling booths on a sale order it will open the
product configurator and ask the event, booth category and booths.
It's only possible to select booths belonging to the same category for one sale order line.

A new model is introduced by this module :

- Booth Registration that allows multiple registrations to a booth.
  The booth will be set unavailable when the sale order linked to the registration is paid.
  All the others registrations will be cancelled

LINKS

Task-2318655
Closes : odoo/odoo#71104
This commit is contained in:
Patrick Hoste
2021-09-02 14:14:33 +00:00
parent 3c5c3c257e
commit b3c1141f8b
29 changed files with 1120 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import wizard
+34
View File
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Events Booths Sales",
'category': 'Marketing/Events',
'version': '1.0',
'summary': "Manage event booths sale",
'description': """
Sell your event booths and track payments on sale orders.
""",
'depends': ['event_booth', 'event_sale'],
'data': [
'security/ir.model.access.csv',
'data/product_data.xml',
'data/event_booth_category_data.xml',
'views/product_views.xml',
'views/sale_order_views.xml',
'views/event_type_booth_views.xml',
'views/event_booth_category_views.xml',
'views/event_booth_registration_views.xml',
'views/event_booth_views.xml',
'wizard/event_booth_configurator_views.xml',
],
'auto_install': True,
'assets': {
'web.assets_backend': [
'event_booth_sale/static/src/js/event_booth_configurator_form_controller.js',
'event_booth_sale/static/src/js/event_booth_configurator_form_view.js',
'event_booth_sale/static/src/js/event_booth_configurator_widget.js',
]
},
'license': 'LGPL-3',
}
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="event_booth.event_booth_category_standard" model="event.booth.category">
<field name="product_id" ref="event_booth_sale.product_product_event_booth"/>
<field name="price">100</field>
</record>
<record id="event_booth.event_booth_category_premium" model="event.booth.category">
<field name="product_id" ref="event_booth_sale.product_product_event_booth"/>
<field name="price">500</field>
</record>
<record id="event_booth.event_booth_category_vip" model="event.booth.category">
<field name="product_id" ref="event_booth_sale.product_product_event_booth"/>
<field name="price">1000</field>
</record>
</data></odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data noupdate="1">
<record id="product_product_event_booth" model="product.product">
<field name="name">Event Booth</field>
<field name="is_event_booth" eval="True"/>
<field name="purchase_ok" eval="False"/>
<field name="list_price">100.0</field>
<field name="description_sale" eval="False"/>
<field name="categ_id" ref="event_sale.product_category_events"/>
<field name="invoice_policy">order</field>
<field name="type">service</field>
</record>
</data></odoo>
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
from . import product
from . import event_booth_registration
from . import event_booth
from . import event_booth_category
from . import event_type_booth
from . import sale_order
from . import sale_order_line
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import 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, update booths accordingly as they are booked when invoice is paid.
"""
res = super(AccountMove, self).action_invoice_paid()
self.mapped('line_ids.sale_line_ids')._update_event_booths(set_paid=True)
return res
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class EventBooth(models.Model):
_inherit = 'event.booth'
# registrations
event_booth_registration_ids = fields.One2many('event.booth.registration', 'event_booth_id')
# sale information
sale_order_line_registration_ids = fields.Many2many(
'sale.order.line', 'event_booth_registration',
'event_booth_id', 'sale_order_line_id', string='SO Lines with reservations',
groups='sales_team.group_sale_salesman', copy=False)
sale_order_line_id = fields.Many2one(
'sale.order.line', string='Final Sale Order Line', ondelete='set null',
readonly=True, states={'available': [('readonly', False)]},
groups='sales_team.group_sale_salesman', copy=False)
sale_order_id = fields.Many2one(
related='sale_order_line_id.order_id', store='True', readonly=True,
groups='sales_team.group_sale_salesman')
is_paid = fields.Boolean('Is Paid', copy=False)
@api.ondelete(at_uninstall=False)
def _unlink_except_linked_sale_order(self):
booth_with_so = self.filtered('sale_order_id')
if booth_with_so:
raise UserError(_(
'You can\'t delete the following booths as they are linked to sales orders: '
'%(booths)s', booths=', '.join(booth_with_so.mapped('name'))))
def action_set_paid(self):
self.write({'is_paid': True})
def action_view_sale_order(self):
self.sale_order_id.ensure_one()
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
def _get_booth_multiline_description(self):
return '%s : \n%s' % (
self.event_id.display_name,
'\n'.join(['- %s' % booth.name for booth in self])
)
@@ -0,0 +1,84 @@
# -*- 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 EventBoothCategory(models.Model):
_inherit = 'event.booth.category'
def _default_product_id(self):
return self.env.ref('event_booth_sale.product_product_event_booth', raise_if_not_found=False)
product_id = fields.Many2one(
'product.product', string='Product', required=True,
domain=[('is_event_booth', '=', 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')
image_1920 = fields.Image(compute='_compute_image_1920', readonly=False, store=True)
@api.depends('product_id')
def _compute_image_1920(self):
for category in self:
category.image_1920 = category.image_1920 if category.image_1920 else category.product_id.image_1920
@api.depends('product_id')
def _compute_price(self):
""" By default price comes from category but can be changed by event
people as product may be shared accross various categories. """
for category in self:
if category.product_id and category.product_id.list_price:
category.price = category.product_id.list_price + category.product_id.price_extra
@api.depends('product_id', 'price')
def _compute_price_reduce(self):
for category in self:
product = category.product_id
list_price = product.list_price + product.price_extra
discount = (list_price - product.price) / list_price if list_price else 0.0
category.price_reduce = (1.0 - discount) * category.price
def _init_column(self, column_name):
""" Initialize product_id for existing columns when installing sale
bridge, to ensure required attribute is fulfilled. """
if column_name != "product_id":
return super(EventBoothCategory, self)._init_column(column_name)
# fetch void columns
self.env.cr.execute("SELECT id FROM %s WHERE product_id IS NULL" % self._table)
booth_category_ids = self.env.cr.fetchall()
if not booth_category_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_booth_product = self._default_product_id()
if default_booth_product:
product_id = default_booth_product.id
else:
product_id = self.env['product.product'].create({
'name': 'Generic Event Booth Product',
'categ_id': self.env.ref('event_sale.product_category_events').id,
'list_price': 100,
'standard_price': 0,
'is_event_booth': True,
'type': 'service',
'invoice_policy': 'order',
}).id
self.env['ir.model.data'].create({
'name': 'product_product_event_booth',
'module': 'event_booth_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(booth_category_ids))
)
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class EventBoothRegistration(models.Model):
"""event.booth.registrations are used to allow multiple partners to book the same booth.
Whenever a partner has paid his registration all the others linked to the booth will be deleted."""
_name = 'event.booth.registration'
_description = 'Event Booth Registration'
sale_order_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', required=True)
event_booth_id = fields.Many2one('event.booth', string='Booth', required=True)
partner_id = fields.Many2one(
'res.partner', related='sale_order_line_id.order_partner_id', store=True)
contact_name = fields.Char(string='Contact Name', compute='_compute_contact_name', readonly=False, store=True)
contact_email = fields.Char(string='Contact Email', compute='_compute_contact_email', readonly=False, store=True)
contact_phone = fields.Char(string='Contact Phone', compute='_compute_contact_phone', readonly=False, store=True)
contact_mobile = fields.Char(string='Contact Mobile', compute='_compute_contact_mobile', readonly=False, store=True)
_sql_constraints = [('unique_registration', 'unique(sale_order_line_id, event_booth_id)',
'There can be only one registration for a booth by sale order line')]
@api.depends('partner_id')
def _compute_contact_name(self):
for registration in self:
if not registration.contact_name:
registration.contact_name = registration.partner_id.name or False
@api.depends('partner_id')
def _compute_contact_email(self):
for registration in self:
if not registration.contact_email:
registration.contact_email = registration.partner_id.email or False
@api.depends('partner_id')
def _compute_contact_phone(self):
for registration in self:
if not registration.contact_phone:
registration.contact_phone = registration.partner_id.phone or False
@api.depends('partner_id')
def _compute_contact_mobile(self):
for registration in self:
if not registration.contact_mobile:
registration.contact_mobile = registration.partner_id.mobile or False
@api.model
def _get_fields_for_booth_confirmation(self):
return ['sale_order_line_id', 'partner_id', 'contact_name', 'contact_email', 'contact_phone', 'contact_mobile']
def action_confirm(self):
for registration in self:
values = {
field: registration[field].id if isinstance(registration[field], models.BaseModel) else registration[field]
for field in self._get_fields_for_booth_confirmation()
}
registration.event_booth_id.action_confirm(values)
self._cancel_pending_registrations()
def _cancel_pending_registrations(self):
body = '<p>%(message)s: <ul>%(booth_names)s</ul></p>' % {
'message': _('Your order has been cancelled because the following booths have been reserved'),
'booth_names': ''.join('<li>%s</li>' % booth.display_name for booth in self.event_booth_id)
}
other_registrations = self.search([
('event_booth_id', 'in', self.event_booth_id.ids),
('id', 'not in', self.ids)
])
for order in other_registrations.sale_order_line_id.order_id:
order.sudo().message_post(
body=body,
partner_ids=order.user_id.partner_id.ids,
)
order.sudo().action_cancel()
other_registrations.unlink()
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class EventTypeBooth(models.Model):
_inherit = 'event.type.booth'
product_id = fields.Many2one(related='booth_category_id.product_id')
price = fields.Float(related='booth_category_id.price')
@api.model
def _get_event_booth_fields_whitelist(self):
res = super(EventTypeBooth, self)._get_event_booth_fields_whitelist()
return res + ['product_id', 'price']
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
is_event_booth = fields.Boolean(string='Is an Event Booth')
@api.onchange('is_event_booth')
def _onchange_is_event_booth(self):
if self.is_event_booth:
self.type = 'service'
self.invoice_policy = 'order'
class Product(models.Model):
_inherit = 'product.product'
@api.onchange('is_event_booth')
def _onchange_is_event_booth(self):
if self.is_event_booth:
self.type = 'service'
self.invoice_policy = 'order'
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
event_booth_ids = fields.One2many('event.booth', 'sale_order_id', string='Booths')
event_booth_count = fields.Integer(string='Booth Count', compute='_compute_event_booth_count')
@api.depends('event_booth_ids')
def _compute_event_booth_count(self):
if self.ids:
slot_data = self.env['event.booth'].read_group(
[('sale_order_id', 'in', self.ids)],
['sale_order_id'], ['sale_order_id']
)
slot_mapped = dict((data['sale_order_id'][0], data['sale_order_id_count']) for data in slot_data)
else:
slot_mapped = dict()
for so in self:
so.event_booth_count = slot_mapped.get(so.id, 0)
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
for so in self:
so.order_line._update_event_booths()
return res
def action_view_booth_list(self):
action = self.env['ir.actions.act_window']._for_xml_id('event_booth.event_booth_action')
action['domain'] = [('sale_order_id', 'in', self.ids)]
return action
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# 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_booth_category_id = fields.Many2one('event.booth.category', string='Booths Category', ondelete='set null')
event_booth_pending_ids = fields.Many2many(
'event.booth', string='Pending Booths', search='_search_event_booth_pending_ids',
compute='_compute_event_booth_pending_ids', inverse='_inverse_event_booth_pending_ids',
help='Used to create registration when providing the desired event booth.'
)
event_booth_registration_ids = fields.One2many(
'event.booth.registration', 'sale_order_line_id', string='Confirmed Registration')
event_booth_ids = fields.One2many('event.booth', 'sale_order_line_id', string='Confirmed Booths')
is_event_booth = fields.Boolean(related='product_id.is_event_booth', readonly=True)
@api.depends('event_booth_ids')
def _compute_name_short(self):
wbooth = self.filtered(lambda line: line.event_booth_pending_ids)
for record in wbooth:
record.name_short = record.event_booth_pending_ids.event_id.name
super(SaleOrderLine, self - wbooth)._compute_name_short()
@api.depends('event_booth_registration_ids')
def _compute_event_booth_pending_ids(self):
for so_line in self:
so_line.event_booth_pending_ids = so_line.event_booth_registration_ids.event_booth_id
def _inverse_event_booth_pending_ids(self):
for so_line in self:
self.env['event.booth.registration'].create([{
'event_booth_id': booth.id,
'sale_order_line_id': so_line.id,
'partner_id': so_line.order_id.partner_id.id
} for booth in so_line.event_booth_pending_ids])
def _search_event_booth_pending_ids(self, operator, value):
return [('event_booth_registration_ids.event_booth_id', operator, value)]
@api.constrains('event_booth_registration_ids')
def _check_event_booth_registration_ids(self):
if len(self.event_booth_registration_ids.event_booth_id.event_id) > 1:
raise ValidationError(_('Registrations from the same Order Line must belong to a single event.'))
@api.onchange('product_id')
def _onchange_product_id_booth(self):
"""We reset the event when the selected product doesn't belong to any pending booths."""
if self.event_id and (not self.product_id or self.product_id not in self.event_booth_pending_ids.product_id):
self.event_id = None
@api.onchange('event_id')
def _onchange_event_id_booth(self):
"""We reset the pending booths when the event changes to avoid inconsistent state."""
if self.event_booth_pending_ids and (not self.event_id or self.event_id != self.event_booth_pending_ids.event_id):
self.event_booth_pending_ids = None
@api.onchange('event_booth_pending_ids')
def _onchange_event_booth_pending_ids(self):
# trigger the description computation
self.product_id_change()
def _update_event_booths(self, set_paid=False):
for so_line in self.filtered('is_event_booth'):
if so_line.event_booth_pending_ids and not so_line.event_booth_ids:
unavailable = so_line.event_booth_pending_ids.filtered(lambda booth: not booth.is_available)
if unavailable:
raise ValidationError(
_('The following booths are unavailable, please remove them to continue : %(booth_names)s',
booth_names=''.join('\n\t- %s' % booth.display_name for booth in unavailable)))
so_line.event_booth_registration_ids.action_confirm()
if so_line.event_booth_ids and set_paid:
so_line.event_booth_ids.action_set_paid()
return True
def get_sale_order_line_multiline_description_sale(self, product):
if self.event_booth_pending_ids:
booths = self.event_booth_pending_ids.with_context(
lang=self.order_id.partner_id.lang,
)
return booths._get_booth_multiline_description()
return super(SaleOrderLine, self).get_sale_order_line_multiline_description_sale(product)
def _get_display_price(self, product):
if self.event_booth_pending_ids and self.event_id:
company = self.event_id.company_id or self.env.company
currency = company.currency_id
total_price = sum([booth.price for booth in self.event_booth_pending_ids])
return currency._convert(
total_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(SaleOrderLine, self)._get_display_price(product)
@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_event_booth_registration_salesman,access.event.booth.registration.salesman,model_event_booth_registration,sales_team.group_sale_salesman,1,1,1,1
access_event_booth_registration_event_desk,access.event.booth.registration.event.desk,model_event_booth_registration,event.group_event_registration_desk,1,0,0,0
access_event_booth_registration_event_user,access.event.booth.registration.event.user,model_event_booth_registration,event.group_event_user,1,1,1,1
access_event_booth_configurator_salesman,access.event.booth.configurator.salesman,model_event_booth_configurator,sales_team.group_sale_salesman,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_event_booth_registration_salesman access.event.booth.registration.salesman model_event_booth_registration sales_team.group_sale_salesman 1 1 1 1
3 access_event_booth_registration_event_desk access.event.booth.registration.event.desk model_event_booth_registration event.group_event_registration_desk 1 0 0 0
4 access_event_booth_registration_event_user access.event.booth.registration.event.user model_event_booth_registration event.group_event_user 1 1 1 1
5 access_event_booth_configurator_salesman access.event.booth.configurator.salesman model_event_booth_configurator sales_team.group_sale_salesman 1 1 1 0
@@ -0,0 +1,44 @@
odoo.define('event_booth_sale.event_booth_configurator_form_controller', function (require) {
'use strict';
const FormController = require('web.FormController');
/**
* This controller is overridden to allow configuring sale_order_lines through a popup
* window when a product with 'is_event_booth' is selected.
*
* This allows keeping an editable list view for sales order and remove the noise of
* those 3 fields ('event_id', 'event_booth_category_id' and 'event_booth_ids')
*/
const EventBoothConfiguratorFormController = FormController.extend({
//--------------------------------------------------------------------------
// Overrides
//--------------------------------------------------------------------------
saveRecord: function () {
const self = this;
return this._super.apply(this, arguments).then(function () {
const state = self.model.get(self.handle, {raw: true});
self.do_action({
type: 'ir.actions.act_window_close',
infos: {
eventBoothConfiguration: {
event_id: {id: state.data.event_id},
event_booth_category_id: {id: state.data.event_booth_category_id},
event_booth_pending_ids: {
operation: 'MULTI',
commands: [{
operation: 'REPLACE_WITH',
ids: state.data.event_booth_ids,
}],
}
}
}
});
});
}
});
return EventBoothConfiguratorFormController;
});
@@ -0,0 +1,21 @@
odoo.define('event_booth_sale.event_booth_configurator_form_view', function (require) {
'use strict';
const EventBoothConfiguratorFormController = require('event_booth_sale.event_booth_configurator_form_controller');
const FormView = require('web.FormView');
const viewRegistry = require('web.view_registry');
/**
* @see EventBoothConfiguratorFormController for more information.
*/
const EventBoothConfiguratorFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Controller: EventBoothConfiguratorFormController,
}),
});
viewRegistry.add('event_booth_configurator_form', EventBoothConfiguratorFormView);
return EventBoothConfiguratorFormView;
});
@@ -0,0 +1,149 @@
odoo.define('event_booth_sale.product_configurator', function (require) {
const ProductConfiguratorWidget = require('sale.product_configurator');
/**
* Extension of the ProductConfiguratorWidget to support event booth configuration.
* It opens when an event booth product_product is set.
*
* The event booth information include:
* - event_id
* - event_booth_category_id
* - event_booth_ids
*
*/
ProductConfiguratorWidget.include({
//--------------------------------------------------------------------------
// Overrides
//--------------------------------------------------------------------------
/**
* @returns {boolean}
*
* @override
* @private
*/
_isConfigurableLine: function () {
return this.recordData.is_event_booth || this._super.apply(this, arguments);
},
/**
* Opens the event booth configurator in 'edit' mode
*
* @override
* @private
*/
_onEditLineConfiguration: function () {
if (this.recordData.is_event_booth) {
const defaultValues = {
default_product_id: this.recordData.product_id.data.id,
default_sale_order_line_id: this.recordData.id || null
};
if (this.recordData.event_id) {
defaultValues.default_event_id = this.recordData.event_id.data.id;
}
if (this.recordData.event_booth_category_id) {
defaultValues.default_event_booth_category_id = this.recordData.event_booth_category_id.data.id;
}
if (this.recordData.event_booth_pending_ids) {
defaultValues.default_event_booth_ids = this.recordData.event_booth_pending_ids.res_ids;
}
this._openEventBoothConfigurator(defaultValues, this.dataPointID);
} else {
this._super.apply(this, arguments);
}
},
/**
* @param {integer} productId: product.product ID
* @param {String} dataPointId
* @returns {Promise<Boolean>} stopPropagation true if a suitable configurator has been found.
*
* @override
* @private
*/
_onProductChange: function (productId, dataPointId) {
return this._super.apply(this, arguments).then((stopPropagation) => {
if (stopPropagation) {
return Promise.resolve(true);
} else {
return this._isProductEventBooth(productId, dataPointId);
}
});
},
//--------------------------------------------------------------------------
// Private
//-------------------------------------------------------------------------
/**
* This method will check if the productId needs a configuration or not.
*
* @param {integer} productId: product.product ID
* @param {String} dataPointId
* @returns {Promise<Boolean>} stopPropagation true if the product is an event booth.
*
* @private
*/
_isProductEventBooth: function (productId, dataPointId) {
const self = this;
return this._rpc({
model: 'product.product',
method: 'read',
args: [productId, ['is_event_booth']],
}).then(function (result) {
if (result && result[0].is_event_booth) {
self._openEventBoothConfigurator({
default_product_id: productId
}, dataPointId);
return Promise.resolve(true);
}
return Promise.resolve(false);
});
},
/**
* Opens the event booth configurator allowing to configure the SO line with its informations.
*
* When the window is closed, configured values are used to trigger a 'field_changed'
* event to modify the current SO line.
*
* If the window is closed without providing the required values 'event_id', 'event_booth_category_id'
* and 'event_booth_pending_ids', the product_id field is cleaned.
*
* @param {Object} additionalContext: various "default_" values added to context when calling
* configuration wizard model;
* @param dataPointId
*
* @private
*/
_openEventBoothConfigurator: function (additionalContext, dataPointId) {
const self = this;
this.do_action('event_booth_sale.event_booth_configurator_action', {
additional_context: additionalContext,
on_close: function (result) {
if (result && !result.special) {
self.trigger_up('field_changed', {
changes: result.eventBoothConfiguration,
dataPointID: dataPointId,
onSuccess: function () {
self._onLineConfigured();
}
});
} else if (!self.recordData.event_id || !self.recordData.event_booth_pending_ids) {
self.trigger_up('field_changed', {
changes: {
product_id: false,
name: '',
},
dataPointID: dataPointId,
});
}
}
});
},
});
return ProductConfiguratorWidget;
});
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_event_internals
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.event_booth.tests.common import TestEventBoothCommon
from odoo.addons.event_sale.tests.common import TestEventSaleCommon
class TestEventBoothSaleCommon(TestEventBoothCommon, TestEventSaleCommon):
@classmethod
def setUpClass(cls):
super(TestEventBoothSaleCommon, cls).setUpClass()
cls.event_booth_product = cls.env['product.product'].create({
'name': 'Test Booth Product',
'description_sale': 'Mighty Booth Description',
'list_price': 20,
'is_event_booth': True,
'standard_price': 60.0,
'type': 'service',
})
(cls.event_booth_category_1 + cls.event_booth_category_2).write({
'product_id': cls.event_booth_product.id,
})
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo import Command
from odoo.addons.event_booth_sale.tests.common import TestEventBoothSaleCommon
from odoo.fields import Datetime as FieldsDatetime
from odoo.tests.common import users, Form
class TestEventData(TestEventBoothSaleCommon):
@users('user_eventmanager')
def test_event_configuration_booths_from_type(self):
""" Test data computation (related to booths) of event coming from its event.type template. """
# setup test records
event_type_nobooth = self.env['event.type'].create({
'name': 'No booth',
})
event_type_wbooths = self.env['event.type'].create({
'name': 'Using booths',
'event_type_booth_ids': [
Command.clear(),
Command.create({
'name': 'Standard Booth',
'booth_category_id': self.event_booth_category_1.id,
}),
Command.create({
'name': 'Premium Booth',
'booth_category_id': self.event_booth_category_2.id,
})
]
})
# no booth by default as no booths on type
event = self.env['event.event'].create({
'name': 'Event',
'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)),
'event_type_id': event_type_nobooth.id
})
self.assertEqual(event.event_booth_ids, self.env['event.booth'])
# manually create booths: ok
event.write({
'event_booth_ids': [
Command.create({
'name': 'Custom Standard Booth 1',
'booth_category_id': self.event_booth_category_1.id,
}),
Command.create({
'name': 'Custom Standard Booth 2',
'booth_category_id': self.event_booth_category_1.id,
})
]
})
self.assertEqual(event.event_booth_count, 2)
self.assertEqual(event.event_booth_count_available, 2)
self.assertEqual(event.event_booth_ids.product_id, self.event_booth_product)
# updating partner is independent from availability
event.event_booth_ids[1].write({'partner_id': self.event_customer.id})
self.assertEqual(event.event_booth_count, 2)
self.assertEqual(event.event_booth_count_available, 2)
self.assertEqual(event.event_booth_ids[1].message_partner_ids, self.event_customer)
# one booth is sold
event.event_booth_ids[1].write({'state': 'unavailable'})
self.assertEqual(event.event_booth_count, 2)
self.assertEqual(event.event_booth_count_available, 1)
# partner is reset: booth still unavailable but follower removed
event.event_booth_ids[1].write({'partner_id': False})
self.assertEqual(event.event_booth_count, 2)
self.assertEqual(event.event_booth_count_available, 1)
self.assertEqual(event.event_booth_ids[1].message_partner_ids, self.env['res.partner'])
# add group or the test will fail
self.user_eventmanager.write({'groups_id': [
(4, self.env.ref('sales_team.group_sale_salesman').id),
]})
# change event type to one using booths: include event type booths and keep reserved booths
event_form = Form(event)
event_form.event_type_id = event_type_wbooths
self.assertEqual(event_form.event_booth_count, 3)
self.assertEqual(
set(r['name'] for r in event_form.event_booth_ids._records),
set(('Custom Standard Booth 2', 'Standard Booth', 'Premium Booth')),
'Should keep booths with reservation, remove unused ones and add type ones'
)
event_form.save()
self.assertEqual(event.event_booth_count_available, 2)
self.assertEqual(event.event_booth_category_ids, self.event_booth_category_1 + self.event_booth_category_2)
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="event_booth_category_view_form" model="ir.ui.view">
<field name="name">event.booth.category.view.form.inherit.sale</field>
<field name="model">event.booth.category</field>
<field name="inherit_id" ref="event_booth.event_booth_category_view_form"/>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<group name="main" position="inside">
<group string="Booth Details">
<field name="product_id" context="{'default_is_event_booth': True, 'default_type': 'service'}"/>
<field name="price"/>
</group>
</group>
</field>
</record>
<record id="event_booth_category_view_tree" model="ir.ui.view">
<field name="name">event.booth.category.view.tree.inherit.sale</field>
<field name="model">event.booth.category</field>
<field name="inherit_id" ref="event_booth.event_booth_category_view_tree"/>
<field name="priority">3</field>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="product_id"/>
<field name="price"/>
</field>
</field>
</record>
</data></odoo>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="event_booth_registration_view_form" model="ir.ui.view">
<field name="name">event.booth.registration.view.form</field>
<field name="model">event.booth.registration</field>
<field name="arch" type="xml">
<form string="Booth Registration">
<sheet>
<div class="oe_title">
<h1><field name="partner_id"/></h1>
</div>
<group>
<group string="Details">
<field name="event_booth_id"/>
<field name="sale_order_line_id"/>
</group>
<group string="Contact">
<field name="contact_name"/>
<field name="contact_email"/>
<field name="contact_phone"/>
<field name="contact_mobile"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="event_booth_registration_view_tree" model="ir.ui.view">
<field name="name">event.booth.registration.view.tree</field>
<field name="model">event.booth.registration</field>
<field name="arch" type="xml">
<tree string="Booth Registration">
<field name="partner_id"/>
<field name="event_booth_id"/>
<field name="sale_order_line_id"/>
<field name="contact_name" optional="hide"/>
<field name="contact_email" optional="hide"/>
<field name="contact_phone" optional="hide"/>
<field name="contact_mobile" optional="hide"/>
</tree>
</field>
</record>
</data></odoo>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="event_booth_view_form_from_event" model="ir.ui.view">
<field name="name">event.booth.view.form.inherit.sale</field>
<field name="model">event.booth</field>
<field name="inherit_id" ref="event_booth.event_booth_view_form_from_event"/>
<field name="priority">10</field>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button name="action_view_sale_order" type="object" class="oe_stat_button"
icon="fa-usd" groups="sales_team.group_sale_salesman"
string="Sale Order" attrs="{'invisible': [('sale_order_id', '=', False)]}">
</button>
</div>
<div name="button_box" position="after">
<field name="is_paid" invisible="1"/>
<widget name="web_ribbon" title="Paid" attrs="{'invisible': [('is_paid', '=', False)]}"/>
</div>
<field name="booth_category_id" position="after">
<field name="product_id" attrs="{'invisible': [('booth_category_id', '=', False)]}"/>
<field name="price" attrs="{'invisible': [('booth_category_id', '=', False)]}"/>
</field>
<group name="renter" position="after">
<group name="sales" groups="base.group_no_one"
attrs="{'invisible': [('sale_order_line_id', '=', False)]}">
<field name="sale_order_id"/>
<field name="sale_order_line_id"/>
</group>
</group>
<xpath expr="//sheet" position="inside">
<notebook groups="base.group_no_one">
<page string="Registrations">
<field name="event_booth_registration_ids" readonly="1"/>
</page>
</notebook>
</xpath>
</field>
</record>
<record id="event_booth_view_tree" model="ir.ui.view">
<field name="name">event.booth.view.tree.inherit.sale</field>
<field name="model">event.booth</field>
<field name="inherit_id" ref="event_booth.event_booth_view_tree"/>
<field name="arch" type="xml">
<field name="booth_category_id" position="after">
<field name="price"/>
</field>
</field>
</record>
<record id="event_booth_view_search" model="ir.ui.view">
<field name="name">event.booth.view.search.inherit.sale</field>
<field name="model">event.booth</field>
<field name="inherit_id" ref="event_booth.event_booth_view_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="sale_order_id"/>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="event_type_booth_view_form_from_type" model="ir.ui.view">
<field name="name">event.booth.view.form.from.type.inherit.sale</field>
<field name="model">event.type.booth</field>
<field name="inherit_id" ref="event_booth.event_type_booth_view_form_from_type"/>
<field name="arch" type="xml">
<field name="booth_category_id" position="after">
<field name="product_id"/>
<field name="price"/>
</field>
</field>
</record>
<record id="event_type_booth_view_tree_from_type" model="ir.ui.view">
<field name="name">event.booth.view.tree.from.type.inherit.sale</field>
<field name="model">event.type.booth</field>
<field name="inherit_id" ref="event_booth.event_type_booth_view_tree_from_type"/>
<field name="arch" type="xml">
<field name="booth_category_id" position="after">
<field name="product_id"/>
<field name="price"/>
</field>
</field>
</record>
</data></odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="product_template_view_form" model="ir.ui.view">
<field name="name">product.template.view.form.inherit.event.booth.sale</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<field name="event_ok" position="after">
<field name="is_event_booth"/>
</field>
</field>
</record>
</data></odoo>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="sale_order_view_form" model="ir.ui.view">
<field name="name">sale.order.view.form.inherit.event.booth.sale</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_attendee_list']" position="after">
<field name="event_booth_count" invisible="1"/>
<button name="action_view_booth_list" type="object" class="oe_stat_button" icon="fa-university"
attrs="{'invisible': [('event_booth_count', '=', 0)]}">
<field name="event_booth_count" widget="statinfo" string="Booths"/>
</button>
</xpath>
<xpath expr="//field[@name='order_line']//tree//field[@name='event_ticket_id']" position="after">
<field name="is_event_booth" optional="hide"/>
<field name="event_booth_category_id" optional="hide"/>
<field name="event_booth_pending_ids" optional="hide"/>
</xpath>
<xpath expr="//field[@name='order_line']//tree//field[@name='product_uom_qty']" position="attributes">
<attribute name="attrs">{'readonly': [('is_event_booth', '=', True)]}</attribute>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_booth_configurator
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
from odoo.exceptions import ValidationError
class EventBoothConfigurator(models.TransientModel):
_name = 'event.booth.configurator'
_description = 'Event Booth Configurator'
product_id = fields.Many2one('product.product', string='Product', readonly=True)
sale_order_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', readonly=True)
event_id = fields.Many2one('event.event', string='Event', required=True)
event_booth_category_available_ids = fields.Many2many(related='event_id.event_booth_category_available_ids', readonly=True)
event_booth_category_id = fields.Many2one(
'event.booth.category', string='Booth Category', required=True,
compute='_compute_event_booth_category_id', readonly=False, store=True)
event_booth_ids = fields.Many2many(
'event.booth', string='Booth', required=True,
compute='_compute_event_booth_ids', readonly=False, store=True)
@api.depends('event_id')
def _compute_event_booth_category_id(self):
self.event_booth_category_id = False
@api.depends('event_id', 'event_booth_category_id')
def _compute_event_booth_ids(self):
self.event_booth_ids = False
@api.constrains('event_booth_ids')
def _check_if_no_booth_ids(self):
if any(not wizard.event_booth_ids for wizard in self):
raise ValidationError(_('You have to select at least one booth.'))
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo><data>
<record id="event_booth_configurator_view_form" model="ir.ui.view">
<field name="name">event.booth.configurator.view.form</field>
<field name="model">event.booth.configurator</field>
<field name="arch" type="xml">
<form js_class="event_booth_configurator_form">
<group>
<field name="product_id" invisible="1"/>
<field name="event_id" options="{'no_open': True, 'no_create': True}"
domain="[
('event_booth_ids.product_id', '=', product_id),
('is_finished', '=', False)
]"/>
<field name="event_booth_category_available_ids" invisible="1"/>
<field name="event_booth_category_id" options="{'no_open': True, 'no_create': True}"
attrs="{'invisible': [('event_id', '=', False)]}"
domain="[('id', 'in', event_booth_category_available_ids)]"/>
<field name="event_booth_ids" options="{'no_open': True, 'no_create': True}"
widget="many2many_checkboxes" attrs="{'invisible': [('event_booth_category_id', '=', False)]}"
domain="[
('event_id', '=', event_id),
('booth_category_id', '=', event_booth_category_id),
('product_id', '=', product_id),
('state', '=', 'available')
]"/>
</group>
<footer>
<button string="Ok" class="btn-primary" special="save"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="event_booth_configurator_action" model="ir.actions.act_window">
<field name="name">Configure an event booth</field>
<field name="res_model">event.booth.configurator</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data></odoo>