Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,57 @@
/** @odoo-module **/
import {patch} from "@web/core/utils/patch";
import {_t} from "web.core";
import Domain from "web.Domain";
import DomainSelector from "web.DomainSelector";
import basic_fields from "web.basic_fields";
/**
* The redraw in the Debug Field does not trigger correctly
* so we overwrite it with the v14 Version
*
*/
patch(DomainSelector.prototype, "web.DomainSelector", {
/**
* @override
*/
_onDebugInputChange(e) {
if (!$(".o_add_advanced_search").length) {
return this._super(...arguments);
}
const rawDomain = e.currentTarget.value;
try {
Domain.prototype.stringToArray(rawDomain);
} catch (err) {
// If there is a syntax error, just ignore the change
this.displayNotification({
title: _t("Syntax error"),
message: _t("Domain not properly formed"),
type: "danger",
});
return;
}
this._redraw(Domain.prototype.stringToArray(rawDomain)).then(
function () {
this.trigger_up("domain_changed", {
child: this,
alreadyRedrawn: true,
});
}.bind(this)
);
},
});
patch(basic_fields.FieldDomain.prototype, "web.basic_fields", {
/**
* Odoo restricts re-rendering the domain from the debug editor for supposedly
* performance reasons. We didn't ever came up with those and in v17 it's supported
* in the new advanced search.
* @override
*/
// eslint-disable-next-line
_onDomainSelectorValueChange(event) {
this._super(...arguments);
// Deactivate all debug conditions that cripple the functionality
this.debugEdition = false;
},
});

View file

@ -0,0 +1,202 @@
/** @odoo-module **/
import BasicModel from "web.BasicModel";
import {ComponentAdapter} from "web.OwlCompatibility";
import {Dropdown} from "@web/core/dropdown/dropdown";
import FieldManagerMixin from "web.FieldManagerMixin";
import {FieldMany2One} from "web.relational_fields";
import {SelectCreateDialog} from "web.view_dialogs";
import {patch} from "@web/core/utils/patch";
import {session} from "@web/session";
const {Component, xml} = owl;
patch(Dropdown.prototype, "dropdown", {
onWindowClicked(ev) {
// This patch is created to prevent the closing of the Filter menu
// when a selection is made in the RecordPicker
if ($(ev.target.closest("ul.dropdown-menu")).attr("id") !== undefined) {
const dropdown = $("body > ul.dropdown-menu");
for (let i = 0; i < dropdown.length; i++) {
if (
$(ev.target.closest("ul.dropdown-menu")).attr("id") ===
$(dropdown[i]).attr("id")
) {
return;
}
}
}
this._super(ev);
},
});
export const FakeMany2oneFieldWidget = FieldMany2One.extend(FieldManagerMixin, {
supportedFieldTypes: ["many2many", "many2one", "one2many"],
/**
* @override
*/
init: function (parent) {
this.componentAdapter = parent;
const options = this.componentAdapter.props.attrs;
// Create a dummy record with only a dummy m2o field to search on
const model = new BasicModel("dummy");
const params = {
fieldNames: ["dummy"],
modelName: "dummy",
context: {},
type: "record",
viewType: "default",
fieldsInfo: {default: {dummy: {}}},
fields: {
dummy: {
string: options.string,
relation: options.model,
context: options.context,
domain: options.domain,
type: "many2one",
},
},
};
// Emulate `model.load()`, without RPC-calling `default_get()`
this.dataPointID = model._makeDataPoint(params).id;
model.generateDefaultValues(this.dataPointID, {});
this._super(this.componentAdapter, "dummy", this._get_record(model), {
mode: "edit",
attrs: {
options: {
no_create_edit: true,
no_create: true,
no_open: true,
no_quick_create: true,
},
},
});
FieldManagerMixin.init.call(this, model);
},
/**
* Get record
*
* @param {BasicModel} model
* @returns {String}
*/
_get_record: function (model) {
return model.get(this.dataPointID);
},
/**
* @override
*/
_confirmChange: function (id, fields, event) {
this.componentAdapter.trigger("change", event.data.changes[fields[0]]);
this.dataPointID = id;
return this.reset(this._get_record(this.model), event);
},
/**
* Stop propagation of the 'Search more..' dialog click event.
* Otherwise, the filter's dropdown will be closed after a selection.
*
* @override
*/
_searchCreatePopup: function (view, ids, context, dynamicFilters) {
const options = this._getSearchCreatePopupOptions(
view,
ids,
context,
dynamicFilters
);
const dialog = new SelectCreateDialog(
this,
_.extend({}, this.nodeOptions, options)
);
// Hack to stop click event propagation
dialog._opened.then(() =>
dialog.$el
.get(0)
.addEventListener("click", (event) => event.stopPropagation())
);
return dialog.open();
},
_onFieldChanged: function (event) {
const self = this;
event.stopPropagation();
if (event.data.changes.dummy.display_name === undefined) {
return this._rpc({
model: this.field.relation,
method: "name_get",
args: [event.data.changes.dummy.id],
context: session.user_context,
}).then(function (result) {
event.data.changes.dummy.display_name = result[0][1];
return (
self
._applyChanges(
event.data.dataPointID,
event.data.changes,
event
)
// eslint-disable-next-line no-empty-function
.then(event.data.onSuccess || function () {})
// eslint-disable-next-line no-empty-function
.guardedCatch(event.data.onFailure || function () {})
);
});
}
return (
this._applyChanges(event.data.dataPointID, event.data.changes, event)
// eslint-disable-next-line no-empty-function
.then(event.data.onSuccess || function () {})
// eslint-disable-next-line no-empty-function
.guardedCatch(event.data.onFailure || function () {})
);
},
});
export class FakeMany2oneFieldWidgetAdapter extends ComponentAdapter {
constructor() {
super(...arguments);
this.env = Component.env;
}
renderWidget() {
this.widget._render();
}
get widgetArgs() {
if (this.props.widgetArgs) {
return this.props.widgetArgs;
}
return [this.props.attrs];
}
}
/**
* A record selector widget.
*
* Underneath, it implements and extends the `FieldManagerMixin`, and acts as if it
* were a reduced dummy controller. Some actions "mock" the underlying model, since
* sometimes we use a char widget to fill related fields (which is not supported by
* that widget), and fields need an underlying model implementation, which can only
* hold fake data, given a search view has no data on it by definition.
*
* @extends Component
*/
export class RecordPicker extends Component {
setup() {
this.attrs = {
string: this.props.string,
model: this.props.model,
domain: this.props.domain,
context: this.props.context,
};
this.FakeMany2oneFieldWidget = FakeMany2oneFieldWidget;
}
}
RecordPicker.template = xml`
<div>
<FakeMany2oneFieldWidgetAdapter
Component="FakeMany2oneFieldWidget"
class="d-block"
attrs="attrs"
/>
</div>`;
RecordPicker.components = {FakeMany2oneFieldWidgetAdapter};

View file

@ -0,0 +1,21 @@
/** @odoo-module **/
import {Dropdown} from "@web/core/dropdown/dropdown";
import {patch} from "web.utils";
patch(Dropdown.prototype, "web.Dropdown", {
/**
* Our many2one widget in the filter menus has a dropdown that propagates some
* custom events through the bus to the search more pop-up. This is not replicable
* in core but we can simply cut it here
* @override
*/
onDropdownStateChanged(args) {
const direct_siblings =
args.emitter.rootRef.el.parentElement === this.rootRef.el.parentElement;
if (!direct_siblings && args.emitter.myActiveEl !== this.myActiveEl) {
return;
}
return this._super(...arguments);
},
});

View file

@ -0,0 +1,58 @@
/** @odoo-module **/
/*
Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2020 Tecnativa - Alexandre Díaz
Copyright 2022 Camptocamp SA - Iván Todorovich
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
*/
import {_t} from "web.core";
const JOIN_MAPPING = {
"&": _t(" and "),
"|": _t(" or "),
"!": _t(" is not "),
};
const HUMAN_DOMAIN_METHODS = {
DomainTree: function () {
const human_domains = [];
_.each(this.children, (child) => {
human_domains.push(HUMAN_DOMAIN_METHODS[child.template].apply(child));
});
return `(${human_domains.join(JOIN_MAPPING[this.operator])})`;
},
DomainSelector: function () {
const result = HUMAN_DOMAIN_METHODS.DomainTree.apply(this, arguments);
// Remove surrounding parenthesis
return result.slice(1, -1);
},
DomainLeaf: function () {
const chain = [];
let operator = this.operator_mapping[this.operator],
value = `"${this.value}"`;
// Humanize chain
const chain_splitted = this.chain.split(".");
const len = chain_splitted.length;
for (let x = 0; x < len; ++x) {
const element = chain_splitted[x];
chain.push(
_.findWhere(this.fieldSelector.popover.pages[x], {name: element})
.string || element
);
}
// Special beautiness for some values
if (this.operator === "=" && _.isBoolean(this.value)) {
operator = this.operator_mapping[this.value ? "set" : "not set"];
value = "";
} else if (_.isArray(this.value)) {
value = `["${this.value.join('", "')}"]`;
}
return `${chain.join("→")} ${operator || this.operator} ${value}`.trim();
},
};
export function getHumanDomain(domainSelector) {
return HUMAN_DOMAIN_METHODS.DomainSelector.apply(domainSelector);
}

View file

@ -0,0 +1,59 @@
/** @odoo-module **/
import Domain from "web.Domain";
import DomainSelectorDialog from "web.DomainSelectorDialog";
import config from "web.config";
import {getHumanDomain} from "../../../js/utils.esm";
import {standaloneAdapter} from "web.OwlCompatibility";
import {useModel} from "web.Model";
const {Component, useRef} = owl;
class AdvancedFilterItem extends Component {
setup() {
this.itemRef = useRef("dropdown-item");
this.model = useModel("searchModel");
}
/**
* Prevent propagation of dropdown-item-selected event, so that it
* doesn't reach the FilterMenu onFilterSelected event handler.
*/
mounted() {
$(this.itemRef.el).on("dropdown-item-selected", (event) =>
event.stopPropagation()
);
}
/**
* Open advanced search dialog
*
* @returns {DomainSelectorDialog} The opened dialog itself.
*/
onClick() {
const adapterParent = standaloneAdapter({Component});
const dialog = new DomainSelectorDialog(
adapterParent,
this.model.config.modelName,
"[]",
{
debugMode: config.isDebug(),
readonly: false,
}
);
// Add 1st domain node by default
dialog.opened(() => dialog.domainSelector._onAddFirstButtonClick());
// Configure handler
dialog.on("domain_selected", this, function (e) {
const preFilter = {
description: getHumanDomain(dialog.domainSelector),
domain: Domain.prototype.arrayToString(e.data.domain),
type: "filter",
};
this.model.dispatch("createNewFilters", [preFilter]);
});
return dialog.open();
}
}
AdvancedFilterItem.components = {AdvancedFilterItem};
AdvancedFilterItem.template = "web_advanced_search.AdvancedFilterItem";
export default AdvancedFilterItem;

View file

