Skip to content
Snippets Groups Projects
Commit 10255e54 authored by Victor Feyens's avatar Victor Feyens
Browse files

[REF] event_sale: convert configurator to OWL

And stop relying on dumb event_ok field, when the product_type already 
holds the information (recently moved from sale_stock to sale).

Task-2918791

Part-of: odoo/odoo#101304
parent 5563c5dd
Branches
Tags
No related merge requests found
......@@ -48,9 +48,6 @@ this event.
'web.assets_tests': [
'event_sale/static/tests/tours/**/*',
],
'web.qunit_suite_tests': [
'event_sale/static/tests/event_configurator.test.js',
],
},
'license': 'LGPL-3',
}
......@@ -21,12 +21,14 @@ class SaleOrder(models.Model):
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):
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)
if any(line.event_ok for line in so.order_line):
return self.env['ir.actions.act_window'] \
.with_context(default_sale_order_id=so.id) \
._for_xml_id('event_sale.action_sale_order_event_registration')
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):
......@@ -68,6 +70,7 @@ class SaleOrderLine(models.Model):
'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')
......@@ -89,7 +92,9 @@ class SaleOrderLine(models.Model):
RegistrationSudo = self.env['event.registration'].sudo()
registrations = RegistrationSudo.search([('sale_order_line_id', 'in', self.ids)])
registrations_vals = []
for so_line in self.filtered('event_ok'):
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()
......
......@@ -28,8 +28,8 @@ class EventConfiguratorRecord extends Record {
await super.save(options);
this.model.action.doAction({type: 'ir.actions.act_window_close', infos: {
eventConfiguration: {
event_id: {id: this.data.event_id[0]},
event_ticket_id: {id: this.data.event_ticket_id[0]}
event_id: this.data.event_id,
event_ticket_id: this.data.event_ticket_id,
}
}});
}
......
odoo.define('event_sale.product_configurator', function (require) {
var ProductConfiguratorWidget = require('sale.product_configurator');
/**
* Extension of the ProductConfiguratorWidget to support event product configuration.
* It opens when an event product_product is set.
*
* The event information include:
* - event_id
* - event_ticket_id
*
*/
ProductConfiguratorWidget.include({
/**
* @returns {boolean}
*
* @override
* @private
*/
_isConfigurableLine: function () {
return this.recordData.event_ok || this._super.apply(this, arguments);
},
/**
* @param {integer} productId
* @param {String} dataPointID
* @returns {Promise<Boolean>} stopPropagation true if a suitable configurator has been found.
*
* @override
* @private
*/
_onProductChange: function (productId, dataPointId) {
var self = this;
return this._super.apply(this, arguments).then(function (stopPropagation) {
if (stopPropagation || productId === undefined) {
return Promise.resolve(true);
} else {
return self._checkForEvent(productId, dataPointId);
}
});
},
/**
* This method will check if the productId needs configuration or not:
*
* @param {integer} productId
* @param {string} dataPointID
* @returns {Promise<Boolean>} stopPropagation true if the product is an event ticket.
*
* @private
*/
_checkForEvent: function (productId, dataPointId) {
var self = this;
return this._rpc({
model: 'product.product',
method: 'read',
args: [productId, ['detailed_type']],
}).then(function (result) {
if (Array.isArray(result) && result.length && result[0].detailed_type === 'event') {
self._openEventConfigurator({
default_product_id: productId
},
dataPointId
);
return Promise.resolve(true);
}
return Promise.resolve(false);
});
},
/**
* Opens the event configurator in 'edit' mode.
*
* @override
* @private
*/
_onEditLineConfiguration: function () {
if (this.recordData.event_ok) {
var defaultValues = {
default_product_id: this.recordData.product_id.data.id
};
if (this.recordData.event_id) {
defaultValues.default_event_id = this.recordData.event_id.data.id;
}
if (this.recordData.event_ticket_id) {
defaultValues.default_event_ticket_id = this.recordData.event_ticket_id.data.id;
}
this._openEventConfigurator(defaultValues, this.dataPointID);
} else {
this._super.apply(this, arguments);
}
},
/**
* Opens the event configurator to allow configuring the SO line with events information.
*
* 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' and
* 'event_ticket_id', the product_id field is cleaned.
*
* @param {Object} data various "default_" values
* @param {string} dataPointId
*
* @private
*/
_openEventConfigurator: function (data, dataPointId) {
var self = this;
this.do_action('event_sale.event_configurator_action', {
additional_context: data,
on_close: function (result) {
if (result && !result.special) {
self.trigger_up('field_changed', {
dataPointID: dataPointId,
changes: result.eventConfiguration,
onSuccess: function () {
// Call post-init function.
self._onLineConfigured();
}
});
} else {
if (!self.recordData.event_id || !self.recordData.event_ticket_id) {
self.trigger_up('field_changed', {
dataPointID: dataPointId,
changes: {
product_id: false,
name: ''
},
});
}
}
}
});
}
});
return ProductConfiguratorWidget;
});
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { SaleOrderLineProductField } from '@sale/js/sale_product_field';
patch(SaleOrderLineProductField.prototype, 'event_sale', {
async _onProductUpdate() {
this._super(...arguments);
if (this.props.record.data.product_type === 'event') {
this._openEventConfigurator();
}
},
_editLineConfiguration() {
this._super(...arguments);
if (this.props.record.data.product_type === 'event') {
this._openEventConfigurator();
}
},
get isConfigurableLine() {
return this._super(...arguments) || Boolean(this.props.record.data.event_ticket_id);
},
async _openEventConfigurator() {
let actionContext = {
'default_product_id': this.props.record.data.product_id[0],
};
if (this.props.record.data.event_id) {
actionContext.default_event_id = this.props.record.data.event_id[0];
}
if (this.props.record.data.event_ticket_id) {
actionContext.default_event_ticket_id = this.props.record.data.event_ticket_id[0];
}
this.action.doAction(
'event_sale.event_configurator_action',
{
additionalContext: actionContext,
onClose: async (closeInfo) => {
if (!closeInfo || closeInfo.special) {
// wizard popup closed or 'Cancel' button triggered
if (!this.props.record.data.event_ticket_id) {
// remove product if event configuration was cancelled.
this.props.record.update({
[this.props.name]: undefined,
});
}
} else {
const eventConfiguration = closeInfo.eventConfiguration;
this.props.record.update({
'event_id': eventConfiguration.event_id,
'event_ticket_id': eventConfiguration.event_ticket_id,
});
}
}
}
);
},
});
odoo.define('event.configurator.tests', function (require) {
"use strict";
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
var getArch = function (){
return '<form>' +
'<sheet>' +
'<field name="sale_order_line" widget="section_and_note_one2many">' +
'<tree editable="top"><control>' +
'<create string="Add a product"/>' +
'<create string="Add a section" context="{\'default_display_type\': \'line_section\'}"/>' +
'<create string="Add a note" context="{\'default_display_type\': \'line_note\'}"/>' +
'</control>' +
'<field name="product_id" widget="product_configurator" />' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>';
};
QUnit.module('Event Configurator', {
beforeEach: function () {
this.data = {
'product.product': {
fields: {
id: {type: 'integer'},
detailed_type: {type: 'selection'},
rent_ok: {type: 'boolean'}//sale_rental purposes
},
records: [{
id: 1,
display_name: "Customizable Event",
detailed_type: 'event',
rent_ok: false//sale_rental purposes
}, {
id: 2,
display_name: "Desk",
detailed_type: 'service',
rent_ok: false//sale_rental purposes
}]
},
sale_order: {
fields: {
id: {type: 'integer'},
sale_order_line: {
string: 'lines',
type: 'one2many',
relation: 'sale_order_line'
},
}
},
sale_order_line: {
fields: {
product_id: {
string: 'product',
type: 'many2one',
relation: 'product.product'
},
event_ok: {type: 'boolean'},
rent_ok: {type: 'boolean'},//sale_rental purposes
event_id: {
string: 'event',
type: 'many2one',
relation: 'event'
},
event_ticket_id: {
string: 'event_ticket',
type: 'many2one',
relation: 'event_ticket'
}
}
}
};
}
}, function (){
QUnit.test('Select a regular product and verify that the event configurator is not opened', async function (assert) {
// When the module event_booth_sale is installed, it will also call the rpc: read/detailed_type when selecting a product.
// This means that 2 asserts are done when this module is installed.
// Note that: this test will fail if the module event_booth_sale is not installed.
assert.expect(2);
var form = await createView({
View: FormView,
model: 'sale_order',
data: this.data,
arch: getArch(),
mockRPC: function (route, params) {
if (params.method === 'read' && params.args[1][0] === 'detailed_type') {
assert.ok(true);
return Promise.resolve([{detailed_type: 'service'}]);
}
return this._super.apply(this, arguments);
},
intercepts: {
do_action: function (ev) {
if (ev.data.action === 'event_sale.event_configurator_action') {
assert.ok(false, "Should not execute the configure action");
}
},
}
});
await testUtils.dom.click(form.$("a:contains('Add a product')"));
await testUtils.fields.many2one.searchAndClickItem("product_id", {item: 'Desk'})
form.destroy();
});
QUnit.test('Select a configurable event and verify that the event configurator is opened', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'sale_order',
data: this.data,
arch: getArch(),
mockRPC: function (route, params) {
if (params.method === 'read' && params.args[1][0] === 'detailed_type') {
assert.ok(true);
return Promise.resolve([{detailed_type: 'event'}]);
}
return this._super.apply(this, arguments);
},
intercepts: {
do_action: function (ev) {
if (ev.data.action === 'event_sale.event_configurator_action') {
assert.ok(true);
}
},
}
});
await testUtils.dom.click(form.$("a:contains('Add a product')"));
await testUtils.fields.many2one.searchAndClickItem("product_id", {item: 'Customizable Event'});
form.destroy();
});
});
});
......@@ -42,24 +42,23 @@ tour.register('event_configurator_tour', {
}, {
trigger: '.o_event_sale_js_event_configurator_ok'
}, {
trigger: 'textarea[name="name"]',
run: function () {
var $textarea = $('textarea[name="name"]');
if ($textarea.val().includes('Design Fair Los Angeles') && $textarea.val().includes('VIP')) {
$textarea.addClass('tour_success');
}
}
}, {
trigger: 'textarea[name="name"].tour_success',
trigger: "td[name='name'][data-tooltip*='VIP']",
run: function () {} // check
}, {
trigger: 'ul.nav a:contains("Order Lines")',
run: 'click'
}, {
content: "search the partner",
trigger: 'div[name="partner_id"] input',
run: 'text Azure'
}, {
content: "select the partner",
trigger: 'ul.ui-autocomplete > li > a:contains(Azure)',
}, {
trigger: 'td:contains("Event")',
run: 'click'
}, {
trigger: '.o_edit_product_configuration'
trigger: 'button.fa-pencil'
}, {
trigger: 'div[name="event_ticket_id"] input',
run: 'click'
......@@ -70,17 +69,9 @@ tour.register('event_configurator_tour', {
}, {
trigger: '.o_event_sale_js_event_configurator_ok'
}, {
trigger: 'textarea[name="name"]',
run: function () {
var $textarea = $('textarea[name="name"]');
if ($textarea.val().includes('Standard')) {
$textarea.addClass('tour_success_2');
}
}
}, {
trigger: 'textarea[name="name"].tour_success_2',
trigger: "td[name='name'][data-tooltip*='Standard']",
run: function () {} // check
}, ...tour.stepUtils.discardForm()
}, ...tour.stepUtils.saveForm()
]);
});
......@@ -19,7 +19,7 @@
('date_end','&gt;=',time.strftime('%Y-%m-%d 00:00:00')),
'|', ('company_id', '=', False), ('company_id', '=', parent.company_id)
]"
attrs="{'invisible': [('event_ok', '=', False)], 'required': [('event_ok', '!=', False)]}"
attrs="{'invisible': [('product_type', '!=', 'event')], 'required': [('product_type', '=', 'event')]}"
options="{'no_open': True, 'no_create': True}"
/>
<field
......@@ -29,15 +29,13 @@
'|', ('company_id', '=', False), ('company_id', '=', parent.company_id)
]"
attrs="{
'invisible': ['|', ('event_ok', '=', False), ('event_id', '=', False)],
'required': [('event_ok', '!=', False), ('event_id', '!=', False)],
'invisible': ['|', ('product_type', '!=', 'event'), ('event_id', '=', False)],
'required': [('product_type', '=', 'event'), ('event_id', '!=', False)],
}"
options="{'no_open': True, 'no_create': True}"
/>
<field name="event_ok" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']//tree//field[@name='product_template_id']" position="after">
<field name="event_ok" invisible="1" />
<field name="event_id" optional="hide" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="event_ticket_id" optional="hide" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
</xpath>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment