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.define("dms.tour", function (require) {
"use strict";
var tour = require("web_tour.tour");
tour.register(
"dms_portal_mail_tour",
{
test: true,
url: "/my",
},
[
{
content: "Go /my/dms url",
trigger: 'a[href*="/my/dms"]',
},
{
content: "Go to Mails directory",
extra_trigger: "li.breadcrumb-item:contains('Documents')",
trigger: ".tr_dms_directory_link:contains('Mails')",
},
{
content: "Go to Mail_01.eml",
extra_trigger: "li.breadcrumb-item:contains('Mails')",
trigger: ".tr_dms_file_link:contains('Mail_01.eml')",
},
]
);
tour.register(
"dms_portal_partners_tour",
{
test: true,
url: "/my",
},
[
{
content: "Go /my/dms url",
trigger: 'a[href*="/my/dms"]',
},
{
content: "Go to Partners directory",
extra_trigger: "li.breadcrumb-item:contains('Documents')",
trigger: ".tr_dms_directory_link:contains('Partners')",
},
{
content: "Go to Joel Willis",
extra_trigger: "li.breadcrumb-item:contains('Partners')",
trigger: ".tr_dms_directory_link:contains('Joel Willis')",
},
{
content: "Go to test.txt",
extra_trigger: "li.breadcrumb-item:contains('Joel Willis')",
trigger: ".tr_dms_file_link:contains('test.txt')",
},
]
);
});

View file

@ -0,0 +1,81 @@
/** ********************************************************************************
Copyright 2020 Creu Blanca
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
**********************************************************************************/
odoo.define("dms.fields_path", function (require) {
"use strict";
var fields = require("web.basic_fields");
var registry = require("web.field_registry");
var FieldPathJson = fields.FieldText.extend({
events: _.extend({}, fields.FieldText.prototype.events, {
"click a": "_onNodeClicked",
}),
init: function () {
this._super.apply(this, arguments);
this.max_width = this.nodeOptions.width || 500;
this.seperator = this.nodeOptions.seperator || "/";
this.prefix = this.nodeOptions.prefix || false;
this.suffix = this.nodeOptions.suffix || false;
},
_renderReadonly: function () {
this.$el.empty();
this._renderPath();
},
_renderPath: function () {
var text_width_measure = "";
var path = JSON.parse(this.value || "[]");
$.each(
_.clone(path).reverse(),
function (index, element) {
text_width_measure += element.name + "/";
if (text_width_measure.length >= this.max_width) {
this.$el.prepend($("<span/>").text(".."));
} else if (index === 0) {
if (this.suffix) {
this.$el.prepend($("<span/>").text(this.seperator));
}
this.$el.prepend($("<span/>").text(element.name));
this.$el.prepend($("<span/>").text(this.seperator));
} else {
this.$el.prepend(
$("<a/>", {
class: "oe_form_uri",
"data-model": element.model,
"data-id": element.id,
href: "#",
text: element.name,
})
);
if (index !== path.length - 1) {
this.$el.prepend($("<span/>").text(this.seperator));
} else if (this.prefix) {
this.$el.prepend($("<span/>").text(this.seperator));
}
}
return text_width_measure.length < this.max_width;
}.bind(this)
);
},
_onNodeClicked: function (event) {
event.preventDefault();
this.do_action({
type: "ir.actions.act_window",
res_model: $(event.currentTarget).data("model"),
res_id: $(event.currentTarget).data("id"),
views: [[false, "form"]],
target: "current",
context: {},
});
},
});
registry.add("path_json", FieldPathJson);
return {
FieldPathJson: FieldPathJson,
};
});

View file