@ -0,0 +1,95 @@
/** @odoo-module **/
import CustomFilterItem from "web.CustomFilterItem";
import {RecordPicker} from "../../../js/RecordPicker.esm";
import {patch} from "@web/core/utils/patch";
/**
* Patches the CustomFilterItem for legacy widgets.
*
* Tree views still use this old legacy widget, so we need to patch it.
* This is likely to disappear in 17.0
*/
patch(CustomFilterItem.prototype, "web_advanced_search.legacy.CustomFilterItem", {
/**
* Ideally we'd want this in setup, but CustomFilterItem does its initialization
* in the constructor, which can't be patched.
*
* Doing it here works just as well.
*
* @override
*/
async willStart() {
this.OPERATORS.relational = this.OPERATORS.char;
this.FIELD_TYPES.many2one = "relational";
this.FIELD_TYPES.many2many = "relational";
this.FIELD_TYPES.one2many = "relational";
return this._super(...arguments);
},
/**
* @override
*/
_setDefaultValue(condition) {
const res = this._super(...arguments);
const fieldType = this.fields[condition.field].type;
const genericType = this.FIELD_TYPES[fieldType];
if (genericType === "relational") {
condition.value = 0;
condition.displayedValue = "";
}
return res;
},
/**
* Add displayed value to preFilters for "relational" types.
*
* @override
*/
onApply() {
// To avoid the complete override, we patch this.conditions.map()
const originalMapFn = this.conditions.map;
const self = this;
this.conditions.map = function () {
const preFilters = originalMapFn.apply(this, arguments);
for (const condition of this) {
const field = self.fields[condition.field];
const type = self.FIELD_TYPES[field.type];
if (type === "relational") {
const idx = this.indexOf(condition);
const preFilter = preFilters[idx];
const operator = self.OPERATORS[type][condition.operator];
const descriptionArray = [
field.string,
operator.description,
`"${condition.displayedValue}"`,
];
preFilter.description = descriptionArray.join(" ");
}
}
return preFilters;
};
const res = this._super(...arguments);
// Restore original map()
this.conditions.map = originalMapFn;
return res;
},
/**
* @private
* @param {Object} condition
* @param {OwlEvent} ev
*/
onRelationalChanged(condition, ev) {
if (ev.detail) {
condition.value = ev.detail.id;
condition.displayedValue = ev.detail.display_name;
}
},
});
patch(CustomFilterItem, "web_advanced_search.legacy.CustomFilterItem", {
components: {
...CustomFilterItem.components,
RecordPicker,
},
});
export default CustomFilterItem;

View file

@ -0,0 +1,20 @@
/** @odoo-module **/
import AdvancedFilterItem from "./advanced_filter_item.esm";
import FilterMenu from "web.FilterMenu";
import {patch} from "@web/core/utils/patch";
/**
* Patches the FilterMenu for legacy widgets.
*
* Tree views still use this old legacy widget, so we need to patch it.
* This is likely to disappear in 17.0
*/
patch(FilterMenu, "web_advanced_search.legacy.FilterMenu", {
components: {
...FilterMenu.components,
AdvancedFilterItem,
},
});
export default FilterMenu;

View file

@ -0,0 +1,57 @@
/** @odoo-module **/
import Domain from "web.Domain";
import DomainSelectorDialog from "web.DomainSelectorDialog";
import config from "web.config";
import {getHumanDomain} from "../../js/utils.esm";
import {standaloneAdapter} from "web.OwlCompatibility";
const {Component, useRef} = owl;
class AdvancedFilterItem extends Component {
setup() {
this.itemRef = useRef("dropdown-item");
}
/**
* Prevent propagation of dropdown-item-selected event, so that it
* doesn't reach the FilterMenu onFilterSelected event handler.
*/
mounted() {
$(this.itemRef.el).on("dropdown-item-selected", (event) =>
event.stopPropagation()
);
}
/**
* Open advanced search dialog
*
* @returns {DomainSelectorDialog} The opened dialog itself.
*/
onClick() {
const adapterParent = standaloneAdapter({Component});
const dialog = new DomainSelectorDialog(
adapterParent,
this.env.searchModel.resModel,
"[]",
{
debugMode: config.isDebug(),
readonly: false,
}
);
// Add 1st domain node by default
dialog.opened(() => dialog.domainSelector._onAddFirstButtonClick());
// Configure handler
dialog.on("domain_selected", this, function (e) {
const preFilter = {
description: getHumanDomain(dialog.domainSelector),
domain: Domain.prototype.arrayToString(e.data.domain),
type: "filter",
};
this.env.searchModel.createNewFilters([preFilter]);
});
return dialog.open();
}
}
AdvancedFilterItem.components = {AdvancedFilterItem};
AdvancedFilterItem.template = "web_advanced_search.AdvancedFilterItem";
export default AdvancedFilterItem;

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<templates>
<t t-name="web_advanced_search.AdvancedFilterItem" owl="1">
<a
role="menuitem"
t-on-click="onClick"
class=" dropdown-item o_add_advanced_search"
> Add Advanced Filter </a>
</t>
</templates>

View file

