[IMP] purchase(_*): convert order view to OWL

Task-2918791

X-original-commit: b2457fbc45aa8a2714c92ea48333fa8108acf405
Part-of: odoo/odoo#101352
This commit is contained in:
Victor Feyens
2022-09-28 11:14:36 +02:00
parent da71f04db3
commit 5f621c9fe2
8 changed files with 220 additions and 520 deletions
+1 -1
View File
@@ -132,7 +132,7 @@
<field name="name">purchase.order.form</field>
<field name="model">purchase.order</field>
<field name="arch" type="xml">
<form string="Purchase Order" class="o_purchase_order" js_class="legacy_form">
<form string="Purchase Order" class="o_purchase_order">
<header>
<button name="action_rfq_send" states="draft" string="Send by Email" type="object" context="{'send_rfq':True}" class="oe_highlight" data-hotkey="g"/>
<button name="print_quotation" string="Print RFQ" type="object" states="draft" class="oe_highlight" groups="base.group_user" data-hotkey="k"/>
@@ -21,9 +21,6 @@
'web.assets_backend': [
'purchase_product_matrix/static/src/**/*',
],
'web.qunit_suite_tests': [
'purchase_product_matrix/static/tests/section_and_note_widget_tests.js',
],
'web.assets_tests': [
'purchase_product_matrix/static/tests/tours/**/*',
],
@@ -1,183 +0,0 @@
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;
});
@@ -0,0 +1,154 @@
/** @odoo-module **/
import Dialog from 'web.Dialog';
import { qweb } from "web.core";
import { registry } from '@web/core/registry';
import { Many2OneField } from '@web/views/fields/many2one/many2one_field';
import { formatMonetary } from "@web/views/fields/formatters";
const { onWillUpdateProps } = owl;
export class PurchaseOrderLineProductField extends Many2OneField {
setup() {
super.setup();
onWillUpdateProps(async (nextProps) => {
if (nextProps.record.mode === 'edit' && nextProps.value) {
if (
!this.props.value ||
this.props.value[0] != nextProps.value[0]
) {
// Field was updated if line was open in edit mode,
// field is not emptied,
// new value is different than existing value.
this._onProductTemplateUpdate();
}
}
});
}
get configurationButtonHelp() {
return this.env._t("Edit Configuration");
}
get isConfigurableTemplate() {
return this.props.record.data.is_configurable_product;
}
async _onProductTemplateUpdate() {
const result = await this.orm.call(
'product.template',
'get_single_product_variant',
[this.props.record.data.product_template_id[0]],
);
if(result && result.product_id) {
if (this.props.record.data.product_id != result.product_id.id) {
this.props.record.update({
// TODO right name get (same problem as configurator)
product_id: [result.product_id, 'whatever'],
});
}
} else {
this._openGridConfigurator(false);
}
}
onEditConfiguration() {
if (this.props.record.data.is_configurable_product) {
this._openGridConfigurator(true);
}
}
async _openGridConfigurator(edit) {
const PurchaseOrderRecord = this.props.record.model.root;
// fetch matrix information from server;
await PurchaseOrderRecord.update({
grid_product_tmpl_id: this.props.record.data.product_template_id,
});
let updatedLineAttributes = [];
if (edit) {
// provide attributes of edited line to automatically focus on matching cell in the matrix
for (let ptnvav of this.props.record.data.product_no_variant_attribute_value_ids.records) {
updatedLineAttributes.push(ptnvav.data.id);
}
for (let ptav of this.props.record.data.product_template_attribute_value_ids.records) {
updatedLineAttributes.push(ptav.data.id);
}
updatedLineAttributes.sort((a, b) => { return a - b; });
}
this._openMatrixConfigurator(
PurchaseOrderRecord.data.grid,
this.props.record.data.product_template_id[0],
updatedLineAttributes,
);
if (!edit) {
// remove new line used to open the matrix
PurchaseOrderRecord.data.order_line.removeRecord(this.props.record);
}
}
_openMatrixConfigurator(jsonInfo, productTemplateId, editedCellAttributes) {
const infos = JSON.parse(jsonInfo);
const saleOrderRecord = this.props.record.model.root;
const MatrixDialog = new Dialog(this, {
title: this.env._t('Choose Product Variants'),
size: 'extra-large', // adapt size depending on matrix size?
$content: $(qweb.render(
'product_matrix.matrix', {
header: infos.header,
rows: infos.matrix,
format({price, currency_id}) {
if (!price) { return ""; }
return formatMonetary(price, {
currencyId: currency_id,
});
}
}
)),
buttons: [
{text: this.env._t('Confirm'), classes: 'btn-primary', close: true, click: function (result) {
const $inputs = this.$('.o_matrix_input');
var matrixChanges = [];
_.each($inputs, function (matrixInput) {
if (matrixInput.value && matrixInput.value !== matrixInput.attributes.value.nodeValue) {
matrixChanges.push({
qty: parseFloat(matrixInput.value),
ptav_ids: matrixInput.attributes.ptav_ids.nodeValue.split(",").map(function (id) {
return parseInt(id);
}),
});
}
});
if (matrixChanges.length > 0) {
// NB: server also removes current line opening the matrix
saleOrderRecord.update({
grid: JSON.stringify({changes: matrixChanges, product_template_id: productTemplateId}),
grid_update: true // to say that the changes to grid have to be applied to the SO.
});
}
}},
{text: this.env._t('Close'), close: true},
],
}).open();
MatrixDialog.opened(function () {
MatrixDialog.$content.closest('.o_dialog_container').removeClass('d-none');
if (editedCellAttributes.length > 0) {
const str = editedCellAttributes.toString();
MatrixDialog.$content.find('.o_matrix_input').filter((k, v) => v.attributes.ptav_ids.nodeValue === str)[0].focus();
} else {
MatrixDialog.$content.find('.o_matrix_input:first()').focus();
}
});
}
}
PurchaseOrderLineProductField.template = "purchase.PurchaseProductField";
registry.category("fields").add("pol_product_many2one", PurchaseOrderLineProductField);
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="purchase.PurchaseProductField" t-inherit="web.Many2OneField" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-if='hasExternalButton']" position="before">
<t t-if="isConfigurableTemplate">
<button
type="button"
class="btn btn-secondary fa fa-pencil"
tabindex="-1"
draggable="false"
t-att-aria-label="configurationButtonHelp"
t-att-data-tooltip="configurationButtonHelp"
t-on-click="onEditConfiguration"/>
</t>
</xpath>
</t>
</templates>
@@ -1,260 +0,0 @@
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, { position: 'bottom' });
assert.strictEqual(form.$('.o_data_row').text(), 'TableChair');
form.destroy();
});
});
});
@@ -11,21 +11,19 @@ tour.register('purchase_matrix_tour', {
}, {
trigger: ".o_list_button_add",
extra_trigger: ".o_purchase_order"
}, {
trigger: '.o_required_modifier[name=partner_id] input',
run: 'text Agrolait',
}, {
trigger: '.ui-menu-item > a:contains("Agrolait")',
auto: true,
}, {
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);
}
run: "text Matrix",
}, {
trigger: 'ul.ui-autocomplete a:contains("Matrix")',
run: 'click'
}, {
trigger: '.o_product_variant_matrix',
run: function () {
@@ -35,29 +33,16 @@ tour.register('purchase_matrix_tour', {
}, {
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'
extra_trigger: '.o_form_status_indicator_buttons_hidden', // wait for save to be finished
}, {
trigger: '.o_edit_product_configuration',
run: 'click' // edit the matrix
trigger: '[name=product_template_id] button.fa-pencil', // edit the matrix
}, {
trigger: '.o_product_variant_matrix',
run: function () {
@@ -71,29 +56,18 @@ tour.register('purchase_matrix_tour', {
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')",
extra_trigger: '.o_form_editable',
trigger: 'a:contains("Add a product")',
extra_trigger: '.o_form_status_indicator_buttons_hidden',
}, {
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);
}
run: 'text Matrix',
}, {
trigger: 'ul.ui-autocomplete a:contains("Matrix")',
run: 'click'
}, {
trigger: "input[value='4']",
trigger: 'input[value="4"]',
run: function () {
// update some values of the matrix
$("input[value='4']").slice(0, 4).val(8.2);
@@ -1,39 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- TODO ensure barcode working with matrix (configurator mobile task ?) -->
<record id="purchase_order_form_matrix" model="ir.ui.view">
<field name="name">purchase.order.form.inherit.matrix</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//tree/field[@name='product_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//tree/field[@name='product_id']" position="after">
<field name="product_template_id"
string="Product"
attrs="{
'readonly': [('state', 'in', ('purchase', 'to approve','done', 'cancel'))],
'required': [('display_type', '=', False)],
}"
options="{'no_open': True}"
context="{'partner_id': parent.partner_id}"
widget="matrix_configurator"/>
<field name="product_template_attribute_value_ids" invisible="1" />
<field name="product_no_variant_attribute_value_ids" invisible="1" />
<field name="is_configurable_product" invisible="1" />
</xpath>
<field name="partner_id" position="after">
<field name="grid" invisible="1"/>
<field name="grid_product_tmpl_id" invisible="1"/>
<field name="grid_update" invisible="1"/>
</field>
<xpath expr="//group[@name='other_info']" position="inside">
<field name="report_grids" groups="base.group_no_one"/>
</xpath>
</field>
</record>
<record id="purchase_order_form_matrix" model="ir.ui.view">
<field name="name">purchase.order.form.inherit.matrix</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//tree/field[@name='product_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//tree/field[@name='product_id']" position="after">
<field name="product_template_id"
string="Product"
attrs="{
'readonly': [('state', 'in', ('purchase', 'to approve','done', 'cancel'))],
'required': [('display_type', '=', False)],
}"
options="{'no_open': True}"
context="{'partner_id': parent.partner_id}"
widget="pol_product_many2one"/>
<field name="product_template_attribute_value_ids" invisible="1"/>
<field name="product_no_variant_attribute_value_ids" invisible="1"/>
<field name="is_configurable_product" invisible="1"/>
</xpath>
<field name="partner_id" position="after">
<field name="grid" invisible="1"/>
<field name="grid_product_tmpl_id" invisible="1"/>
<field name="grid_update" invisible="1"/>
</field>
<group name="other_info" position="inside">
<field name="report_grids" groups="base.group_no_one"/>
</group>
</field>
</record>
</odoo>