@ -0,0 +1,34 @@
/** @odoo-module **/
import {registry} from "@web/core/registry";
import {Component, onWillUpdateProps} from "@odoo/owl";
import {useService} from "@web/core/utils/hooks";
class DmsPathField extends Component {
setup() {
super.setup();
this.action = useService("action");
this.formatData(this.props);
onWillUpdateProps((nextProps) => this.formatData(nextProps));
}
formatData(props) {
this.data = JSON.parse(props.value || "[]");
}
_onNodeClicked(event) {
event.preventDefault();
this.action.doAction({
type: "ir.actions.act_window",
res_model: $(event.currentTarget).data("model"),
res_id: $(event.currentTarget).data("id"),
views: [[false, "form"]],
target: "current",
context: {},
});
}
}
DmsPathField.supportedTypes = ["text"];
DmsPathField.template = "dms.DmsPathField";
registry.category("fields").add("path_json", DmsPathField);

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="dms.DmsPathField" owl="1">
<t t-set="path_length" t-value="data.length - 1" />
<t t-foreach="data" t-as="elem" t-key="elem_index" style="display: inline">
<t t-if="elem_index !== path_length">
<span style="display: inline">/</span>
<a
class="oe_form_uri"
data-model="dms.directory"
t-att-data-id="elem.id"
href="#"
t-on-click.prevent="(ev) => this._onNodeClicked(ev)"
>
<t t-esc="elem.name" />
</a>
</t>
<t t-else="">
<span style="display: inline">/</span>
<span style="display: inline">
<t t-esc="elem.name" />
</span>
</t>
</t>
</t>
</templates>

View file

@ -0,0 +1,167 @@
/** @odoo-module */
import {useBus, useService} from "@web/core/utils/hooks";
import {_t} from "web.core";
const {useRef, useEffect, useState} = owl;
export const FileDropZone = {
setup() {
this._super();
this.dragState = useState({
showDragZone: false,
});
this.root = useRef("root");
this.rpc = useService("rpc");
useEffect(
(el) => {
if (!el) {
return;
}
const highlight = this.highlight.bind(this);
const unhighlight = this.unhighlight.bind(this);
const drop = this.onDrop.bind(this);
el.addEventListener("dragover", highlight);
el.addEventListener("dragleave", unhighlight);
el.addEventListener("drop", drop);
return () => {
el.removeEventListener("dragover", highlight);
el.removeEventListener("dragleave", unhighlight);
el.removeEventListener("drop", drop);
};
},
() => [document.querySelector(".o_content")]
);
},
highlight(ev) {
ev.stopPropagation();
ev.preventDefault();
this.dragState.showDragZone = true;
},
unhighlight(ev) {
ev.stopPropagation();
ev.preventDefault();
this.dragState.showDragZone = false;
},
async onDrop(ev) {
ev.preventDefault();
await this.env.bus.trigger("change_file_input", {
files: ev.dataTransfer.files,
});
},
};
export const FileUpload = {
setup() {
this._super();
this.actionService = useService("action");
this.notification = useService("notification");
this.orm = useService("orm");
this.http = useService("http");
this.fileInput = useRef("fileInput");
this.root = useRef("root");
this.rpc = useService("rpc");
useBus(this.env.bus, "change_file_input", async (ev) => {
this.fileInput.el.files = ev.detail.files;
await this.onChangeFileInput();
});
},
uploadDocument() {
this.fileInput.el.click();
},
async onChangeFileInput() {
const params = {
csrf_token: odoo.csrf_token,
ufile: [...this.fileInput.el.files],
model: "dms.file",
id: 0,
};
const fileData = await this.http.post(
"/web/binary/upload_attachment",
params,
"text"
);
const attachments = JSON.parse(fileData);
if (attachments.error) {
throw new Error(attachments.error);
}
this.onUpload(attachments);
},
async onUpload(attachments) {
const self = this;
const attachmentIds = attachments.map((a) => a.id);
const ctx = Object.assign(
{},
this.actionService.currentController.props.context,
this.props.context
);
const controllerID = this.actionService.currentController.jsId;
if (!attachmentIds.length) {
this.notification.add(_t("An error occurred during the upload"));
return;
}
// Search the correct directory_id value according to the domain
if (this.props.domain) {
for (const domain_item of this.props.domain) {
if (domain_item.length === 3) {
if (domain_item[0] === "directory_id" && domain_item[1] === "=") {
ctx.default_directory_id = domain_item[2];
}
}
}
}
if (!ctx.default_directory_id) {
self.actionService.restore(controllerID);
return self.notification.add(
this.env._t("You must select a directory first"),
{
type: "danger",
}
);
}
const attachment_datas = await this.orm.call(
"dms.file",
"get_dms_files_from_attachments",
["", attachmentIds]
);
const attachments_args = [];
attachment_datas.forEach((attachment_data) => {
attachments_args.push({
name: attachment_data.name,
content: attachment_data.datas,
mimetype: attachment_data.mimetype,
});
});
this.orm
.call("dms.file", "create", [attachments_args], {
context: ctx,
})
.then(() => {
self.actionService.restore(controllerID);
})
.catch((error) => {
self.notification.add(error.data.message, {
type: "danger",
});
self.actionService.restore(controllerID);
});
},
};