@ -0,0 +1,105 @@
/** @odoo-module **/
import {CustomFilterItem} from "@web/search/filter_menu/custom_filter_item";
import {RecordPicker} from "../../js/RecordPicker.esm";
import {patch} from "@web/core/utils/patch";
/**
* Patches the CustomFilterItem for owl widgets.
*/
patch(CustomFilterItem.prototype, "web_advanced_search.CustomFilterItem", {
/**
* @override
*/
setup() {
this._super.apply(this, arguments);
this.OPERATORS.relational = this.OPERATORS.char;
this.FIELD_TYPES.many2one = "relational";
this.FIELD_TYPES.many2many = "relational";
this.FIELD_TYPES.one2many = "relational";
},
/**
* @override
*/
setDefaultValue(condition) {
const fieldType = this.fields[condition.field].type;
const genericType = this.FIELD_TYPES[fieldType];
if (genericType === "relational") {
condition.value = 0;
condition.displayedValue = "";
return;
}
return this._super.apply(this, arguments);
},
/**
* Add displayed value to preFilters for "relational" types.
*
* @override
*/
onApply() {
// To avoid the complete override, we patch this.conditions.map()
const originalMapFn = this.conditions.map;
const self = this;
this.conditions.map = function () {
const preFilters = originalMapFn.apply(this, arguments);
for (const condition of this) {
const field = self.fields[condition.field];
const type = self.FIELD_TYPES[field.type];
if (type === "relational") {
const idx = this.indexOf(condition);
const preFilter = preFilters[idx];
const operator = self.OPERATORS[type][condition.operator];
if (
["=", "!="].includes(operator.symbol) &&
operator.value === undefined
) {
const descriptionArray = [
field.string,
operator.description,
`"${condition.displayedValue}"`,
];
preFilter.description = descriptionArray.join(" ");
}
}
}
return preFilters;
};
const res = this._super.apply(this, arguments);
// Restore original map()
this.conditions.map = originalMapFn;
return res;
},
/**
* @private
* @param {Object} condition
* @param {OwlEvent} ev
*/
onRelationalChanged(condition, ev) {
if (ev.detail) {
condition.value = ev.detail.id;
condition.displayedValue = ev.detail.display_name;
}
},
onValueChange(condition, ev) {
if (!ev.target.value) {
return this.setDefaultValue(condition);
}
const field = this.fields[condition.field];
const type = this.FIELD_TYPES[field.type];
if (type === "relational") {
condition.value = ev.target.value;
condition.displayedValue = ev.target.value;
} else {
this._super.apply(this, arguments);
}
},
});
patch(CustomFilterItem, "web_advanced_search.CustomFilterItem", {
components: {
...CustomFilterItem.components,
RecordPicker,
},
});
export default CustomFilterItem;

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<templates>
<t t-inherit="web.CustomFilterItem" t-inherit-mode="extension" owl="1">
<xpath expr="//select[@t-elif]" position="after">
<t
t-elif="['many2one', 'many2many', 'one2many'].includes(fieldType) and ['=', '!='].includes(selectedOperator.symbol)"
>
<RecordPicker
model="fields[condition.field].relation"
string="fields[condition.field].string"
context="fields[condition.field].context"
t-on-change="(ev) => this.onRelationalChanged(condition,ev)"
/>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,16 @@
/** @odoo-module **/
import AdvancedFilterItem from "./advanced_filter_item.esm";
import {FilterMenu} from "@web/search/filter_menu/filter_menu";
import {patch} from "@web/core/utils/patch";
/**
* Patches the FilterMenu for owl widgets.
*/
patch(FilterMenu, "web_advanced_search.FilterMenu", {
components: {
...FilterMenu.components,
AdvancedFilterItem,
},
});
export default FilterMenu;

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<templates>
<t t-inherit="web.legacy.FilterMenu" t-inherit-mode="extension" owl="1">
<CustomFilterItem position="after">
<AdvancedFilterItem />
</CustomFilterItem>
</t>
<t t-inherit="web.FilterMenu" t-inherit-mode="extension" owl="1">
<CustomFilterItem position="after">
<AdvancedFilterItem />
</CustomFilterItem>
</t>
</templates>