summaryrefslogtreecommitdiff
path: root/addons/purchase_product_matrix/static
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/purchase_product_matrix/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/purchase_product_matrix/static')
-rw-r--r--addons/purchase_product_matrix/static/src/js/product_matrix_configurator.js183
-rw-r--r--addons/purchase_product_matrix/static/tests/section_and_note_widget_tests.js260
-rw-r--r--addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js111
3 files changed, 554 insertions, 0 deletions
diff --git a/addons/purchase_product_matrix/static/src/js/product_matrix_configurator.js b/addons/purchase_product_matrix/static/src/js/product_matrix_configurator.js
new file mode 100644
index 00000000..907b2d4b
--- /dev/null
+++ b/addons/purchase_product_matrix/static/src/js/product_matrix_configurator.js
@@ -0,0 +1,183 @@
+odoo.define('purchase.product_matrix_configurator', function (require) {
+
+var relationalFields = require('web.relational_fields');
+var FieldsRegistry = require('web.field_registry');
+var core = require('web.core');
+var _t = core._t;
+
+/**
+ * The purchase.product_matrix_configurator widget is a widget extending FieldMany2One
+ * It triggers the opening of the matrix edition when the product has multiple variants.
+ *
+ *
+ * !!! WARNING !!!
+ *
+ * This widget is only designed for Purchase Order Lines.
+ * !!! It should only be used on a product_template field !!!
+ */
+var MatrixConfiguratorWidget = relationalFields.FieldMany2One.extend({
+ events: _.extend({}, relationalFields.FieldMany2One.prototype.events, {
+ 'click .o_edit_product_configuration': '_onEditProductConfiguration'
+ }),
+
+ /**
+ * @override
+ */
+ _render: function () {
+ this._super.apply(this, arguments);
+ if (this.mode === 'edit' && this.value &&
+ (this._isConfigurableProduct())) {
+ this._addProductLinkButton();
+ this._addConfigurationEditButton();
+ } else if (this.mode === 'edit' && this.value) {
+ this._addProductLinkButton();
+ } else {
+ this.$('.o_edit_product_configuration').hide();
+ }
+ },
+
+ /**
+ * Add button linking to product_id/product_template_id form.
+ */
+ _addProductLinkButton: function () {
+ if (this.$('.o_external_button').length === 0) {
+ var $productLinkButton = $('<button>', {
+ type: 'button',
+ class: 'fa fa-external-link btn btn-secondary o_external_button',
+ tabindex: '-1',
+ draggable: false,
+ 'aria-label': _t('External Link'),
+ title: _t('External Link')
+ });
+
+ var $inputDropdown = this.$('.o_input_dropdown');
+ $inputDropdown.after($productLinkButton);
+ }
+ },
+
+ /**
+ * If current product is configurable,
+ * Show edit button (in Edit Mode) after the product/product_template
+ */
+ _addConfigurationEditButton: function () {
+ var $inputDropdown = this.$('.o_input_dropdown');
+
+ if ($inputDropdown.length !== 0 &&
+ this.$('.o_edit_product_configuration').length === 0) {
+ var $editConfigurationButton = $('<button>', {
+ type: 'button',
+ class: 'fa fa-pencil btn btn-secondary o_edit_product_configuration',
+ tabindex: '-1',
+ draggable: false,
+ 'aria-label': _t('Edit Configuration'),
+ title: _t('Edit Configuration')
+ });
+
+ $inputDropdown.after($editConfigurationButton);
+ }
+ },
+
+ /**
+ * Hook to override with _onEditProductConfiguration
+ * to know if edit pencil button has to be put next to the field
+ *
+ * @private
+ */
+ _isConfigurableProduct: function () {
+ return this.recordData.is_configurable_product;
+ },
+
+ /**
+ * Override catching changes on product_id or product_template_id.
+ * Calls _onTemplateChange in case of product_template change.
+ * Calls _onProductChange in case of product change.
+ * Shouldn't be overridden by product configurators
+ * or only to setup some data for further computation
+ * before calling super.
+ *
+ * @override
+ */
+ reset: async function (record, ev) {
+ await this._super(...arguments);
+ if (ev && ev.target === this && ev.data.changes && ev.data.changes.product_template_id && record.data.product_template_id.data.id) {
+ this._onTemplateChange(record.data.product_template_id.data.id, ev.data.dataPointID);
+ }
+ },
+
+ /**
+ * Hook for product_template based configurators
+ * (product configurator, matrix, ...).
+ *
+ * @param {integer} productTemplateId
+ * @param {String} dataPointID
+ *
+ * @private
+ */
+ _onTemplateChange: function (productTemplateId, dataPointId) {
+ var self = this;
+ this._rpc({
+ model: 'product.template',
+ method: 'get_single_product_variant',
+ args: [
+ productTemplateId
+ ]
+ }).then(function (result) {
+ if (result.product_id) {
+ self.trigger_up('field_changed', {
+ dataPointID: dataPointId,
+ changes: {
+ product_id: {id: result.product_id},
+ },
+ });
+ } else {
+ self._openMatrix(productTemplateId, dataPointId, false);
+ }
+ });
+ },
+
+ /**
+ * Hook for editing a configured line.
+ * The button triggering this function is only shown in Edit mode,
+ * when _isConfigurableProduct is True.
+ *
+ * @private
+ */
+ _onEditProductConfiguration: function () {
+ if (this.recordData.is_configurable_product) {
+ this._openMatrix(this.recordData.product_template_id.data.id, this.dataPointID, true);
+ }
+ },
+
+ _openMatrix: function (productTemplateId, dataPointId, edit) {
+ var attribs = edit ? this._getPTAVS() : [];
+ this.trigger_up('open_matrix', {
+ product_template_id: productTemplateId,
+ model: 'purchase.order',
+ dataPointId: dataPointId,
+ edit: edit,
+ editedCellAttributes: attribs,
+ // used to focus the cell representing the line on which the pencil was clicked.
+ });
+ },
+
+ /**
+ * Returns the list of attribute ids (product.template.attribute.value)
+ * from the current POLine.
+ */
+ _getPTAVS: function () {
+ var PTAVSIDS = [];
+ _.each(this.recordData.product_no_variant_attribute_value_ids.res_ids, function (id) {
+ PTAVSIDS.push(id);
+ });
+ _.each(this.recordData.product_template_attribute_value_ids.res_ids, function (id) {
+ PTAVSIDS.push(id);
+ });
+ return PTAVSIDS.sort(function (a, b) {return a - b;});
+ }
+});
+
+FieldsRegistry.add('matrix_configurator', MatrixConfiguratorWidget);
+
+return MatrixConfiguratorWidget;
+
+});
diff --git a/addons/purchase_product_matrix/static/tests/section_and_note_widget_tests.js b/addons/purchase_product_matrix/static/tests/section_and_note_widget_tests.js
new file mode 100644
index 00000000..7dfc29e6
--- /dev/null
+++ b/addons/purchase_product_matrix/static/tests/section_and_note_widget_tests.js
@@ -0,0 +1,260 @@
+odoo.define('purchase_product_matrix.section_and_note_widget_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+var createView = testUtils.createView;
+
+function getGrid(product) {
+ return JSON.stringify({
+ header: [{name: product.name}, {name: "M"}, {name: "L"}],
+ matrix: [[
+ {name: "Men"},
+ {ptav_ids: [10, 13], qty: 0, is_possible_combination: true},
+ {ptav_ids: [11, 13], qty: 0, is_possible_combination: true},
+ ], [
+ {name: "Women"},
+ {ptav_ids: [10, 14], qty: 0, is_possible_combination: true},
+ {ptav_ids: [11, 14], qty: 0, is_possible_combination: true},
+ ]],
+ });
+}
+
+QUnit.module('section_and_note: purchase_product_matrix', {
+ beforeEach: function () {
+ this.data = {
+ purchase_order: {
+ fields: {
+ order_line_ids: {
+ string: "Lines",
+ type: 'one2many',
+ relation: 'order_line',
+ relation_field: 'order_id',
+ },
+ grid: {string: "Grid", type: 'char'},
+ grid_product_tmpl_id: {string: "Grid Product", type: 'many2one', relation: 'product'},
+ },
+ onchanges: {
+ grid_product_tmpl_id: (obj) => {
+ const product = this.data.product.records.find((p) => {
+ return p.id === obj.grid_product_tmpl_id;
+ });
+ obj.grid = product ? getGrid(product) : false;
+ },
+ grid: () => {},
+ },
+ },
+ order_line: {
+ fields: {
+ order_id: {string: "Invoice", type: 'many2one', relation: 'invoice'},
+ product_template_id: {string: "Product", type: 'many2one', relation: 'product'},
+ },
+ },
+ product: {
+ fields: {
+ name: {string: "Name", type: 'char'},
+ },
+ records: [
+ {id: 1, name: 'A configurable product'},
+ ],
+ },
+ };
+ },
+}, function () {
+ QUnit.test('can configure a product with the matrix', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'purchase_order',
+ data: this.data,
+ arch: `<form>
+ <field name="grid" invisible="1"/>
+ <field name="grid_product_tmpl_id" invisible="1"/>
+ <field name="order_line_ids" widget="section_and_note_one2many">
+ <tree editable="bottom">
+ <field name="product_template_id" widget="matrix_configurator"/>
+ </tree>
+ </field>
+ </form>`,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange' && args.args[2] === 'grid') {
+ // should trigger an onchange on the grid field and let the
+ // business logic create rows according to the matrix content
+ assert.deepEqual(args.args[1].grid, JSON.stringify({
+ changes: [{qty: 2, ptav_ids: [10, 13]}, {qty: 3, ptav_ids: [11, 14]}],
+ product_template_id: 1,
+ }));
+ }
+ if (args.method === 'get_single_product_variant') {
+ assert.strictEqual(args.args[0], 1);
+ return Promise.resolve({mode: 'matrix'});
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click('.o_field_x2many_list_row_add a');
+ await testUtils.fields.many2one.searchAndClickItem("product_template_id", {item: 'configurable'});
+
+ assert.containsOnce(document.body, '.modal .o_product_variant_matrix');
+ const $matrix = $('.modal .o_product_variant_matrix');
+ assert.strictEqual($matrix.text().replace(/[\n\r\s\u00a0]+/g, ' '),
+ ' A configurable product M L Men Women ');
+
+ // select 2 M-Men and 3 L-Women
+ await testUtils.fields.editInput($matrix.find('.o_matrix_input[ptav_ids="10,13"]'), '2');
+ await testUtils.fields.editInput($matrix.find('.o_matrix_input[ptav_ids="11,14"]'), '3');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ form.destroy();
+ });
+
+ QUnit.test('can open the matrix twice with 2 different products', async function (assert) {
+ assert.expect(5);
+
+ this.data.product.records.push({ id: 101, name: "Product A" });
+ this.data.product.records.push({ id: 102, name: "Product B" });
+
+ const form = await createView({
+ View: FormView,
+ model: 'purchase_order',
+ data: this.data,
+ arch: `<form>
+ <field name="grid" invisible="1"/>
+ <field name="grid_product_tmpl_id" invisible="1"/>
+ <field name="order_line_ids" widget="section_and_note_one2many">
+ <tree editable="bottom">
+ <field name="product_template_id" widget="matrix_configurator"/>
+ </tree>
+ </field>
+ </form>`,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange' && args.args[2] === 'grid') {
+ // should trigger an onchange on the grid field and let the
+ // business logic create rows according to the matrix content
+ assert.deepEqual(args.args[1].grid, JSON.stringify({
+ changes: [{qty: 2, ptav_ids: [10, 13]}, {qty: 3, ptav_ids: [11, 14]}],
+ product_template_id: 102,
+ }));
+ }
+ if (args.method === 'get_single_product_variant') {
+ return Promise.resolve({mode: 'matrix'});
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // open the matrix with "Product A" and close it
+ await testUtils.dom.click('.o_field_x2many_list_row_add a');
+ await testUtils.fields.many2one.searchAndClickItem("product_template_id", {item: 'Product A'});
+
+ assert.containsOnce(document.body, '.modal .o_product_variant_matrix');
+ let $matrix = $('.modal .o_product_variant_matrix');
+ assert.strictEqual($matrix.text().replace(/[\n\r\s\u00a0]+/g, ' '),
+ ' Product A M L Men Women ');
+
+ await testUtils.dom.click($('.modal .modal-footer .btn-secondary')); // close
+
+ // re-open the matrix with "Product B"
+ await testUtils.dom.click('.o_field_x2many_list_row_add a');
+ await testUtils.fields.many2one.searchAndClickItem("product_template_id", {item: 'Product B'});
+
+ assert.containsOnce(document.body, '.modal .o_product_variant_matrix');
+ $matrix = $('.modal .o_product_variant_matrix');
+ assert.strictEqual($matrix.text().replace(/[\n\r\s\u00a0]+/g, ' '),
+ ' Product B M L Men Women ');
+
+ // select 2 M-Men and 3 L-Women
+ await testUtils.fields.editInput($matrix.find('.o_matrix_input[ptav_ids="10,13"]'), '2');
+ await testUtils.fields.editInput($matrix.find('.o_matrix_input[ptav_ids="11,14"]'), '3');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ form.destroy();
+ });
+
+ QUnit.test('_onTemplateChange is executed after product template quick create', async function (assert) {
+ assert.expect(1);
+
+ let created_product_template;
+
+ const form = await createView({
+ View: FormView,
+ model: 'purchase_order',
+ data: this.data,
+ arch: `<form>
+ <field name="order_line_ids" widget="section_and_note_one2many">
+ <tree editable="bottom">
+ <field name="product_template_id" widget="matrix_configurator"/>
+ </tree>
+ </field>
+ </form>`,
+ async mockRPC(route, args) {
+ if (route === '/web/dataset/call_kw/product.template/get_single_product_variant') {
+ assert.strictEqual(args.args[0], created_product_template[0]);
+ }
+
+ const result = await this._super(...arguments);
+ if (args.method === 'name_create') {
+ created_product_template = result;
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click('.o_field_x2many_list_row_add a');
+ await testUtils.fields.many2one.searchAndClickItem("product_template_id", {search: 'new product'});
+
+ form.destroy();
+ });
+
+ QUnit.test('drag and drop rows containing matrix_configurator many2one', async function (assert) {
+ assert.expect(4);
+
+ this.data.order_line.fields.sequence = {string: "Sequence", type: 'number'};
+ this.data.order_line.fields.order_id.relation = 'purchase_order';
+ this.data.purchase_order.records = [
+ {id: 1, order_line_ids: [1, 2]}
+ ];
+ this.data.order_line.records = [
+ {id: 1, sequence: 4, product_template_id: 1, order_id: 1},
+ {id: 2, sequence: 14, product_template_id: 2, order_id: 1},
+ ];
+ this.data.product.records.push(
+ {id: 1, name: "Chair"},
+ {id: 2, name: "Table"}
+ );
+
+ const form = await createView({
+ View: FormView,
+ model: 'purchase_order',
+ data: this.data,
+ arch: `<form>
+ <field name="order_line_ids" widget="section_and_note_one2many">
+ <tree editable="bottom">
+ <field name="sequence" widget="handle"/>
+ <field name="product_template_id" widget="matrix_configurator"/>
+ </tree>
+ </field>
+ </form>`,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsN(form, '.o_data_row', 2);
+ assert.strictEqual(form.$('.o_data_row').text(), 'ChairTable');
+ assert.containsN(form, '.o_data_row .o_row_handle', 2);
+
+ // move first row below second
+ const $firstHandle = form.$('.o_data_row:nth(0) .o_row_handle');
+ const $secondHandle = form.$('.o_data_row:nth(1) .o_row_handle');
+ await testUtils.dom.dragAndDrop($firstHandle, $secondHandle);
+
+ assert.strictEqual(form.$('.o_data_row').text(), 'TableChair');
+
+ form.destroy();
+ });
+});
+});
diff --git a/addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js b/addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js
new file mode 100644
index 00000000..010c0b18
--- /dev/null
+++ b/addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js
@@ -0,0 +1,111 @@
+odoo.define('purchase_product_matrix.purchase_matrix_tour', function (require) {
+"use strict";
+
+var tour = require('web_tour.tour');
+
+tour.register('purchase_matrix_tour', {
+ url: "/web",
+ test: true,
+}, [tour.stepUtils.showAppsMenuItem(), {
+ trigger: '.o_app[data-menu-xmlid="purchase.menu_purchase_root"]',
+}, {
+ trigger: ".o_list_button_add",
+ extra_trigger: ".o_purchase_order"
+}, {
+ trigger: "a:contains('Add a product')"
+}, {
+ trigger: 'div[name="product_template_id"] input',
+ run: function () {
+ var $input = $('div[name="product_template_id"] input');
+ $input.click();
+ $input.val('Matrix');
+ var keyDownEvent = jQuery.Event("keydown");
+ keyDownEvent.which = 42;
+ $input.trigger(keyDownEvent);
+ }
+}, {
+ trigger: 'ul.ui-autocomplete a:contains("Matrix")',
+ run: 'click'
+}, {
+ trigger: '.o_product_variant_matrix',
+ run: function () {
+ // fill the whole matrix with 1's
+ $('.o_matrix_input').val(1);
+ }
+}, {
+ trigger: 'span:contains("Confirm")',
+ run: 'click'
+}, {
+ trigger: ".o_form_editable .o_field_many2one[name='partner_id'] input",
+ extra_trigger: ".o_purchase_order",
+ run: 'text Agrolait'
+}, {
+ trigger: ".ui-menu-item > a",
+ auto: true,
+ in_modal: false,
+}, {
+ trigger: '.o_form_button_save:contains("Save")',
+ run: 'click' // SAVE Sales Order.
+},
+// Open the matrix through the pencil button next to the product in line edit mode.
+{
+ trigger: '.o_form_button_edit:contains("Edit")',
+ run: 'click' // Edit Sales Order.
+}, {
+ trigger: 'span:contains("Matrix (PAV11, PAV22, PAV31)\nPA4: PAV41")',
+ extra_trigger: '.o_form_editable',
+ run: 'click'
+}, {
+ trigger: '.o_edit_product_configuration',
+ run: 'click' // edit the matrix
+}, {
+ trigger: '.o_product_variant_matrix',
+ run: function () {
+ // update some of the matrix values.
+ $('.o_matrix_input').slice(8, 16).val(4);
+ } // set the qty to 4 for half of the matrix products.
+}, {
+ trigger: 'span:contains("Confirm")',
+ run: 'click' // apply the matrix
+}, {
+ trigger: '.o_form_button_save:contains("Save")',
+ extra_trigger: '.o_field_cell.o_data_cell.o_list_number:contains("4.00")',
+ run: 'click' // SAVE Sales Order, after matrix has been applied (extra_trigger).
+}, {
+ trigger: '.o_form_button_edit:contains("Edit")',
+ run: 'click' // Edit Sales Order.
+},
+// Ensures the matrix is opened with the values, when adding the same product.
+{
+ trigger: "a:contains('Add a product')"
+}, {
+ trigger: 'div[name="product_template_id"] input',
+ run: function () {
+ var $input = $('div[name="product_template_id"] input');
+ $input.click();
+ $input.val('Matrix');
+ var keyDownEvent = jQuery.Event("keydown");
+ keyDownEvent.which = 42;
+ $input.trigger(keyDownEvent);
+ }
+}, {
+ trigger: 'ul.ui-autocomplete a:contains("Matrix")',
+ run: 'click'
+}, {
+ trigger: "input[value='4']",
+ run: function () {
+ // update some values of the matrix
+ $("input[value='4']").slice(0, 4).val(8.2);
+ }
+}, {
+ trigger: 'span:contains("Confirm")',
+ run: 'click' // apply the matrix
+}, {
+ trigger: '.o_form_button_save:contains("Save")',
+ extra_trigger: '.o_field_cell.o_data_cell.o_list_number:contains("8.20")',
+ run: 'click' // SAVE Sales Order, after matrix has been applied (extra_trigger).
+},
+]);
+
+
+});