View file

@ -0,0 +1,35 @@
/** @odoo-module **/
import {registry} from "@web/core/registry";
import {BinaryField} from "@web/views/fields/binary/binary_field";
import {useService} from "@web/core/utils/hooks";
export class PreviewRecordField extends BinaryField {
setup() {
super.setup();
this.messaging = useService("messaging");
this.dialog = useService("dialog");
}
onFilePreview() {
const self = this;
this.messaging.get().then((messaging) => {
const attachmentList = messaging.models.AttachmentList.insert({
selectedAttachment: messaging.models.Attachment.insert({
id: self.props.record.resId,
filename: self.props.record.data.display_name || "",
name: self.props.record.data.display_name || "",
mimetype: self.props.record.data.mimetype,
model_name: self.props.record.resModel,
}),
});
this.dialog = messaging.models.Dialog.insert({
attachmentListOwnerAsAttachmentView: attachmentList,
});
});
return;
}
}
PreviewRecordField.template = "dms.FilePreviewField";
registry.category("fields").add("preview_binary", PreviewRecordField);

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t
t-name="dms.FilePreviewField"
t-inherit="web.BinaryField"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//div[hasclass('d-inline-flex')]" position="inside">
<t
t-set="readable_types"
t-value="[
'image/bmp',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/tiff',
'image/x-icon',
'application/pdf',
'audio/mpeg',
'video/x-matroska',
'video/mp4',
'video/webm',
]"
/>
<t t-if="readable_types.includes(props.record.data.mimetype)">
<button
class="btn btn-secondary fa fa-search preview_file"
data-tooltip="Preview"
aria-label="Preview"
t-on-click="onFilePreview"
/>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,14 @@
/** @odoo-module **/
// /** ********************************************************************************
// Copyright 2020 Creu Blanca
// Copyright 2017-2019 MuK IT GmbH
// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
// **********************************************************************************/
import {KanbanController} from "@web/views/kanban/kanban_controller";
export class FileKanbanController extends KanbanController {
setup() {
super.setup();
}
}

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t
t-name="dms.FileKanbanView.Buttons"
t-inherit="web.KanbanView.Buttons"
t-inherit-mode="primary"
owl="1"
>
<div role="toolbar" position="inside">
<input
type="file"
multiple="true"
t-ref="uploadFileInput"
class="o_input_file o_hidden"
t-on-change.stop="onFileInputChange"
/>
<button
type="button"
t-attf-class="btn btn-primary o_file_kanban_upload"
t-on-click.stop.prevent="() => this.uploadFileInputRef.el.click()"
>
Upload
</button>
</div>
</t>
</templates>

View file

@ -0,0 +1,62 @@
/** @odoo-module **/
import {KanbanRecord} from "@web/views/kanban/kanban_record";
import {useService} from "@web/core/utils/hooks";
const videoReadableTypes = ["x-matroska", "mp4", "webm"];
const audioReadableTypes = ["mp3", "ogg", "wav", "aac", "mpa", "flac", "m4a"];
export class FileKanbanRecord extends KanbanRecord {
setup() {
super.setup();
this.messaging = useService("messaging");
this.dialog = useService("dialog");
}
isVideo(mimetype) {
return videoReadableTypes.includes(mimetype);
}
isAudio(mimetype) {
return audioReadableTypes.includes(mimetype);
}
/**
* @override
*
* Override to open the preview upon clicking the image, if compatible.
*/
onGlobalClick(ev) {
const self = this;
if (ev.target.closest(".o_kanban_dms_file_preview")) {
this.messaging.get().then((messaging) => {
const file_type = self.props.record.data.name.split(".")[1];
let mimetype = "";
if (self.isVideo(file_type)) {
mimetype = `video/${file_type}`;
} else if (self.isAudio(file_type)) {
mimetype = "audio/mpeg";
} else {
mimetype = self.props.record.data.mimetype;
}
const attachmentList = messaging.models.AttachmentList.insert({
selectedAttachment: messaging.models.Attachment.insert({
id: self.props.record.data.id,
filename: self.props.record.data.name,
name: self.props.record.data.name,
mimetype: mimetype,
model_name: self.props.record.resModel,
}),
});
this.dialog = messaging.models.Dialog.insert({
attachmentListOwnerAsAttachmentView: attachmentList,
});
});
return;
}
return super.onGlobalClick(...arguments);
}
}

