[REF] *: extractProps receive dynamicInfo
Before this commit, the use of multiple <field> with the same name in a view was not well supported. Why was this? Some Field components need to know information related to the <field> such as context, domain, required and readonly. The solution used before this commit to access this information is to use the getFieldContext, getFieldDomain, isReadonly, isRequired functions of the model. Unfortunately, these only take into account the last occurrence of the <field> because the model is not aware that the same field is present several times on the view. The information must therefore not come from the model. For example, it was not possible to have the same field twice with 2 different domains. It will use the domain of the last field for both. Solution: We will add the object "dynamicInfo" to the fieldInfo passed to the Fields extractProps function. This object will contain a getter to get the value of required, readonly, domain and context for the current <field>. If a Field needs one of its information, it will just have to get it from extractProps. Part of Task: 3179751 closes odoo/odoo#115197 Related: odoo/enterprise#38151 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -39,15 +39,14 @@ export const many2ManyAttendee = {
|
||||
|
||||
registry.category("fields").add("many2manyattendee", many2ManyAttendee);
|
||||
|
||||
export function preloadMany2ManyAttendee(orm, record, fieldName) {
|
||||
const context = record.getFieldContext(fieldName);
|
||||
export function preloadMany2ManyAttendee(orm, record, fieldName, { context }) {
|
||||
return orm.call(
|
||||
"res.partner",
|
||||
"get_attendee_detail",
|
||||
[record.data[fieldName].records.map(rec => rec.resId), [record.resId || false]],
|
||||
[record.data[fieldName].records.map((rec) => rec.resId), [record.resId || false]],
|
||||
{
|
||||
context,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -36,11 +36,12 @@ FilterableSelectionField.props = {
|
||||
export const filterableSelectionField = {
|
||||
...selectionField,
|
||||
component: FilterableSelectionField,
|
||||
extractProps: (fieldInfo) => ({
|
||||
...selectionField.extractProps(fieldInfo),
|
||||
whitelisted_values: fieldInfo.options.whitelisted_values,
|
||||
blacklisted_values: fieldInfo.options.blacklisted_values,
|
||||
}),
|
||||
extractProps({ options }) {
|
||||
const props = selectionField.extractProps(...arguments);
|
||||
props.whitelisted_values = options.whitelisted_values;
|
||||
props.blacklisted_values = options.blacklisted_values;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("filterable_selection", filterableSelectionField);
|
||||
|
||||
@@ -57,17 +57,18 @@ export function useAssignUserCommand() {
|
||||
|
||||
const provide = async (env, options) => {
|
||||
const value = options.searchValue.trim();
|
||||
let domain = component.props.record.getFieldDomain(component.props.name);
|
||||
const context = component.props.record.getFieldContext(component.props.name);
|
||||
let domain =
|
||||
component.props.domain || component.props.record.getFieldDomain(component.props.name);
|
||||
const context = component.props.context;
|
||||
if (type === "many2many") {
|
||||
const selectedUserIds = getCurrentIds();
|
||||
if (selectedUserIds.length) {
|
||||
domain = Domain.and([domain, [["id", "not in", selectedUserIds]]]);
|
||||
domain = Domain.and([domain, [["id", "not in", selectedUserIds]]]).toList();
|
||||
}
|
||||
}
|
||||
const searchResult = await orm.call(component.relation, "name_search", [], {
|
||||
name: value,
|
||||
args: domain.toList(),
|
||||
args: domain,
|
||||
operator: "ilike",
|
||||
limit: 80,
|
||||
context,
|
||||
|
||||
@@ -17,6 +17,11 @@ export class FieldMany2ManyTagsEmailTagsList extends TagsList {}
|
||||
FieldMany2ManyTagsEmailTagsList.template = "FieldMany2ManyTagsEmailTagsList";
|
||||
|
||||
export class FieldMany2ManyTagsEmail extends Many2ManyTagsField {
|
||||
static props = {
|
||||
...Many2ManyTagsField.props,
|
||||
context: { type: Object, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
@@ -54,7 +59,7 @@ export class FieldMany2ManyTagsEmail extends Many2ManyTagsField {
|
||||
dialogDefs.push(
|
||||
this.openMany2xRecord({
|
||||
resId: record.resId,
|
||||
context: props.record.getFieldContext(this.props.name),
|
||||
context: props.context,
|
||||
title: sprintf(_t("Edit: %s"), record.data.display_name),
|
||||
})
|
||||
);
|
||||
@@ -101,6 +106,11 @@ FieldMany2ManyTagsEmail.components = {
|
||||
export const fieldMany2ManyTagsEmail = {
|
||||
...many2ManyTagsField,
|
||||
component: FieldMany2ManyTagsEmail,
|
||||
extractProps(fieldInfo, dynamicInfo) {
|
||||
const props = many2ManyTagsField.extractProps(...arguments);
|
||||
props.context = dynamicInfo.context;
|
||||
return props;
|
||||
},
|
||||
relatedFields: (fieldInfo) => {
|
||||
return [...many2ManyTagsField.relatedFields(fieldInfo), { name: "email", type: "char" }];
|
||||
},
|
||||
|
||||
+14
-8
@@ -29,6 +29,8 @@ export class Many2OneAvatarUserField extends Many2OneAvatarField {
|
||||
static template = "mail.Many2OneAvatarUserField";
|
||||
static props = {
|
||||
...Many2OneAvatarField.props,
|
||||
context: { type: Object, optional: true },
|
||||
domain: { type: Array, optional: true },
|
||||
withCommand: { type: Boolean, optional: true },
|
||||
};
|
||||
}
|
||||
@@ -38,10 +40,13 @@ export const many2OneAvatarUserField = {
|
||||
...many2OneAvatarField,
|
||||
component: Many2OneAvatarUserField,
|
||||
additionalClasses: ["o_field_many2one_avatar"],
|
||||
extractProps: (fieldInfo) => ({
|
||||
...many2OneAvatarField.extractProps(fieldInfo),
|
||||
withCommand: fieldInfo.viewType === "form",
|
||||
}),
|
||||
extractProps(fieldInfo, dynamicInfo) {
|
||||
const props = many2OneAvatarField.extractProps(...arguments);
|
||||
props.context = dynamicInfo.context;
|
||||
props.domain = dynamicInfo.domain;
|
||||
props.withCommand = fieldInfo.viewType === "form";
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2one_avatar_user", many2OneAvatarUserField);
|
||||
@@ -77,10 +82,11 @@ export const kanbanMany2OneAvatarUserField = {
|
||||
...kanbanMany2OneAvatarField,
|
||||
component: KanbanMany2OneAvatarUserField,
|
||||
additionalClasses: [...kanbanMany2OneAvatarField.additionalClasses, "o_field_many2one_avatar"],
|
||||
extractProps: (fieldInfo) => ({
|
||||
...kanbanMany2OneAvatarField.extractProps(fieldInfo),
|
||||
displayAvatarName: fieldInfo.options.display_avatar_name || false,
|
||||
}),
|
||||
extractProps({ options }) {
|
||||
const props = kanbanMany2OneAvatarField.extractProps(...arguments);
|
||||
props.displayAvatarName = options.display_avatar_name || false;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("kanban.many2one_avatar_user", kanbanMany2OneAvatarUserField);
|
||||
|
||||
@@ -153,11 +153,12 @@ FieldMany2OneMailingFilter.defaultProps = {
|
||||
export const fieldMany2OneMailingFilter = {
|
||||
...many2OneField,
|
||||
component: FieldMany2OneMailingFilter,
|
||||
extractProps: (fieldInfo) => ({
|
||||
...many2OneField.extractProps(fieldInfo),
|
||||
domain_field: fieldInfo.options.domain_field,
|
||||
model_field: fieldInfo.options.model_field,
|
||||
}),
|
||||
extractProps({ options }) {
|
||||
const props = many2OneField.extractProps(...arguments);
|
||||
props.domain_field = options.domain_field;
|
||||
props.model_field = options.model_field;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("mailing_filter", fieldMany2OneMailingFilter);
|
||||
|
||||
@@ -609,12 +609,13 @@ export const massMailingHtmlField = {
|
||||
...htmlField,
|
||||
component: MassMailingHtmlField,
|
||||
displayName: _lt("Email"),
|
||||
extractProps: (fieldInfo) => ({
|
||||
...htmlField.extractProps(fieldInfo),
|
||||
filterTemplates: fieldInfo.options.filterTemplates,
|
||||
inlineField: fieldInfo.options['inline-field'],
|
||||
iframeHtmlClass: fieldInfo.attrs.iframeHtmlClass,
|
||||
}),
|
||||
extractProps({ attrs, options }) {
|
||||
const props = htmlField.extractProps(...arguments);
|
||||
props.filterTemplates = options.filterTemplates;
|
||||
props.inlineField = options['inline-field'];
|
||||
props.iframeHtmlClass = attrs.iframeHtmlClass;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("mass_mailing_html", massMailingHtmlField);
|
||||
|
||||
+6
-6
@@ -169,12 +169,12 @@ export const projectTaskStateSelection = {
|
||||
...stateSelectionField,
|
||||
component: ProjectTaskStateSelection,
|
||||
fieldDependencies: [{ name: "project_id", type: "many2one" }],
|
||||
extractProps: ({ options, viewType }) => ({
|
||||
...stateSelectionField.extractProps({ options }),
|
||||
isToggleMode: Boolean(options.is_toggle_mode),
|
||||
viewType,
|
||||
}),
|
||||
|
||||
extractProps({ options, viewType }) {
|
||||
const props = stateSelectionField.extractProps(...arguments);
|
||||
props.isToggleMode = Boolean(options.is_toggle_mode);
|
||||
props.viewType = viewType;
|
||||
return props;
|
||||
},
|
||||
}
|
||||
|
||||
registry.category("fields").add("project_task_state_selection", projectTaskStateSelection);
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
<templates>
|
||||
<t t-name="project.ProjectTaskStateSelection" t-inherit="web.StateSelectionField" t-inherit-mode="primary" owl="1">
|
||||
<!-- Readonly button -->
|
||||
<xpath expr="//t[@t-if='isReadonly']/button/span[1]" position="attributes">
|
||||
<xpath expr="//t[@t-if='props.readonly']/button/span[1]" position="attributes">
|
||||
<attribute name="t-attf-class">{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }}</attribute>
|
||||
</xpath>
|
||||
<!-- Waiting state button -->
|
||||
<xpath expr="//t[@t-if='isReadonly']" position="after">
|
||||
<xpath expr="//t[@t-if='props.readonly']" position="after">
|
||||
<t t-elif="currentValue == '04_waiting_normal' and ['kanban', 'list'].includes(this.props.viewType)">
|
||||
<button class="d-flex align-items-center btn fw-normal p-0 justify-content-center " title="This task is blocked by another unfinished task">
|
||||
<i class="fa fa-lg fa-hourglass-o"></i>
|
||||
@@ -13,7 +13,7 @@
|
||||
</t>
|
||||
</xpath>
|
||||
<!-- The toggle mark as done button -->
|
||||
<xpath expr="//t[@t-if='isReadonly']" position="after">
|
||||
<xpath expr="//t[@t-if='props.readonly']" position="after">
|
||||
<t t-elif="isToggleMode and currentValue == '01_in_progress'">
|
||||
<button t-if="['kanban', 'list'].includes(this.props.viewType) or this.env.isSmall" class="d-flex align-items-center btn fw-normal p-0" t-on-click.stop="toggleState">
|
||||
<i t-attf-class="{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }}"></i>
|
||||
|
||||
@@ -21,10 +21,11 @@ export class PrioritySwitchField extends PriorityField {
|
||||
export const prioritySwitchField = {
|
||||
...priorityField,
|
||||
component: PrioritySwitchField,
|
||||
extractProps: (fieldInfo) => ({
|
||||
...priorityField.extractProps(fieldInfo),
|
||||
withCommand: fieldInfo.viewType === "form",
|
||||
}),
|
||||
extractProps({ viewType }) {
|
||||
const props = priorityField.extractProps(...arguments);
|
||||
props.withCommand = viewType === "form";
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("priority_switch", prioritySwitchField);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { Many2OneField, many2OneField } from '@web/views/fields/many2one/many2one_field';
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2OneField, many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
export class SaleOrderLineProductField extends Many2OneField {
|
||||
static props = {
|
||||
...Many2OneField.props,
|
||||
readonlyField: { type: Boolean, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
@@ -14,7 +18,10 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
// NB: quick creation doesn't go through here, but through quickCreate
|
||||
// below
|
||||
if (value) {
|
||||
if (Array.isArray(value[0]) && this.props.record.data[this.props.name] != value[0]) {
|
||||
if (
|
||||
Array.isArray(value[0]) &&
|
||||
this.props.record.data[this.props.name] != value[0]
|
||||
) {
|
||||
// product (existing)
|
||||
newValue = true;
|
||||
} else {
|
||||
@@ -24,7 +31,7 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
}
|
||||
}
|
||||
if (newValue) {
|
||||
if (this.relation === 'product.template') {
|
||||
if (this.relation === "product.template") {
|
||||
this._onProductTemplateUpdate();
|
||||
} else {
|
||||
this._onProductUpdate();
|
||||
@@ -36,9 +43,9 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
// HACK to make quick creation also open
|
||||
// configurators if needed
|
||||
this.quickCreate = async (name, params = {}) => {
|
||||
await this.props.record.update({ [this.props.name]: [false, name]});
|
||||
await this.props.record.update({ [this.props.name]: [false, name] });
|
||||
|
||||
if (this.relation === 'product.template') {
|
||||
if (this.relation === "product.template") {
|
||||
this._onProductTemplateUpdate();
|
||||
} else {
|
||||
this._onProductUpdate();
|
||||
@@ -51,11 +58,10 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
// product form should be accessible if the widget field is readonly
|
||||
// or if the line cannot be edited (e.g. locked SO)
|
||||
return (
|
||||
this.props.record.isReadonly(this.props.name)
|
||||
|| this.props.record.model.root.isReadonly
|
||||
&& this.props.record.model.root.activeFields.order_line
|
||||
&& this.props.record.model.root.isReadonly('order_line')
|
||||
)
|
||||
this.props.readonlyField ||
|
||||
(this.props.record.model.root.activeFields.order_line &&
|
||||
this.props.record.model.root._isReadonly("order_line"))
|
||||
);
|
||||
}
|
||||
get hasExternalButton() {
|
||||
// Keep external button, even if field is specified as 'no_open' so that the user is not
|
||||
@@ -66,19 +72,23 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
get hasConfigurationButton() {
|
||||
return this.isConfigurableLine || this.isConfigurableTemplate;
|
||||
}
|
||||
get isConfigurableLine() { return false; }
|
||||
get isConfigurableTemplate() { return false; }
|
||||
get isConfigurableLine() {
|
||||
return false;
|
||||
}
|
||||
get isConfigurableTemplate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get configurationButtonHelp() {
|
||||
return this.env._t("Edit Configuration");
|
||||
}
|
||||
|
||||
get configurationButtonIcon() {
|
||||
return 'btn btn-secondary fa ' + this.configurationButtonFAIcon();
|
||||
return "btn btn-secondary fa " + this.configurationButtonFAIcon();
|
||||
}
|
||||
|
||||
configurationButtonFAIcon() {
|
||||
return 'fa-pencil';
|
||||
return "fa-pencil";
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
@@ -86,14 +96,13 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
if (this.props.readonly) {
|
||||
ev.stopPropagation();
|
||||
this.openAction();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
super.onClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
async _onProductTemplateUpdate() { }
|
||||
async _onProductUpdate() { } // event_booth_sale, event_sale, sale_renting
|
||||
async _onProductTemplateUpdate() {}
|
||||
async _onProductUpdate() {} // event_booth_sale, event_sale, sale_renting
|
||||
|
||||
onEditConfiguration() {
|
||||
if (this.isConfigurableLine) {
|
||||
@@ -102,9 +111,8 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
this._editProductConfiguration();
|
||||
}
|
||||
}
|
||||
_editLineConfiguration() { } // event_booth_sale, event_sale, sale_renting
|
||||
_editProductConfiguration() { } // sale_product_configurator, sale_product_matrix
|
||||
|
||||
_editLineConfiguration() {} // event_booth_sale, event_sale, sale_renting
|
||||
_editProductConfiguration() {} // sale_product_configurator, sale_product_matrix
|
||||
}
|
||||
|
||||
SaleOrderLineProductField.template = "sale.SaleProductField";
|
||||
@@ -112,6 +120,11 @@ SaleOrderLineProductField.template = "sale.SaleProductField";
|
||||
export const saleOrderLineProductField = {
|
||||
...many2OneField,
|
||||
component: SaleOrderLineProductField,
|
||||
extractProps(fieldInfo, dynamicInfo) {
|
||||
const props = many2OneField.extractProps(...arguments);
|
||||
props.readonlyField = dynamicInfo.readonly;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("sol_product_many2one", saleOrderLineProductField);
|
||||
|
||||
@@ -20,11 +20,10 @@ patch(PhoneField, "sms.PhoneField", {
|
||||
});
|
||||
|
||||
const patchDescr = {
|
||||
extractProps(fieldInfo) {
|
||||
return {
|
||||
...this._super(fieldInfo),
|
||||
enableButton: fieldInfo.options.enable_sms,
|
||||
};
|
||||
extractProps({ options }) {
|
||||
const props = this._super(...arguments);
|
||||
props.enableButton = options.enable_sms;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ var concurrency = require('web.concurrency');
|
||||
var Context = require('web.Context');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
const pyUtils = require('web.py_utils');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
@@ -1211,6 +1210,7 @@ var BasicModel = AbstractModel.extend({
|
||||
resolve(changedFields);
|
||||
}
|
||||
}).guardedCatch(reject);
|
||||
|
||||
} else {
|
||||
resolve(changedFields);
|
||||
}
|
||||
@@ -3761,7 +3761,6 @@ var BasicModel = AbstractModel.extend({
|
||||
current_company_id,
|
||||
id: evalContext.id || false,
|
||||
},
|
||||
pyUtils.context(),
|
||||
session.user_context,
|
||||
element.context,
|
||||
evalContext,
|
||||
@@ -3798,7 +3797,6 @@ var BasicModel = AbstractModel.extend({
|
||||
current_company_id,
|
||||
id: evalContext.id || false,
|
||||
},
|
||||
pyUtils.context(),
|
||||
session.user_context,
|
||||
element.context,
|
||||
);
|
||||
|
||||
@@ -7,13 +7,13 @@ import { deserializeDate, deserializeDateTime } from "@web/core/l10n/dates";
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import { escape } from "@web/core/utils/strings";
|
||||
import { mapDoActionOptionAPI } from "@web/legacy/backend_utils";
|
||||
import {
|
||||
mapActiveFieldsToFieldsInfo,
|
||||
mapViews,
|
||||
mapWowlValueToLegacy,
|
||||
} from "@web/views/legacy_utils";
|
||||
import { Model } from "@web/views/model";
|
||||
import { evalDomain } from "@web/views/utils";
|
||||
import {
|
||||
mapWowlValueToLegacy,
|
||||
mapViews,
|
||||
mapActiveFieldsToFieldsInfo,
|
||||
} from "@web/views/legacy_utils";
|
||||
import BasicModel from "web.BasicModel";
|
||||
import Context from "web.Context";
|
||||
|
||||
@@ -70,6 +70,11 @@ class DataPoint {
|
||||
return this.model.__bm__._getLazyEvalContext(datapoint);
|
||||
}
|
||||
|
||||
getEvalContext(forDomain) {
|
||||
const datapoint = this.model.__bm__.localData[this.__bm_handle__];
|
||||
return this.model.__bm__._getEvalContext(datapoint, forDomain, false, true);
|
||||
}
|
||||
|
||||
get fieldNames() {
|
||||
return Object.keys(this.activeFields);
|
||||
}
|
||||
@@ -207,7 +212,7 @@ export class Record extends DataPoint {
|
||||
|
||||
const isSet = activeField && activeField.field && activeField.field.isSet;
|
||||
|
||||
if (this.isRequired(fieldName) && isSet && !isSet(this.data[fieldName])) {
|
||||
if (this._isRequired(fieldName) && isSet && !isSet(this.data[fieldName])) {
|
||||
this.setInvalidField(fieldName);
|
||||
continue;
|
||||
}
|
||||
@@ -230,7 +235,7 @@ export class Record extends DataPoint {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!isSet && this.isRequired(fieldName) && !this.data[fieldName]) {
|
||||
if (!isSet && this._isRequired(fieldName) && !this.data[fieldName]) {
|
||||
this._setInvalidField(fieldName);
|
||||
}
|
||||
}
|
||||
@@ -278,7 +283,7 @@ export class Record extends DataPoint {
|
||||
* @param {string} fieldName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReadonly(fieldName) {
|
||||
_isReadonly(fieldName) {
|
||||
const { readonly } = this.activeFields[fieldName].modifiers || {};
|
||||
return evalDomain(readonly, this.evalContext);
|
||||
}
|
||||
@@ -288,7 +293,7 @@ export class Record extends DataPoint {
|
||||
* @param {string} fieldName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isRequired(fieldName) {
|
||||
_isRequired(fieldName) {
|
||||
const { required } = this.activeFields[fieldName].modifiers || {};
|
||||
return evalDomain(required, this.evalContext);
|
||||
}
|
||||
@@ -446,21 +451,18 @@ export class Record extends DataPoint {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getFieldContext(fieldName) {
|
||||
getFieldDomain(fieldName) {
|
||||
const { domain } = this.fields[fieldName];
|
||||
return domain ? new Domain(domain).toList(this.getEvalContext(true)) : [];
|
||||
}
|
||||
|
||||
_getFieldContext(fieldName) {
|
||||
return this.model.__bm__.localData[this.__bm_handle__].getContext({
|
||||
fieldName,
|
||||
viewType: this.__viewType,
|
||||
});
|
||||
}
|
||||
|
||||
getFieldDomain(fieldName) {
|
||||
const domain = this.model.__bm__.localData[this.__bm_handle__].getDomain({
|
||||
fieldName,
|
||||
viewType: this.__viewType,
|
||||
});
|
||||
return new Domain(JSON.parse(JSON.stringify(domain))); // legacy pyjs inserts weird structures
|
||||
}
|
||||
|
||||
async update(changes) {
|
||||
if (this.batchingUpdateProm) {
|
||||
// Assign changes in the current batch
|
||||
|
||||
@@ -82,10 +82,8 @@ export const badgeSelectionField = {
|
||||
|
||||
registry.category("fields").add("selection_badge", badgeSelectionField);
|
||||
|
||||
export function preloadSelection(orm, record, fieldName) {
|
||||
export function preloadSelection(orm, record, fieldName, { domain }) {
|
||||
const field = record.fields[fieldName];
|
||||
const context = record.evalContext;
|
||||
const domain = record.getFieldDomain(fieldName).toList(context);
|
||||
return orm.call(field.relation, "name_search", ["", domain]);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,6 @@ export class BooleanField extends Component {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
get isReadonly() {
|
||||
return (
|
||||
!this.props.record.isInEdition ||
|
||||
this.props.record.isReadonly(this.props.name) ||
|
||||
this.props.readonly
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} newValue
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.BooleanField" owl="1">
|
||||
<CheckBox id="props.id" value="props.record.data[props.name] or false" className="'d-inline-block'" disabled="isReadonly" onChange.bind="onChange" />
|
||||
<CheckBox id="props.id" value="props.record.data[props.name] or false" className="'d-inline-block'" disabled="props.readonly" onChange.bind="onChange" />
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { booleanField, BooleanField } from "../boolean/boolean_field";
|
||||
|
||||
export class BooleanToggleField extends BooleanField {
|
||||
static template = "web.BooleanToggleField";
|
||||
|
||||
get isReadonly() {
|
||||
return this.props.record.isReadonly(this.props.name);
|
||||
}
|
||||
|
||||
async onChange(newValue) {
|
||||
await this.props.record.update({ [this.props.name]: newValue });
|
||||
return this.props.record.save();
|
||||
@@ -21,6 +17,11 @@ export const booleanToggleField = {
|
||||
...booleanField,
|
||||
component: BooleanToggleField,
|
||||
displayName: _lt("Toggle"),
|
||||
extractProps(fieldInfo, dynamicInfo) {
|
||||
return {
|
||||
readonly: dynamicInfo.readonly,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("boolean_toggle", booleanToggleField);
|
||||
|
||||
@@ -7,7 +7,7 @@ export class ListBooleanToggleField extends BooleanToggleField {
|
||||
static template = "web.ListBooleanToggleField";
|
||||
|
||||
async onClick() {
|
||||
if (!this.props.readonly) {
|
||||
if (!this.props.readonly && this.props.record.isInEdition) {
|
||||
await this.props.record.update({
|
||||
[this.props.name]: !this.props.record.data[this.props.name],
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, onWillUpdateProps, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "../standard_field_props";
|
||||
|
||||
import { Component, useState, onWillUpdateProps } from "@odoo/owl";
|
||||
|
||||
export class ColorField extends Component {
|
||||
static template = "web.ColorField";
|
||||
static props = {
|
||||
@@ -20,15 +19,16 @@ export class ColorField extends Component {
|
||||
this.state.color = nextProps.record.data[nextProps.name] || "";
|
||||
});
|
||||
}
|
||||
|
||||
get isReadonly() {
|
||||
return this.props.record.isReadonly(this.props.name);
|
||||
}
|
||||
}
|
||||
|
||||
export const colorField = {
|
||||
component: ColorField,
|
||||
supportedTypes: ["char"],
|
||||
extractProps(fieldInfo, dynamicInfo) {
|
||||
return {
|
||||
readonly: dynamicInfo.readonly,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("color", colorField);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.ColorField" owl="1">
|
||||
<div class="o_field_color d-flex" t-att-class="{ 'o_field_cursor_disabled': isReadonly }" t-attf-style="background: #{state.color or 'url(/web/static/img/transparent.png)'}">
|
||||
<input t-on-click.stop="" class="w-100 h-100 opacity-0" type="color" t-att-value="state.color" t-att-disabled="isReadonly" t-on-input="(ev) => this.state.color = ev.target.value" t-on-change="(ev) => this.props.record.update({ [this.props.name]: ev.target.value })" />
|
||||
<div class="o_field_color d-flex" t-att-class="{ 'o_field_cursor_disabled': props.readonly }" t-attf-style="background: #{state.color or 'url(/web/static/img/transparent.png)'}">
|
||||
<input t-on-click.stop="" class="w-100 h-100 opacity-0" type="color" t-att-value="state.color" t-att-disabled="props.readonly" t-on-input="(ev) => this.state.color = ev.target.value" t-on-change="(ev) => this.props.record.update({ [this.props.name]: ev.target.value })" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export class DomainField extends Component {
|
||||
};
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
context: { type: Object, optional: true },
|
||||
editInDialog: { type: Boolean, optional: true },
|
||||
resModel: { type: String, optional: true },
|
||||
};
|
||||
@@ -61,7 +62,7 @@ export class DomainField extends Component {
|
||||
}
|
||||
|
||||
getContext(p) {
|
||||
return p.record.getFieldContext(p.name);
|
||||
return p.context;
|
||||
}
|
||||
getResModel(p) {
|
||||
let resModel = p.resModel;
|
||||
@@ -149,10 +150,13 @@ export const domainField = {
|
||||
displayName: _lt("Domain"),
|
||||
supportedTypes: ["char"],
|
||||
isEmpty: () => false,
|
||||
extractProps: ({ options }) => ({
|
||||
editInDialog: options.in_dialog,
|
||||
resModel: options.model,
|
||||
}),
|
||||
extractProps({ options }, dynamicInfo) {
|
||||
return {
|
||||
editInDialog: options.in_dialog,
|
||||
resModel: options.model,
|
||||
context: dynamicInfo.context,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("domain", domainField);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { makeContext } from "@web/core/context";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
@@ -122,13 +124,53 @@ export class Field extends Component {
|
||||
const modifiers = fieldInfo.modifiers || {};
|
||||
readonlyFromModifiers = evalDomain(modifiers.readonly, evalContext);
|
||||
|
||||
if (this.props.attrs) {
|
||||
fieldInfo = {
|
||||
...fieldInfo,
|
||||
attrs: { ...fieldInfo.attrs, ...this.props.attrs },
|
||||
if (this.field.extractProps) {
|
||||
if (this.props.attrs) {
|
||||
fieldInfo = {
|
||||
...fieldInfo,
|
||||
attrs: { ...fieldInfo.attrs, ...this.props.attrs },
|
||||
};
|
||||
}
|
||||
|
||||
const dynamicInfo = {
|
||||
get context() {
|
||||
const evalContext = record.getEvalContext
|
||||
? record.getEvalContext(false)
|
||||
: record.evalContext;
|
||||
|
||||
const context = {};
|
||||
for (const key in record.context) {
|
||||
if (!key.startsWith("default_")) {
|
||||
context[key] = record.context[key];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...context,
|
||||
...makeContext([fieldInfo.context], evalContext),
|
||||
};
|
||||
},
|
||||
get domain() {
|
||||
const evalContext = record.getEvalContext
|
||||
? record.getEvalContext(true)
|
||||
: record.evalContext;
|
||||
|
||||
return fieldInfo.domain
|
||||
? new Domain(evaluateExpr(fieldInfo.domain, evalContext)).toList()
|
||||
: undefined;
|
||||
},
|
||||
readonly: readonlyFromModifiers,
|
||||
get required() {
|
||||
return (
|
||||
fieldInfo.modifiers &&
|
||||
fieldInfo.modifiers.required &&
|
||||
evalDomain(fieldInfo.modifiers.required, record.evalContext)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
propsFromNode = this.field.extractProps(fieldInfo, dynamicInfo);
|
||||
}
|
||||
propsFromNode = this.field.extractProps ? this.field.extractProps(fieldInfo) : {};
|
||||
}
|
||||
|
||||
const props = { ...this.props };
|
||||
|
||||
@@ -29,10 +29,11 @@ export class FloatFactorField extends FloatField {
|
||||
export const floatFactorField = {
|
||||
...floatField,
|
||||
component: FloatFactorField,
|
||||
extractProps: (fieldInfo) => ({
|
||||
...floatField.extractProps(fieldInfo),
|
||||
factor: fieldInfo.options.factor,
|
||||
}),
|
||||
extractProps({ options }) {
|
||||
const props = floatField.extractProps(...arguments);
|
||||
props.factor = options.factor;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("float_factor", floatFactorField);
|
||||
|
||||
@@ -12,6 +12,7 @@ export class FontSelectionField extends Component {
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
placeholder: { type: String, optional: true },
|
||||
required: { type: Boolean, optional: true },
|
||||
};
|
||||
|
||||
get options() {
|
||||
@@ -19,9 +20,6 @@ export class FontSelectionField extends Component {
|
||||
(option) => option[0] !== false && option[1] !== ""
|
||||
);
|
||||
}
|
||||
get isRequired() {
|
||||
return this.props.record.isRequired(this.props.name);
|
||||
}
|
||||
get string() {
|
||||
return formatSelection(this.props.record.data[this.props.name], {
|
||||
selection: this.options,
|
||||
@@ -45,9 +43,12 @@ export const fontSelectionField = {
|
||||
component: FontSelectionField,
|
||||
displayName: _lt("Font Selection"),
|
||||
supportedTypes: ["selection"],
|
||||
extractProps: ({ attrs }) => ({
|
||||
placeholder: attrs.placeholder,
|
||||
}),
|
||||
extractProps({ attrs }, dynamicInfo) {
|
||||
return {
|
||||
placeholder: attrs.placeholder,
|
||||
required: dynamicInfo.required,
|
||||
};
|
||||
},
|
||||
legacySpecialData: "_fetchSpecialRelation",
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
t-att-selected="false === value"
|
||||
t-att-value="stringify(false)"
|
||||
t-esc="this.props.placeholder || ''"
|
||||
t-attf-style="{{ isRequired ? 'display:none' : '' }}"
|
||||
t-attf-style="{{ props.required ? 'display:none' : '' }}"
|
||||
/>
|
||||
<t t-foreach="options" t-as="option" t-key="option[0]">
|
||||
<option
|
||||
|
||||
+1
-3
@@ -49,10 +49,8 @@ export const many2ManyCheckboxesField = {
|
||||
|
||||
registry.category("fields").add("many2many_checkboxes", many2ManyCheckboxesField);
|
||||
|
||||
export function preloadMany2ManyCheckboxes(orm, record, fieldName) {
|
||||
export function preloadMany2ManyCheckboxes(orm, record, fieldName, { domain }) {
|
||||
const field = record.fields[fieldName];
|
||||
const context = record.evalContext;
|
||||
const domain = record.getFieldDomain(fieldName).toList(context);
|
||||
return orm.call(field.relation, "name_search", ["", domain]);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ export class Many2ManyTagsField extends Component {
|
||||
canCreateEdit: { type: Boolean, optional: true },
|
||||
colorField: { type: String, optional: true },
|
||||
createDomain: { type: [Array, Boolean], optional: true },
|
||||
domain: { type: Array, optional: true },
|
||||
context: { type: Object, optional: true },
|
||||
placeholder: { type: String, optional: true },
|
||||
nameCreateField: { type: String, optional: true },
|
||||
string: { type: String, optional: true },
|
||||
@@ -48,6 +50,7 @@ export class Many2ManyTagsField extends Component {
|
||||
canQuickCreate: true,
|
||||
canCreateEdit: true,
|
||||
nameCreateField: "name",
|
||||
context: {},
|
||||
};
|
||||
|
||||
static RECORD_COLORS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
@@ -96,7 +99,7 @@ export class Many2ManyTagsField extends Component {
|
||||
if (this.props.canQuickCreate) {
|
||||
this.quickCreate = async (name) => {
|
||||
const created = await this.orm.call(this.relation, "name_create", [name], {
|
||||
context: this.context,
|
||||
context: this.props.context,
|
||||
});
|
||||
return saveRecord([created[0]]);
|
||||
};
|
||||
@@ -106,12 +109,6 @@ export class Many2ManyTagsField extends Component {
|
||||
get relation() {
|
||||
return this.props.record.fields[this.props.name].relation;
|
||||
}
|
||||
get domain() {
|
||||
return this.props.record.getFieldDomain(this.props.name);
|
||||
}
|
||||
get context() {
|
||||
return this.props.record.getFieldContext(this.props.name);
|
||||
}
|
||||
get evalContext() {
|
||||
return this.props.record.evalContext;
|
||||
}
|
||||
@@ -152,9 +149,9 @@ export class Many2ManyTagsField extends Component {
|
||||
|
||||
getDomain() {
|
||||
return Domain.and([
|
||||
this.domain,
|
||||
this.props.domain || this.props.record.getFieldDomain(this.props.name),
|
||||
Domain.not([["id", "in", this.props.record.data[this.props.name].currentIds]]),
|
||||
]).toList(this.context);
|
||||
]).toList(this.props.context);
|
||||
}
|
||||
|
||||
focusTag(index) {
|
||||
@@ -267,7 +264,7 @@ export const many2ManyTagsField = {
|
||||
}
|
||||
return relatedFields;
|
||||
},
|
||||
extractProps: ({ attrs, options, string }) => {
|
||||
extractProps({ attrs, options, string }, dynamicInfo) {
|
||||
const noCreate = Boolean(options.no_create);
|
||||
const canCreate = attrs.can_create && Boolean(JSON.parse(attrs.can_create)) && !noCreate;
|
||||
const noQuickCreate = Boolean(options.no_quick_create);
|
||||
@@ -279,6 +276,8 @@ export const many2ManyTagsField = {
|
||||
canQuickCreate: canCreate && !noQuickCreate,
|
||||
canCreateEdit: canCreate && !noCreateEdit,
|
||||
createDomain: options.create,
|
||||
context: dynamicInfo.context,
|
||||
domain: dynamicInfo.domain,
|
||||
placeholder: attrs.placeholder,
|
||||
string,
|
||||
};
|
||||
@@ -374,10 +373,11 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
|
||||
export const many2ManyTagsFieldColorEditable = {
|
||||
...many2ManyTagsField,
|
||||
component: Many2ManyTagsFieldColorEditable,
|
||||
extractProps: (fieldInfo) => ({
|
||||
...many2ManyTagsField.extractProps(fieldInfo),
|
||||
canEditColor: !fieldInfo.options.no_edit_color && !!fieldInfo.options.color_field,
|
||||
}),
|
||||
extractProps({ options }) {
|
||||
const props = many2ManyTagsField.extractProps(...arguments);
|
||||
props.canEditColor = !options.no_edit_color && !!options.color_field;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("form.many2many_tags", many2ManyTagsFieldColorEditable);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
activeActions="activeActions"
|
||||
update="update"
|
||||
quickCreate="activeActions.create ? quickCreate : null"
|
||||
context="context"
|
||||
context="props.context"
|
||||
getDomain.bind="getDomain"
|
||||
isToMany="true"
|
||||
nameCreateField="props.nameCreateField"
|
||||
|
||||
+13
-10
@@ -32,10 +32,12 @@ export class Many2ManyTagsAvatarField extends Many2ManyTagsField {
|
||||
export const many2ManyTagsAvatarField = {
|
||||
...many2ManyTagsField,
|
||||
component: Many2ManyTagsAvatarField,
|
||||
extractProps: (fieldInfo) => ({
|
||||
...many2ManyTagsField.extractProps(fieldInfo),
|
||||
withCommand: fieldInfo.viewType === "form",
|
||||
}),
|
||||
extractProps({ viewType }, dynamicInfo) {
|
||||
const props = many2ManyTagsField.extractProps(...arguments);
|
||||
props.withCommand = viewType === "form";
|
||||
props.domain = dynamicInfo.domain;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2many_tags_avatar", many2ManyTagsAvatarField);
|
||||
@@ -139,19 +141,15 @@ export class KanbanMany2ManyTagsAvatarFieldTagsList extends TagsList {
|
||||
export class KanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {
|
||||
static template = "web.KanbanMany2ManyTagsAvatarField";
|
||||
static components = {
|
||||
...Many2ManyTagsAvatarField.component,
|
||||
...Many2ManyTagsAvatarField.components,
|
||||
TagsList: KanbanMany2ManyTagsAvatarFieldTagsList,
|
||||
};
|
||||
itemsVisible = 2;
|
||||
|
||||
get isFieldReadonly() {
|
||||
return this.props.record.isReadonly(this.props.name);
|
||||
}
|
||||
|
||||
get popoverProps() {
|
||||
return {
|
||||
...this.props,
|
||||
readonly: this.isFieldReadonly,
|
||||
readonly: this.props.readonly,
|
||||
deleteTag: this.deleteTag.bind(this),
|
||||
updateTag: this.updateTag.bind(this),
|
||||
};
|
||||
@@ -178,6 +176,11 @@ export class KanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {
|
||||
export const kanbanMany2ManyTagsAvatarField = {
|
||||
...many2ManyTagsAvatarField,
|
||||
component: KanbanMany2ManyTagsAvatarField,
|
||||
extractProps(fieldInfo, dynamicInfo) {
|
||||
const props = many2ManyTagsAvatarField.extractProps(...arguments);
|
||||
props.readonly = dynamicInfo.readonly;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("kanban.many2many_tags_avatar", kanbanMany2ManyTagsAvatarField);
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@
|
||||
owl="1">
|
||||
<xpath expr="//TagsList" position="attributes">
|
||||
<attribute name="popoverProps">popoverProps</attribute>
|
||||
<attribute name="readonly">isFieldReadonly</attribute>
|
||||
<attribute name="readonly">props.readonly</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name="web.Many2ManyTagsAvatarFieldPopover" t-inherit="web.Many2ManyTagsAvatarField" t-inherit-mode="primary" owl="1">
|
||||
|
||||
@@ -52,6 +52,8 @@ export class Many2OneField extends Component {
|
||||
canWrite: { type: Boolean, optional: true },
|
||||
canQuickCreate: { type: Boolean, optional: true },
|
||||
canCreateEdit: { type: Boolean, optional: true },
|
||||
context: { type: Object, optional: true },
|
||||
domain: { type: Array, optional: true },
|
||||
nameCreateField: { type: String, optional: true },
|
||||
searchLimit: { type: Number, optional: true },
|
||||
relation: { type: String, optional: true },
|
||||
@@ -70,6 +72,7 @@ export class Many2OneField extends Component {
|
||||
searchLimit: 7,
|
||||
string: "",
|
||||
canScanBarcode: false,
|
||||
context: {},
|
||||
};
|
||||
|
||||
static SEARCH_MORE_LIMIT = 320;
|
||||
@@ -98,7 +101,7 @@ export class Many2OneField extends Component {
|
||||
onRecordSaved: async (record) => {
|
||||
const resId = this.value[0];
|
||||
const fields = ["display_name"];
|
||||
const context = this.props.record.getFieldContext(this.props.name);
|
||||
const { context } = this.props;
|
||||
const records = await this.orm.read(this.relation, [resId], fields, { context });
|
||||
await this.updateRecord(m2oTupleFromData(records[0]));
|
||||
if (this.props.record.model.root.id !== this.props.record.id) {
|
||||
@@ -151,15 +154,12 @@ export class Many2OneField extends Component {
|
||||
get string() {
|
||||
return this.props.string || this.props.record.fields[this.props.name].string || "";
|
||||
}
|
||||
get context() {
|
||||
return this.props.record.getFieldContext(this.props.name);
|
||||
}
|
||||
get domain() {
|
||||
return this.props.record.getFieldDomain(this.props.name);
|
||||
}
|
||||
get hasExternalButton() {
|
||||
return this.props.canOpen && !!this.value && !this.state.isFloating;
|
||||
}
|
||||
get context() {
|
||||
return this.props.context;
|
||||
}
|
||||
get displayName() {
|
||||
return this.value ? this.value[1].split("\n")[0] : "";
|
||||
}
|
||||
@@ -203,7 +203,7 @@ export class Many2OneField extends Component {
|
||||
};
|
||||
}
|
||||
getDomain() {
|
||||
return this.domain.toList(this.context);
|
||||
return this.props.domain || this.props.record.getFieldDomain(this.props.name);
|
||||
}
|
||||
async openAction() {
|
||||
const action = await this.orm.call(this.relation, "get_formview_action", [[this.resId]], {
|
||||
@@ -299,7 +299,7 @@ export const many2OneField = {
|
||||
component: Many2OneField,
|
||||
displayName: _lt("Many2one"),
|
||||
supportedTypes: ["many2one"],
|
||||
extractProps: ({ attrs, options, string }) => {
|
||||
extractProps({ attrs, options, string }, dynamicInfo) {
|
||||
const canCreate =
|
||||
attrs.can_create && Boolean(JSON.parse(attrs.can_create)) && !options.no_create;
|
||||
return {
|
||||
@@ -309,6 +309,8 @@ export const many2OneField = {
|
||||
canWrite: attrs.can_write && Boolean(JSON.parse(attrs.can_write)),
|
||||
canQuickCreate: canCreate && !options.no_quick_create,
|
||||
canCreateEdit: canCreate && !options.no_create_edit,
|
||||
context: dynamicInfo.context,
|
||||
domain: dynamicInfo.domain,
|
||||
nameCreateField: options.create_name_field,
|
||||
canScanBarcode: !!options.can_scan_barcode,
|
||||
string,
|
||||
|
||||
@@ -70,17 +70,8 @@ export class KanbanMany2OneAvatarField extends Many2OneAvatarField {
|
||||
readonly: false,
|
||||
};
|
||||
}
|
||||
get isFieldReadonly() {
|
||||
return this.props.record.isReadonly(this.props.name);
|
||||
}
|
||||
get many2OneProps() {
|
||||
return {
|
||||
...super.many2OneProps,
|
||||
readonly: this.isFieldReadonly,
|
||||
};
|
||||
}
|
||||
openPopover(ev) {
|
||||
if (this.isFieldReadonly) {
|
||||
if (this.props.readonly) {
|
||||
return;
|
||||
}
|
||||
if (this.closePopoverFn) {
|
||||
@@ -102,10 +93,10 @@ export class KanbanMany2OneAvatarField extends Many2OneAvatarField {
|
||||
}
|
||||
|
||||
get canDisplayDelete() {
|
||||
return !this.isFieldReadonly && this.props.record.data[this.props.name] && !isMobileOS();
|
||||
return !this.props.readonly && this.props.record.data[this.props.name] && !isMobileOS();
|
||||
}
|
||||
async remove(ev) {
|
||||
if (this.isFieldReadonly) {
|
||||
if (this.props.readonly) {
|
||||
return;
|
||||
}
|
||||
await this.props.record.update({ [this.props.name]: false });
|
||||
@@ -117,6 +108,11 @@ export const kanbanMany2OneAvatarField = {
|
||||
...many2OneField,
|
||||
component: KanbanMany2OneAvatarField,
|
||||
additionalClasses: ["o_field_many2one_avatar_kanban"],
|
||||
extractProps(fieldInfo, dynamicInfo) {
|
||||
const props = many2OneField.extractProps(...arguments);
|
||||
props.readonly = dynamicInfo.readonly;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
registry.category("fields").add("many2one_avatar", many2OneAvatarField);
|
||||
registry.category("fields").add("kanban.many2one_avatar", kanbanMany2OneAvatarField);
|
||||
|
||||
@@ -15,10 +15,11 @@ export const many2OneBarcodeField = {
|
||||
...many2OneField,
|
||||
component: Many2OneBarcodeField,
|
||||
displayName: _lt("Many2OneBarcode"),
|
||||
extractProps: (fieldInfo) => ({
|
||||
...many2OneField.extractProps(fieldInfo),
|
||||
canScanBarcode: true,
|
||||
}),
|
||||
extractProps() {
|
||||
const props = many2OneField.extractProps(...arguments);
|
||||
props.canScanBarcode = true;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2one_barcode", many2OneBarcodeField);
|
||||
|
||||
@@ -62,9 +62,6 @@ export class PriorityField extends Component {
|
||||
? this.state.index
|
||||
: this.options.findIndex((o) => o[0] === this.props.record.data[this.props.name]);
|
||||
}
|
||||
get isReadonly() {
|
||||
return this.props.record.isReadonly(this.props.name);
|
||||
}
|
||||
|
||||
getTooltip(value) {
|
||||
return this.tooltipLabel && this.tooltipLabel !== value
|
||||
@@ -93,9 +90,12 @@ export const priorityField = {
|
||||
component: PriorityField,
|
||||
displayName: _lt("Priority"),
|
||||
supportedTypes: ["selection"],
|
||||
extractProps: ({ viewType }) => ({
|
||||
withCommand: viewType === "form",
|
||||
}),
|
||||
extractProps({ viewType }, dynamicInfo) {
|
||||
return {
|
||||
withCommand: viewType === "form",
|
||||
readonly: dynamicInfo.readonly,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("priority", priorityField);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="o_priority" role="radiogroup" name="priority" aria-label="Priority">
|
||||
<t t-foreach="options" t-as="value" t-key="value">
|
||||
<t t-if="!value_first">
|
||||
<t t-if="isReadonly">
|
||||
<t t-if="props.readonly">
|
||||
<span
|
||||
class="o_priority_star fa"
|
||||
role="radio"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
id="generateUniqueDomID()"
|
||||
canChangeDefinition="state.canChangeDefinition"
|
||||
comodel="propertyConfiguration.comodel || ''"
|
||||
context="context"
|
||||
context="props.context"
|
||||
domain="propertyConfiguration.domain || '[]'"
|
||||
readonly="props.readonly"
|
||||
selection="propertyConfiguration.selection"
|
||||
|
||||
@@ -27,6 +27,7 @@ export class PropertiesField extends Component {
|
||||
};
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
context: { type: Object, optional: true },
|
||||
columns: { type: Number, optional: true },
|
||||
hideKanbanOption: { type: Boolean, optional: true },
|
||||
};
|
||||
@@ -73,15 +74,6 @@ export class PropertiesField extends Component {
|
||||
* Public methods / Getters
|
||||
* -------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the current context
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
get context() {
|
||||
return this.props.record.getFieldContext(this.props.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current properties value.
|
||||
*
|
||||
@@ -476,7 +468,7 @@ export class PropertiesField extends Component {
|
||||
propertyDefinition: this.propertiesList.find(
|
||||
(property) => property.name === currentName(propertyName)
|
||||
),
|
||||
context: this.context,
|
||||
context: this.props.context,
|
||||
onChange: this.onPropertyDefinitionChange.bind(this),
|
||||
onDelete: () => this.onPropertyDelete(currentName(propertyName)),
|
||||
onPropertyMove: (direction) =>
|
||||
@@ -522,10 +514,13 @@ export const propertiesField = {
|
||||
component: PropertiesField,
|
||||
displayName: _lt("Properties"),
|
||||
supportedTypes: ["properties"],
|
||||
extractProps: ({ attrs }) => ({
|
||||
columns: parseInt(attrs.columns || "1"),
|
||||
hideKanbanOption: archParseBoolean(attrs.hideKanbanOption),
|
||||
}),
|
||||
extractProps({ attrs }, dynamicInfo) {
|
||||
return {
|
||||
context: dynamicInfo.context,
|
||||
columns: parseInt(attrs.columns || "1"),
|
||||
hideKanbanOption: archParseBoolean(attrs.hideKanbanOption),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("properties", propertiesField);
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
id="domId"
|
||||
canChangeDefinition="state.canChangeDefinition"
|
||||
comodel="propertyConfiguration.comodel || ''"
|
||||
context="context"
|
||||
context="props.context"
|
||||
domain="propertyConfiguration.domain || '[]'"
|
||||
readonly="props.readonly"
|
||||
selection="propertyConfiguration.selection"
|
||||
|
||||
@@ -81,10 +81,8 @@ export const radioField = {
|
||||
|
||||
registry.category("fields").add("radio", radioField);
|
||||
|
||||
export async function preloadRadio(orm, record, fieldName) {
|
||||
export async function preloadRadio(orm, record, fieldName, { domain }) {
|
||||
const field = record.fields[fieldName];
|
||||
const context = record.evalContext;
|
||||
const domain = record.getFieldDomain(fieldName).toList(context);
|
||||
const records = await orm.searchRead(field.relation, domain, ["id"]);
|
||||
return await orm.call(field.relation, "name_get", [records.map((record) => record.id)]);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ export const referenceField = {
|
||||
displayName: _lt("Reference"),
|
||||
supportedTypes: ["reference", "char"],
|
||||
legacySpecialData: "_fetchSpecialReference",
|
||||
extractProps: (fieldInfo) => ({
|
||||
extractProps({ options }) {
|
||||
/*
|
||||
1 - <field name="ref" options="{'model_field': 'model_id'}" />
|
||||
2 - <field name="ref" options="{'hide_model': True}" />
|
||||
@@ -140,9 +140,10 @@ export const referenceField = {
|
||||
|
||||
We want to display the model selector only in the 4th case.
|
||||
*/
|
||||
...many2OneField.extractProps(fieldInfo),
|
||||
hideModelSelector: !!fieldInfo.options.hide_model || !!fieldInfo.options.model_field,
|
||||
}),
|
||||
const props = many2OneField.extractProps(...arguments);
|
||||
props.hideModelSelector = !!options.hide_model || !!options.model_field;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("reference", referenceField);
|
||||
|
||||
@@ -11,6 +11,7 @@ export class SelectionField extends Component {
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
placeholder: { type: String, optional: true },
|
||||
required: { type: Boolean, optional: true },
|
||||
};
|
||||
|
||||
get options() {
|
||||
@@ -45,9 +46,6 @@ export class SelectionField extends Component {
|
||||
? rawValue[0]
|
||||
: rawValue;
|
||||
}
|
||||
get isRequired() {
|
||||
return this.props.record.isRequired(this.props.name);
|
||||
}
|
||||
|
||||
stringify(value) {
|
||||
return JSON.stringify(value);
|
||||
@@ -81,17 +79,18 @@ export const selectionField = {
|
||||
supportedTypes: ["many2one", "selection"],
|
||||
legacySpecialData: "_fetchSpecialRelation",
|
||||
isEmpty: (record, fieldName) => record.data[fieldName] === false,
|
||||
extractProps: ({ attrs }) => ({
|
||||
placeholder: attrs.placeholder,
|
||||
}),
|
||||
extractProps({ attrs }, dynamicInfo) {
|
||||
return {
|
||||
placeholder: attrs.placeholder,
|
||||
required: dynamicInfo.required,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("selection", selectionField);
|
||||
|
||||
export function preloadSelection(orm, record, fieldName) {
|
||||
export function preloadSelection(orm, record, fieldName, { domain }) {
|
||||
const field = record.fields[fieldName];
|
||||
const context = record.evalContext;
|
||||
const domain = record.getFieldDomain(fieldName).toList(context);
|
||||
return orm.call(field.relation, "name_search", ["", domain]);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
t-att-selected="false === value"
|
||||
t-att-value="stringify(false)"
|
||||
t-esc="this.props.placeholder || ''"
|
||||
t-attf-style="{{ isRequired ? 'display:none' : '' }}"
|
||||
t-attf-style="{{ props.required ? 'display:none' : '' }}"
|
||||
/>
|
||||
<t t-foreach="options" t-as="option" t-key="option[0]">
|
||||
<option
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useCommand } from "@web/core/commands/command_hook";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { standardFieldProps } from "../standard_field_props";
|
||||
import { formatSelection } from "../formatters";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { standardFieldProps } from "../standard_field_props";
|
||||
|
||||
export class StateSelectionField extends Component {
|
||||
static template = "web.StateSelectionField";
|
||||
@@ -65,9 +65,6 @@ export class StateSelectionField extends Component {
|
||||
}
|
||||
return formatSelection(this.currentValue, { selection: this.options });
|
||||
}
|
||||
get isReadonly() {
|
||||
return this.props.record.isReadonly(this.props.name);
|
||||
}
|
||||
|
||||
statusColor(value) {
|
||||
return this.colors[value] ? this.colorPrefix + this.colors[value] : "";
|
||||
@@ -83,10 +80,13 @@ export const stateSelectionField = {
|
||||
component: StateSelectionField,
|
||||
displayName: _lt("Label Selection"),
|
||||
supportedTypes: ["selection"],
|
||||
extractProps: ({ options, viewType }) => ({
|
||||
showLabel: viewType === "list" && !options.hide_label,
|
||||
withCommand: viewType === "form",
|
||||
}),
|
||||
extractProps({ options, viewType }, dynamicInfo) {
|
||||
return {
|
||||
showLabel: viewType === "list" && !options.hide_label,
|
||||
withCommand: viewType === "form",
|
||||
readonly: dynamicInfo.readonly,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("state_selection", stateSelectionField);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.StateSelectionField" owl="1">
|
||||
<t t-if="isReadonly">
|
||||
<t t-if="props.readonly">
|
||||
<button class="d-flex align-items-center btn fw-normal p-0">
|
||||
<span t-attf-class="o_status {{ statusColor(currentValue) }} "/>
|
||||
<span t-if="props.showLabel" class="o_status_label ms-1" t-esc="label"/>
|
||||
|
||||
@@ -236,17 +236,17 @@ export const statusBarField = {
|
||||
|
||||
registry.category("fields").add("statusbar", statusBarField);
|
||||
|
||||
export async function preloadStatusBar(orm, record, fieldName) {
|
||||
export async function preloadStatusBar(orm, record, fieldName, { domain }) {
|
||||
const fieldNames = ["id", "display_name"];
|
||||
const foldField = record.activeFields[fieldName].options.fold_field;
|
||||
if (foldField) {
|
||||
fieldNames.push(foldField);
|
||||
}
|
||||
|
||||
const context = record.evalContext;
|
||||
let domain = record.getFieldDomain(fieldName).toList(context);
|
||||
if (domain.length && record.data[fieldName]) {
|
||||
domain = Domain.or([[["id", "=", record.data[fieldName][0]]], domain]).toList(context);
|
||||
domain = Domain.or([[["id", "=", record.data[fieldName][0]]], domain]).toList(
|
||||
record.evalContext
|
||||
);
|
||||
}
|
||||
|
||||
const relation = record.fields[fieldName].relation;
|
||||
|
||||
@@ -74,11 +74,12 @@ export const timezoneMismatchField = {
|
||||
...selectionField,
|
||||
component: TimezoneMismatchField,
|
||||
additionalClasses: ["d-flex"],
|
||||
extractProps: (fieldInfo) => ({
|
||||
...selectionField.extractProps(fieldInfo),
|
||||
tzOffsetField: fieldInfo.options.tz_offset_field,
|
||||
mismatchTitle: fieldInfo.options.mismatch_title,
|
||||
}),
|
||||
extractProps({ options }) {
|
||||
const props = selectionField.extractProps(...arguments);
|
||||
props.tzOffsetField = options.tz_offset_field;
|
||||
props.mismatchTitle = options.mismatch_title;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("timezone_mismatch", timezoneMismatchField);
|
||||
|
||||
@@ -32,9 +32,8 @@ export class X2ManyField extends Component {
|
||||
crudOptions: { type: Object, optional: true },
|
||||
string: { type: String, optional: true },
|
||||
views: { type: Object, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
archInfo: {},
|
||||
domain: { type: Array, optional: true },
|
||||
context: { type: Object },
|
||||
};
|
||||
|
||||
setup() {
|
||||
@@ -239,9 +238,8 @@ export class X2ManyField extends Component {
|
||||
}
|
||||
|
||||
async onAdd({ context, editable } = {}) {
|
||||
const record = this.props.record;
|
||||
const domain = record.getFieldDomain(this.props.name).toList();
|
||||
context = makeContext([record.getFieldContext(this.props.name), context]);
|
||||
const domain = this.props.domain || this.props.record.getFieldDomain(this.props.name);
|
||||
context = makeContext([this.props.context, context]);
|
||||
if (this.isMany2Many) {
|
||||
const { string } = this.props;
|
||||
const title = sprintf(this.env._t("Add: %s"), string);
|
||||
@@ -274,9 +272,11 @@ export const x2ManyField = {
|
||||
displayName: _lt("Relational table"),
|
||||
supportedTypes: ["one2many", "many2many"],
|
||||
useSubView: true,
|
||||
extractProps: ({ attrs, viewMode, views, widget, options, string }) => {
|
||||
extractProps: ({ attrs, viewMode, views, widget, options, string }, dynamicInfo) => {
|
||||
const props = {
|
||||
addLabel: attrs["add-label"],
|
||||
context: dynamicInfo.context,
|
||||
domain: dynamicInfo.domain,
|
||||
viewMode,
|
||||
views,
|
||||
crudOptions: options,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useSortable } from "@web/core/utils/sortable";
|
||||
import { getTabableElements } from "@web/core/utils/ui";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { getTooltipInfo } from "@web/views/fields/field_tooltip";
|
||||
import { getClassNameFromDecoration } from "@web/views/utils";
|
||||
import { evalDomain, getClassNameFromDecoration } from "@web/views/utils";
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
import { useBounceButton } from "@web/views/view_hook";
|
||||
import { Widget } from "@web/views/widgets/widget";
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
onPatched,
|
||||
onWillPatch,
|
||||
onWillUpdateProps,
|
||||
useEffect,
|
||||
useExternalListener,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "@odoo/owl";
|
||||
|
||||
const formatters = registry.category("formatters");
|
||||
@@ -258,7 +258,7 @@ export class ListRenderer extends Component {
|
||||
}
|
||||
|
||||
getFieldProps(record, column) {
|
||||
if (this.getCellReadonly(column, record)) {
|
||||
if (this.isCellReadonly(column, record)) {
|
||||
return {
|
||||
readonly: true,
|
||||
};
|
||||
@@ -445,7 +445,7 @@ export class ListRenderer extends Component {
|
||||
}
|
||||
// in findNextFocusableOnRow test is done by using classList
|
||||
// refactor
|
||||
if (!this.getCellReadonly(column, editedRecord)) {
|
||||
if (!this.isCellReadonly(column, editedRecord)) {
|
||||
const cell = this.tableRef.el.querySelector(
|
||||
`.o_selected_row td[name='${column.name}']`
|
||||
);
|
||||
@@ -695,9 +695,8 @@ export class ListRenderer extends Component {
|
||||
}
|
||||
|
||||
isSortable(column) {
|
||||
const { hasLabel, name } = column;
|
||||
const { hasLabel, name, options } = column;
|
||||
const { sortable } = this.fields[name];
|
||||
const { options } = this.props.list.activeFields[name];
|
||||
return (sortable || options.allow_order) && hasLabel;
|
||||
}
|
||||
|
||||
@@ -765,20 +764,21 @@ export class ListRenderer extends Component {
|
||||
}
|
||||
const classNames = [...this.cellClassByColumn[column.id]];
|
||||
if (column.type === "field") {
|
||||
if (record.isRequired(column.name)) {
|
||||
const { required } = column.modifiers;
|
||||
if (required && evalDomain(required, record.evalContext)) {
|
||||
classNames.push("o_required_modifier");
|
||||
}
|
||||
if (record.isInvalid(column.name)) {
|
||||
classNames.push("o_invalid_cell");
|
||||
}
|
||||
if (this.getCellReadonly(column, record)) {
|
||||
if (this.isCellReadonly(column, record)) {
|
||||
classNames.push("o_readonly_modifier");
|
||||
}
|
||||
if (this.canUseFormatter(column, record)) {
|
||||
// generate field decorations classNames (only if field-specific decorations
|
||||
// have been defined in an attribute, e.g. decoration-danger="other_field = 5")
|
||||
// only handle the text-decoration.
|
||||
const { decorations } = record.activeFields[column.name];
|
||||
const { decorations } = column;
|
||||
for (const decoName in decorations) {
|
||||
if (evaluateExpr(decorations[decoName], record.evalContext)) {
|
||||
classNames.push(getClassNameFromDecoration(decoName));
|
||||
@@ -788,7 +788,7 @@ export class ListRenderer extends Component {
|
||||
if (
|
||||
record.isInEdition &&
|
||||
this.props.list.editedRecord &&
|
||||
this.props.list.editedRecord.isReadonly(column.name)
|
||||
this.isCellReadonly(column, this.props.list.editedRecord)
|
||||
) {
|
||||
classNames.push("text-muted");
|
||||
} else {
|
||||
@@ -798,10 +798,10 @@ export class ListRenderer extends Component {
|
||||
return classNames.join(" ");
|
||||
}
|
||||
|
||||
getCellReadonly(column, record) {
|
||||
isCellReadonly(column, record) {
|
||||
return (
|
||||
record.isReadonly(column.name) ||
|
||||
(column.relatedPropertyField && record.selected && record.model.multiEdit)
|
||||
(column.relatedPropertyField && record.selected && record.model.multiEdit) ||
|
||||
evalDomain(column.modifiers.readonly, record.evalContext)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1780,7 +1780,7 @@ export class ListRenderer extends Component {
|
||||
viewMode: "list",
|
||||
resModel: this.props.list.resModel,
|
||||
field: this.fields[column.name],
|
||||
fieldInfo: this.props.list.activeFields[column.name],
|
||||
fieldInfo: column,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
t-on-click="(ev) => this.onCellClicked(record, column, ev)" tabindex="-1">
|
||||
<t t-if="!isInvisible">
|
||||
<t t-if="canUseFormatter(column, record)" t-out="getFormattedValue(column, record)"/>
|
||||
<Field t-else="" name="column.name" record="record" type="column.widget" class="getFieldClass(column)" fieldInfo="props.archInfo.activeFields[column.name]" t-props="getFieldProps(record, column)"/>
|
||||
<Field t-else="" name="column.name" record="record" type="column.widget" class="getFieldClass(column)" fieldInfo="column" t-props="getFieldProps(record, column)"/>
|
||||
</t>
|
||||
</td>
|
||||
</t>
|
||||
|
||||
@@ -596,7 +596,7 @@ export class Record extends DataPoint {
|
||||
|
||||
const isSet = activeField && activeField.field && activeField.field.isSet;
|
||||
|
||||
if (this.isRequired(fieldName) && isSet && !isSet(this.data[fieldName])) {
|
||||
if (this._isRequired(fieldName) && isSet && !isSet(this.data[fieldName])) {
|
||||
this.setInvalidField(fieldName);
|
||||
continue;
|
||||
}
|
||||
@@ -614,7 +614,7 @@ export class Record extends DataPoint {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!isSet && this.isRequired(fieldName) && !this.data[fieldName]) {
|
||||
if (!isSet && this._isRequired(fieldName) && !this.data[fieldName]) {
|
||||
this.setInvalidField(fieldName);
|
||||
}
|
||||
}
|
||||
@@ -715,7 +715,7 @@ export class Record extends DataPoint {
|
||||
!allFields &&
|
||||
fieldName in this.activeFields &&
|
||||
!this.activeFields[fieldName].forceSave &&
|
||||
this.isReadonly(fieldName)
|
||||
this._isReadonly(fieldName)
|
||||
) {
|
||||
delete changes[fieldName];
|
||||
continue;
|
||||
@@ -744,25 +744,18 @@ export class Record extends DataPoint {
|
||||
};
|
||||
}
|
||||
|
||||
getFieldContext(fieldName) {
|
||||
getFieldDomain(fieldName) {
|
||||
const { domain } = this.fields[fieldName];
|
||||
return domain ? new Domain(domain).toList(this.evalContext) : [];
|
||||
}
|
||||
|
||||
_getFieldContext(fieldName) {
|
||||
return Object.assign(
|
||||
this.context,
|
||||
makeContext([this.activeFields[fieldName].context], this.evalContext)
|
||||
);
|
||||
}
|
||||
|
||||
getFieldDomain(fieldName) {
|
||||
const rawDomains = [
|
||||
this._domains[fieldName] || [],
|
||||
this.activeFields[fieldName].domain || this.fields[fieldName].domain || [],
|
||||
];
|
||||
|
||||
const evalContext = this.evalContext;
|
||||
return Domain.and(
|
||||
rawDomains.map((d) => (typeof d === "string" ? evaluateExpr(d, evalContext) : d))
|
||||
);
|
||||
}
|
||||
|
||||
isFieldDirty(fieldName) {
|
||||
return this.isDirty && !!this._changes[fieldName];
|
||||
}
|
||||
@@ -787,7 +780,7 @@ export class Record extends DataPoint {
|
||||
* @param {string} fieldName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReadonly(fieldName) {
|
||||
_isReadonly(fieldName) {
|
||||
const { readonly } = this.activeFields[fieldName].modifiers || {};
|
||||
return evalDomain(readonly, this.evalContext);
|
||||
}
|
||||
@@ -797,7 +790,7 @@ export class Record extends DataPoint {
|
||||
* @param {string} fieldName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isRequired(fieldName) {
|
||||
_isRequired(fieldName) {
|
||||
const required = this._requiredFields[fieldName];
|
||||
return required ? evalDomain(required, this.evalContext) : false;
|
||||
}
|
||||
@@ -845,12 +838,25 @@ export class Record extends DataPoint {
|
||||
if (!info.loadOnTypes.includes(this.fields[fieldName].type)) {
|
||||
return;
|
||||
}
|
||||
const domain = this.getFieldDomain(fieldName).toList(this.evalContext).toString();
|
||||
|
||||
const evalContext = this.evalContext;
|
||||
const domain = this.activeFields[fieldName].domain
|
||||
? new Domain(
|
||||
evaluateExpr(this.activeFields[fieldName].domain, evalContext)
|
||||
).toList()
|
||||
: this.getFieldDomain(fieldName);
|
||||
|
||||
const getExtraKey = info.extraMemoizationKey || (() => null);
|
||||
const key = JSON.stringify([domain, getExtraKey(this, fieldName)]);
|
||||
const key = JSON.stringify([domain.toString(), getExtraKey(this, fieldName)]);
|
||||
if (this.preloadedDataCaches[fieldName] !== key) {
|
||||
const context = this._getFieldContext(fieldName);
|
||||
this.preloadedDataCaches[fieldName] = key;
|
||||
this.preloadedData[fieldName] = await info.preload(this.model.orm, this, fieldName);
|
||||
this.preloadedData[fieldName] = await info.preload(
|
||||
this.model.orm,
|
||||
this,
|
||||
fieldName,
|
||||
{ context, domain }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1331,7 +1337,7 @@ export class Record extends DataPoint {
|
||||
value &&
|
||||
(!value[1] || activeField.options.always_reload)
|
||||
) {
|
||||
const context = this.getFieldContext(fieldName);
|
||||
const context = this._getFieldContext(fieldName);
|
||||
const result = await this.model.orm.nameGet(relation, [value[0]], { context });
|
||||
return result[0];
|
||||
}
|
||||
@@ -1348,7 +1354,7 @@ export class Record extends DataPoint {
|
||||
value = { resModel, resId: parseInt(resId, 10) };
|
||||
}
|
||||
const { resModel, resId } = value;
|
||||
const context = this.getFieldContext(fieldName);
|
||||
const context = this._getFieldContext(fieldName);
|
||||
const nameGet = await this.model.orm.nameGet(resModel, [resId], { context });
|
||||
return {
|
||||
resModel,
|
||||
@@ -1739,8 +1745,8 @@ class DynamicList extends DataPoint {
|
||||
if (
|
||||
!Object.keys(changes).filter(
|
||||
(fieldName) =>
|
||||
record.isReadonly(fieldName) ||
|
||||
(record.isRequired(fieldName) && !changes[fieldName])
|
||||
record._isReadonly(fieldName) ||
|
||||
(record._isRequired(fieldName) && !changes[fieldName])
|
||||
).length
|
||||
) {
|
||||
result.push(record);
|
||||
|
||||
@@ -2015,62 +2015,6 @@ odoo.define('web.basic_model_tests', function (require) {
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('has a proper evaluation context', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const unpatchDate = testUtils.mock.patchDate(1997, 0, 9, 12, 0, 0);
|
||||
this.params.fieldNames = Object.keys(this.data.partner.fields);
|
||||
this.params.res_id = 1;
|
||||
|
||||
var model = await createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
var resultID = await model.load(this.params);
|
||||
const { evalContext } = model.get(resultID);
|
||||
assert.strictEqual(typeof evalContext.datetime, "object");
|
||||
assert.strictEqual(typeof evalContext.relativedelta, "object");
|
||||
assert.strictEqual(typeof evalContext.time, "object");
|
||||
assert.strictEqual(typeof evalContext.context_today, "function");
|
||||
assert.strictEqual(typeof evalContext.tz_offset, "function");
|
||||
const blackListedKeys = [
|
||||
"time",
|
||||
"datetime",
|
||||
"relativedelta",
|
||||
"context_today",
|
||||
"tz_offset",
|
||||
];
|
||||
// Remove uncomparable values from the evaluation context
|
||||
for (const key of blackListedKeys) {
|
||||
delete evalContext[key];
|
||||
}
|
||||
assert.deepEqual(evalContext, {
|
||||
active: true,
|
||||
active_id: 1,
|
||||
active_ids: [1],
|
||||
active_model: "partner",
|
||||
bar: 1,
|
||||
category: [12],
|
||||
current_company_id: false,
|
||||
current_date: moment().format('YYYY-MM-DD'),
|
||||
today: moment().format('YYYY-MM-DD'),
|
||||
now: moment().utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||
date: "2017-01-25",
|
||||
display_name: "first partner",
|
||||
foo: "blip",
|
||||
id: 1,
|
||||
product_id: 37,
|
||||
product_ids: [],
|
||||
qux: false,
|
||||
reference: false,
|
||||
total: 0,
|
||||
x_active: true,
|
||||
}, "should use the proper eval context");
|
||||
model.destroy();
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('x2manys in contexts and domains are correctly evaluated', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
|
||||
@@ -3760,60 +3760,6 @@ QUnit.module("Fields", (hooks) => {
|
||||
await click(target, ".o_field_widget[name='trululu'] input");
|
||||
});
|
||||
|
||||
QUnit.test("many2one in one2many: domain updated by an onchange", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
trululu() {},
|
||||
};
|
||||
|
||||
let domain = [];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree editable="bottom">
|
||||
<field name="foo" />
|
||||
<field name="trululu" />
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
mockRPC(route, { kwargs, method }) {
|
||||
if (method === "onchange") {
|
||||
return Promise.resolve({
|
||||
domain: {
|
||||
trululu: domain,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (method === "name_search") {
|
||||
assert.deepEqual(kwargs.args, domain, "sent domain should be correct");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// add a first row with a specific domain for the m2o
|
||||
domain = [["id", "in", [10]]]; // domain for subrecord 1
|
||||
await click(target, ".o_field_x2many_list_row_add a");
|
||||
await click(target, ".o_field_widget[name=trululu] input");
|
||||
// add some value to foo field to make record dirty
|
||||
await editInput(target, "tr.o_selected_row .o_field_widget[name='foo'] input", "new value");
|
||||
|
||||
// add a second row with another domain for the m2o
|
||||
domain = [["id", "in", [5]]]; // domain for subrecord 2
|
||||
await click(target, ".o_field_x2many_list_row_add a");
|
||||
await click(target, ".o_field_widget[name=trululu] input");
|
||||
|
||||
// check again the first row to ensure that the domain hasn't change
|
||||
domain = [["id", "in", [10]]]; // domain for subrecord 1 should have been kept
|
||||
await click(target.querySelectorAll(".o_data_row .o_data_cell")[1]);
|
||||
await click(target, ".o_field_widget[name=trululu] input");
|
||||
});
|
||||
|
||||
QUnit.test("search more in many2one: no text in input", async function (assert) {
|
||||
// when the user clicks on 'Search More...' in a many2one dropdown, and there is no text
|
||||
// in the input (i.e. no value to search on), we bypass the name_search that is meant to
|
||||
|
||||
@@ -354,7 +354,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
serverData.views = {
|
||||
"turtle,false,form": `
|
||||
<form>
|
||||
<field name="parent_id" domain="[('id', 'in', parent.turtles)]"/>
|
||||
<field name="parent_id"/>
|
||||
</form>`,
|
||||
};
|
||||
await makeView({
|
||||
@@ -367,6 +367,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
<tree>
|
||||
<field name="parent_id"/>
|
||||
</tree>
|
||||
<form>
|
||||
<field name="parent_id" domain="[('id', 'in', parent.turtles)]"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
mockRPC(route, { kwargs }) {
|
||||
|
||||
@@ -8304,26 +8304,6 @@ QUnit.module("Views", (hooks) => {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("fields and record contexts are not mixed", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form><field name="trululu" context="{'test': 1}"/></form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "name_search") {
|
||||
assert.strictEqual(args.kwargs.context.test, 1);
|
||||
assert.notOk("mainContext" in args.kwargs.context);
|
||||
}
|
||||
},
|
||||
resId: 2,
|
||||
context: { mainContext: 3 },
|
||||
});
|
||||
await click(target.querySelector(".o_field_widget[name=trululu] input"));
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"do not activate an hidden tab when switching between records",
|
||||
async function (assert) {
|
||||
@@ -10879,69 +10859,6 @@ QUnit.module("Views", (hooks) => {
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("domain returned by onchange is cleared on discard", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: function () {},
|
||||
};
|
||||
|
||||
const domain = [["id", "=", 1]];
|
||||
let expectedDomain = domain;
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="trululu"/>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "onchange" && args.args[0][0] === 1) {
|
||||
// onchange returns a domain only on record 1
|
||||
return Promise.resolve({
|
||||
domain: {
|
||||
trululu: domain,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (args.method === "name_search") {
|
||||
assert.deepEqual(args.kwargs.args, expectedDomain);
|
||||
}
|
||||
},
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=foo] input").value,
|
||||
"yop",
|
||||
"should be on record 1"
|
||||
);
|
||||
|
||||
// change foo to trigger the onchange
|
||||
await editInput(target, ".o_field_widget[name=foo] input", "new value");
|
||||
|
||||
// open many2one dropdown to check if the domain is applied
|
||||
target.querySelector(".o-autocomplete.dropdown input").focus(); // autocomplete closes with the blur, so it must have the focus
|
||||
await click(target.querySelector(".o-autocomplete.dropdown input"));
|
||||
|
||||
// switch to another record (should ask to discard changes, and reset the domain)
|
||||
target.querySelector(".o_pager_next").focus(); // change the focus before click, this will close the autocomplete
|
||||
await click(target.querySelector(".o_pager_next"));
|
||||
|
||||
assert.containsNone(document.body, ".modal", "should not open modal");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=foo] input").value,
|
||||
"blip",
|
||||
"should be on record 2"
|
||||
);
|
||||
|
||||
// open many2one dropdown to check if the domain is applied
|
||||
expectedDomain = [];
|
||||
await click(target.querySelector(".o-autocomplete.dropdown input"));
|
||||
});
|
||||
|
||||
QUnit.test("discard after a failed save (and close notifications)", async function (assert) {
|
||||
patchWithCleanup(browser, { setTimeout: () => 1 });
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export class HtmlField extends Component {
|
||||
// Ensure all external links are opened in a new tab.
|
||||
retargetLinks(this.readonlyElementRef.el);
|
||||
|
||||
const hasReadonlyModifiers = Boolean(this.props.record.isReadonly(this.props.name));
|
||||
const hasReadonlyModifiers = this.props.hasReadonlyModifiers;
|
||||
if (!hasReadonlyModifiers) {
|
||||
const $el = $(this.readonlyElementRef.el);
|
||||
$el.off('.checklistBinding');
|
||||
@@ -631,13 +631,14 @@ HtmlField.props = {
|
||||
isInlineStyle: { type: Boolean, optional: true },
|
||||
wrapper: { type: String, optional: true },
|
||||
wysiwygOptions: { type: Object },
|
||||
hasReadonlyModifiers: { type: Boolean, optional: true },
|
||||
};
|
||||
|
||||
export const htmlField = {
|
||||
component: HtmlField,
|
||||
displayName: _lt("Html"),
|
||||
supportedTypes: ["html"],
|
||||
extractProps: ({ attrs, options }) => {
|
||||
extractProps({ attrs, options }, dynamicInfo) {
|
||||
const wysiwygOptions = {
|
||||
placeholder: attrs.placeholder,
|
||||
noAttachment: options['no-attachment'],
|
||||
@@ -683,6 +684,7 @@ export const htmlField = {
|
||||
wrapper: options.wrapper,
|
||||
|
||||
wysiwygOptions,
|
||||
hasReadonlyModifiers: dynamicInfo.readonly,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user