View file

@ -0,0 +1,20 @@
/** @odoo-module */
// /** ********************************************************************************
// Copyright 2020 Creu Blanca
// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
// **********************************************************************************/
import {KanbanRenderer} from "@web/views/kanban/kanban_renderer";
import {FileKanbanRecord} from "./file_kanban_record.esm";
export class FileKanbanRenderer extends KanbanRenderer {
setup() {
super.setup();
}
}
FileKanbanRenderer.components = {
...KanbanRenderer.components,
KanbanRecord: FileKanbanRecord,
};

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="dms.KanbanRenderer"
t-inherit="web.KanbanRenderer"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<div t-if="dragState.showDragZone" class="o_dropzone">
<i class="fa fa-cloud-upload fa-10x" />
</div>
</xpath>
</t>
<t
t-name="dms.KanbanButtons"
t-inherit="web.KanbanView.Buttons"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//t[@t-if='canCreate']" position="after">
<button
type="button"
class="d-none d-md-inline btn btn-primary mx-1"
t-on-click.prevent="uploadDocument"
>
Upload
</button>
</xpath>
<xpath expr="//t[@t-if='canCreate']" position="before">
<button
type="button"
class="d-inline d-md-none btn btn-primary mx-1"
t-on-click.prevent="uploadDocument"
>
Scan
</button>
</xpath>
<xpath expr="//div" position="inside">
<input
type="file"
name="ufile"
class="d-none"
t-ref="fileInput"
multiple="1"
accept="*"
t-on-change="onChangeFileInput"
/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,28 @@
/** @odoo-module **/
// /** ********************************************************************************
// Copyright 2020 Creu Blanca
// Copyright 2017-2019 MuK IT GmbH
// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
// **********************************************************************************/
import {registry} from "@web/core/registry";
import {patch} from "@web/core/utils/patch";
import {kanbanView} from "@web/views/kanban/kanban_view";
import {FileKanbanRenderer} from "./file_kanban_renderer.esm";
import {FileKanbanController} from "./file_kanban_controller.esm";
import {FileDropZone, FileUpload} from "./dms_file_upload.esm";
patch(FileKanbanRenderer.prototype, "file_kanban_renderer_dzone", FileDropZone);
patch(FileKanbanController.prototype, "filee_kanban_controller_upload", FileUpload);
FileKanbanRenderer.template = "dms.KanbanRenderer";
export const FileKanbanView = {
...kanbanView,
buttonTemplate: "dms.KanbanButtons",
Controller: FileKanbanController,
Renderer: FileKanbanRenderer,
};
registry.category("views").add("file_kanban", FileKanbanView);

View file

@ -0,0 +1,15 @@
/** @odoo-module **/
// /** ********************************************************************************
// Copyright 2020 Creu Blanca
// Copyright 2017-2019 MuK IT GmbH
// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
// **********************************************************************************/
import {ListController} from "@web/views/list/list_controller";
export class FileListController extends ListController {
setup() {
super.setup();
}
}

View file

@ -0,0 +1,18 @@
/** @odoo-module */
// /** ********************************************************************************
// Copyright 2020 Creu Blanca
// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
// **********************************************************************************/
import {ListRenderer} from "@web/views/list/list_renderer";
export class FileListRenderer extends ListRenderer {
setup() {
super.setup();
}
}
FileListRenderer.components = {
...FileListRenderer.components,
};

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="dms.ListRenderer"
t-inherit="web.ListRenderer"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
<div t-if="dragState.showDragZone" class="o_dropzone">
<i class="fa fa-cloud-upload fa-10x" />
</div>
</xpath>
</t>
<t
t-name="dms.ListButtons"
t-inherit="web.ListView.Buttons"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button
type="button"
class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1"
t-on-click.prevent="uploadDocument"
>
Upload
</button>
</xpath>
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button
type="button"
class="d-inline d-md-none o_button_upload_expense btn btn-primary mx-1"
t-on-click.prevent="uploadDocument"
>
Scan
</button>
</xpath>
<xpath expr="//div" position="inside">
<input
type="file"
name="ufile"
class="d-none"
t-ref="fileInput"
multiple="1"
accept="*"
t-on-change="onChangeFileInput"
/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,28 @@
/** @odoo-module **/
// /** ********************************************************************************
// Copyright 2020 Creu Blanca
// Copyright 2017-2019 MuK IT GmbH
// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
// **********************************************************************************/
import {registry} from "@web/core/registry";
import {patch} from "@web/core/utils/patch";
import {listView} from "@web/views/list/list_view";
import {FileListRenderer} from "./file_list_renderer.esm";
import {FileListController} from "./file_list_controller.esm";
import {FileDropZone, FileUpload} from "./dms_file_upload.esm";
patch(FileListRenderer.prototype, "file_list_renderer_dzone", FileDropZone);
patch(FileListController.prototype, "file_list_controller_upload", FileUpload);
FileListRenderer.template = "dms.ListRenderer";
export const FileListView = {
...listView,
buttonTemplate: "dms.ListButtons",
Controller: FileListController,
Renderer: FileListRenderer,
};
registry.category("views").add("file_list", FileListView);

View file

@ -0,0 +1,98 @@
/* global base64js*/
/* Copyright 2020 Creu Blanca
* Copyright 2021 Tecnativa - Alexandre D. Díaz
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("dms.DragDrop", function (require) {
"use strict";
const DropTargetMixin = require("web_drop_target");
const core = require("web.core");
const _t = core._t;
return _.extend({}, DropTargetMixin.DropTargetMixin, {
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this._get_directory_id(
this._searchPanel ? this._searchPanel.getDomain() : []
);
},
_drop_zone_selector: ".o_kanban_view",
/**
* @override
*/
_handle_drop_items: function (drop_items) {
_.each(drop_items, this._handle_file_drop_attach, this);
},
/**
* @override
*/
_get_record_id: function () {
// Don't need the record id to work
return true;
},
/**
* @override
*/
_create_attachment: function (file, reader, res_model) {
// Helper to upload an attachment and update the sidebar
const ctx = this.renderer.state.getContext();
console.log(ctx);
if (this.directory_id) {
ctx.default_directory_id = this.directory_id;
}
console.log(ctx);
if (typeof ctx.default_directory_id === "undefined") {
return this.displayNotification({
message: _t("You must select a directory first"),
type: "danger",
});
}
return this._rpc({
model: res_model,
method: "create",
args: [
{
name: file.name,
content: base64js.fromByteArray(new Uint8Array(reader.result)),
},
],
kwargs: {
context: ctx,
},
}).then(() => this.reload());
},
/**
* @private
* @param {Array} domain
*/
_get_directory_id: function (domain) {
let directory_id = false;
_.each(domain, (leaf) => {
if (
leaf[0] === "directory_id" &&
(leaf[1] === "child_of" || leaf[1] === "=")
) {
directory_id = leaf[2];
}
});
this.directory_id = directory_id;
},
/**
* @override
*/
_update: function (state, params) {
this._get_directory_id(params.domain);
return this._super.apply(this, arguments).then((result) => {
this._update_overlay();
return result;
});
},
});
});

View file

@ -0,0 +1,32 @@
/** @odoo-module **/
/* Copyright 2021-2024 Tecnativa - Víctor Martínez
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
import {SearchModel} from "@web/search/search_model";
import {patch} from "@web/core/utils/patch";
patch(SearchModel.prototype, "dms.SearchPanel", {
setup() {
this._super(...arguments);
},
_getCategoryDomain(excludedCategoryId) {
const domain = this._super.apply(this, arguments);
for (const category of this.categories) {
if (category.id === excludedCategoryId) {
continue;
}
// Make sure to filter selected category only for DMS hierarchies,
// not other Odoo models such as product categories
// where child_of could be better than "=" operator
if (category.activeValueId && this.resModel.startsWith("dms")) {
domain.push([category.fieldName, "=", category.activeValueId]);
}
if (domain.length === 0 && this.resModel === "dms.directory") {
domain.push([category.fieldName, "=", false]);
}
}
return domain;
},
});