project management system Commit
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# pylint: disable=pointless-statement
|
||||
{
|
||||
"name": "ONLYOFFICE Documents",
|
||||
"summary": "Edit and collaborate on office files within Odoo Documents.",
|
||||
"description": "The ONLYOFFICE app allows users to edit and collaborate on office files within Odoo Documents using ONLYOFFICE Docs. You can work with text documents, spreadsheets, and presentations, co-author documents in real time using two co-editing modes (Fast and Strict), Track Changes, comments, and built-in chat.", # noqa: E501
|
||||
"author": "ONLYOFFICE",
|
||||
"website": "https://github.com/ONLYOFFICE/onlyoffice_odoo",
|
||||
"category": "Productivity",
|
||||
"version": "5.2.1",
|
||||
"depends": ["onlyoffice_odoo", "documents"],
|
||||
# always loaded
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/onlyoffice_templates_share.xml",
|
||||
],
|
||||
"license": "LGPL-3",
|
||||
"support": "support@onlyoffice.com",
|
||||
"images": [
|
||||
"static/description/main_screenshot.png",
|
||||
"static/description/editors.png",
|
||||
"static/description/edit_files.png",
|
||||
"static/description/create_files.png",
|
||||
],
|
||||
"installable": True,
|
||||
"application": True,
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"onlyoffice_odoo_documents/static/src/models/*.js",
|
||||
"onlyoffice_odoo_documents/static/src/components/*/*.xml",
|
||||
"onlyoffice_odoo_documents/static/src/documents_view/**/*",
|
||||
"onlyoffice_odoo_documents/static/src/onlyoffice_create_template/**/*",
|
||||
"onlyoffice_odoo_documents/static/src/css/*",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import controllers
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
#
|
||||
# (c) Copyright Ascensio System SIA 2024
|
||||
#
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from mimetypes import guess_type
|
||||
from urllib.request import urlopen
|
||||
|
||||
import markupsafe
|
||||
import requests
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request
|
||||
from odoo.tools.translate import _
|
||||
|
||||
from odoo.addons.documents.controllers.documents import ShareRoute
|
||||
from odoo.addons.onlyoffice_odoo.controllers.controllers import Onlyoffice_Connector
|
||||
from odoo.addons.onlyoffice_odoo.utils import config_utils, file_utils, jwt_utils, url_utils
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_mobile_regex = r"android|avantgo|playbook|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino" # noqa: E501
|
||||
|
||||
|
||||
class OnlyofficeDocuments_Connector(http.Controller):
|
||||
@http.route("/onlyoffice/documents/file/create", auth="user", methods=["POST"], type="json")
|
||||
def post_file_create(self, folder_id, supported_format, title, url=None):
|
||||
result = {"error": None, "file_id": None, "document_id": None}
|
||||
|
||||
try:
|
||||
_logger.info(f"Getting new file template {request.env.user.lang} {supported_format}")
|
||||
|
||||
if url:
|
||||
response = requests.get(url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
file_data = response.content
|
||||
else:
|
||||
file_data = file_utils.get_default_file_template(request.env.user.lang, supported_format)
|
||||
|
||||
data = {
|
||||
"name": title + "." + supported_format,
|
||||
"mimetype": file_utils.get_mime_by_ext(supported_format),
|
||||
"raw": file_data,
|
||||
"folder_id": int(folder_id),
|
||||
}
|
||||
|
||||
document = request.env["documents.document"].create(data)
|
||||
request.env["onlyoffice.odoo.documents.access"].create(
|
||||
{
|
||||
"document_id": document.id,
|
||||
"internal_users": "none",
|
||||
"link_access": "none",
|
||||
}
|
||||
)
|
||||
request.env["onlyoffice.odoo.documents.access.user"].create(
|
||||
{
|
||||
"document_id": document.id,
|
||||
"user_id": request.env.user.partner_id.id,
|
||||
"role": "edit",
|
||||
}
|
||||
)
|
||||
result["file_id"] = document.attachment_id.id
|
||||
result["document_id"] = document.id
|
||||
|
||||
except Exception as ex:
|
||||
_logger.exception(f"Failed to create document {str(ex)}")
|
||||
result["error"] = _("Failed to create document")
|
||||
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
class OnlyofficeDocuments_Inherited_Connector(Onlyoffice_Connector):
|
||||
@http.route(["/onlyoffice/documents/share/<access_token>/"], type="http", auth="public")
|
||||
def render_shared_document_editor(self, access_token=None):
|
||||
try:
|
||||
document = ShareRoute._from_access_token(access_token, skip_log=True)
|
||||
|
||||
if not document or not document.exists():
|
||||
raise request.not_found()
|
||||
|
||||
return request.render(
|
||||
"onlyoffice_odoo.onlyoffice_editor", self.prepare_share_editor(document, access_token)
|
||||
)
|
||||
|
||||
except Exception:
|
||||
_logger.error("Ffailed to open shared document")
|
||||
|
||||
return request.not_found()
|
||||
|
||||
@http.route("/onlyoffice/editor/document/<int:document_id>", auth="public", type="http", website=True)
|
||||
def render_document_editor(self, document_id, access_token=None):
|
||||
return request.render(
|
||||
"onlyoffice_odoo.onlyoffice_editor", self.prepare_document_editor(document_id, access_token)
|
||||
)
|
||||
|
||||
def prepare_document_editor(self, document_id, access_token):
|
||||
document = request.env["documents.document"].browse(int(document_id))
|
||||
if document.is_locked and document.lock_uid.id != request.env.user.id:
|
||||
_logger.error("Document is locked by another user")
|
||||
raise Forbidden()
|
||||
try:
|
||||
document.check_access_rule("read")
|
||||
except AccessError:
|
||||
_logger.error("User has no read access rights to open this document")
|
||||
raise Forbidden() # noqa: B904
|
||||
|
||||
attachment = self.get_attachment(document.attachment_id.id)
|
||||
if not attachment:
|
||||
_logger.error("Current document has no attachments")
|
||||
raise Forbidden() # noqa: B904
|
||||
|
||||
try:
|
||||
document.check_access_rule("write")
|
||||
return self.prepare_editor_values(attachment, access_token, True)
|
||||
except AccessError:
|
||||
_logger.debug("Current user has no write access")
|
||||
return self.prepare_editor_values(attachment, access_token, False)
|
||||
|
||||
def prepare_share_editor(self, document, access_token):
|
||||
role = None
|
||||
access = (
|
||||
request.env["onlyoffice.odoo.documents.access"].sudo().search([("document_id", "=", document.id)], limit=1)
|
||||
)
|
||||
if access:
|
||||
if access.link_access == "none":
|
||||
raise AccessError(_("User has no read access rights to open this document"))
|
||||
else:
|
||||
role = access.link_access
|
||||
|
||||
attachment = self.get_attachment(document.attachment_id.id)
|
||||
data = attachment.sudo().read(["id", "checksum", "public", "name", "access_token"])[0]
|
||||
key = str(data["id"]) + str(data["checksum"])
|
||||
docserver_url = config_utils.get_doc_server_public_url(request.env)
|
||||
odoo_url = config_utils.get_base_or_odoo_url(request.env)
|
||||
|
||||
filename = self.filter_xss(data["name"])
|
||||
access_token = access_token.decode("utf-8") if isinstance(access_token, bytes) else access_token
|
||||
document_type = file_utils.get_file_type(filename)
|
||||
is_mobile = bool(re.search(_mobile_regex, request.httprequest.headers.get("User-Agent"), re.IGNORECASE))
|
||||
|
||||
root_config = {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"type": "mobile" if is_mobile else "desktop",
|
||||
"documentType": document_type,
|
||||
"document": {
|
||||
"title": filename,
|
||||
"url": odoo_url + "documents/content/" + access_token,
|
||||
"fileType": file_utils.get_file_ext(filename),
|
||||
"key": key,
|
||||
"permissions": {"edit": False},
|
||||
},
|
||||
"editorConfig": {
|
||||
"mode": "view",
|
||||
"lang": request.env.user.lang,
|
||||
"user": {"id": str(request.env.user.id), "name": request.env.user.name},
|
||||
"customization": {},
|
||||
},
|
||||
}
|
||||
|
||||
if not role or role == "view":
|
||||
root_config["editorConfig"]["mode"] = "view"
|
||||
root_config["document"]["permissions"]["edit"] = False
|
||||
elif role == "commenter":
|
||||
root_config["editorConfig"]["mode"] = "edit"
|
||||
root_config["document"]["permissions"]["edit"] = False
|
||||
root_config["document"]["permissions"]["comment"] = True
|
||||
elif role == "reviewer":
|
||||
root_config["editorConfig"]["mode"] = "edit"
|
||||
root_config["document"]["permissions"]["edit"] = False
|
||||
root_config["document"]["permissions"]["review"] = True
|
||||
elif role == "edit":
|
||||
root_config["editorConfig"]["mode"] = "edit"
|
||||
root_config["document"]["permissions"]["edit"] = True
|
||||
elif role == "form_filling":
|
||||
root_config["editorConfig"]["mode"] = "edit"
|
||||
root_config["document"]["permissions"]["edit"] = False
|
||||
root_config["document"]["permissions"]["fillForms"] = True
|
||||
elif role == "custom_filter":
|
||||
root_config["editorConfig"]["mode"] = "edit"
|
||||
root_config["document"]["permissions"]["edit"] = True
|
||||
root_config["document"]["permissions"]["modifyFilter"] = False
|
||||
|
||||
if role and role != "view":
|
||||
public_user = request.env.ref("base.public_user")
|
||||
security_token = jwt_utils.encode_payload(
|
||||
request.env, {"id": public_user.id}, config_utils.get_internal_jwt_secret(request.env)
|
||||
)
|
||||
security_token = security_token.decode("utf-8") if isinstance(security_token, bytes) else security_token
|
||||
root_config["editorConfig"]["callbackUrl"] = (
|
||||
odoo_url + "onlyoffice/documents/share/callback/" + access_token + "/" + security_token
|
||||
)
|
||||
|
||||
if jwt_utils.is_jwt_enabled(request.env):
|
||||
root_config["token"] = jwt_utils.encode_payload(request.env, root_config)
|
||||
|
||||
return {
|
||||
"docTitle": filename,
|
||||
"docIcon": f"/onlyoffice_odoo/static/description/editor_icons/{document_type}.ico",
|
||||
"docApiJS": docserver_url + "web-apps/apps/api/documents/api.js",
|
||||
"editorConfig": markupsafe.Markup(json.dumps(root_config)),
|
||||
}
|
||||
|
||||
@http.route(
|
||||
"/onlyoffice/documents/share/callback/<access_token>/<oo_security_token>",
|
||||
auth="public",
|
||||
methods=["POST"],
|
||||
type="http",
|
||||
csrf=False,
|
||||
)
|
||||
def share_callback(self, access_token, oo_security_token):
|
||||
response_json = {"error": 0}
|
||||
|
||||
try:
|
||||
body = request.get_json_data()
|
||||
user = self.get_user_from_token(oo_security_token)
|
||||
document = ShareRoute._from_access_token(access_token, skip_log=True)
|
||||
|
||||
if not document or not document.exists():
|
||||
raise request.not_found()
|
||||
|
||||
access = (
|
||||
request.env["onlyoffice.odoo.documents.access"]
|
||||
.sudo()
|
||||
.search([("document_id", "=", document.id)], limit=1)
|
||||
)
|
||||
if access:
|
||||
if access.link_access == "view":
|
||||
raise Exception("No access rights to overwrite this document for access via share link")
|
||||
else:
|
||||
raise Exception("No access rights to overwrite this document for access via share link")
|
||||
|
||||
attachment = request.env["ir.attachment"].sudo().browse([document.attachment_id.id]).exists().ensure_one()
|
||||
|
||||
if jwt_utils.is_jwt_enabled(request.env):
|
||||
token = body.get("token")
|
||||
|
||||
if not token:
|
||||
token = request.httprequest.headers.get(config_utils.get_jwt_header(request.env))
|
||||
if token:
|
||||
token = token[len("Bearer ") :]
|
||||
|
||||
if not token:
|
||||
raise Exception("expected JWT")
|
||||
|
||||
body = jwt_utils.decode_token(request.env, token)
|
||||
if body.get("payload"):
|
||||
body = body["payload"]
|
||||
|
||||
status = body["status"]
|
||||
|
||||
if (status == 2) | (status == 3): # mustsave, corrupted
|
||||
file_url = url_utils.replace_public_url_to_internal(request.env, body.get("url"))
|
||||
datas = base64.encodebytes(urlopen(file_url, timeout=120).read())
|
||||
document = request.env["documents.document"].sudo().browse(int(attachment.res_id))
|
||||
document.with_user(user).sudo().write(
|
||||
{
|
||||
"name": attachment.name,
|
||||
"datas": datas,
|
||||
"mimetype": guess_type(file_url)[0],
|
||||
}
|
||||
)
|
||||
document.sudo().message_post(body=_("Document edited by %(user)s", user=user.name))
|
||||
|
||||
except Exception as ex:
|
||||
response_json["error"] = 1
|
||||
response_json["message"] = http.serialize_exception(ex)
|
||||
|
||||
return request.make_response(
|
||||
data=json.dumps(response_json),
|
||||
status=500 if response_json["error"] == 1 else 200,
|
||||
headers=[("Content-Type", "application/json")],
|
||||
)
|
||||
|
||||
|
||||
class OnlyOfficeShareRoute(ShareRoute):
|
||||
@http.route("/documents/<access_token>", type="http", auth="public")
|
||||
def documents_home(self, access_token):
|
||||
response = super(OnlyOfficeShareRoute, self).documents_home(access_token) # noqa: UP008
|
||||
|
||||
document_sudo = self._from_access_token(access_token)
|
||||
|
||||
if not request.env.user._is_public() or not hasattr(response, "qcontext"):
|
||||
return
|
||||
|
||||
qcontext = response.qcontext
|
||||
|
||||
if document_sudo.type == "binary" and document_sudo.attachment_id:
|
||||
can_view = file_utils.can_view(document_sudo.name)
|
||||
if can_view:
|
||||
qcontext["onlyoffice_supported"] = True
|
||||
|
||||
if document_sudo.type == "folder":
|
||||
data = []
|
||||
sub_documents_sudo = ShareRoute._get_folder_children(document_sudo)
|
||||
for document in sub_documents_sudo:
|
||||
data.append({"document": document, "onlyoffice_supported": file_utils.can_view(document.name)})
|
||||
qcontext["onlyoffice_supported"] = data
|
||||
|
||||
return response
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
Prerequisites
|
||||
=============
|
||||
|
||||
To be able to work with office files within Odoo Enterprise, you will need an instance of ONLYOFFICE Docs. You can install the `self-hosted version`_ of the editors or opt for `ONLYOFFICE Docs`_ which doesn't require downloading and installation.
|
||||
|
||||
ONLYOFFICE app configuration
|
||||
============================
|
||||
|
||||
**Please note**: All the settings are configured from the `main ONLYOFFICE app for Odoo`_ which is installed automatically when you install ONLYOFFICE app for Odoo Enterprise.
|
||||
To adjust the main app settings within your Odoo, go to *Home menu -> Settings -> ONLYOFFICE*.
|
||||
|
||||
In the **Document Server Url**, specify the URL of the installed ONLYOFFICE Docs or the address of ONLYOFFICE Docs Cloud.
|
||||
|
||||
**Document Server JWT Secret**: JWT is enabled by default and the secret key is generated automatically to restrict the access to ONLYOFFICE Docs. if you want to specify your own secret key in this field, also specify the same secret key in the ONLYOFFICE Docs `config file`_ to enable the validation.
|
||||
|
||||
**Document Server JWT Header**: Standard JWT header used in ONLYOFFICE is Authorization. In case this header is in conflict with your setup, you can change the header to the custom one.
|
||||
|
||||
In case your network configuration doesn't allow requests between the servers via public addresses, specify the ONLYOFFICE Docs address for internal requests from the Odoo server and vice versa.
|
||||
|
||||
If you would like the editors to open in the same tab instead of a new one, check the corresponding setting "Open file in the same tab".
|
||||
|
||||
.. image:: settings.png
|
||||
:width: 800
|
||||
|
||||
|
||||
Contact us
|
||||
==========
|
||||
|
||||
If you have any questions or suggestions regarding the ONLYOFFICE app for Odoo, please let us know at https://forum.onlyoffice.com
|
||||
|
||||
.. _self-hosted version: https://www.onlyoffice.com/download-docs.aspx
|
||||
.. _ONLYOFFICE Docs: https://www.onlyoffice.com/docs-registration.aspx
|
||||
.. _config file: https://api.onlyoffice.com/docs/docs-api/additional-api/signature/
|
||||
.. _main ONLYOFFICE app for Odoo: https://apps.odoo.com/apps/modules/16.0/onlyoffice_odoo/
|
||||
|
After Width: | Height: | Size: 48 KiB |
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "Zugriffsebene"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "Anhang"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "Leer"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "Kommentator"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "Erstellen"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "Erstellen mit ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "Berechtigungen erstellen und festlegen"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Erstellt von"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Erstellt am"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "Benutzerdefinierter Filter"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Anzeigename"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "Dokument"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "Dokument kann nicht erstellt werden"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "Ausfüllen von Formularen"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "Aus Vorlage"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "Zugriff für interne Benutzer"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Zuletzt aktualisiert von"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Zuletzt aktualisiert am"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "Linkzugriff"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "Neues Dokument"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "Neues Dokument erstellt in Dokumente"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "Kein Dokument zum Teilen ausgewählt."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "ONLYOFFICE Docs-Server"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE Dokumente"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documents Access"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documents Access Users"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "Nur Besitzer oder Administrator kann Dokumente freigeben"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr "Oo Attachment Version"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "In ONLYOFFICE öffnen"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "PDF-Formular"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "Präsentation"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "Überprüfer"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "Tabelle"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "Der 30-tägige Testzeitraum ist vorbei. Sie können sich nicht mehr mit dem Demo-Server von ONLYOFFICE Docs verbinden"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "Betrachter"
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr ""
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "Nivel de acceso"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "Archivo adjunto"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "En blanco"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "Comentador"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "Crear"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "Crear con ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "Crear y establecer permisos"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Creado por"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Creado"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "Filtro personalizado"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Mostrar nombre"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "Error al crear documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "Rellenado de formularios"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "Desde plantilla"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "Acceso de usuarios internos"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Última actualización por"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Última actualización"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "Acceso mediante enlace"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "Nuevo documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "Nuevo documento creado en Documentos"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "No se ha seleccionado ningún documento para compartir."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "Ninguno"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "Servidor de ONLYOFFICE Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE Documents"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documents Access"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documents Access Users"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "Solo el propietario o el administrador pueden compartir documentos."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "Abrir en ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "Formulario PDF"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "Presentación"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "Revisor"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "Hoja de cálculo"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "El período de prueba de 30 días ha terminado, ya no se puede conectar al servidor de ONLYOFFICE Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "Usuario"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "Visor"
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "Niveau d'accès"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "Pièce jointe"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "Vierge"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "Commentateur"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "Créer"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "Créer avec ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "Créer et définir les permissions"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Créé par"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Créé le"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "Filtre personnalisé"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Afficher le nom"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "Document"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "Éditeur"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "Échec de la création du document"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "Remplissage de formulaires"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "À partir d'un modèle"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "Accès des utilisateurs internes"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Dernière mise à jour par"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Dernière mise à jour le"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "Accès par lien"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "Nouveau document"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "Nouveau document créé dans Documents"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "Aucun document sélectionné pour le partage."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "Aucun"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "Serveur ONLYOFFICE Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE Documents"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documents Access"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documents Access Users"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "Seul le propriétaire ou l'administrateur peut partager des documents."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "Ouvrir dans ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "Formulaire PDF"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "Présentation"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "Réviseur"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "Feuille de calcul"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "La période de test de 30 jours est terminée, vous ne pouvez plus vous connecter à la démo ONLYOFFICE Serveur Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "Lecteur"
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "Livello di accesso"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "Allegato"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "Vuoto"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "Annulla"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "Commentatore"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "Crea"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "Crea con ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "Crea e imposta permessi"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Creato da"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Creato il"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "Filtro personalizzato"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Visualizza nome"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "Non è stato possibile creare un documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "Compilazione del modulo"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "Da modello"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "Accesso utenti interni"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Ultimo aggiornamento da"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Ultimo aggiornamento il"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "Accesso utenti interni"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "Nuovo documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "Nuovo documento è stato creato in Documenti"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "Nessun documento selezionato per la condivisione."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "Nessuno"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "Server ONLYOFFICE Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE Documents"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documents Access"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documents Access Users"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "Solo il proprietario o l’amministratore può condividere documenti."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "Aprire in ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "Modulo PDF"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "Presentazione"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "Recensore"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "Foglio di calcolo"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "Il periodo di prova di 30 giorni è terminato, non puoi più connetterti alla demo ONLYOFFICE Docs Server"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "Utente"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "Visualizzatore"
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=1; plural=0;"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "アクセスレベル"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "添付ファイル"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "空白"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "コメント可"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "作成"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "ONLYOFFICEで作成"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "作成 & 権限設定"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "作成者"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "作成日"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "カスタムフィルター"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "表示名"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "ドキュメント"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr "%(user)sによって編集されたドキュメント"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "編集可"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "文書の作成に失敗しました"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "フォーム入力"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "テンプレートから"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "内部ユーザーアクセス"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents____last_update
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access____last_update
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "最終変更日"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "最終更新者"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "最終更新日"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "リンクアクセス"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "新規ドキュメント"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "「文書」に新規文書が作成されました"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "共有するドキュメントが選択されていません。"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "なし"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "ONLYOFFICE Docsサーバ"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE Documents"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documentsアクセス"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documentsアクセスユーザー"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "ドキュメントを共有できるのは所有者または管理者のみです。"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment"
|
||||
msgstr "Oo添付バージョン"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "ONLYOFFICEで開く"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "PDFフォーム"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "プレゼンテーション"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "レビュー可"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "スプレッドシート"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "30日間のテスト期間が終了し、ONLYOFFICE Docsのデモサーバーに接続できなくなりました"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "タイトル"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "ユーザー"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr "ユーザーにはこのドキュメントを開く権限がありません"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "閲覧のみ"
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "Nível de Acesso"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "Anexo"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "Em branco"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "Comentarista"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "Criar"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "Crie com o ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "Criar e Definir Permissões"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Criado por"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Criado em"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "Filtro Personalizado"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nome de exibição"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "Falha ao criar documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "Preenchimento de formulário"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "Do Modelo"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "Acesso de Usuários Internos"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Última atualização por"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Última atualização em"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "Acesso ao Link"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "Novo Documento"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "Novo documento criado em Documentos"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "Nenhum documento selecionado para compartilhamento."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "Nenhum"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "Servidor ONLYOFFICE Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE Documents"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documents Access"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documents Access Users"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "Somente o proprietário ou administrador pode compartilhar documentos."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "Abrir no ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "Formulário PDF"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "Apresentação"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "Revisor"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "Planilha"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "O período de teste de 30 dias acabou, você não pode mais se conectar ao ONLYOFFICE de demonstração Servidor Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "Titulo"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "Usuários"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "Visualizador"
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "Уровень доступа"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "Вложение"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "Пусто"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "Отменить"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "Комментатор"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "Создать"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "Создать с помощью ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "Создать и установить разрешения"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Создано"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Дата создания"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "Пользовательский фильтр"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Отображаемое имя"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "Документ"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "Редактор"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "Не удалось создать документ"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "Заполнение формы"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "Из шаблона"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "Доступ внутренних пользователей"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Последнее обновление"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Дата последнего обновления"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "Доступ по ссылке:"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "Новый документ"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "Новый документ создан в Документах"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "Не выбран документ для предоставления доступа."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "Нет"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "Сервер ONLYOFFICE Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE Документы"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documents Access"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documents Access Users"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "Предоставлять доступ к документам может только владелец или администратор."
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "Открыть в ONLYOFFICE"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "PDF форма"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "Презентация"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "Рецензент"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "Таблица"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "30-дневный тестовый период закончился, вы больше не можете подключиться к демо-серверу ONLYOFFICE Docs"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "Название"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "Пользователь"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "Зритель"
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * onlyoffice_odoo_documents
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0+e-20250520\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 16:20+0000\n"
|
||||
"PO-Revision-Date: 2025-11-12 16:20+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=1; plural=0;"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__role
|
||||
msgid "Access Level"
|
||||
msgstr "访问级别"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr "附件"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "Blank"
|
||||
msgstr "空白"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__commenter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__commenter
|
||||
msgid "Commenter"
|
||||
msgstr "可评论"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create"
|
||||
msgstr "创建"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/onlyoffice_odoo_documents_controller_mixin.xml:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "Create with ONLYOFFICE"
|
||||
msgstr "使用 ONLYOFFICE 创建"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Create&Set Permissions"
|
||||
msgstr "创建并设置权限"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "创建者"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__create_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr "创建时间"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__custom_filter
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__custom_filter
|
||||
msgid "Custom Filter"
|
||||
msgstr "自定义筛选器"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__display_name
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "显示名称"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_documents_document
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__document_id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__document_id
|
||||
msgid "Document"
|
||||
msgstr "文档"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Document edited by %(user)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__edit
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__edit
|
||||
msgid "Editor"
|
||||
msgstr "可编辑"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "Failed to create document"
|
||||
msgstr "文档创建失败"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__form_filling
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__form_filling
|
||||
msgid "Form Filling"
|
||||
msgstr "可填写表单"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.xml:0
|
||||
msgid "From Template"
|
||||
msgstr "来自模板"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__id
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__internal_users
|
||||
msgid "Internal Users Access"
|
||||
msgstr "内部用户访问"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_uid
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "最后更新者"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__write_date
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "最后更新时间"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access__link_access
|
||||
msgid "Link Access"
|
||||
msgstr "链接访问"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New Document"
|
||||
msgstr "新建文档"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/documents_view/create_mode_dialog/create_mode_dialog.js:0
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/onlyoffice_create_template_dialog.js:0
|
||||
msgid "New document created in Documents"
|
||||
msgstr "在“文档”中新建的文档"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "No document selected for sharing."
|
||||
msgstr "未选择要共享的文档。"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__none
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__none
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid "ONLYOFFICE Docs server"
|
||||
msgstr "ONLYOFFICE 文档服务器"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents
|
||||
msgid "ONLYOFFICE Documents"
|
||||
msgstr "ONLYOFFICE 文档"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access
|
||||
msgid "ONLYOFFICE Documents Access"
|
||||
msgstr "ONLYOFFICE Documents Access"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model,name:onlyoffice_odoo_documents.model_onlyoffice_odoo_documents_access_user
|
||||
msgid "ONLYOFFICE Documents Access Users"
|
||||
msgstr "ONLYOFFICE Documents Access Users"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
msgid "Only the owner or administrator can share documents."
|
||||
msgstr "只有所有者或管理员可以共享文档。"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_ir_attachment__oo_attachment_version
|
||||
msgid "Oo Attachment Version"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/components/documents_inspector_onlyoffice/documents_inspector_onlyoffice.xml:0
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.public_folder_page
|
||||
#: model_terms:ir.ui.view,arch_db:onlyoffice_odoo_documents.share_file
|
||||
msgid "Open in ONLYOFFICE"
|
||||
msgstr "用 ONLYOFFICE 打开"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "PDF form"
|
||||
msgstr "PDF 表单"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Presentation"
|
||||
msgstr "演示文稿"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__reviewer
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__reviewer
|
||||
msgid "Reviewer"
|
||||
msgstr "可审阅"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Spreadsheet"
|
||||
msgstr "电子表格"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/models/documents_inspector_onlyoffice.js:0
|
||||
msgid ""
|
||||
"The 30-day test period is over, you can no longer connect to demo ONLYOFFICE"
|
||||
" Docs server"
|
||||
msgstr "30 天测试期结束后,您将无法再访问 ONLYOFFICE文档服务器演示版"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-javascript
|
||||
#: code:addons/onlyoffice_odoo_documents/static/src/onlyoffice_create_template/create_dialog.xml:0
|
||||
msgid "Title"
|
||||
msgstr "標題"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#: model:ir.model.fields,field_description:onlyoffice_odoo_documents.field_onlyoffice_odoo_documents_access_user__user_id
|
||||
msgid "User"
|
||||
msgstr "用户"
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/controllers/controllers.py:0
|
||||
msgid "User has no read access rights to open this document"
|
||||
msgstr ""
|
||||
|
||||
#. module: onlyoffice_odoo_documents
|
||||
#. odoo-python
|
||||
#: code:addons/onlyoffice_odoo_documents/models/documents.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_documents_access_user.py:0
|
||||
#: code:addons/onlyoffice_odoo_documents/models/onlyoffice_odoo_documents.py:0
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__internal_users__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access__link_access__view
|
||||
#: model:ir.model.fields.selection,name:onlyoffice_odoo_documents.selection__onlyoffice_odoo_documents_access_user__role__view
|
||||
msgid "Viewer"
|
||||
msgstr "可查看"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from . import documents
|
||||
from . import ir_attachment
|
||||
from . import onlyoffice_odoo_documents
|
||||
from . import onlyoffice_documents_access
|
||||
from . import onlyoffice_documents_access_user
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
from odoo import _, api, models
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
_inherit = "documents.document"
|
||||
|
||||
@api.depends("checksum")
|
||||
def _compute_thumbnail(self):
|
||||
super()._compute_thumbnail()
|
||||
|
||||
for record in self:
|
||||
if record.mimetype == "application/pdf":
|
||||
record.thumbnail = False
|
||||
record.thumbnail_status = False
|
||||
return
|
||||
|
||||
@api.readonly
|
||||
def permission_panel_data(self):
|
||||
result = super().permission_panel_data()
|
||||
|
||||
if result["record"]["type"] == "binary":
|
||||
roles = list(self._get_available_roles(self.name).items())
|
||||
|
||||
for key in ["access_via_link", "access_internal", "doc_access_roles"]:
|
||||
if key in result["selections"]:
|
||||
result["selections"][key] = roles + result["selections"][key]
|
||||
|
||||
document_id = result["record"]["id"]
|
||||
|
||||
access = self.env["onlyoffice.odoo.documents.access"].search([("document_id", "=", document_id)])
|
||||
if access and access.exists():
|
||||
result["record"]["access_internal"] = access.internal_users
|
||||
result["record"]["access_via_link"] = access.link_access
|
||||
|
||||
access_user = self.env["onlyoffice.odoo.documents.access.user"].search([("document_id", "=", document_id)])
|
||||
if access_user and access_user.exists():
|
||||
user_roles = {access.user_id.id: access.role for access in access_user if access.user_id}
|
||||
for access_id in result["record"].get("access_ids", []):
|
||||
partner_id = access_id["partner_id"]["id"]
|
||||
if partner_id in user_roles:
|
||||
access_id["role"] = user_roles[partner_id]
|
||||
|
||||
return result
|
||||
|
||||
def _get_available_roles(self, filename):
|
||||
ext = filename.split(".")[-1].lower() if "." in filename else ""
|
||||
|
||||
roles = {
|
||||
"commenter": _("Commenter"),
|
||||
"reviewer": _("Reviewer"),
|
||||
"form_filling": _("Form Filling"),
|
||||
"custom_filter": _("Custom Filter"),
|
||||
}
|
||||
|
||||
if ext == "docx":
|
||||
roles.pop("form_filling", None)
|
||||
roles.pop("custom_filter", None)
|
||||
elif ext == "xlsx":
|
||||
roles.pop("reviewer", None)
|
||||
roles.pop("form_filling", None)
|
||||
elif ext == "pptx":
|
||||
roles.pop("reviewer", None)
|
||||
roles.pop("form_filling", None)
|
||||
roles.pop("custom_filter", None)
|
||||
elif ext == "pdf":
|
||||
roles.pop("commenter", None)
|
||||
roles.pop("reviewer", None)
|
||||
roles.pop("custom_filter", None)
|
||||
else:
|
||||
roles = {
|
||||
"view": _("Viewer"),
|
||||
"edit": _("Editor"),
|
||||
}
|
||||
|
||||
return roles
|
||||
|
||||
def action_update_access_rights(
|
||||
self,
|
||||
access_internal=None,
|
||||
access_via_link=None,
|
||||
is_access_via_link_hidden=None,
|
||||
partners=None,
|
||||
notify=False,
|
||||
message="",
|
||||
):
|
||||
def convert_custom_role(role):
|
||||
if role in ["commenter", "reviewer", "form_filling"]:
|
||||
return "view"
|
||||
elif role == "custom_filter":
|
||||
return "edit"
|
||||
return role
|
||||
|
||||
if partners:
|
||||
partners_with_standard_roles = {}
|
||||
for partner_id, role_data in partners.items():
|
||||
if isinstance(role_data, list):
|
||||
role = role_data[0]
|
||||
expiration_date = role_data[1]
|
||||
partners_with_standard_roles[partner_id] = [convert_custom_role(role), expiration_date]
|
||||
else:
|
||||
partners_with_standard_roles[partner_id] = convert_custom_role(role_data)
|
||||
else:
|
||||
partners_with_standard_roles = partners
|
||||
|
||||
result = super().action_update_access_rights(
|
||||
convert_custom_role(access_internal),
|
||||
convert_custom_role(access_via_link),
|
||||
is_access_via_link_hidden,
|
||||
partners_with_standard_roles,
|
||||
notify,
|
||||
message,
|
||||
)
|
||||
|
||||
specification = self._permission_specification()
|
||||
records = self.sudo().with_context(active_test=False).web_search_read([("id", "=", self.id)], specification)
|
||||
record = records["records"][0]
|
||||
|
||||
user_accesses = []
|
||||
users_to_remove = []
|
||||
|
||||
if partners:
|
||||
for partner_id, role_data in partners.items():
|
||||
partner = self.env["res.partner"].browse(int(partner_id))
|
||||
if partner.exists():
|
||||
role = role_data[0] if isinstance(role_data, list) else role_data
|
||||
|
||||
if role is False:
|
||||
users_to_remove.append(partner.id)
|
||||
else:
|
||||
user_accesses.append(
|
||||
{
|
||||
"user_id": partner.id,
|
||||
"role": role,
|
||||
}
|
||||
)
|
||||
|
||||
access = self.env["onlyoffice.odoo.documents.access"].search([("document_id", "=", self.id)])
|
||||
|
||||
if not access_internal:
|
||||
if access and access.exists():
|
||||
access_internal = access.internal_users
|
||||
else:
|
||||
access_internal = record["access_internal"]
|
||||
|
||||
if not access_via_link:
|
||||
if access and access.exists():
|
||||
access_via_link = access.link_access
|
||||
else:
|
||||
access_via_link = record["access_via_link"]
|
||||
|
||||
vals = {
|
||||
"document_id": self.id,
|
||||
"internal_users": access_internal,
|
||||
"link_access": access_via_link,
|
||||
"user_accesses": user_accesses,
|
||||
"users_to_remove": users_to_remove,
|
||||
}
|
||||
|
||||
self.env["onlyoffice.odoo.documents"].advanced_share_save(vals)
|
||||
|
||||
return result
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
oo_attachment_version = fields.Integer(default=1)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class OnlyofficeDocumentsAccessUser(models.Model):
|
||||
_name = "onlyoffice.odoo.documents.access"
|
||||
_description = "ONLYOFFICE Documents Access"
|
||||
|
||||
document_id = fields.Many2one("documents.document", required=True, ondelete="cascade")
|
||||
internal_users = fields.Selection(
|
||||
[
|
||||
("none", _("None")),
|
||||
("view", _("Viewer")),
|
||||
("commenter", _("Commenter")),
|
||||
("reviewer", _("Reviewer")),
|
||||
("edit", _("Editor")),
|
||||
("form_filling", _("Form Filling")),
|
||||
("custom_filter", _("Custom Filter")),
|
||||
],
|
||||
default="none",
|
||||
string="Internal Users Access",
|
||||
)
|
||||
link_access = fields.Selection(
|
||||
[
|
||||
("none", _("None")),
|
||||
("view", _("Viewer")),
|
||||
("commenter", _("Commenter")),
|
||||
("reviewer", _("Reviewer")),
|
||||
("edit", _("Editor")),
|
||||
("form_filling", _("Form Filling")),
|
||||
("custom_filter", _("Custom Filter")),
|
||||
],
|
||||
default="view",
|
||||
string="Link Access",
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class OnlyofficeDocumentsAccessUser(models.Model):
|
||||
_name = "onlyoffice.odoo.documents.access.user"
|
||||
_description = "ONLYOFFICE Documents Access Users"
|
||||
|
||||
document_id = fields.Many2one("documents.document", required=True, ondelete="cascade")
|
||||
user_id = fields.Many2one("res.partner", required=True, string="User")
|
||||
role = fields.Selection(
|
||||
[
|
||||
("none", _("None")),
|
||||
("view", _("Viewer")),
|
||||
("commenter", _("Commenter")),
|
||||
("reviewer", _("Reviewer")),
|
||||
("edit", _("Editor")),
|
||||
("form_filling", _("Form Filling")),
|
||||
("custom_filter", _("Custom Filter")),
|
||||
],
|
||||
required=True,
|
||||
string="Access Level",
|
||||
)
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
from odoo import _, api, models
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class OnlyofficeDocuments(models.Model):
|
||||
_name = "onlyoffice.odoo.documents"
|
||||
_description = "ONLYOFFICE Documents"
|
||||
|
||||
def _get_available_roles(self, filename):
|
||||
ext = filename.split(".")[-1].lower() if "." in filename else ""
|
||||
|
||||
roles = {
|
||||
"none": _("None"),
|
||||
"view": _("Viewer"),
|
||||
"commenter": _("Commenter"),
|
||||
"reviewer": _("Reviewer"),
|
||||
"edit": _("Editor"),
|
||||
"form_filling": _("Form Filling"),
|
||||
"custom_filter": _("Custom Filter"),
|
||||
}
|
||||
|
||||
if ext == "docx":
|
||||
roles.pop("form_filling", None)
|
||||
roles.pop("custom_filter", None)
|
||||
elif ext == "xlsx":
|
||||
roles.pop("reviewer", None)
|
||||
roles.pop("form_filling", None)
|
||||
elif ext == "pptx":
|
||||
roles.pop("reviewer", None)
|
||||
roles.pop("form_filling", None)
|
||||
roles.pop("custom_filter", None)
|
||||
elif ext == "pdf":
|
||||
roles.pop("commenter", None)
|
||||
roles.pop("reviewer", None)
|
||||
roles.pop("custom_filter", None)
|
||||
else:
|
||||
roles = {
|
||||
"none": _("None"),
|
||||
"view": _("Viewer"),
|
||||
"edit": _("Editor"),
|
||||
}
|
||||
|
||||
return roles
|
||||
|
||||
@api.model
|
||||
def advanced_share_save(self, vals):
|
||||
document_id = vals.get("document_id")
|
||||
if not document_id:
|
||||
raise AccessError(_("No document selected for sharing."))
|
||||
|
||||
is_admin = self.env.user.has_group("base.group_system")
|
||||
document = self.env["documents.document"].browse(document_id)
|
||||
if not is_admin and document.create_uid != self.env.user:
|
||||
raise AccessError(_("Only the owner or administrator can share documents."))
|
||||
|
||||
access = self.env["onlyoffice.odoo.documents.access"].search([("document_id", "=", document_id)], limit=1)
|
||||
if not access:
|
||||
access = self.env["onlyoffice.odoo.documents.access"].create(
|
||||
{
|
||||
"document_id": document_id,
|
||||
"internal_users": vals.get("internal_users", "none"),
|
||||
"link_access": vals.get("link_access", "none"),
|
||||
}
|
||||
)
|
||||
else:
|
||||
access.write(
|
||||
{
|
||||
"internal_users": vals.get("internal_users"),
|
||||
"link_access": vals.get("link_access"),
|
||||
}
|
||||
)
|
||||
|
||||
users_to_remove = vals.get("users_to_remove", [])
|
||||
if users_to_remove:
|
||||
self.env["onlyoffice.odoo.documents.access.user"].search(
|
||||
[("document_id", "=", document_id), ("user_id", "in", users_to_remove)]
|
||||
).unlink()
|
||||
|
||||
user_accesses = vals.get("user_accesses", [])
|
||||
existing_accesses = (
|
||||
self.env["onlyoffice.odoo.documents.access.user"]
|
||||
.search([("document_id", "=", document_id)])
|
||||
.mapped("user_id.id")
|
||||
)
|
||||
|
||||
for user_data in user_accesses:
|
||||
if user_data["user_id"] in existing_accesses:
|
||||
self.env["onlyoffice.odoo.documents.access.user"].search(
|
||||
[("document_id", "=", document_id), ("user_id", "=", user_data["user_id"])]
|
||||
).write({"role": user_data["role"]})
|
||||
else:
|
||||
if self.env["res.partner"].search_count([("id", "=", user_data["user_id"])]):
|
||||
self.env["onlyoffice.odoo.documents.access.user"].create(
|
||||
{
|
||||
"document_id": document_id,
|
||||
"user_id": user_data["user_id"],
|
||||
"role": user_data["role"],
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_onlyoffice_odoo_documents,onlyoffice.odoo.documents,model_onlyoffice_odoo_documents,base.group_user,1,1,1,1
|
||||
access_onlyoffice_odoo_documents_access_user,onlyoffice.odoo.documents.access.user,model_onlyoffice_odoo_documents_access_user,base.group_user,1,1,1,1
|
||||
access_onlyoffice_odoo_documents_access,onlyoffice.odoo.documents.access,model_onlyoffice_odoo_documents_access,base.group_user,1,1,1,1
|
||||
|
|
After Width: | Height: | Size: 384 KiB |
|
After Width: | Height: | Size: 410 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1,89 @@
|
|||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h2 class="oe_slogan">
|
||||
Edit and collaborate on office files within Odoo Documents
|
||||
</h2>
|
||||
<div class="oe_span12">
|
||||
<span class="text-center">
|
||||
<p class="oe_mt32">
|
||||
Work with office documents uploaded in Odoo using ONLYOFFICE Docs (in any Odoo
|
||||
section where you can upload/attach files). For example, you can edit attached
|
||||
deal notes in the Sales section, open chat attachments in the Discuss section,
|
||||
etc.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container">
|
||||
<div class="oe_row">
|
||||
<div class="oe_span12">
|
||||
<h6>Edit files in the Documents module</h6>
|
||||
</div>
|
||||
|
||||
<div class="oe_span12">
|
||||
<span class="text-center">
|
||||
<p class="oe_mt32">
|
||||
ONLYOFFICE app for Odoo Enterprise allows opening the created files for
|
||||
editing. Just select the needed document in the file list and click the
|
||||
ONLYOFFICE icon on the right-side panel.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="oe_span12">
|
||||
<div class="oe_demo oe_picture1 oe_screenshot">
|
||||
<img src="edit_files.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container mt-5">
|
||||
<div class="oe_row">
|
||||
<div class="oe_span12">
|
||||
<h6>Create new files in the Documents module</h6>
|
||||
</div>
|
||||
|
||||
<div class="oe_span12">
|
||||
<span class="text-center">
|
||||
<p class="oe_mt32">
|
||||
To create new office files (docs, sheets, slides, PDF forms), click Create with
|
||||
ONLYOFFICE on the top panel within the Documents module. Select the file type
|
||||
and specify its name in the pop-up window.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="oe_span12">
|
||||
<div class="oe_demo oe_screenshot">
|
||||
<img src="create_files.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container mt-5">
|
||||
<div class="oe_row">
|
||||
<div class="oe_span12">
|
||||
<h5>App features and file formats</h5>
|
||||
</div>
|
||||
|
||||
<div class="oe_span12">
|
||||
<ul>
|
||||
<li>Edit text documents, spreadsheets, and presentations (DOCX, XLSX, PPTX)</li>
|
||||
<li>Collaborate on documents with your colleagues in real time</li>
|
||||
<li>Edit form templates in PDF</li>
|
||||
<li>Read PDF files</li>
|
||||
<li>
|
||||
Open other office file formats for viewing, including RTF, TXT, CSV, etc.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="oe_demo oe_screenshot">
|
||||
<img src="editors.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- -->
|
||||
<!-- (c) Copyright Ascensio System SIA 2024 -->
|
||||
<!-- -->
|
||||
<templates>
|
||||
<t t-name="onlyoffice.documents.ControlPanel" t-inherit="documents.ControlPanel" t-inherit-mode="extension">
|
||||
<xpath expr="//*[hasclass('o_documents_share_button')]" position="after">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary o_onlyoffice_open"
|
||||
t-if="userIsInternal and showOnlyofficeButton(singleSelection)"
|
||||
t-on-click.prevent="onlyofficeEditorUrl"
|
||||
> Open in ONLYOFFICE </button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { FormGallery } from "@onlyoffice_odoo/views/form_gallery/form_gallery"
|
||||
import { CreateDialog } from "@onlyoffice_odoo_documents/onlyoffice_create_template/onlyoffice_create_template_dialog"
|
||||
import { Dialog } from "@web/core/dialog/dialog"
|
||||
import { useHotkey } from "@web/core/hotkeys/hotkey_hook"
|
||||
import { _t } from "@web/core/l10n/translation"
|
||||
import { rpc } from "@web/core/network/rpc"
|
||||
import { useService } from "@web/core/utils/hooks"
|
||||
|
||||
const { Component, useState } = owl
|
||||
|
||||
export class CreateModeDialog extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm")
|
||||
this.rpc = rpc
|
||||
this.data = this.env.dialogData
|
||||
useHotkey("escape", () => this.data.close())
|
||||
|
||||
this.dialogTitle = _t("Create with ONLYOFFICE")
|
||||
this.state = useState({
|
||||
isChosen: false,
|
||||
selectedMode: null,
|
||||
})
|
||||
this.dialogService = useService("dialog")
|
||||
this.notification = useService("notification")
|
||||
}
|
||||
|
||||
async _choiceDialog() {
|
||||
if (this._buttonDisabled()) {
|
||||
return
|
||||
}
|
||||
this.state.isChosen = true
|
||||
const selectedMode = this.state.selectedMode
|
||||
if (selectedMode === "blank") {
|
||||
this.create()
|
||||
} else if (selectedMode === "template") {
|
||||
this.formGallery()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
create() {
|
||||
this.dialogService.add(CreateDialog, {
|
||||
context: this.props.context,
|
||||
folderId: this.props.folderId,
|
||||
model: this.props.model,
|
||||
onShare: this.props.onShare,
|
||||
})
|
||||
this.data.close()
|
||||
}
|
||||
|
||||
formGallery() {
|
||||
const download = async (form) => {
|
||||
const json = await this.rpc("/onlyoffice/documents/file/create", {
|
||||
folder_id: this.props.folderId,
|
||||
supported_format: form.attributes.file_oform.data[0].attributes.url.split(".").pop(),
|
||||
title: form.attributes.name_form,
|
||||
url: form.attributes.file_oform.data[0].attributes.url,
|
||||
})
|
||||
const result = JSON.parse(json)
|
||||
if (result.error) {
|
||||
this.notification.add(result.error, {
|
||||
sticky: false,
|
||||
type: "error",
|
||||
})
|
||||
} else {
|
||||
this.props.model.load()
|
||||
this.props.model.notify()
|
||||
this.notification.add(_t("New document created in Documents"), {
|
||||
sticky: false,
|
||||
type: "info",
|
||||
})
|
||||
const { same_tab } = JSON.parse(await this.orm.call("onlyoffice.odoo", "get_same_tab"))
|
||||
if (same_tab) {
|
||||
const action = {
|
||||
params: { attachment_id: result.file_id },
|
||||
tag: "onlyoffice_editor",
|
||||
target: "current",
|
||||
type: "ir.actions.client",
|
||||
}
|
||||
return this.action.doAction(action)
|
||||
}
|
||||
window.open(`/onlyoffice/editor/document/${result.document_id}`, "_blank")
|
||||
}
|
||||
}
|
||||
this.dialogService.add(
|
||||
FormGallery,
|
||||
{
|
||||
onDownload: download,
|
||||
showType: true,
|
||||
},
|
||||
{
|
||||
onClose: () => {
|
||||
this.data.close()
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
_selectedMode(format) {
|
||||
this.state.selectedMode = format
|
||||
}
|
||||
|
||||
_isSelected(format) {
|
||||
return this.state.selectedMode === format
|
||||
}
|
||||
|
||||
_buttonDisabled() {
|
||||
return this.state.isChosen || this.state.selectedMode === null
|
||||
}
|
||||
}
|
||||
CreateModeDialog.components = { Dialog }
|
||||
CreateModeDialog.template = "onlyoffice_odoo_documents.CreateModeDialog"
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="onlyoffice_odoo_documents.CreateModeDialog" owl="1">
|
||||
<div>
|
||||
<Dialog title="dialogTitle" size="'md'" contentClass="'o-onlyoffice-create-templates-dialog'" footer="true">
|
||||
<div class="o-onlyoffice-create-templates-container">
|
||||
<div
|
||||
class="o-onlyoffice-create-template"
|
||||
t-on-click="() => this._selectedMode('blank')"
|
||||
t-on-dblclick="_choiceDialog"
|
||||
t-att-class="_isSelected('blank') ? 'o-template-selected': ''"
|
||||
>
|
||||
<img
|
||||
class="o-onlyoffice-create-icon"
|
||||
src="/onlyoffice_odoo_documents/static/svg/choice/blank.svg"
|
||||
t-att-title="Blank"
|
||||
/>
|
||||
<div class="o-template-item-name">Blank</div>
|
||||
</div>
|
||||
<div
|
||||
class="o-onlyoffice-create-template"
|
||||
t-on-click="() => this._selectedMode('template')"
|
||||
t-on-dblclick="_choiceDialog"
|
||||
t-att-class="_isSelected('template') ? 'o-template-selected': ''"
|
||||
>
|
||||
<img
|
||||
class="o-onlyoffice-create-icon"
|
||||
src="/onlyoffice_odoo_documents/static/svg/choice/template.svg"
|
||||
t-att-title="Template"
|
||||
/>
|
||||
<div class="o-template-item-name">From Template</div>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button
|
||||
class="btn btn-primary o-onlyoffice-create-ok-button"
|
||||
t-att-disabled="_buttonDisabled()"
|
||||
t-on-click="_choiceDialog"
|
||||
>
|
||||
<t>Create</t>
|
||||
</button>
|
||||
<button class="btn btn-secondary" t-on-click="data.close">
|
||||
<t>Cancel</t>
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { DocumentsKanbanController } from "@documents/views/kanban/documents_kanban_controller"
|
||||
import { patch } from "@web/core/utils/patch"
|
||||
import { OnlyofficeDocumentsControllerMixin } from "../onlyoffice_odoo_documents_controller_mixin"
|
||||
|
||||
patch(DocumentsKanbanController.prototype, OnlyofficeDocumentsControllerMixin())
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { DocumentsListController } from "@documents/views/list/documents_list_controller"
|
||||
import { patch } from "@web/core/utils/patch"
|
||||
import { OnlyofficeDocumentsControllerMixin } from "../onlyoffice_odoo_documents_controller_mixin"
|
||||
|
||||
patch(DocumentsListController.prototype, OnlyofficeDocumentsControllerMixin())
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks"
|
||||
import { CreateModeDialog } from "./create_mode_dialog/create_mode_dialog"
|
||||
|
||||
export const OnlyofficeDocumentsControllerMixin = () => ({
|
||||
setup() {
|
||||
super.setup(...arguments)
|
||||
this.action = useService("action")
|
||||
this.dialogService = useService("dialog")
|
||||
this.notification = useService("notification")
|
||||
},
|
||||
|
||||
// eslint-disable-next-line sort-keys
|
||||
async onClickCreateOnlyoffice() {
|
||||
this.dialogService.add(CreateModeDialog, {
|
||||
context: this.props.context,
|
||||
folderId: this.env.searchModel.getSelectedFolderId(),
|
||||
model: this.env.model,
|
||||
onShare: (document_id) => this.onClickAdvancedShare(document_id, true),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="onlyoffice_odoo_documents.OnlyofficeIcon">
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.68812 12.8529L0.341584 10.3813C-0.113861 10.1656 -0.113861 9.83208 0.341584 9.63593L2.20297 8.77283L5.66832 10.3813C6.12376 10.5971 6.85644 10.5971 7.29208 10.3813L10.7574 8.77283L12.6188 9.63593C13.0743 9.8517 13.0743 10.1852 12.6188 10.3813L7.27228 12.8529C6.85644 13.0491 6.12376 13.0491 5.68812 12.8529Z"
|
||||
fill="black"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.68812 9.81244L0.341584 7.34084C-0.113861 7.12506 -0.113861 6.79159 0.341584 6.59544L2.16337 5.75195L5.68812 7.38007C6.14356 7.59584 6.87624 7.59584 7.31188 7.38007L10.8366 5.75195L12.6584 6.59544C13.1139 6.81121 13.1139 7.14468 12.6584 7.34084L7.31188 9.81244C6.85644 10.0282 6.12376 10.0282 5.68812 9.81244Z"
|
||||
fill="black"
|
||||
fill-opacity="0.75"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.68812 6.85043L0.341584 4.37883C-0.113861 4.16306 -0.113861 3.82959 0.341584 3.63343L5.68812 1.16183C6.14356 0.946056 6.87624 0.946056 7.31188 1.16183L12.6584 3.63343C13.1139 3.8492 13.1139 4.18267 12.6584 4.37883L7.31188 6.85043C6.85644 7.04659 6.12376 7.04659 5.68812 6.85043Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
</t>
|
||||
<t
|
||||
t-name="onlyoffice_odoo_documents.DocumentsViews.ControlPanel"
|
||||
t-inherit="documents.DocumentsViews.ControlPanel"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//ul" position="inside">
|
||||
<button
|
||||
type="button"
|
||||
t-attf-class="btn btn-link dropdown-item o_documents_kanban_onlyoffice_odoo_documents {{additionalClasses}}"
|
||||
t-att-disabled="!canUploadInFolder(folder)"
|
||||
t-on-click.stop.prevent="onClickCreateOnlyoffice"
|
||||
>
|
||||
<t t-call="onlyoffice_odoo_documents.OnlyofficeIcon" /> Create with ONLYOFFICE </button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System SIA 2024
|
||||
*
|
||||
*/
|
||||
|
||||
import { DocumentsControlPanel } from "@documents/views/search/documents_control_panel"
|
||||
|
||||
import { _t } from "@web/core/l10n/translation"
|
||||
import { patch } from "@web/core/utils/patch"
|
||||
|
||||
let formats = []
|
||||
const loadFormats = async () => {
|
||||
try {
|
||||
const response = await fetch("/onlyoffice_odoo/static/assets/document_formats/onlyoffice-docs-formats.json")
|
||||
formats = await response.json()
|
||||
} catch (error) {
|
||||
console.error("Error loading formats data:", error)
|
||||
}
|
||||
}
|
||||
|
||||
loadFormats()
|
||||
|
||||
patch(DocumentsControlPanel.prototype, {
|
||||
showOnlyofficeButton(records) {
|
||||
if (records?.data?.display_name) {
|
||||
const ext = records?.data?.display_name.split(".").pop()
|
||||
return this.onlyofficeCanEdit(ext) || this.onlyofficeCanView(ext)
|
||||
}
|
||||
return false
|
||||
},
|
||||
// eslint-disable-next-line sort-keys
|
||||
onlyofficeCanEdit(extension) {
|
||||
const format = formats.find((f) => f.name === extension.toLowerCase())
|
||||
return format && format.actions && format.actions.includes("edit")
|
||||
},
|
||||
onlyofficeCanView(extension) {
|
||||
const format = formats.find((f) => f.name === extension.toLowerCase())
|
||||
return format && format.actions && (format.actions.includes("view") || format.actions.includes("edit"))
|
||||
},
|
||||
async onlyofficeEditorUrl() {
|
||||
const doc = this.env.model.root.selection[0]
|
||||
const demo = JSON.parse(await this.orm.call("onlyoffice.odoo", "get_demo"))
|
||||
if (demo && demo.mode && demo.date) {
|
||||
const isValidDate = (d) => d instanceof Date && !isNaN(d)
|
||||
demo.date = new Date(Date.parse(demo.date))
|
||||
if (isValidDate(demo.date)) {
|
||||
const today = new Date()
|
||||
const difference = Math.floor((today - demo.date) / (1000 * 60 * 60 * 24))
|
||||
if (difference > 30) {
|
||||
this.notification.add(
|
||||
_t("The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server"),
|
||||
{
|
||||
title: _t("ONLYOFFICE Docs server"),
|
||||
type: "warning",
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
const { same_tab } = JSON.parse(await this.orm.call("onlyoffice.odoo", "get_same_tab"))
|
||||
if (same_tab) {
|
||||
const action = {
|
||||
params: { document_id: doc.data.id },
|
||||
tag: "onlyoffice_editor",
|
||||
target: "current",
|
||||
type: "ir.actions.client",
|
||||
}
|
||||
return this.actionService.doAction(action)
|
||||
}
|
||||
window.open(`/onlyoffice/editor/document/${doc.data.id}`, "_blank")
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="onlyoffice_odoo_documents.CreateDialog" owl="1">
|
||||
<div>
|
||||
<Dialog
|
||||
t-if="state.isOpen"
|
||||
title="dialogTitle"
|
||||
contentClass="'o-onlyoffice-create-templates-dialog'"
|
||||
footer="true"
|
||||
>
|
||||
<div class="o-onlyoffice-create-field-box">
|
||||
<div class="o_wrap_field d-flex d-sm-contents flex-column mb-3 mb-sm-0">
|
||||
<div class="o_cell o_wrap_label flex-grow-1 flex-sm-grow-0 me-3 text-900">
|
||||
<label class="o_form_label" for="name">Title</label>
|
||||
</div>
|
||||
<div class="o_wrap_input flex-grow-1 flex-sm-grow-0 w-100">
|
||||
<div class="o_field_widget o_field_char">
|
||||
<input
|
||||
class="o_input"
|
||||
id="title"
|
||||
t-model.trim="state.title"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
t-ref="autofocus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o-onlyoffice-create-templates-container">
|
||||
<div
|
||||
class="o-onlyoffice-create-template"
|
||||
t-on-click="() => this._selectedFormat('docx')"
|
||||
t-on-dblclick="() => this._createFile(false)"
|
||||
t-att-class="_isSelected('docx') ? 'o-template-selected': ''"
|
||||
>
|
||||
<img
|
||||
class="o-onlyoffice-create-icon"
|
||||
src="/onlyoffice_odoo_documents/static/svg/formats/docx.svg"
|
||||
t-att-title="Document"
|
||||
/>
|
||||
<div class="o-template-item-name">Document</div>
|
||||
</div>
|
||||
<div
|
||||
class="o-onlyoffice-create-template"
|
||||
t-on-click="() => this._selectedFormat('xlsx')"
|
||||
t-on-dblclick="() => this._createFile(false)"
|
||||
t-att-class="_isSelected('xlsx') ? 'o-template-selected': ''"
|
||||
>
|
||||
<img
|
||||
class="o-onlyoffice-create-icon"
|
||||
src="/onlyoffice_odoo_documents/static/svg/formats/xlsx.svg"
|
||||
t-att-title="Spreadsheet"
|
||||
/>
|
||||
<div class="o-template-item-name">Spreadsheet</div>
|
||||
</div>
|
||||
<div
|
||||
class="o-onlyoffice-create-template"
|
||||
t-on-click="() => this._selectedFormat('pptx')"
|
||||
t-on-dblclick="() => this._createFile(false)"
|
||||
t-att-class="_isSelected('pptx') ? 'o-template-selected': ''"
|
||||
>
|
||||
<img
|
||||
class="o-onlyoffice-create-icon"
|
||||
src="/onlyoffice_odoo_documents/static/svg/formats/pptx.svg"
|
||||
t-att-title="Presentation"
|
||||
/>
|
||||
<div class="o-template-item-name">Presentation</div>
|
||||
</div>
|
||||
<div
|
||||
class="o-onlyoffice-create-template"
|
||||
t-on-click="() => this._selectedFormat('pdf')"
|
||||
t-on-dblclick="() => this._createFile(false)"
|
||||
t-att-class="_isSelected('pdf') ? 'o-template-selected': ''"
|
||||
>
|
||||
<img
|
||||
class="o-onlyoffice-create-icon"
|
||||
src="/onlyoffice_odoo_documents/static/svg/formats/pdf.svg"
|
||||
t-att-title="PDF"
|
||||
/>
|
||||
<div class="o-template-item-name">PDF form</div>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button
|
||||
class="btn btn-primary o-onlyoffice-create-ok-button"
|
||||
t-att-disabled="_buttonDisabled()"
|
||||
t-on-click="() => this._createFile(false)"
|
||||
>
|
||||
<t>Create</t>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary o-onlyoffice-create-ok-button"
|
||||
t-att-disabled="_buttonDisabled()"
|
||||
t-on-click="() => this._createFile(true)"
|
||||
>
|
||||
<t>Create&Set Permissions</t>
|
||||
</button>
|
||||
<button class="btn btn-secondary" t-on-click="data.close">
|
||||
<t>Cancel</t>
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { DocumentsPermissionPanel } from "@documents/components/documents_permission_panel/documents_permission_panel"
|
||||
import { Dialog } from "@web/core/dialog/dialog"
|
||||
|
||||
import { useHotkey } from "@web/core/hotkeys/hotkey_hook"
|
||||
import { _t } from "@web/core/l10n/translation"
|
||||
import { rpc } from "@web/core/network/rpc"
|
||||
import { KeepLast } from "@web/core/utils/concurrency"
|
||||
import { useService, useAutofocus } from "@web/core/utils/hooks"
|
||||
import { getDefaultConfig } from "@web/views/view"
|
||||
|
||||
const { Component, useState, useSubEnv } = owl
|
||||
|
||||
export class CreateDialog extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm")
|
||||
this.rpc = rpc
|
||||
this.viewService = useService("view")
|
||||
this.notificationService = useService("notification")
|
||||
this.actionService = useService("action")
|
||||
this.inputRef = useAutofocus()
|
||||
this.dialogService = useService("dialog")
|
||||
|
||||
this.data = this.env.dialogData
|
||||
useHotkey("escape", () => this.data.close())
|
||||
|
||||
this.dialogTitle = _t("Create with ONLYOFFICE")
|
||||
this.state = useState({
|
||||
isCreating: false,
|
||||
isOpen: true,
|
||||
selectedFormat: "docx",
|
||||
title: _t("New Document"),
|
||||
})
|
||||
useSubEnv({ config: { ...getDefaultConfig() } })
|
||||
this.keepLast = new KeepLast()
|
||||
|
||||
if (this.inputRef.el) {
|
||||
this.inputRef.el.focus()
|
||||
}
|
||||
}
|
||||
|
||||
async _createFile(configureAccess = false) {
|
||||
if (this._buttonDisabled()) {
|
||||
return
|
||||
}
|
||||
this.state.isCreating = true
|
||||
const selectedFormat = this.state.selectedFormat
|
||||
const title = this.state.title
|
||||
|
||||
const json = await this.rpc("/onlyoffice/documents/file/create", {
|
||||
folder_id: this.props.folderId,
|
||||
supported_format: selectedFormat,
|
||||
title: title,
|
||||
})
|
||||
|
||||
const result = JSON.parse(json)
|
||||
|
||||
this.props.model.load()
|
||||
this.props.model.notify()
|
||||
|
||||
if (result.error) {
|
||||
this.notificationService.add(result.error, {
|
||||
sticky: false,
|
||||
type: "error",
|
||||
})
|
||||
} else {
|
||||
this.notificationService.add(_t("New document created in Documents"), {
|
||||
sticky: false,
|
||||
type: "info",
|
||||
})
|
||||
|
||||
if (configureAccess) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
this.data.close()
|
||||
const document = { id: result.document_id }
|
||||
this.dialogService.add(DocumentsPermissionPanel, { document })
|
||||
} else {
|
||||
const { same_tab } = JSON.parse(await this.orm.call("onlyoffice.odoo", "get_same_tab"))
|
||||
if (same_tab) {
|
||||
const action = {
|
||||
params: { attachment_id: result.file_id },
|
||||
tag: "onlyoffice_editor",
|
||||
target: "current",
|
||||
type: "ir.actions.client",
|
||||
}
|
||||
return this.actionService.doAction(action)
|
||||
}
|
||||
this.data.close()
|
||||
return window.open(`/onlyoffice/editor/document/${result.document_id}`, "_blank")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectedFormat(format) {
|
||||
this.state.selectedFormat = format
|
||||
}
|
||||
|
||||
_isSelected(format) {
|
||||
return this.state.selectedFormat === format
|
||||
}
|
||||
|
||||
_hasSelection() {
|
||||
// eslint-disable-next-line no-constant-binary-expression, no-implicit-coercion
|
||||
return !!this.state.selectedFormat !== null
|
||||
}
|
||||
|
||||
_buttonDisabled() {
|
||||
return this.state.isCreating || !this._hasSelection() || !this.state.title
|
||||
}
|
||||
}
|
||||
CreateDialog.components = { Dialog }
|
||||
CreateDialog.template = "onlyoffice_odoo_documents.CreateDialog"
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
$margin-right: 15px;
|
||||
|
||||
.o-onlyoffice-create-templates-dialog {
|
||||
.o-onlyoffice-create-field-box {
|
||||
display: flex;
|
||||
padding: $margin-right;
|
||||
}
|
||||
|
||||
.o-onlyoffice-create-templates-container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.o-onlyoffice-create-template {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ced4da;
|
||||
padding: $margin-right;
|
||||
margin: $margin-right;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&.o-template-selected {
|
||||
border-color: $o-brand-primary;
|
||||
box-shadow: 0 0 0 2px $o-brand-primary;
|
||||
}
|
||||
|
||||
.o-onlyoffice-create-icon {
|
||||
margin-right: $margin-right;
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2491_15883)">
|
||||
<path d="M7 1.5H20.5859C20.7185 1.50004 20.8457 1.55275 20.9395 1.64648L26.3535 7.06055C26.4472 7.15428 26.5 7.28151 26.5 7.41406V29C26.5 29.8284 25.8284 30.5 25 30.5H7C6.17157 30.5 5.5 29.8284 5.5 29V3C5.5 2.17157 6.17157 1.5 7 1.5Z" fill="white" stroke="#BBBBBB"/>
|
||||
<path d="M20.5 2V5.1C20.5 5.94008 20.5 6.36012 20.6635 6.68099C20.8073 6.96323 21.0368 7.1927 21.319 7.33651C21.6399 7.5 22.0599 7.5 22.9 7.5H26" stroke="#BBBBBB"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2491_15883">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 683 B |
|
|
@ -0,0 +1,14 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2491_11540)">
|
||||
<path d="M7 1.5H20.5859C20.7185 1.50004 20.8457 1.55275 20.9395 1.64648L26.3535 7.06055C26.4472 7.15428 26.5 7.28151 26.5 7.41406V29C26.5 29.8284 25.8284 30.5 25 30.5H7C6.17157 30.5 5.5 29.8284 5.5 29V3C5.5 2.17157 6.17157 1.5 7 1.5Z" fill="white" stroke="#BBBBBB"/>
|
||||
<path d="M20.5 2V5.1C20.5 5.94008 20.5 6.36012 20.6635 6.68099C20.8073 6.96323 21.0368 7.1927 21.319 7.33651C21.6399 7.5 22.0599 7.5 22.9 7.5H26" stroke="#BBBBBB"/>
|
||||
<path opacity="0.6" d="M19.5808 17.2273C19.7671 16.9242 20.2329 16.9242 20.4192 17.2273L22.9344 21.3182C23.1207 21.6212 22.8878 22 22.5152 22H17.4848C17.1122 22 16.8793 21.6212 17.0656 21.3182L19.5808 17.2273Z" fill="#A671D8"/>
|
||||
<path d="M14.1215 13.2432C14.3201 12.9189 14.8165 12.9189 15.015 13.2432L19.9301 21.2703C20.1287 21.5946 19.8805 22 19.4834 22H9.51663C9.11952 22 8.87133 21.5946 9.06988 21.2703L14.1215 13.2432Z" fill="#A671D8"/>
|
||||
<circle cx="19" cy="14" r="1" fill="#A671D8"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2491_11540">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.68812 12.8529L0.341584 10.3813C-0.113861 10.1656 -0.113861 9.83208 0.341584 9.63593L2.20297 8.77283L5.66832 10.3813C6.12376 10.5971 6.85644 10.5971 7.29208 10.3813L10.7574 8.77283L12.6188 9.63593C13.0743 9.8517 13.0743 10.1852 12.6188 10.3813L7.27228 12.8529C6.85644 13.0491 6.12376 13.0491 5.68812 12.8529Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.68812 9.81244L0.341584 7.34084C-0.113861 7.12506 -0.113861 6.79159 0.341584 6.59544L2.16337 5.75195L5.68812 7.38007C6.14356 7.59584 6.87624 7.59584 7.31188 7.38007L10.8366 5.75195L12.6584 6.59544C13.1139 6.81121 13.1139 7.14468 12.6584 7.34084L7.31188 9.81244C6.85644 10.0282 6.12376 10.0282 5.68812 9.81244Z" fill="white" fill-opacity="0.75"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.68812 6.85043L0.341584 4.37883C-0.113861 4.16306 -0.113861 3.82959 0.341584 3.63343L5.68812 1.16183C6.14356 0.946056 6.87624 0.946056 7.31188 1.16183L12.6584 3.63343C13.1139 3.8492 13.1139 4.18267 12.6584 4.37883L7.31188 6.85043C6.85644 7.04659 6.12376 7.04659 5.68812 6.85043Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.68812 12.8529L0.341584 10.3813C-0.113861 10.1656 -0.113861 9.83208 0.341584 9.63593L2.20297 8.77283L5.66832 10.3813C6.12376 10.5971 6.85644 10.5971 7.29208 10.3813L10.7574 8.77283L12.6188 9.63593C13.0743 9.8517 13.0743 10.1852 12.6188 10.3813L7.27228 12.8529C6.85644 13.0491 6.12376 13.0491 5.68812 12.8529Z" fill="black" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.68812 9.81244L0.341584 7.34084C-0.113861 7.12506 -0.113861 6.79159 0.341584 6.59544L2.16337 5.75195L5.68812 7.38007C6.14356 7.59584 6.87624 7.59584 7.31188 7.38007L10.8366 5.75195L12.6584 6.59544C13.1139 6.81121 13.1139 7.14468 12.6584 7.34084L7.31188 9.81244C6.85644 10.0282 6.12376 10.0282 5.68812 9.81244Z" fill="black" fill-opacity="0.75"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.68812 6.85043L0.341584 4.37883C-0.113861 4.16306 -0.113861 3.82959 0.341584 3.63343L5.68812 1.16183C6.14356 0.946056 6.87624 0.946056 7.31188 1.16183L12.6584 3.63343C13.1139 3.8492 13.1139 4.18267 12.6584 4.37883L7.31188 6.85043C6.85644 7.04659 6.12376 7.04659 5.68812 6.85043Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.5H20.5859C20.7185 1.50004 20.8457 1.55275 20.9395 1.64648L26.3535 7.06055C26.4472 7.15428 26.5 7.28151 26.5 7.41406V29C26.5 29.8284 25.8284 30.5 25 30.5H7C6.17157 30.5 5.5 29.8284 5.5 29V3C5.5 2.17157 6.17157 1.5 7 1.5Z" fill="white" stroke="#BBBBBB"/>
|
||||
<path d="M20.5 2V6C20.5 6.82843 21.1716 7.5 22 7.5H26" stroke="#BBBBBB"/>
|
||||
<path d="M13 20C13.5523 20 14 20.4477 14 21C14 21.5523 13.5523 22 13 22H10C9.44772 22 9 21.5523 9 21C9 20.4477 9.44772 20 10 20H13ZM13 16C13.5523 16 14 16.4477 14 17C14 17.5523 13.5523 18 13 18H10C9.44772 18 9 17.5523 9 17C9 16.4477 9.44772 16 10 16H13ZM22 12C22.5523 12 23 12.4477 23 13C23 13.5523 22.5523 14 22 14H10C9.44772 14 9 13.5523 9 13C9 12.4477 9.44772 12 10 12H22Z" fill="#A9CBDD"/>
|
||||
<path d="M16 18C16 16.8954 16.8954 16 18 16H26C27.1046 16 28 16.8954 28 18V26C28 27.1046 27.1046 28 26 28H18C16.8954 28 16 27.1046 16 26V18Z" fill="#287CA9"/>
|
||||
<path d="M26.93 18.4596L25.14 25.5996H23.16L22.33 22.1496C22.31 22.0763 22.2867 21.9696 22.26 21.8296C22.2333 21.6829 22.2 21.5229 22.16 21.3496C22.1267 21.1696 22.0967 20.9996 22.07 20.8396C22.0433 20.6729 22.0233 20.5396 22.01 20.4396C21.9967 20.5396 21.9733 20.6729 21.94 20.8396C21.9133 20.9996 21.8833 21.1696 21.85 21.3496C21.8167 21.5229 21.7833 21.6829 21.75 21.8296C21.7233 21.9696 21.7 22.0763 21.68 22.1496L20.84 25.5996H18.87L17.07 18.4596H18.73L19.58 22.2096C19.6067 22.3096 19.6367 22.4429 19.67 22.6096C19.7033 22.7696 19.7367 22.9429 19.77 23.1296C19.8033 23.3096 19.8333 23.4863 19.86 23.6596C19.8933 23.8263 19.9167 23.9696 19.93 24.0896C19.95 23.9296 19.9767 23.7463 20.01 23.5396C20.05 23.3329 20.09 23.1229 20.13 22.9096C20.17 22.6963 20.21 22.4963 20.25 22.3096C20.29 22.1229 20.3267 21.9729 20.36 21.8596L21.21 18.4596H22.8L23.65 21.8596C23.67 21.9729 23.7 22.1229 23.74 22.3096C23.7867 22.4963 23.83 22.6996 23.87 22.9196C23.9167 23.1329 23.9567 23.3429 23.99 23.5496C24.0233 23.7563 24.05 23.9363 24.07 24.0896C24.09 23.9229 24.12 23.7229 24.16 23.4896C24.2067 23.2496 24.2533 23.0129 24.3 22.7796C24.3467 22.5396 24.3867 22.3529 24.42 22.2196L25.27 18.4596H26.93Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.5H20.5859C20.7185 1.50004 20.8457 1.55275 20.9395 1.64648L26.3535 7.06055C26.4472 7.15428 26.5 7.28151 26.5 7.41406V29C26.5 29.8284 25.8284 30.5 25 30.5H7C6.17157 30.5 5.5 29.8284 5.5 29V3C5.5 2.17157 6.17157 1.5 7 1.5Z" fill="white" stroke="#BBBBBB"/>
|
||||
<path d="M20.5 2V6C20.5 6.82843 21.1716 7.5 22 7.5H26" stroke="#BBBBBB"/>
|
||||
<path d="M16 18C16 16.8954 16.8954 16 18 16H26C27.1046 16 28 16.8954 28 18V26C28 27.1046 27.1046 28 26 28H18C16.8954 28 16 27.1046 16 26V18Z" fill="#E54D39"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.8924 17.5C20.8924 18.8103 20.5181 20.3557 19.8855 21.7815C19.2505 23.2131 18.3933 24.4378 17.5 25.1598L18.4707 26.5C20.8784 24.8781 23.5444 23.7664 26.1241 24.1387L26.5 22.5276C24.302 21.7881 22.5384 19.5689 22.5384 17.5H20.8924ZM21.9659 20.9307C21.8013 21.4516 21.6064 21.9659 21.3859 22.462C21.2669 22.7306 21.1403 22.9954 21.0043 23.2563C21.7701 22.9505 22.5614 22.7104 23.3692 22.5642C22.8309 22.0858 22.3589 21.5364 21.9659 20.9307Z" fill="white"/>
|
||||
<path d="M13 20C13.5523 20 14 20.4477 14 21C14 21.5523 13.5523 22 13 22H10C9.44772 22 9 21.5523 9 21C9 20.4477 9.44772 20 10 20H13ZM13 16C13.5523 16 14 16.4477 14 17C14 17.5523 13.5523 18 13 18H10C9.44772 18 9 17.5523 9 17C9 16.4477 9.44772 16 10 16H13ZM22 12C22.5523 12 23 12.4477 23 13C23 13.5523 22.5523 14 22 14H10C9.44772 14 9 13.5523 9 13C9 12.4477 9.44772 12 10 12H22Z" fill="#F5B8B0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.5H20.5859C20.7185 1.50004 20.8457 1.55275 20.9395 1.64648L26.3535 7.06055C26.4472 7.15428 26.5 7.28151 26.5 7.41406V29C26.5 29.8284 25.8284 30.5 25 30.5H7C6.17157 30.5 5.5 29.8284 5.5 29V3C5.5 2.17157 6.17157 1.5 7 1.5Z" fill="white" stroke="#BBBBBB"/>
|
||||
<path d="M20.5 2V6C20.5 6.82843 21.1716 7.5 22 7.5H26" stroke="#BBBBBB"/>
|
||||
<path d="M16.4993 10.0208C17.5098 10.1052 18.4856 10.4446 19.3333 11.011C20.3199 11.6702 21.0891 12.6072 21.5432 13.7034C21.9973 14.7996 22.1164 16.0064 21.885 17.1702C21.6535 18.3341 21.0816 19.4033 20.2425 20.2425C19.4033 21.0816 18.3341 21.6535 17.1702 21.885C16.0064 22.1164 14.7996 21.9973 13.7034 21.5432C12.6072 21.0891 11.6702 20.3199 11.011 19.3333C10.4446 18.4856 10.1052 17.5098 10.0208 16.4993C9.99788 16.2242 10.2242 16.0003 10.5003 16.0003H15.5003C15.7762 16.0001 16.0001 15.7762 16.0003 15.5003V10.5003C16.0003 10.2242 16.2242 9.99788 16.4993 10.0208ZM13.5013 9.02469C13.7758 8.99742 14.0003 9.22429 14.0003 9.50027V13.5003C14.0001 13.7762 13.7762 14.0001 13.5003 14.0003H9.50027C9.22429 14.0003 8.99742 13.7758 9.02469 13.5013C9.07339 13.0159 9.1928 12.5385 9.38016 12.0862C9.63143 11.4796 9.99989 10.9284 10.4641 10.4641C10.9284 9.99989 11.4796 9.63143 12.0862 9.38016C12.5385 9.1928 13.0159 9.07339 13.5013 9.02469Z" fill="#FAC299"/>
|
||||
<path d="M16 18C16 16.8954 16.8954 16 18 16H26C27.1046 16 28 16.8954 28 18V26C28 27.1046 27.1046 28 26 28H18C16.8954 28 16 27.1046 16 26V18Z" fill="#F36700"/>
|
||||
<path d="M22.0606 18.4596C22.9739 18.4596 23.6473 18.6596 24.0806 19.0596C24.5206 19.4529 24.7406 20.0029 24.7406 20.7096C24.7406 21.0296 24.6939 21.3363 24.6006 21.6296C24.5073 21.9163 24.3506 22.1729 24.1306 22.3996C23.9173 22.6263 23.6339 22.8063 23.2806 22.9396C22.9339 23.0729 22.5039 23.1396 21.9906 23.1396H21.3906V25.5996H19.6906V18.4596H22.0606ZM22.0006 19.8496H21.3906V21.7496H21.8406C22.0739 21.7496 22.2773 21.7163 22.4506 21.6496C22.6306 21.5829 22.7706 21.4763 22.8706 21.3296C22.9706 21.1829 23.0206 20.9929 23.0206 20.7596C23.0206 20.4663 22.9373 20.2429 22.7706 20.0896C22.6039 19.9296 22.3473 19.8496 22.0006 19.8496Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.5H20.5859C20.7185 1.50004 20.8457 1.55275 20.9395 1.64648L26.3535 7.06055C26.4472 7.15428 26.5 7.28151 26.5 7.41406V29C26.5 29.8284 25.8284 30.5 25 30.5H7C6.17157 30.5 5.5 29.8284 5.5 29V3C5.5 2.17157 6.17157 1.5 7 1.5Z" fill="white" stroke="#BBBBBB"/>
|
||||
<path d="M20.5 2V6C20.5 6.82843 21.1716 7.5 22 7.5H26" stroke="#BBBBBB"/>
|
||||
<path d="M16 18C16 16.8954 16.8954 16 18 16H26C27.1046 16 28 16.8954 28 18V26C28 27.1046 27.1046 28 26 28H18C16.8954 28 16 27.1046 16 26V18Z" fill="#3AA133"/>
|
||||
<path d="M25.4432 25.5996H23.4832L21.9532 23.1196L20.4232 25.5996H18.5432L20.9132 21.9296L18.6832 18.4596H20.5732L22.0032 20.8596L23.3832 18.4596H25.2732L23.0332 22.0496L25.4432 25.5996Z" fill="white"/>
|
||||
<path d="M14 20C14.5523 20 15 20.4477 15 21C15 21.5523 14.5523 22 14 22H10C9.44772 22 9 21.5523 9 21C9 20.4477 9.44772 20 10 20H14ZM14 16C14.5523 16 15 16.4477 15 17C15 17.5523 14.5523 18 14 18H10C9.44772 18 9 17.5523 9 17C9 16.4477 9.44772 16 10 16H14ZM14 12C14.5523 12 15 12.4477 15 13C15 13.5523 14.5523 14 14 14H10C9.44772 14 9 13.5523 9 13C9 12.4477 9.44772 12 10 12H14ZM22 12C22.5523 12 23 12.4477 23 13C23 13.5523 22.5523 14 22 14H18C17.4477 14 17 13.5523 17 13C17 12.4477 17.4477 12 18 12H22Z" fill="#B0D9AD"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<template id="onlyoffice_odoo_documents.share_file" inherit_id="documents.share_file">
|
||||
<xpath expr="//div[hasclass('mt16') and hasclass('mb8')]/a" position="inside">
|
||||
<a
|
||||
t-if="onlyoffice_supported"
|
||||
class="btn btn-secondary rounded-pill mt-2"
|
||||
t-att-href="'/onlyoffice/documents/share/' + document.access_token"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
class="mb-1"
|
||||
src="/onlyoffice_odoo_documents/static/svg/edit_black.svg"
|
||||
role="img"
|
||||
/> Open in ONLYOFFICE </a>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="onlyoffice_odoo_documents.public_folder_page" inherit_id="documents.public_folder_page">
|
||||
<!-- For binary files -->
|
||||
<xpath expr="//article[@id='documents-binary']//div[hasclass('o_card_right')]" position="before">
|
||||
<t t-set="current_doc_id" t-value="str(document.id)" />
|
||||
<t t-set="is_supported" t-value="False" />
|
||||
|
||||
<t t-foreach="onlyoffice_supported" t-as="supported_doc">
|
||||
<t t-if="str(supported_doc['document'].id) == current_doc_id and
|
||||
supported_doc['onlyoffice_supported']">
|
||||
<t t-set="is_supported" t-value="True" />
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-if="is_supported">
|
||||
<div class="o_card_right d-flex flex-column flex-nowrap justify-content-end text-end me-1">
|
||||
<a
|
||||
t-att-href="'/onlyoffice/documents/share/' + document.access_token"
|
||||
title="Open in ONLYOFFICE"
|
||||
target="_blank"
|
||||
>
|
||||
<img class="mb-1" src="/onlyoffice_odoo_documents/static/svg/edit_black.svg" role="img" />
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "Project Dashboards Management",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Project",
|
||||
"summary": "Beautiful & interactive project dashboards",
|
||||
"description": """
|
||||
Enterprise-grade project dashboards with:
|
||||
- Notebook embedding
|
||||
- Popup dashboards
|
||||
- Full-page dashboards
|
||||
- Modern UI with charts & KPIs
|
||||
""",
|
||||
"author": "Pranay",
|
||||
"website": "https://ftprotech.in",
|
||||
"license": "LGPL-3",
|
||||
"depends": [
|
||||
"base",
|
||||
"project",
|
||||
"web",
|
||||
"project_task_timesheet_extended",
|
||||
"hr_timesheet",
|
||||
],
|
||||
"data": [
|
||||
"views/project_project_views.xml",
|
||||
"views/project_dashboard_actions.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
'https://cdn.jsdelivr.net/npm/apexcharts@3.35.0/dist/apexcharts.min.js',
|
||||
"project_dashboards_management/static/src/js/dashboard/*.js",
|
||||
"project_dashboards_management/static/src/js/external_lib/apexcharts/*.js",
|
||||
"project_dashboards_management/static/src/xml/**/*.xml",
|
||||
"project_dashboards_management/static/src/css/*.css",
|
||||
],
|
||||
|
||||
},
|
||||
'external_dependencies': {
|
||||
'lib': {
|
||||
'apexcharts': 'https://cdn.jsdelivr.net/npm/apexcharts',
|
||||
'html2canvas': 'https://cdn.jsdelivr.net/npm/html2canvas'
|
||||
},
|
||||
},
|
||||
"installable": True,
|
||||
"application": False,
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import project_dashboard_controller
|
||||
from . import portfolio_dashboard_controller
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class ProjectPortfolioDashboardController(http.Controller):
|
||||
|
||||
@http.route('/project_portfolio/get_dashboard_data', type='json', auth='user')
|
||||
def get_portfolio_dashboard_data(self, portfolio_id, **kwargs):
|
||||
try:
|
||||
Portfolio = request.env['project.portfolio']
|
||||
portfolio = Portfolio.browse(portfolio_id)
|
||||
|
||||
if not portfolio.exists():
|
||||
return {'success': False, 'error': 'Portfolio not found'}
|
||||
|
||||
# Get portfolio data
|
||||
portfolio_data = {
|
||||
'id': portfolio.id,
|
||||
'name': portfolio.name,
|
||||
'code': portfolio.code,
|
||||
'owner_id': portfolio.owner_id.id,
|
||||
'owner_name': portfolio.owner_id.name if portfolio.owner_id else '',
|
||||
'total_estimated_budget': portfolio.total_estimated_budget,
|
||||
'total_client_budget': portfolio.total_client_budget,
|
||||
'total_actual_budget': portfolio.total_actual_budget,
|
||||
'budget_variance': portfolio.budget_variance,
|
||||
'budget_variance_percent': portfolio.budget_variance_percent,
|
||||
'client_budget_variance': portfolio.client_budget_variance,
|
||||
'client_budget_variance_percent': portfolio.client_budget_variance_percent,
|
||||
'planned_client_budget_variance': portfolio.planned_client_budget_variance,
|
||||
'planned_client_budget_variance_percent': portfolio.planned_client_budget_variance_percent,
|
||||
'total_profit': portfolio.total_profit,
|
||||
'total_loss': portfolio.total_loss,
|
||||
'net_profit': portfolio.net_profit,
|
||||
'roi_estimate': portfolio.roi_estimate,
|
||||
'planned_roi_estimate': portfolio.planned_roi_estimate,
|
||||
'actual_roi_estimate': portfolio.actual_roi_estimate,
|
||||
'budget_status': portfolio.budget_status,
|
||||
'avg_time_variance': portfolio.avg_time_variance,
|
||||
'on_time_completion_rate': portfolio.on_time_completion_rate,
|
||||
'overall_efficiency': portfolio.overall_efficiency,
|
||||
'total_resource_cost': portfolio.total_resource_cost,
|
||||
'total_material_cost': portfolio.total_material_cost,
|
||||
'total_equipment_cost': portfolio.total_equipment_cost,
|
||||
}
|
||||
|
||||
# Get projects data
|
||||
projects_data = []
|
||||
for project in portfolio.project_ids.filtered(lambda p: p.active):
|
||||
projects_data.append({
|
||||
'id': project.id,
|
||||
'name': project.name,
|
||||
'partner_id': project.partner_id.id if project.partner_id else False,
|
||||
'partner_name': project.partner_id.name if project.partner_id else '',
|
||||
'stage_id': project.stage_id.id if project.stage_id else False,
|
||||
'stage_name': project.stage_id.name if project.stage_id else '',
|
||||
'estimated_amount': project.estimated_amount,
|
||||
'project_cost': project.project_cost,
|
||||
'actual_cost': project.actual_cost,
|
||||
'profit': project.profit,
|
||||
'loss': project.loss,
|
||||
'difference': project.difference,
|
||||
'profit_percentage': project.profit_percentage,
|
||||
'loss_percentage': project.loss_percentage,
|
||||
'budget_variance': project.estimated_amount - project.actual_cost if project.estimated_amount and project.actual_cost else 0,
|
||||
'budget_variance_percent': (
|
||||
(project.estimated_amount - project.actual_cost) / project.estimated_amount * 100)
|
||||
if project.estimated_amount and project.actual_cost else 0,
|
||||
'active': project.active,
|
||||
})
|
||||
|
||||
# Get budget data
|
||||
budget_data = {
|
||||
'total_estimated_budget': portfolio.total_estimated_budget,
|
||||
'total_client_budget': portfolio.total_client_budget,
|
||||
'total_actual_budget': portfolio.total_actual_budget,
|
||||
'budget_variance': portfolio.budget_variance,
|
||||
'budget_variance_percent': portfolio.budget_variance_percent,
|
||||
'client_budget_variance': portfolio.client_budget_variance,
|
||||
'client_budget_variance_percent': portfolio.client_budget_variance_percent,
|
||||
}
|
||||
|
||||
# Get performance data
|
||||
performance_data = {
|
||||
'avg_time_variance': portfolio.avg_time_variance,
|
||||
'on_time_rate': portfolio.on_time_completion_rate,
|
||||
'efficiency': portfolio.overall_efficiency,
|
||||
}
|
||||
|
||||
# Get cost breakdown
|
||||
cost_breakdown = {
|
||||
'manpower': portfolio.total_resource_cost,
|
||||
'materials': portfolio.total_material_cost,
|
||||
'equipment': portfolio.total_equipment_cost,
|
||||
'external': sum(portfolio.project_ids.mapped('total_external_costs')),
|
||||
'other': 0,
|
||||
}
|
||||
|
||||
# Get employee performance
|
||||
employee_performance = []
|
||||
for perf in portfolio.employee_performance_ids:
|
||||
employee_performance.append({
|
||||
'id': perf.id,
|
||||
'employee_id': perf.employee_id.id,
|
||||
'employee_name': perf.employee_id.name,
|
||||
'department_id': perf.department_id.id if perf.department_id else False,
|
||||
'department_name': perf.department_id.name if perf.department_id else '',
|
||||
'job_id': perf.job_id.id if perf.job_id else False,
|
||||
'job_title': perf.job_id.name if perf.job_id else '',
|
||||
'total_estimated_hours': perf.total_estimated_hours,
|
||||
'total_actual_hours': perf.total_actual_hours,
|
||||
'time_variance': perf.time_variance,
|
||||
'time_variance_percent': perf.time_variance_percent,
|
||||
'on_time_completion_rate': perf.on_time_completion_rate,
|
||||
'tasks_completed': perf.tasks_completed,
|
||||
'efficiency_rate': perf.efficiency_rate,
|
||||
'performance_status': perf.performance_status,
|
||||
})
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'portfolio_data': portfolio_data,
|
||||
'projects_data': projects_data,
|
||||
'budget_data': budget_data,
|
||||
'performance_data': performance_data,
|
||||
'cost_breakdown': cost_breakdown,
|
||||
'employee_performance': employee_performance,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
from odoo import http, fields
|
||||
from odoo.http import request
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectDashboardController(http.Controller):
|
||||
|
||||
@http.route('/project_dashboard/get_dashboard_data', type='json', auth='user')
|
||||
def get_dashboard_data(self, project_id, **kwargs):
|
||||
"""Return all dashboard data in a single API call"""
|
||||
try:
|
||||
# Get the project
|
||||
Project = request.env['project.project'].sudo()
|
||||
project = Project.browse(project_id)
|
||||
|
||||
if not project.exists():
|
||||
return {'error': 'Project not found'}
|
||||
|
||||
# 1. Get project data
|
||||
project_data = self._get_project_data(project)
|
||||
print(project_data)
|
||||
|
||||
# 2. Get tasks data
|
||||
tasks_data = self._get_tasks_data(project)
|
||||
print(tasks_data)
|
||||
|
||||
# 3. Get employee performance
|
||||
employee_performance = self._get_employee_performance(project)
|
||||
print(employee_performance)
|
||||
|
||||
# 4. Get budget data
|
||||
budget_data = self._get_budget_data(project)
|
||||
print(budget_data)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'project_data': project_data,
|
||||
'tasks_data': tasks_data,
|
||||
'employee_performance': employee_performance,
|
||||
'budget_data': budget_data,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("Error fetching dashboard data: %s", str(e))
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _get_project_data(self, project):
|
||||
"""Extract project data"""
|
||||
return {
|
||||
'id': project.id,
|
||||
'name': project.name,
|
||||
'sequence_name': project.sequence_name,
|
||||
'stage_id': project.stage_id.id if project.stage_id else False,
|
||||
'stage_name': project.stage_id.name if project.stage_id else '',
|
||||
'user_id': project.user_id.id if project.user_id else False,
|
||||
'user_name': project.user_id.name if project.user_id else '',
|
||||
'project_lead': project.project_lead.id if project.project_lead else False,
|
||||
'project_lead_name': project.project_lead.name if project.project_lead else '',
|
||||
'members': [{
|
||||
'id': member.id,
|
||||
'name': member.name
|
||||
} for member in project.members_ids],
|
||||
'estimated_hours': project.estimated_hours if project.estimated_hours > 0 else project.actual_hours,
|
||||
'task_estimated_hours': project.task_estimated_hours,
|
||||
'actual_hours': project.actual_hours,
|
||||
'project_state': project.project_state,
|
||||
'date_start': str(project.date_start) if project.date_start else '',
|
||||
'date': str(project.date) if project.date else '',
|
||||
'total_planned_budget_amount': project.total_planned_budget_amount,
|
||||
'initial_estimated_resource_cost': project.initial_estimated_resource_cost,
|
||||
'initial_estimated_material_cost': project.initial_estimated_material_cost,
|
||||
'initial_estimated_equipment_cost': project.initial_estimated_equiipment_cost,
|
||||
'all_deliverables_submitted': project.all_deliverables_submitted,
|
||||
'final_qa_done': project.final_qa_done,
|
||||
'client_signoff_closure': project.client_signoff_closure,
|
||||
'billing_completed': project.billing_completed,
|
||||
'training_completed': project.training_completed,
|
||||
'lessons_learned': project.lessons_learned or '',
|
||||
'challenges_faced': project.challenges_faced or '',
|
||||
'future_recommendations': project.future_recommendations or '',
|
||||
'showable_stage_ids': [stage.id for stage in project.showable_stage_ids]
|
||||
}
|
||||
|
||||
def _get_tasks_data(self, project):
|
||||
"""Get all tasks for the project with stats"""
|
||||
tasks = request.env['project.task'].sudo().search([
|
||||
('project_id', '=', project.id)
|
||||
], limit=100)
|
||||
|
||||
task_list = []
|
||||
for task in tasks:
|
||||
completion_rate = 0
|
||||
if task.estimated_hours > 0:
|
||||
completion_rate = min((task.actual_hours / task.estimated_hours if task.estimated_hours > 0 else task.actual_hours) * 100, 100)
|
||||
|
||||
is_overdue = False
|
||||
if task.date_deadline:
|
||||
is_overdue = task.date_deadline < fields.Datetime.now()
|
||||
|
||||
task_list.append({
|
||||
'id': task.id,
|
||||
'name': task.name,
|
||||
'sequence_name': task.sequence_name,
|
||||
'stage_id': task.stage_id.id if task.stage_id else False,
|
||||
'stage_name': task.stage_id.name if task.stage_id else '',
|
||||
'user_ids': [{
|
||||
'id': user.id,
|
||||
'name': user.name
|
||||
} for user in task.user_ids],
|
||||
'priority': task.priority,
|
||||
'estimated_hours': task.estimated_hours if task.estimated_hours > 0 else task.actual_hours,
|
||||
'actual_hours': task.actual_hours,
|
||||
'date_deadline': str(task.date_deadline) if task.date_deadline else '',
|
||||
'planned_date_begin': str(task.planned_date_begin) if task.planned_date_begin else '',
|
||||
'suggested_deadline': str(task.suggested_deadline) if task.suggested_deadline else '',
|
||||
'is_suggested_deadline_warning': task.is_suggested_deadline_warning,
|
||||
'state': task.state,
|
||||
'show_approval_flow': task.show_approval_flow,
|
||||
'timelines_requested': task.timelines_requested,
|
||||
'assignees_timelines': task.assignees_timelines or '',
|
||||
'approval_status': task.approval_status,
|
||||
'completion_rate': completion_rate,
|
||||
'is_overdue': is_overdue,
|
||||
'has_warning': task.is_suggested_deadline_warning
|
||||
})
|
||||
|
||||
return task_list
|
||||
|
||||
def _get_employee_performance(self, project):
|
||||
"""Calculate employee performance metrics for a single project"""
|
||||
|
||||
if project.privacy_visibility != 'followers':
|
||||
return []
|
||||
|
||||
Task = request.env['project.task'].sudo()
|
||||
AAL = request.env['account.analytic.line'].sudo()
|
||||
|
||||
# Get all tasks for the project
|
||||
tasks = Task.search([
|
||||
('project_id', '=', project.id),
|
||||
])
|
||||
|
||||
if not tasks:
|
||||
return []
|
||||
|
||||
# Prefetch timesheets grouped by (employee_id, task_id)
|
||||
timesheets = AAL.search([
|
||||
('task_id', 'in', tasks.ids),
|
||||
('employee_id', '!=', False),
|
||||
])
|
||||
|
||||
# Create a map for actual hours: {(employee_id, task_id): actual_hours}
|
||||
actual_map = {}
|
||||
for line in timesheets:
|
||||
key = (line.employee_id.id, line.task_id.id)
|
||||
actual_map.setdefault(key, 0.0)
|
||||
actual_map[key] += line.unit_amount
|
||||
|
||||
employee_map = {}
|
||||
|
||||
for task in tasks:
|
||||
# Skip tasks where no employee has actually worked (actual hours = 0 for all)
|
||||
task_has_work = False
|
||||
for user in task.user_ids:
|
||||
if not user.employee_id:
|
||||
continue
|
||||
emp_id = user.employee_id.id
|
||||
if actual_map.get((emp_id, task.id), 0.0) > 0:
|
||||
task_has_work = True
|
||||
break
|
||||
|
||||
# If no one worked on this task, skip it entirely
|
||||
if not task_has_work:
|
||||
continue
|
||||
|
||||
# Store which employees we've already processed for this task
|
||||
processed_employees = set()
|
||||
|
||||
# Get actual hours for each employee on this task
|
||||
employee_actual_hours = {}
|
||||
for user in task.user_ids:
|
||||
if not user.employee_id:
|
||||
continue
|
||||
emp_id = user.employee_id.id
|
||||
actual_hours = actual_map.get((emp_id, task.id), 0.0)
|
||||
# Only include employees who actually worked on this task
|
||||
if actual_hours > 0:
|
||||
employee_actual_hours[emp_id] = actual_hours
|
||||
|
||||
# If no employee has actual hours for this task, skip
|
||||
if not employee_actual_hours:
|
||||
continue
|
||||
|
||||
# Case 1: Task has approval flow and is not generic
|
||||
if task.show_approval_flow and not task.is_generic:
|
||||
# First, check assignees_timelines
|
||||
timeline_employees = set()
|
||||
if task.assignees_timelines:
|
||||
for timeline in task.assignees_timelines:
|
||||
employee = timeline.assigned_to.employee_id
|
||||
if not employee:
|
||||
continue
|
||||
|
||||
emp_id = employee.id
|
||||
# Skip if employee didn't work on this task
|
||||
if emp_id not in employee_actual_hours:
|
||||
continue
|
||||
|
||||
emp_name = employee.name
|
||||
timeline_employees.add(emp_id)
|
||||
|
||||
emp_vals = employee_map.setdefault(emp_id, {
|
||||
'employee_id': emp_id,
|
||||
'employee_name': emp_name,
|
||||
'total_estimated': 0.0,
|
||||
'total_actual': 0.0,
|
||||
'tasks_count': 0,
|
||||
'on_time_tasks': 0,
|
||||
})
|
||||
|
||||
# Get estimated hours from timeline
|
||||
estimated_hours = timeline.estimated_time or 0.0
|
||||
actual_hours = employee_actual_hours[emp_id]
|
||||
|
||||
emp_vals['total_estimated'] += estimated_hours
|
||||
emp_vals['total_actual'] += actual_hours
|
||||
emp_vals['tasks_count'] += 1
|
||||
|
||||
# On-time logic - only if estimated > 0
|
||||
if estimated_hours > 0 and actual_hours <= estimated_hours:
|
||||
emp_vals['on_time_tasks'] += 1
|
||||
# If estimated = 0, don't count in on-time rate calculation
|
||||
|
||||
# Then check task.user_ids for any employees not in timelines
|
||||
for user in task.user_ids:
|
||||
if not user.employee_id:
|
||||
continue
|
||||
|
||||
emp_id = user.employee_id.id
|
||||
|
||||
# Skip if employee didn't work on this task or already processed
|
||||
if emp_id not in employee_actual_hours or emp_id in timeline_employees:
|
||||
continue
|
||||
|
||||
emp_name = user.employee_id.name
|
||||
|
||||
emp_vals = employee_map.setdefault(emp_id, {
|
||||
'employee_id': emp_id,
|
||||
'employee_name': emp_name,
|
||||
'total_estimated': 0.0,
|
||||
'total_actual': 0.0,
|
||||
'tasks_count': 0,
|
||||
'on_time_tasks': 0,
|
||||
})
|
||||
|
||||
# Get actual hours
|
||||
actual_hours = employee_actual_hours[emp_id]
|
||||
|
||||
# Get estimated hours - from task, use actual if 0
|
||||
estimated_hours = task.estimated_hours or 0.0
|
||||
if estimated_hours == 0:
|
||||
estimated_hours = actual_hours
|
||||
|
||||
emp_vals['total_estimated'] += estimated_hours
|
||||
emp_vals['total_actual'] += actual_hours
|
||||
emp_vals['tasks_count'] += 1
|
||||
|
||||
# On-time logic
|
||||
if estimated_hours > 0 and actual_hours <= estimated_hours:
|
||||
emp_vals['on_time_tasks'] += 1
|
||||
|
||||
else:
|
||||
# Case 2: Generic task or no approval flow
|
||||
if task.is_generic:
|
||||
# For generic tasks, estimated = actual (but only for employees who worked)
|
||||
for user in task.user_ids:
|
||||
if not user.employee_id:
|
||||
continue
|
||||
|
||||
emp_id = user.employee_id.id
|
||||
|
||||
# Skip if employee didn't work on this task
|
||||
if emp_id not in employee_actual_hours:
|
||||
continue
|
||||
|
||||
emp_name = user.employee_id.name
|
||||
|
||||
emp_vals = employee_map.setdefault(emp_id, {
|
||||
'employee_id': emp_id,
|
||||
'employee_name': emp_name,
|
||||
'total_estimated': 0.0,
|
||||
'total_actual': 0.0,
|
||||
'tasks_count': 0,
|
||||
'on_time_tasks': 0,
|
||||
})
|
||||
|
||||
# Get actual hours
|
||||
actual_hours = employee_actual_hours[emp_id]
|
||||
|
||||
# For generic tasks: estimated = actual
|
||||
estimated_hours = actual_hours
|
||||
|
||||
emp_vals['total_estimated'] += estimated_hours
|
||||
emp_vals['total_actual'] += actual_hours
|
||||
emp_vals['tasks_count'] += 1
|
||||
|
||||
# For generic tasks with actual > 0, always count as on-time
|
||||
if actual_hours > 0:
|
||||
emp_vals['on_time_tasks'] += 1
|
||||
else:
|
||||
# Non-generic task without approval flow
|
||||
for user in task.user_ids:
|
||||
if not user.employee_id:
|
||||
continue
|
||||
|
||||
emp_id = user.employee_id.id
|
||||
|
||||
# Skip if employee didn't work on this task
|
||||
if emp_id not in employee_actual_hours:
|
||||
continue
|
||||
|
||||
emp_name = user.employee_id.name
|
||||
|
||||
emp_vals = employee_map.setdefault(emp_id, {
|
||||
'employee_id': emp_id,
|
||||
'employee_name': emp_name,
|
||||
'total_estimated': 0.0,
|
||||
'total_actual': 0.0,
|
||||
'tasks_count': 0,
|
||||
'on_time_tasks': 0,
|
||||
})
|
||||
|
||||
# Get actual hours
|
||||
actual_hours = employee_actual_hours[emp_id]
|
||||
|
||||
# Get estimated hours - from task, use actual if 0 or not set
|
||||
estimated_hours = task.estimated_hours or 0.0
|
||||
if estimated_hours == 0:
|
||||
estimated_hours = actual_hours
|
||||
|
||||
emp_vals['total_estimated'] += estimated_hours
|
||||
emp_vals['total_actual'] += actual_hours
|
||||
emp_vals['tasks_count'] += 1
|
||||
|
||||
# On-time logic
|
||||
if estimated_hours > 0 and actual_hours <= estimated_hours:
|
||||
emp_vals['on_time_tasks'] += 1
|
||||
|
||||
# Calculate metrics
|
||||
performance_data = []
|
||||
for emp_id, emp in employee_map.items():
|
||||
total_est = emp['total_estimated']
|
||||
total_act = emp['total_actual']
|
||||
tasks_count = emp['tasks_count']
|
||||
|
||||
# Skip employees with no actual hours
|
||||
if total_act <= 0 or tasks_count <= 0:
|
||||
continue
|
||||
|
||||
# Time variance - negative means over budget (bad), positive means under budget (good)
|
||||
if total_est > 0:
|
||||
time_variance = ((total_act - total_est) / total_est) * 100
|
||||
# For time variance: positive = over budget (bad), negative = under budget (good)
|
||||
# But we want: positive = good, negative = bad
|
||||
# So we reverse the sign
|
||||
time_variance = -time_variance
|
||||
else:
|
||||
time_variance = 0.0
|
||||
|
||||
# On-time rate
|
||||
on_time_rate = (emp['on_time_tasks'] / tasks_count) * 100 if tasks_count > 0 else 0.0
|
||||
|
||||
# Efficiency
|
||||
efficiency = (total_est / total_act) * 100 if total_act > 0 else 0.0
|
||||
|
||||
# Accuracy - based on absolute deviation from estimate
|
||||
if total_est > 0:
|
||||
accuracy = max(0.0, 100.0 - abs(((total_act - total_est) / total_est) * 100))
|
||||
else:
|
||||
accuracy = 100.0 if total_act == 0 else 0.0
|
||||
|
||||
performance_data.append({
|
||||
'employee_id': emp['employee_id'],
|
||||
'employee_name': emp['employee_name'],
|
||||
'total_estimated': round(total_est, 2),
|
||||
'total_actual': round(total_act, 2),
|
||||
'tasks_count': tasks_count,
|
||||
'on_time_tasks': emp['on_time_tasks'],
|
||||
'time_variance': -(round(time_variance, 2)),
|
||||
'on_time_rate': round(on_time_rate, 2),
|
||||
'efficiency_rate': round(efficiency, 2),
|
||||
'accuracy': round(accuracy, 2),
|
||||
})
|
||||
|
||||
# Sort by efficiency rate (descending)
|
||||
return sorted(performance_data, key=lambda x: x['efficiency_rate'], reverse=True)
|
||||
|
||||
def _get_budget_data(self, project):
|
||||
"""Get budget data with breakdown"""
|
||||
# Get detailed cost breakdowns
|
||||
resource_details = []
|
||||
asset_details = []
|
||||
|
||||
if project.resource_actual_cost_ids:
|
||||
for cost in project.resource_actual_cost_ids:
|
||||
resource_details.append({
|
||||
'employee_id': cost.employee_id.id if cost.employee_id else False,
|
||||
'employee_name': cost.employee_id.name if cost.employee_id else '',
|
||||
'total_cost': cost.total_cost,
|
||||
'total_hours': cost.total_hours
|
||||
})
|
||||
|
||||
if project.external_cost_ids:
|
||||
for cost in project.external_cost_ids:
|
||||
asset_details.append({
|
||||
'asset_id': cost.id if cost else False,
|
||||
'asset_name': cost.name if cost.name else '',
|
||||
'total_cost': cost.total_cost,
|
||||
'quantity': cost.quantity,
|
||||
'state': cost.state
|
||||
})
|
||||
|
||||
|
||||
# Calculate utilization percentages
|
||||
total_budget = project.estimated_amount or 0
|
||||
used_budget = project.actual_cost or 0
|
||||
available_budget = total_budget - used_budget
|
||||
resource_cost = project.total_resource_actual_costs or 0
|
||||
asset_cost = project.total_external_costs or 0
|
||||
planned_resource_cost = project.initial_estimated_resource_cost or 0
|
||||
planned_asset_cost = project.estimated_external_cost or 0
|
||||
|
||||
return {
|
||||
'total_budget': total_budget,
|
||||
'resource_cost': resource_cost,
|
||||
'asset_cost': asset_cost,
|
||||
'resource_percentage': (resource_cost / total_budget) * 100 if total_budget > 0 else 0,
|
||||
'asset_percentage': (asset_cost / total_budget) * 100 if total_budget > 0 else 0,
|
||||
'resource_details': resource_details,
|
||||
'asset_details': asset_details,
|
||||
'used_budget': used_budget,
|
||||
'available_budget': available_budget,
|
||||
'planned_resource_cost': planned_resource_cost,
|
||||
'planned_asset_cost': planned_asset_cost
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import project
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ProjectProject(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
def open_dashboard(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Project Dashboard',
|
||||
'res_model': 'project.project',
|
||||
'view_mode': 'form',
|
||||
'view_type': 'form',
|
||||
'res_id': self.id,
|
||||
'target': 'new',
|
||||
'views': [(False, 'dashboard')],
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_dashboard_data(self, project_id):
|
||||
# Return data for charts and KPIs
|
||||
return {
|
||||
'task_count': 42,
|
||||
'completed_tasks': 35,
|
||||
'timeline_data': [...],
|
||||
'resource_data': [...],
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class ProjectPortfolio(models.Model):
|
||||
_inherit = 'project.portfolio'
|
||||
|
||||
def get_portfolio_projects(self):
|
||||
"""Return portfolio projects data for dashboard"""
|
||||
self.ensure_one()
|
||||
projects = []
|
||||
for project in self.project_ids.filtered(lambda p: p.active):
|
||||
projects.append({
|
||||
'id': project.id,
|
||||
'name': project.name,
|
||||
'estimated_amount': project.estimated_amount,
|
||||
'project_cost': project.project_cost,
|
||||
'actual_cost': project.actual_cost,
|
||||
'profit': project.profit,
|
||||
'loss': project.loss,
|
||||
'budget_variance_percent': (
|
||||
(project.estimated_amount - project.actual_cost) / project.estimated_amount * 100)
|
||||
if project.estimated_amount else 0,
|
||||
'active': project.active,
|
||||
})
|
||||
return projects
|
||||
|
||||
def get_employee_performance(self):
|
||||
"""Return employee performance data for dashboard"""
|
||||
self.ensure_one()
|
||||
performance = []
|
||||
for perf in self.employee_performance_ids:
|
||||
performance.append({
|
||||
'id': perf.id,
|
||||
'employee_id': perf.employee_id.id,
|
||||
'employee_name': perf.employee_id.name,
|
||||
'department_id': perf.department_id.id if perf.department_id else False,
|
||||
'department_name': perf.department_id.name if perf.department_id else '',
|
||||
'total_estimated_hours': perf.total_estimated_hours,
|
||||
'total_actual_hours': perf.total_actual_hours,
|
||||
'time_variance_percent': perf.time_variance_percent,
|
||||
'on_time_completion_rate': perf.on_time_completion_rate,
|
||||
'efficiency_rate': perf.efficiency_rate,
|
||||
'performance_status': perf.performance_status,
|
||||
})
|
||||
return performance
|
||||
|
|
@ -0,0 +1,926 @@
|
|||
/* Portfolio Dashboard Styles */
|
||||
.portfolio-dashboard-widget {
|
||||
padding: 20px;
|
||||
min-height: 900px;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e3e8f0 100%);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Portfolio Header */
|
||||
.portfolio-header {
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
|
||||
color: white;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.portfolio-title h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
color: white;
|
||||
letter-spacing: -0.3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.portfolio-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
margin-right: 6px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.view-switcher {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-view:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-view.active {
|
||||
background: white;
|
||||
color: #1e40af;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-control {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-control:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn-control:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.2px;
|
||||
gap: 6px;
|
||||
min-height: 36px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-action.btn-info {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
}
|
||||
|
||||
.btn-action.btn-success {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
filter: brightness(105%);
|
||||
}
|
||||
|
||||
.btn-action.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8rem;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* KPI Dashboard */
|
||||
.kpi-dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.kpi-section {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kpi-section-header {
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-bottom: 1px solid rgba(203, 213, 224, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-section-header {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
border-bottom-color: rgba(71, 85, 105, 0.4);
|
||||
}
|
||||
|
||||
.kpi-section-header:hover {
|
||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||
}
|
||||
|
||||
.dark-mode .kpi-section-header:hover {
|
||||
background: linear-gradient(135deg, #334155 0%, #475569 100%);
|
||||
}
|
||||
|
||||
.kpi-section-header h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-section-header h4 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.kpi-section-header i {
|
||||
color: #64748b;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-section-header i {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.kpi-section-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* KPI Cards */
|
||||
.kpi-card {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(203, 213, 224, 0.3);
|
||||
}
|
||||
|
||||
.dark-mode .kpi-card {
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border-color: rgba(71, 85, 105, 0.3);
|
||||
}
|
||||
|
||||
.kpi-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark-mode .kpi-card:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.kpi-card.positive {
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.kpi-card.warning {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.kpi-card.negative {
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.kpi-card.budget-status.status-success {
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.kpi-card.budget-status.status-warning {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.kpi-card.budget-status.status-danger {
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.kpi-card.positive .kpi-icon {
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.kpi-card.warning .kpi-icon {
|
||||
background: linear-gradient(135deg, #f59e0b, #fbbf24);
|
||||
}
|
||||
|
||||
.kpi-card.negative .kpi-icon {
|
||||
background: linear-gradient(135deg, #ef4444, #f87171);
|
||||
}
|
||||
|
||||
.kpi-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-value {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.kpi-subtext {
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-subtext {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Charts Section */
|
||||
.charts-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.charts-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.charts-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row .large {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.charts-row .large {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-header h5 {
|
||||
margin: 0 0 6px 0;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dark-mode .chart-header h5 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dark-mode .chart-subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Performance Table */
|
||||
.performance-table {
|
||||
margin-top: 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(203, 213, 224, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dark-mode .table-header {
|
||||
border-bottom-color: rgba(71, 85, 105, 0.4);
|
||||
}
|
||||
|
||||
.table-header h5 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dark-mode .table-header h5 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
border-radius: 0 0 12px 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.badge.status-excellent {
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-good {
|
||||
background: linear-gradient(135deg, #f59e0b, #fbbf24);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-average {
|
||||
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-poor {
|
||||
background: linear-gradient(135deg, #ef4444, #f87171);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-critical {
|
||||
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Loading Overlay */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.dark-mode .loading-overlay {
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.3px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.dark-mode .loading-text {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* No Data Message */
|
||||
.no-data-message {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.no-data-message h4 {
|
||||
color: #1e293b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dark-mode .no-data-message h4 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Alert */
|
||||
.alert {
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.dark-mode .alert-danger {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
/* Progress Bars */
|
||||
.progress {
|
||||
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 20px;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark-mode .progress {
|
||||
background: linear-gradient(135deg, #334155, #475569);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
transition: width 0.6s ease;
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.progress-bar.bg-warning {
|
||||
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
||||
}
|
||||
|
||||
.progress-bar.bg-danger {
|
||||
background: linear-gradient(90deg, #ef4444, #f87171);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1200px) {
|
||||
.portfolio-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.dashboard-controls {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-dashboard-widget {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.portfolio-header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.portfolio-title h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.view-switcher {
|
||||
overflow-x: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kpi-section-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.kpi-section-header h4 {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for expanding sections */
|
||||
.kpi-section-content {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Portfolio Dashboard Styles - Fixed Version */
|
||||
.portfolio-dashboard-widget {
|
||||
padding: 20px;
|
||||
min-height: 900px;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e3e8f0 100%);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Fix for chart containers */
|
||||
.chart-content {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: rgba(241, 245, 249, 0.3);
|
||||
}
|
||||
|
||||
.chart-content .no-data-chart {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Ensure chart elements exist before rendering */
|
||||
#budgetComparisonChart,
|
||||
#roiComparisonChart,
|
||||
#costBreakdownChart,
|
||||
#performanceGauge,
|
||||
#projectHealthChart,
|
||||
#resourceUtilizationChart,
|
||||
#budgetVarianceChart,
|
||||
#profitLossTrendChart,
|
||||
#employeePerformanceChart,
|
||||
#timeVarianceChart,
|
||||
#projectsBudgetChart {
|
||||
min-height: 250px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Loading state for charts */
|
||||
.chart-content.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-content.loading::after {
|
||||
content: '';
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* KPI sections fixes */
|
||||
.kpi-section-header {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.kpi-section-header:hover {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.kpi-section-content {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 500px;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Button fixes */
|
||||
.btn-view {
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-view.active {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-view.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 3px;
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Dark mode fixes */
|
||||
.dark-mode .portfolio-dashboard-widget {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
}
|
||||
|
||||
.dark-mode .chart-content {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
}
|
||||
|
||||
.dark-mode .chart-content .no-data-chart {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Responsive fixes */
|
||||
@media (max-width: 768px) {
|
||||
.chart-content {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.kpi-section-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Error state styling */
|
||||
.alert-danger {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Fix for ApexCharts tooltips in dark mode */
|
||||
.dark-mode .apexcharts-tooltip {
|
||||
background: #1e293b !important;
|
||||
border-color: #334155 !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.dark-mode .apexcharts-tooltip-title {
|
||||
background: #334155 !important;
|
||||
border-color: #475569 !important;
|
||||
color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
/* Ensure proper z-index for controls */
|
||||
.dashboard-controls {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Fix for view switcher active state */
|
||||
.view-switcher .btn-view.active {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Performance table responsive fix */
|
||||
.performance-table .table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Progress bar fixes */
|
||||
.progress {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
/* Badge styles for performance status */
|
||||
.badge.status-excellent {
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-good {
|
||||
background: linear-gradient(135deg, #f59e0b, #fbbf24);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-average {
|
||||
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-poor {
|
||||
background: linear-gradient(135deg, #ef4444, #f87171);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.status-critical {
|
||||
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
||||
color: white;
|
||||
}
|
||||
|
|
@ -0,0 +1,936 @@
|
|||
/* Project Dashboard Styles - Refined MNC Edition */
|
||||
.project-dashboard-widget {
|
||||
padding: 20px;
|
||||
min-height: 850px;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e3e8f0 100%);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Refined Glassmorphism */
|
||||
.glassmorphism {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(31, 38, 135, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glassmorphism:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow:
|
||||
0 16px 48px rgba(31, 38, 135, 0.12),
|
||||
0 4px 16px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Improved Project Header (similar to old but better) */
|
||||
.project-info-banner.dashboard-header {
|
||||
padding: 22px 26px;
|
||||
margin-bottom: 26px;
|
||||
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.project-info-banner.dashboard-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dashboard-header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 6px 0;
|
||||
color: white;
|
||||
letter-spacing: -0.3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-name i {
|
||||
margin-right: 10px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-weight: 400;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.dashboard-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Fixed Control Buttons with visible icons */
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-control {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-control:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn-control i {
|
||||
font-size: 1.1rem;
|
||||
color: white !important;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.btn-control:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-control:disabled i {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.btn-action {
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.2px;
|
||||
gap: 6px;
|
||||
min-height: 40px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-action.btn-success {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
.btn-action.btn-info {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
filter: brightness(105%);
|
||||
}
|
||||
|
||||
/* Refined KPI Grid */
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 18px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
padding: 22px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kpi-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
||||
border-radius: 14px 14px 0 0;
|
||||
}
|
||||
|
||||
.kpi-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.kpi-trend {
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.kpi-trend.positive {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #10b981;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.kpi-trend.warning {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
color: #f59e0b;
|
||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||
}
|
||||
|
||||
.kpi-trend.negative {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.kpi-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
line-height: 1;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.kpi-subtext {
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Refined Charts Grid */
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 22px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.charts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 20px;
|
||||
min-height: 340px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(203, 213, 224, 0.25);
|
||||
}
|
||||
|
||||
.chart-header h5 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.chart-header i {
|
||||
margin-right: 10px;
|
||||
color: #3b82f6;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.chart-info {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
background: rgba(241, 245, 249, 0.7);
|
||||
padding: 6px 12px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
flex: 1;
|
||||
min-height: 280px;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-content.large {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
/* Budget Mini Grid */
|
||||
.budget-mini-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.budget-mini-grid > div {
|
||||
min-height: 100px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: rgba(241, 245, 249, 0.5);
|
||||
}
|
||||
|
||||
/* Refined Performance Table */
|
||||
.performance-table {
|
||||
padding: 22px;
|
||||
margin-top: 26px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(203, 213, 224, 0.25);
|
||||
}
|
||||
|
||||
.table-header h5 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-header i {
|
||||
margin-right: 10px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.table-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin-top: 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(203, 213, 224, 0.25);
|
||||
}
|
||||
|
||||
.table {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
border-top: none;
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
padding: 14px 12px;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid rgba(203, 213, 224, 0.4);
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
border-color: rgba(203, 213, 224, 0.15);
|
||||
padding: 14px 12px;
|
||||
font-weight: 400;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(59, 130, 246, 0.06);
|
||||
}
|
||||
|
||||
/* Enhanced Progress Bars */
|
||||
.progress {
|
||||
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 20px;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
transition: width 0.6s ease;
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.progress-bar.bg-warning {
|
||||
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
||||
}
|
||||
|
||||
.progress-bar.bg-danger {
|
||||
background: linear-gradient(90deg, #ef4444, #f87171);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Loading Overlay */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin: 0 auto 24px;
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner div {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
animation: bounce 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.spinner .bounce1 {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.spinner .bounce2 {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.3;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Floating Action Button */
|
||||
.fab {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
font-size: 1.2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.fab:hover {
|
||||
transform: scale(1.1) rotate(90deg);
|
||||
box-shadow: 0 10px 30px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
/* ===== ENHANCED DARK MODE ===== */
|
||||
.dark-mode .project-dashboard-widget {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
}
|
||||
|
||||
.dark-mode .glassmorphism {
|
||||
background: rgba(15, 23, 42, 0.82);
|
||||
backdrop-filter: blur(30px) saturate(160%);
|
||||
border: 1px solid rgba(71, 85, 105, 0.35);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark-mode .glassmorphism:hover {
|
||||
border-color: rgba(100, 116, 139, 0.5);
|
||||
box-shadow:
|
||||
0 16px 48px rgba(0, 0, 0, 0.35),
|
||||
0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.dark-mode .project-info-banner.dashboard-header {
|
||||
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.dark-mode .project-name {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark-mode .project-name i {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.dark-mode .dashboard-title {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.dark-mode .project-meta {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.dark-mode .btn-control {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dark-mode .btn-control:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.dark-mode .btn-control i {
|
||||
color: white !important;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* Dark Mode KPI Cards */
|
||||
.dark-mode .kpi-card::before {
|
||||
background: linear-gradient(90deg, #60a5fa, #a78bfa);
|
||||
}
|
||||
|
||||
.dark-mode .kpi-value {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-subtext {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark-mode .kpi-icon {
|
||||
background: linear-gradient(135deg, #60a5fa, #a78bfa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Dark Mode Charts with Fixed Legend Contrast */
|
||||
.dark-mode .chart-header h5 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.dark-mode .chart-header i {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark-mode .chart-info {
|
||||
color: #cbd5e1;
|
||||
background: rgba(51, 65, 85, 0.5);
|
||||
border: 1px solid rgba(71, 85, 105, 0.3);
|
||||
}
|
||||
|
||||
.dark-mode .chart-header {
|
||||
border-bottom-color: rgba(71, 85, 105, 0.4);
|
||||
}
|
||||
|
||||
/* FIX for ApexCharts in Dark Mode - High Contrast Legends */
|
||||
.dark-mode .apexcharts-legend-text {
|
||||
color: #e2e8f0 !important;
|
||||
font-weight: 500 !important;
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.dark-mode .apexcharts-tooltip {
|
||||
background: #1e293b !important;
|
||||
border: 1px solid rgba(71, 85, 105, 0.6) !important;
|
||||
color: #e2e8f0 !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
.dark-mode .apexcharts-tooltip-title {
|
||||
background: #334155 !important;
|
||||
border-bottom: 1px solid rgba(71, 85, 105, 0.6) !important;
|
||||
color: #f1f5f9 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.dark-mode .apexcharts-xaxis-label,
|
||||
.dark-mode .apexcharts-yaxis-label {
|
||||
fill: #94a3b8 !important;
|
||||
}
|
||||
|
||||
.dark-mode .apexcharts-gridline {
|
||||
stroke: rgba(71, 85, 105, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Dark Mode Table */
|
||||
.dark-mode .table-header h5 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.dark-mode .table-subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark-mode .table th {
|
||||
background: linear-gradient(135deg, #1e293b, #334155);
|
||||
color: #e2e8f0;
|
||||
border-bottom-color: rgba(71, 85, 105, 0.5);
|
||||
}
|
||||
|
||||
.dark-mode .table td {
|
||||
color: #cbd5e1;
|
||||
border-color: rgba(71, 85, 105, 0.3);
|
||||
}
|
||||
|
||||
.dark-mode .table-hover tbody tr:hover {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.dark-mode .table-hover tbody tr:hover td {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.dark-mode .progress {
|
||||
background: linear-gradient(135deg, #334155, #475569);
|
||||
}
|
||||
|
||||
/* Dark Mode Badges */
|
||||
.dark-mode .badge.bg-light {
|
||||
background: linear-gradient(135deg, #334155, #475569) !important;
|
||||
color: #cbd5e1 !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
/* Loading in Dark Mode */
|
||||
.dark-mode .loading-overlay {
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
}
|
||||
|
||||
.dark-mode .loading-text {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.dark-mode .spinner div {
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
|
||||
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.3);
|
||||
}
|
||||
|
||||
/* Alert in Dark Mode */
|
||||
.dark-mode .alert-danger {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #fca5a5;
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
/* Badge Styles */
|
||||
.badge {
|
||||
font-weight: 600;
|
||||
padding: 6px 12px;
|
||||
border-radius: 18px;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.2px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
background: linear-gradient(135deg, #f1f5f9, #e2e8f0) !important;
|
||||
color: #334155 !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background: linear-gradient(135deg, #10b981, #059669) !important;
|
||||
color: white !important;
|
||||
border-color: #047857 !important;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706) !important;
|
||||
color: white !important;
|
||||
border-color: #b45309 !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626) !important;
|
||||
color: white !important;
|
||||
border-color: #b91c1c !important;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background: linear-gradient(135deg, #0ea5e9, #0284c7) !important;
|
||||
color: white !important;
|
||||
border-color: #0369a1 !important;
|
||||
}
|
||||
|
||||
/* Text Colors */
|
||||
.text-primary { color: #3b82f6 !important; }
|
||||
.text-success { color: #10b981 !important; }
|
||||
.text-info { color: #0ea5e9 !important; }
|
||||
.text-warning { color: #f59e0b !important; }
|
||||
.text-danger { color: #ef4444 !important; }
|
||||
.text-muted { color: #94a3b8 !important; }
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 1200px) {
|
||||
.project-dashboard-widget {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dashboard-header-right {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-dashboard-widget {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.project-info-banner.dashboard-header {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 8px 14px;
|
||||
font-size: 0.8125rem;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.btn-control {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.fab {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional Fix for Chart Tooltips in Dark Mode */
|
||||
.dark-mode .apexcharts-menu {
|
||||
background: #1e293b !important;
|
||||
border: 1px solid rgba(71, 85, 105, 0.6) !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
.dark-mode .apexcharts-menu-item:hover {
|
||||
background: #334155 !important;
|
||||
}
|
||||
|
||||
/* Ensure chart annotations are visible */
|
||||
.dark-mode .apexcharts-annotation-label {
|
||||
background: #1e293b !important;
|
||||
border: 1px solid rgba(71, 85, 105, 0.6) !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.dark-mode .apexcharts-text .apexcharts-datalabel-value{
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.apexcharts-text .apexcharts-datalabel-value{
|
||||
color: green !important;
|
||||
}
|
||||
|
||||
/* Dark mode specific fixes for donut charts */
|
||||
.dark-mode .apexcharts-datalabels-group text {
|
||||
fill: #F8FAFC !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,960 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState, onMounted } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
|
||||
class ProjectDashboardWidget extends Component {
|
||||
static template = "project_dashboards_management.ProjectDashboardWidget";
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
|
||||
setup() {
|
||||
debugger;
|
||||
super.setup();
|
||||
|
||||
this.state = useState({
|
||||
loading: true,
|
||||
darkMode: false,
|
||||
animationEnabled: true,
|
||||
projectData: null,
|
||||
tasksData: [],
|
||||
employeePerformance: [],
|
||||
budgetData: null,
|
||||
error: null
|
||||
});
|
||||
|
||||
this.actionService = useService("action");
|
||||
this.dialogService = useService("dialog");
|
||||
this.orm = useService("orm");
|
||||
this.notificationService = useService("notification");
|
||||
this.rpc = rpc;
|
||||
|
||||
onMounted(() => this.loadDashboardData());
|
||||
this.checkSystemTheme();
|
||||
}
|
||||
|
||||
checkSystemTheme() {
|
||||
const savedTheme = localStorage.getItem('dashboardDarkMode');
|
||||
if (savedTheme !== null) {
|
||||
this.state.darkMode = savedTheme === 'true';
|
||||
} else {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
this.state.darkMode = prefersDark;
|
||||
}
|
||||
document.body.classList.toggle('o_dark_mode', this.state.darkMode);
|
||||
}
|
||||
|
||||
async loadDashboardData() {
|
||||
debugger;
|
||||
|
||||
this.state.loading = true;
|
||||
this.state.error = null;
|
||||
|
||||
try {
|
||||
// Check if we're in a project context
|
||||
if (!this.props.record || this.props.record.resModel !== 'project.project') {
|
||||
this.state.error = "This dashboard widget only works on Project records.";
|
||||
this.state.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = this.props.record.resId;
|
||||
|
||||
// Single API call to the controller
|
||||
const response = await this.rpc('/project_dashboard/get_dashboard_data', {
|
||||
project_id: projectId
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.state.projectData = response.project_data;
|
||||
this.state.tasksData = response.tasks_data;
|
||||
this.state.employeePerformance = response.employee_performance;
|
||||
this.state.budgetData = response.budget_data;
|
||||
|
||||
// Initialize charts after data is loaded
|
||||
setTimeout(() => this.initCharts(), 100);
|
||||
} else {
|
||||
this.state.error = response.error || "Failed to load dashboard data.";
|
||||
this.notificationService.add("Error loading dashboard data", {
|
||||
type: 'danger'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading dashboard data:", error);
|
||||
this.state.error = "Failed to load dashboard data. Please try again.";
|
||||
this.notificationService.add("Error loading dashboard data", {
|
||||
type: 'danger'
|
||||
});
|
||||
} finally {
|
||||
this.state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async initCharts() {
|
||||
if (!window.ApexCharts) {
|
||||
console.warn("ApexCharts not loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Destroy existing charts
|
||||
if (this.charts) {
|
||||
this.charts.forEach(chart => chart.destroy());
|
||||
}
|
||||
this.charts = [];
|
||||
|
||||
// Initialize all charts
|
||||
await Promise.all([
|
||||
|
||||
this.initResourceMiniChart(),
|
||||
this.initAssetMiniChart(),
|
||||
this.initUsageMiniChart(),
|
||||
this.initMainBudgetChart(),
|
||||
this.initEmployeePerformanceChart(),
|
||||
this.initTaskProgressChart(),
|
||||
this.initTimeVarianceChart(),
|
||||
this.initStageDistributionChart(),
|
||||
this.initTimelineChart()
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error initializing charts:", error);
|
||||
}
|
||||
}
|
||||
|
||||
initResourceMiniChart() {
|
||||
if (!this.state.budgetData) return;
|
||||
let data = this.state.budgetData
|
||||
new ApexCharts(
|
||||
document.querySelector("#resourceBudgetMini"),
|
||||
{
|
||||
series: [data.resource_cost, data.planned_resource_cost || 0],
|
||||
chart: { type: 'donut', height: 160 },
|
||||
labels: ['Used', 'Planned'],
|
||||
colors: ['#3B82F6', '#E5E7EB'],
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '78%',
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
label: 'Used',
|
||||
fontSize: '12px',
|
||||
color: '#64748B',
|
||||
formatter: () =>
|
||||
`₹${this.formatNumber(data.resource_cost)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: { enabled: false }, // 🔑 FIX
|
||||
legend: { show: false }, // 🔑 FIX
|
||||
title: {
|
||||
text: 'Resource Budget',
|
||||
align: 'center',
|
||||
style: { fontSize: '13px', fontWeight: 600 }
|
||||
},
|
||||
tooltip: {
|
||||
y: { formatter: v => `₹${this.formatNumber(v)}` }
|
||||
}
|
||||
}
|
||||
).render();
|
||||
}
|
||||
|
||||
|
||||
initAssetMiniChart() {
|
||||
if (!this.state.budgetData) return;
|
||||
let data = this.state.budgetData
|
||||
new ApexCharts(
|
||||
document.querySelector("#assetBudgetMini"),
|
||||
{
|
||||
series: [data.asset_cost, data.planned_asset_cost || 0],
|
||||
chart: { type: 'donut', height: 160 },
|
||||
labels: ['Used', 'Planned'],
|
||||
colors: ['#10B981', '#E5E7EB'],
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '78%',
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
label: 'Used',
|
||||
fontSize: '12px',
|
||||
color: '#64748B',
|
||||
formatter: () =>
|
||||
`₹${this.formatNumber(data.asset_cost)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: { enabled: false }, // 🔑 FIX
|
||||
legend: { show: false },
|
||||
title: {
|
||||
text: 'Asset Budget',
|
||||
align: 'center',
|
||||
style: { fontSize: '13px', fontWeight: 600 }
|
||||
},
|
||||
tooltip: {
|
||||
y: { formatter: v => `₹${this.formatNumber(v)}` }
|
||||
}
|
||||
}
|
||||
).render();
|
||||
}
|
||||
|
||||
|
||||
initUsageMiniChart() {
|
||||
if (!this.state.budgetData) return;
|
||||
let data = this.state.budgetData
|
||||
new ApexCharts(
|
||||
document.querySelector("#usageBudgetMini"),
|
||||
{
|
||||
series: [data.used_budget, data.available_budget],
|
||||
chart: { type: 'donut', height: 160 },
|
||||
labels: ['Used', 'Available'],
|
||||
colors: ['#F59E0B', '#22C55E'],
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '78%',
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
label: 'Used',
|
||||
fontSize: '12px',
|
||||
color: '#64748B',
|
||||
formatter: () =>
|
||||
`${((data.used_budget / data.total_budget) * 100).toFixed(1)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: { enabled: false }, // 🔑 FIX
|
||||
legend: { show: false },
|
||||
title: {
|
||||
text: 'Budget Usage',
|
||||
align: 'center',
|
||||
style: { fontSize: '13px', fontWeight: 600 }
|
||||
},
|
||||
tooltip: {
|
||||
y: { formatter: v => `₹${this.formatNumber(v)}` }
|
||||
}
|
||||
}
|
||||
).render();
|
||||
}
|
||||
|
||||
|
||||
initMainBudgetChart() {
|
||||
if (!this.state.budgetData) return;
|
||||
let data = this.state.budgetData
|
||||
const exceeded = data.used_budget > data.total_budget;
|
||||
|
||||
new ApexCharts(
|
||||
document.querySelector("#budgetMainChart"),
|
||||
{
|
||||
series: [
|
||||
data.resource_cost,
|
||||
data.asset_cost,
|
||||
data.available_budget
|
||||
],
|
||||
chart: {
|
||||
type: 'donut',
|
||||
height: 300
|
||||
},
|
||||
labels: ['Resource Cost', 'Asset Cost', 'Remaining'],
|
||||
colors: ['#3B82F6', '#10B981', '#E5E7EB'],
|
||||
stroke: { width: 3, colors: ['#ffffff']},
|
||||
states: {
|
||||
hover: {
|
||||
filter: { type: 'none' }
|
||||
},
|
||||
active: {
|
||||
filter: { type: 'none' }
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '65%',
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
label: exceeded ? '⚠ Over Budget' : 'Total Budget',
|
||||
color: exceeded ? '#EF4444' : '#374151',
|
||||
formatter: () => `₹${this.formatNumber(data.total_budget)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: v => `₹${this.formatNumber(v)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
).render();
|
||||
}
|
||||
|
||||
initEmployeePerformanceChart() {
|
||||
if (!this.state.employeePerformance.length) return;
|
||||
|
||||
const element = document.querySelector("#employeePerformanceChart");
|
||||
if (!element) return;
|
||||
|
||||
const employees = this.state.employeePerformance.slice(0, 10); // Top 10 employees
|
||||
const categories = employees.map(emp => emp.employee_name.split(' ')[0]); // First names
|
||||
|
||||
const options = {
|
||||
series: [{
|
||||
name: 'Efficiency Rate',
|
||||
data: employees.map(emp => emp.efficiency_rate)
|
||||
}, {
|
||||
name: 'On-Time Rate',
|
||||
data: employees.map(emp => emp.on_time_rate)
|
||||
}],
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 350,
|
||||
stacked: false,
|
||||
toolbar: { show: false }
|
||||
},
|
||||
colors: ['#00e396', '#008ffb'],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '55%',
|
||||
borderRadius: 5
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 2,
|
||||
colors: ['transparent']
|
||||
},
|
||||
xaxis: {
|
||||
categories: categories,
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: '12px'
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'Percentage (%)',
|
||||
style: {
|
||||
}
|
||||
},
|
||||
labels: {
|
||||
formatter: function(val) {
|
||||
return val.toFixed(0) + '%';
|
||||
},
|
||||
style: {
|
||||
}
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return val.toFixed(1) + '%';
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'center',
|
||||
labels: {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chart = new ApexCharts(element, options);
|
||||
chart.render();
|
||||
this.charts.push(chart);
|
||||
}
|
||||
|
||||
initTaskProgressChart() {
|
||||
if (!this.state.tasksData.length) return;
|
||||
|
||||
const element = document.querySelector("#taskProgressChart");
|
||||
if (!element) return;
|
||||
|
||||
// Calculate completion percentages
|
||||
const completedTasks = this.state.tasksData.filter(task => task.state === '1_done' || task.state === '1_canceled').length;
|
||||
const inProgressTasks = this.state.tasksData.filter(task => task.state === '01_in_progress').length;
|
||||
const todoTasks = this.state.tasksData.filter(task => task.state === '04_waiting_normal').length;
|
||||
const totalTasks = this.state.tasksData.length;
|
||||
|
||||
const completionRate = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
|
||||
|
||||
const options = {
|
||||
series: [completionRate],
|
||||
chart: {
|
||||
height: 250,
|
||||
type: 'radialBar',
|
||||
offsetY: -10
|
||||
},
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
startAngle: -135,
|
||||
endAngle: 135,
|
||||
dataLabels: {
|
||||
name: {
|
||||
fontSize: '16px',
|
||||
offsetY: 120
|
||||
},
|
||||
value: {
|
||||
offsetY: 76,
|
||||
fontSize: '34px',
|
||||
formatter: function(val) {
|
||||
return val.toFixed(1) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ['#00e396'],
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shade: 'dark',
|
||||
shadeIntensity: 0.15,
|
||||
inverseColors: false,
|
||||
opacityFrom: 1,
|
||||
opacityTo: 1,
|
||||
stops: [0, 50, 65, 91]
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
dashArray: 4
|
||||
},
|
||||
labels: ['Overall Completion'],
|
||||
};
|
||||
|
||||
const chart = new ApexCharts(element, options);
|
||||
chart.render();
|
||||
this.charts.push(chart);
|
||||
}
|
||||
|
||||
initTimeVarianceChart() {
|
||||
if (!this.state.employeePerformance.length) return;
|
||||
|
||||
const element = document.querySelector("#timeVarianceChart");
|
||||
if (!element) return;
|
||||
|
||||
const employees = this.state.employeePerformance.slice(0, 8);
|
||||
const categories = employees.map(emp => emp.employee_name.split(' ')[0]);
|
||||
|
||||
const options = {
|
||||
series: [{
|
||||
name: 'Time Variance',
|
||||
data: employees.map(emp => emp.time_variance)
|
||||
}],
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 300,
|
||||
toolbar: { show: false }
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 4,
|
||||
horizontal: true,
|
||||
distributed: true,
|
||||
dataLabels: {
|
||||
position: 'top'
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: employees.map(emp => {
|
||||
const variance = emp.time_variance;
|
||||
if (variance <= 0) return '#00e396'; // Green for ahead of schedule
|
||||
if (variance <= 10) return '#ffb800'; // Yellow for minor delay
|
||||
if (variance <= 20) return '#ff9f00'; // Orange for moderate delay
|
||||
return '#ff4560'; // Red for major delay
|
||||
}),
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: function(val) {
|
||||
return val.toFixed(1) + '%';
|
||||
},
|
||||
offsetX: 0,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
colors: ['#fff']
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: categories,
|
||||
labels: {
|
||||
formatter: function(val) {
|
||||
return val.toFixed(1) + '%';
|
||||
},
|
||||
style: {
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: '12px'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return val > 0 ? `+${val.toFixed(1)}% delay` : `${val.toFixed(1)}% ahead`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chart = new ApexCharts(element, options);
|
||||
chart.render();
|
||||
this.charts.push(chart);
|
||||
}
|
||||
|
||||
getDeterministicColor(stageName, usedColors) {
|
||||
const CUSTOM_STAGE_PALETTE = [
|
||||
'#2563EB', // blue
|
||||
'#7C3AED', // violet
|
||||
'#0891B2', // cyan
|
||||
'#0D9488', // teal
|
||||
'#65A30D', // olive
|
||||
'#CA8A04', // amber
|
||||
'#DB2777', // rose
|
||||
'#9333EA', // purple
|
||||
'#0284C7', // sky
|
||||
'#16A34A', // green
|
||||
];
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < stageName.length; i++) {
|
||||
hash = stageName.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
let index = Math.abs(hash) % CUSTOM_STAGE_PALETTE.length;
|
||||
let color = CUSTOM_STAGE_PALETTE[index];
|
||||
|
||||
// 🔒 Avoid collisions with already-used colors
|
||||
let i = 1;
|
||||
while (usedColors.has(color)) {
|
||||
color = CUSTOM_STAGE_PALETTE[(index + i) % CUSTOM_STAGE_PALETTE.length];
|
||||
i++;
|
||||
}
|
||||
|
||||
usedColors.add(color);
|
||||
return color;
|
||||
}
|
||||
|
||||
resolveStageColor(stageName, usedColors) {
|
||||
const name = stageName.toLowerCase();
|
||||
let color;
|
||||
|
||||
if (name.includes('done') || name.includes('complete')) {
|
||||
color = '#00E396'; // emerald
|
||||
}
|
||||
else if (name.includes('cancel') || name.includes('reject')) {
|
||||
color = '#9CA3AF'; // neutral gray
|
||||
}
|
||||
else if (name.includes('hold') || name.includes('wait')) {
|
||||
color = '#F59E0B'; // amber
|
||||
}
|
||||
else if (name.includes('progress') || name.includes('doing')) {
|
||||
color = '#3B82F6'; // blue
|
||||
}
|
||||
else if (name.includes('test') || name.includes('review')) {
|
||||
color = '#8B5CF6'; // violet
|
||||
}
|
||||
else {
|
||||
color = this.getDeterministicColor(stageName, usedColors);
|
||||
return color;
|
||||
}
|
||||
|
||||
// 🔒 IMPORTANT: register semantic colors too
|
||||
if (!usedColors.has(color)) {
|
||||
usedColors.add(color);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
|
||||
initStageDistributionChart() {
|
||||
if (!this.state.tasksData?.length) return;
|
||||
|
||||
const element = document.querySelector("#stageDistributionChart");
|
||||
if (!element) return;
|
||||
|
||||
// 1️⃣ Group by stage
|
||||
const stageCounts = {};
|
||||
this.state.tasksData.forEach(task => {
|
||||
const stage = task.stage_name || 'No Stage';
|
||||
stageCounts[stage] = (stageCounts[stage] || 0) + 1;
|
||||
});
|
||||
|
||||
const stages = Object.keys(stageCounts);
|
||||
const counts = Object.values(stageCounts);
|
||||
const totalTasks = counts.reduce((a, b) => a + b, 0);
|
||||
|
||||
// 2️⃣ Resolve colors
|
||||
const usedColors = new Set();
|
||||
const colors = stages.map(stage =>
|
||||
this.resolveStageColor(stage, usedColors)
|
||||
);
|
||||
|
||||
// 3️⃣ Chart options
|
||||
const options = {
|
||||
series: counts,
|
||||
chart: {
|
||||
type: 'donut',
|
||||
height: 340,
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'easeinout',
|
||||
speed: 900
|
||||
}
|
||||
},
|
||||
|
||||
labels: stages,
|
||||
colors: colors,
|
||||
|
||||
stroke: {
|
||||
width: 3,
|
||||
colors: ['#ffffff']
|
||||
},
|
||||
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '68%',
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
label: 'Total Tasks',
|
||||
fontSize: '14px',
|
||||
color: '#6B7280',
|
||||
formatter: () => totalTasks
|
||||
}
|
||||
}
|
||||
},
|
||||
expandOnClick: true
|
||||
}
|
||||
},
|
||||
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
fontSize: '13px',
|
||||
fontWeight: 600
|
||||
},
|
||||
formatter: (val, opts) => {
|
||||
const count = opts.w.config.series[opts.seriesIndex];
|
||||
return count >= 2 ? count : '';
|
||||
}
|
||||
},
|
||||
|
||||
legend: {
|
||||
position: 'right',
|
||||
fontSize: '14px',
|
||||
markers: {
|
||||
radius: 12
|
||||
},
|
||||
itemMargin: {
|
||||
vertical: 6
|
||||
},
|
||||
labels: {
|
||||
}
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (val) => {
|
||||
const percent = ((val / totalTasks) * 100).toFixed(1);
|
||||
return `${val} tasks (${percent}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chart = new ApexCharts(element, options);
|
||||
chart.render();
|
||||
this.charts.push(chart);
|
||||
}
|
||||
|
||||
|
||||
|
||||
initTimelineChart() {
|
||||
if (!this.state.tasksData.length) return;
|
||||
|
||||
const element = document.querySelector("#timelineChart");
|
||||
if (!element) return;
|
||||
|
||||
// Get tasks with deadlines
|
||||
const tasksWithDeadlines = this.state.tasksData
|
||||
.filter(task => task.date_deadline)
|
||||
.slice(0, 15) // Limit to 15 for readability
|
||||
.map(task => ({
|
||||
x: task.sequence_name || task.name,
|
||||
y: [
|
||||
new Date(task.planned_date_begin || task.date_deadline).getTime(),
|
||||
new Date(task.date_deadline).getTime()
|
||||
],
|
||||
fillColor: this.getTaskColor(task)
|
||||
}));
|
||||
|
||||
const options = {
|
||||
series: [{
|
||||
data: tasksWithDeadlines
|
||||
}],
|
||||
chart: {
|
||||
height: 350,
|
||||
type: 'rangeBar'
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
distributed: true,
|
||||
dataLabels: {
|
||||
hideOverflowingLabels: false
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: function(val, opts) {
|
||||
const hours = (opts.w.config.series[opts.seriesIndex].data[opts.dataPointIndex].estimated_hours || 0).toFixed(1);
|
||||
return hours + 'h';
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
style: {
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: '12px'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
row: {
|
||||
colors: ['#f3f4f6', '#fff'],
|
||||
opacity: 0.5
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||
const data = w.config.series[seriesIndex].data[dataPointIndex];
|
||||
const start = new Date(data.y[0]).toLocaleDateString();
|
||||
const end = new Date(data.y[1]).toLocaleDateString();
|
||||
return `
|
||||
<div class="apexcharts-tooltip-rangebar">
|
||||
<div><strong>${data.x}</strong></div>
|
||||
<div>Start: ${start}</div>
|
||||
<div>End: ${end}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chart = new ApexCharts(element, options);
|
||||
chart.render();
|
||||
this.charts.push(chart);
|
||||
}
|
||||
|
||||
getTaskColor(task) {
|
||||
if (task.is_overdue) return '#ff4560';
|
||||
if (task.has_warning) return '#ffb800';
|
||||
if (task.state === '1_done') return '#00e396';
|
||||
if (task.state === '1_done') return '#94A3B8';
|
||||
if (task.state === '01_in_progress') return '#008ffb';
|
||||
return '#4facfe';
|
||||
}
|
||||
|
||||
formatNumber(num) {
|
||||
if (!num) return '0';
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
getProjectKPIs() {
|
||||
if (!this.state.projectData) return {};
|
||||
|
||||
const project = this.state.projectData;
|
||||
const tasks = this.state.tasksData;
|
||||
|
||||
return {
|
||||
totalTasks: tasks.length,
|
||||
completedTasks: tasks.filter(t => t.state === '1_done' || t.state === '1_canceled').length,
|
||||
overdueTasks: tasks.filter(t => t.is_overdue).length,
|
||||
totalHours: tasks.reduce((sum, t) => sum + (t.estimated_hours || 0), 0),
|
||||
actualHours: tasks.reduce((sum, t) => sum + (t.actual_hours || 0), 0),
|
||||
budgetUsed: this.state.budgetData ?
|
||||
(this.state.budgetData.used_budget) : 0,
|
||||
totalBudget: this.state.budgetData?.total_budget || 0,
|
||||
teamMembers: project.members_ids ? project.members_ids.length : 0,
|
||||
completionRate: tasks.length > 0 ?
|
||||
(tasks.filter(t => t.state === '1_done' || t.state === '1_canceled').length / tasks.length) * 100 : 0
|
||||
};
|
||||
}
|
||||
|
||||
async onClickOpenPopup() {
|
||||
try {
|
||||
const { ProjectDashboardPopup } = await import('./project_dashboard_popup');
|
||||
this.dialogService.add(ProjectDashboardPopup, {
|
||||
projectData: this.state.projectData,
|
||||
tasksData: this.state.tasksData,
|
||||
employeePerformance: this.state.employeePerformance,
|
||||
budgetData: this.state.budgetData,
|
||||
onClose: () => console.log("Popup closed"),
|
||||
onDownload: () => this.onClickDownload(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error opening popup:", error);
|
||||
this.notificationService.add("Error opening popup", { type: 'danger' });
|
||||
}
|
||||
}
|
||||
|
||||
onClickDownload() {
|
||||
try {
|
||||
const reportData = {
|
||||
project: this.state.projectData,
|
||||
summary: this.getProjectKPIs(),
|
||||
tasks_summary: this.state.tasksData.map(task => ({
|
||||
name: task.name,
|
||||
stage: task.stage_id ? task.stage_id[1] : 'No Stage',
|
||||
estimated_hours: task.estimated_hours,
|
||||
actual_hours: task.actual_hours,
|
||||
completion_rate: task.completion_rate,
|
||||
status: task.state
|
||||
})),
|
||||
employee_performance: this.state.employeePerformance,
|
||||
budget_data: this.state.budgetData,
|
||||
generated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const dataStr = JSON.stringify(reportData, null, 2);
|
||||
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||
|
||||
const projectName = this.state.projectData?.sequence_name || 'project';
|
||||
const exportFileName = `${projectName}_dashboard_${new Date().toISOString().split('T')[0]}.json`;
|
||||
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', dataUri);
|
||||
linkElement.setAttribute('download', exportFileName);
|
||||
document.body.appendChild(linkElement);
|
||||
linkElement.click();
|
||||
document.body.removeChild(linkElement);
|
||||
|
||||
this.notificationService.add("Dashboard exported successfully!", { type: 'success' });
|
||||
} catch (error) {
|
||||
console.error("Error downloading dashboard:", error);
|
||||
this.notificationService.add("Error exporting dashboard", { type: 'danger' });
|
||||
}
|
||||
}
|
||||
|
||||
onClickToggleDarkMode() {
|
||||
this.state.darkMode = !this.state.darkMode;
|
||||
localStorage.setItem('dashboardDarkMode', this.state.darkMode);
|
||||
document.body.classList.toggle('o_dark_mode', this.state.darkMode);
|
||||
|
||||
// Update charts theme
|
||||
if (this.charts) {
|
||||
this.charts.forEach(chart => {
|
||||
try {
|
||||
chart.updateOptions({
|
||||
theme: { mode: this.state.darkMode ? 'dark' : 'light' }
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Error updating chart theme:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.notificationService.add(
|
||||
this.state.darkMode ? "Dark mode enabled" : "Light mode enabled",
|
||||
{ type: 'info' }
|
||||
);
|
||||
}
|
||||
|
||||
onClickToggleAnimations() {
|
||||
this.state.animationEnabled = !this.state.animationEnabled;
|
||||
|
||||
if (this.charts) {
|
||||
this.charts.forEach(chart => {
|
||||
try {
|
||||
chart.updateOptions({
|
||||
chart: { animations: { enabled: this.state.animationEnabled } }
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Error updating chart animations:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.notificationService.add(
|
||||
this.state.animationEnabled ? "Animations enabled" : "Animations disabled",
|
||||
{ type: 'info' }
|
||||
);
|
||||
}
|
||||
|
||||
onClickRefresh() {
|
||||
this.loadDashboardData();
|
||||
this.notificationService.add("Dashboard refreshed!", { type: 'info' });
|
||||
}
|
||||
|
||||
// Clean up charts when component is destroyed
|
||||
onWillDestroy() {
|
||||
if (this.charts) {
|
||||
this.charts.forEach(chart => chart.destroy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const projectDashboardWidget = {
|
||||
component: ProjectDashboardWidget,
|
||||
};
|
||||
|
||||
export const projectDashboardAction = {
|
||||
component: ProjectDashboardWidget
|
||||
};
|
||||
registry.category("view_widgets").add("project_dashboard", projectDashboardWidget);
|
||||
registry.category("actions").add("project_dashboard", projectDashboardAction);
|
||||
|
|
@ -0,0 +1,558 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Portfolio Dashboard Widget -->
|
||||
<t t-name="project_dashboards_management.PortfolioDashboardWidget">
|
||||
<div class="portfolio-dashboard-widget" t-att-class="state.darkMode ? 'dark-mode' : ''">
|
||||
|
||||
<!-- Error Message -->
|
||||
<div t-if="state.error" class="alert alert-danger glassmorphism">
|
||||
<i class="fa fa-exclamation-triangle me-2"></i>
|
||||
<t t-esc="state.error"/>
|
||||
</div>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="project-info-banner glassmorphism dashboard-header">
|
||||
<div class="dashboard-header-left">
|
||||
<h3 t-if="state.portfolioData" class="project-name">
|
||||
<i class="fa fa-chart-pie me-2"></i>
|
||||
<t t-esc="state.portfolioData.name"/> Portfolio
|
||||
</h3>
|
||||
<h4 class="dashboard-title">
|
||||
Portfolio Analytics Dashboard
|
||||
</h4>
|
||||
<div class="project-meta">
|
||||
Comprehensive portfolio insights and performance metrics
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-header-right">
|
||||
<div class="meta-items">
|
||||
<span class="meta-item">
|
||||
<i class="fa fa-folder-open me-1"></i>
|
||||
<t t-esc="getPortfolioKPIs().totalProjects"/> Projects
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="fa fa-inr me-1"></i>
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().totalBudget)"/> Total Budget
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="fa fa-users me-1"></i>
|
||||
<t t-esc="state.employeePerformance.length"/> Team Members
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- <div class="view-switcher">-->
|
||||
<!-- <button t-att-class="{'btn-view active': state.activeView === 'overview'}"-->
|
||||
<!-- t-on-click="() => toggleView('overview')">-->
|
||||
<!-- <i class="fa fa-home me-1"></i>Overview-->
|
||||
<!-- </button>-->
|
||||
<!-- <button t-att-class="{'btn-view active': state.activeView === 'financial'}"-->
|
||||
<!-- t-on-click="() => toggleView('financial')">-->
|
||||
<!-- <i class="fa fa-money me-1"></i>Financial-->
|
||||
<!-- </button>-->
|
||||
<!-- <button t-att-class="{'btn-view active': state.activeView === 'performance'}"-->
|
||||
<!-- t-on-click="() => toggleView('performance')">-->
|
||||
<!-- <i class="fa fa-line-chart me-1"></i>Performance-->
|
||||
<!-- </button>-->
|
||||
<!-- <button t-att-class="{'btn-view active': state.activeView === 'projects'}"-->
|
||||
<!-- t-on-click="() => toggleView('projects')">-->
|
||||
<!-- <i class="fa fa-tasks me-1"></i>Projects-->
|
||||
<!-- </button>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div class="project-stats">
|
||||
<button class="btn-control stat-item" t-on-click="onClickToggleAnimations"
|
||||
t-att-title="state.animationEnabled ? 'Disable Animations' : 'Enable Animations'">
|
||||
<i t-if="state.animationEnabled" class="fa fa-pause"></i>
|
||||
<i t-else="" class="fa fa-play"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn-control stat-item" t-on-click="onClickToggleDarkMode"
|
||||
t-att-title="state.darkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'">
|
||||
<i t-if="state.darkMode" class="fa fa-sun-o"></i>
|
||||
<i t-else="" class="fa fa-moon-o"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn-control stat-item" t-on-click="onClickRefresh"
|
||||
t-att-disabled="state.loading">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn-action btn-info stat-item" t-on-click="onClickViewProjects">
|
||||
<i class="fa fa-external-link me-1"></i>View Projects
|
||||
</button>
|
||||
|
||||
<button class="btn-action btn-success stat-item" t-on-click="onClickDownload">
|
||||
<i class="fa fa-download me-1"></i>Export Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Dashboard -->
|
||||
<div t-if="state.portfolioData" class="kpi-dashboard">
|
||||
<!-- Financial KPIs -->
|
||||
<div class="kpi-section glassmorphism">
|
||||
<div class="kpi-section-header" t-on-click="() => toggleKPI('budget')">
|
||||
<h4><i class="fa fa-money me-2"></i>Financial Overview</h4>
|
||||
<i t-if="state.expandedKPIs.budget" class="fa fa-chevron-up"></i>
|
||||
<i t-else="" class="fa fa-chevron-down"></i>
|
||||
</div>
|
||||
<div t-if="state.expandedKPIs.budget" class="kpi-section-content">
|
||||
<div class="kpi-grid">
|
||||
<!-- Budget Status -->
|
||||
<div class="kpi-card glassmorphism" t-att-class="getBudgetStatusClass(getPortfolioKPIs().budgetStatus)">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-balance-scale text-primary"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': getPortfolioKPIs().budgetStatus === 'under',
|
||||
'warning': getPortfolioKPIs().budgetStatus === 'on-track',
|
||||
'negative': getPortfolioKPIs().budgetStatus === 'over'
|
||||
}">
|
||||
<t t-if="getPortfolioKPIs().budgetVariance !== 0">
|
||||
<t t-esc="getPortfolioKPIs().budgetVariance > 0 ? '+' : ''"/>
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().budgetVariance)"/>
|
||||
</t>
|
||||
<t t-else="">On Track</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="getPortfolioKPIs().budgetStatus === 'under' ? 'Under Budget' :
|
||||
getPortfolioKPIs().budgetStatus === 'over' ? 'Over Budget' : 'On Track'"/>
|
||||
</div>
|
||||
<div class="kpi-label">Budget Status</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-if="getPortfolioKPIs().budgetVariance !== 0">
|
||||
<t t-esc="getPortfolioKPIs().budgetVariance > 0 ? '+' : ''"/>
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().budgetVariance)"/> variance
|
||||
</t>
|
||||
<t t-else="">No variance</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Net Profit -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-line-chart text-success"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': getPortfolioKPIs().netProfit >= 0,
|
||||
'negative': getPortfolioKPIs().netProfit < 0
|
||||
}">
|
||||
<t t-if="getPortfolioKPIs().netProfit >= 0">Profit</t>
|
||||
<t t-else="">Loss</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().netProfit)"/>
|
||||
</div>
|
||||
<div class="kpi-label">Net Profit/Loss</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-if="getPortfolioKPIs().netProfit >= 0">Profit achieved</t>
|
||||
<t t-else="">Loss incurred</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Budget -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-inr text-info"></i>
|
||||
</div>
|
||||
<div class="kpi-trend positive">
|
||||
Total
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().totalBudget)"/>
|
||||
</div>
|
||||
<div class="kpi-label">Total Client Budget</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().totalActual)"/> actual spend
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Budget Utilization -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-percent text-warning"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': (getPortfolioKPIs().totalActual / (getPortfolioKPIs().totalBudget || 1) * 100) <= 80,
|
||||
'warning': (getPortfolioKPIs().totalActual / (getPortfolioKPIs().totalBudget || 1) * 100) <= 95,
|
||||
'negative': (getPortfolioKPIs().totalActual / (getPortfolioKPIs().totalBudget || 1) * 100) > 95
|
||||
}">
|
||||
<t t-if="getPortfolioKPIs().totalBudget > 0">
|
||||
<t t-esc="Math.round((getPortfolioKPIs().totalActual / getPortfolioKPIs().totalBudget) * 100)"/>%
|
||||
</t>
|
||||
<t t-else="">0%</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-if="getPortfolioKPIs().totalBudget > 0">
|
||||
<t t-esc="Math.round((getPortfolioKPIs().totalActual / getPortfolioKPIs().totalBudget) * 100)"/>%
|
||||
</t>
|
||||
<t t-else="">0%</t>
|
||||
</div>
|
||||
<div class="kpi-label">Budget Utilization</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().totalActual)"/> of
|
||||
<t t-esc="formatCurrency(getPortfolioKPIs().totalBudget)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance KPIs -->
|
||||
<div class="kpi-section glassmorphism">
|
||||
<div class="kpi-section-header" t-on-click="() => toggleKPI('performance')">
|
||||
<h4><i class="fa fa-tachometer me-2"></i>Performance Metrics</h4>
|
||||
<i t-if="state.expandedKPIs.performance" class="fa fa-chevron-up"></i>
|
||||
<i t-else="" class="fa fa-chevron-down"></i>
|
||||
</div>
|
||||
<div t-if="state.expandedKPIs.performance" class="kpi-section-content">
|
||||
<div class="kpi-grid">
|
||||
<!-- Efficiency -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-bolt text-success"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': getPortfolioKPIs().avgEfficiency >= 90,
|
||||
'warning': getPortfolioKPIs().avgEfficiency >= 70 && getPortfolioKPIs().avgEfficiency < 90,
|
||||
'negative': getPortfolioKPIs().avgEfficiency < 70
|
||||
}">
|
||||
<t t-if="getPortfolioKPIs().avgEfficiency >= 90">Excellent</t>
|
||||
<t t-elif="getPortfolioKPIs().avgEfficiency >= 70">Good</t>
|
||||
<t t-else="">Needs improvement</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="Math.round(getPortfolioKPIs().avgEfficiency)"/>%
|
||||
</div>
|
||||
<div class="kpi-label">Overall Efficiency</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-if="getPortfolioKPIs().avgEfficiency >= 90">Excellent performance</t>
|
||||
<t t-elif="getPortfolioKPIs().avgEfficiency >= 70">Good performance</t>
|
||||
<t t-else="">Needs improvement</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- On-Time Rate -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-clock-o text-info"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': getPortfolioKPIs().onTimeRate >= 90,
|
||||
'warning': getPortfolioKPIs().onTimeRate >= 70 && getPortfolioKPIs().onTimeRate < 90,
|
||||
'negative': getPortfolioKPIs().onTimeRate < 70
|
||||
}">
|
||||
<t t-esc="Math.round(getPortfolioKPIs().onTimeRate)"/>%
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="Math.round(getPortfolioKPIs().onTimeRate)"/>%
|
||||
</div>
|
||||
<div class="kpi-label">On-Time Completion</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-if="getPortfolioKPIs().onTimeRate >= 90">Excellent timing</t>
|
||||
<t t-elif="getPortfolioKPIs().onTimeRate >= 70">Good timing</t>
|
||||
<t t-else="">Needs attention</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Variance -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-hourglass-half text-warning"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': getPortfolioKPIs().timeVariance <= 0,
|
||||
'warning': getPortfolioKPIs().timeVariance > 0 && getPortfolioKPIs().timeVariance <= 10,
|
||||
'negative': getPortfolioKPIs().timeVariance > 10
|
||||
}">
|
||||
<t t-if="getPortfolioKPIs().timeVariance > 0">+</t>
|
||||
<t t-esc="getPortfolioKPIs().timeVariance.toFixed(1)"/>%
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value" t-att-class="{
|
||||
'text-success': getPortfolioKPIs().timeVariance <= 0,
|
||||
'text-warning': getPortfolioKPIs().timeVariance > 0 && getPortfolioKPIs().timeVariance <= 10,
|
||||
'text-danger': getPortfolioKPIs().timeVariance > 10
|
||||
}">
|
||||
<t t-if="getPortfolioKPIs().timeVariance > 0">+</t>
|
||||
<t t-esc="getPortfolioKPIs().timeVariance.toFixed(1)"/>%
|
||||
</div>
|
||||
<div class="kpi-label">Time Variance</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-if="getPortfolioKPIs().timeVariance <= 0">Ahead of schedule</t>
|
||||
<t t-elif="getPortfolioKPIs().timeVariance <= 10">Minor delay</t>
|
||||
<t t-else="">Significant delay</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Projects -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-play-circle text-primary"></i>
|
||||
</div>
|
||||
<div class="kpi-trend positive">
|
||||
<t t-esc="getPortfolioKPIs().activeProjects"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="getPortfolioKPIs().activeProjects"/>
|
||||
</div>
|
||||
<div class="kpi-label">Active Projects</div>
|
||||
<div class="kpi-subtext">
|
||||
of <t t-esc="getPortfolioKPIs().totalProjects"/> total
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Grid -->
|
||||
<div t-if="state.portfolioData" class="charts-grid">
|
||||
<!-- Row 1: Budget Comparison & ROI -->
|
||||
<div class="chart-container glassmorphism budget-dashboard">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-balance-scale me-2"></i>Budget Comparison</h5>
|
||||
<span class="chart-info">
|
||||
Total: <t t-esc="formatCurrency(getPortfolioKPIs().totalBudget)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div id="budgetComparisonChart" class="chart-content large"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-percent me-2"></i>ROI Comparison</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Planned vs Actual
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="roiComparisonChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Cost Breakdown, Performance & Health -->
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-pie-chart me-2"></i>Cost Breakdown</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Distribution across categories
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="costBreakdownChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-tachometer me-2"></i>Performance Gauge</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Overall efficiency
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="performanceGauge" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-heartbeat me-2"></i>Project Health</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Health scores
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="projectHealthChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Resource Utilization -->
|
||||
<div class="chart-container glassmorphism large" t-if="state.activeView === 'performance'">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-users me-2"></i>Resource Utilization</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Top performers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="resourceUtilizationChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 4: View-specific charts -->
|
||||
<div t-if="state.activeView === 'financial'" class="chart-container glassmorphism large">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-chart-bar me-2"></i>Budget Variance</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Project-wise variance
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="budgetVarianceChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<div t-if="state.activeView === 'projects' and state.projectsData.length > 0" class="chart-container glassmorphism large">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-money me-2"></i>Projects Budget</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
<t t-esc="state.projectsData.length"/> projects
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="projectsBudgetChart" class="chart-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employee Performance Table -->
|
||||
<div t-if="state.employeePerformance.length > 0 and state.activeView === 'performance'"
|
||||
class="performance-table glassmorphism">
|
||||
<div class="table-header">
|
||||
<h5><i class="fa fa-user-check me-2"></i>Employee Performance Details</h5>
|
||||
<div class="table-subtitle">Based on timesheet data and task estimates</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Employee</th>
|
||||
<th class="text-center">Department</th>
|
||||
<th class="text-center">Estimated Hours</th>
|
||||
<th class="text-center">Actual Hours</th>
|
||||
<th class="text-center">Time Variance</th>
|
||||
<th class="text-center">On-Time Rate</th>
|
||||
<th class="text-center">Efficiency</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.employeePerformance.slice(0, 10)" t-as="emp" t-key="emp.employee_id">
|
||||
<td>
|
||||
<i class="fa fa-user me-2"></i>
|
||||
<t t-esc="emp.employee_name"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark">
|
||||
<t t-esc="emp.department_id ? emp.department_id[1] : '-'"/>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark">
|
||||
<t t-esc="Math.round(emp.total_estimated_hours)"/>h
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark">
|
||||
<t t-esc="Math.round(emp.total_actual_hours)"/>h
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-att-class="{
|
||||
'badge bg-success': emp.time_variance_percent <= 0,
|
||||
'badge bg-warning': emp.time_variance_percent > 0 && emp.time_variance_percent <= 10,
|
||||
'badge bg-danger': emp.time_variance_percent > 10
|
||||
}">
|
||||
<t t-if="emp.time_variance_percent > 0">+</t>
|
||||
<t t-esc="emp.time_variance_percent.toFixed(1)"/>%
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="progress" style="height: 20px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
t-att-style="'width: ' + emp.on_time_completion_rate + '%;'"
|
||||
t-att-class="{
|
||||
'bg-success': emp.on_time_completion_rate >= 80,
|
||||
'bg-warning': emp.on_time_completion_rate >= 50 && emp.on_time_completion_rate < 80,
|
||||
'bg-danger': emp.on_time_completion_rate < 50
|
||||
}">
|
||||
<span class="progress-text">
|
||||
<t t-esc="emp.on_time_completion_rate.toFixed(1)"/>%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-att-class="{
|
||||
'badge bg-success': emp.efficiency_rate >= 90,
|
||||
'badge bg-warning': emp.efficiency_rate >= 70 && emp.efficiency_rate < 90,
|
||||
'badge bg-danger': emp.efficiency_rate < 70
|
||||
}">
|
||||
<t t-esc="emp.efficiency_rate.toFixed(1)"/>%
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-att-class="'badge ' + getPerformanceStatusClass(emp.performance_status)">
|
||||
<t t-esc="emp.performance_status"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div t-if="state.loading" class="loading-overlay glassmorphism">
|
||||
<div class="loading-content">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
<div class="loading-text">Loading Portfolio Dashboard...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Unavailable Message -->
|
||||
<div t-if="!state.loading and !state.portfolioData" class="no-data-message glassmorphism">
|
||||
<div class="text-center py-5">
|
||||
<i class="fa fa-chart-pie fa-4x text-muted mb-3"></i>
|
||||
<h4>No Portfolio Data Available</h4>
|
||||
<p class="text-muted">This dashboard widget only works when viewing a Portfolio record.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Button -->
|
||||
<div class="fab" t-on-click="onClickRefresh" title="Refresh Dashboard">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Main Dashboard Widget -->
|
||||
<t t-name="project_dashboards_management.ProjectDashboardWidget">
|
||||
<div class="project-dashboard-widget" t-att-class="state.darkMode ? 'dark-mode' : ''">
|
||||
|
||||
<!-- Error Message -->
|
||||
<div t-if="state.error" class="alert alert-danger">
|
||||
<i class="fa fa-exclamation-triangle me-2"></i>
|
||||
<t t-esc="state.error"/>
|
||||
</div>
|
||||
<!-- Control Panel -->
|
||||
<div class="project-info-banner glassmorphism dashboard-header">
|
||||
<div class="dashboard-header-left">
|
||||
<h3 t-if="state.projectData" class="project-name">
|
||||
<i class="fa fa-chart-line me-2"></i>
|
||||
<t t-esc="state.projectData.name"/>
|
||||
</h3>
|
||||
<h4 class="dashboard-title">
|
||||
Analytics Dashboard
|
||||
</h4>
|
||||
<div class="project-meta">
|
||||
Real-time project insights and performance metrics
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-header-right">
|
||||
|
||||
<div class="project-stats">
|
||||
<button class="btn-control stat-item" t-on-click="onClickToggleAnimations"
|
||||
t-att-title="state.animationEnabled ? 'Disable Animations' : 'Enable Animations'">
|
||||
<i t-if="state.animationEnabled" class="fa fa-pause"></i>
|
||||
<i t-else="" class="fa fa-play"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn-control stat-item" t-on-click="onClickToggleDarkMode"
|
||||
t-att-title="state.darkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'">
|
||||
<i t-if="state.darkMode" class="fa fa-sun-o"></i>
|
||||
<i t-else="" class="fa fa-moon-o"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn-control stat-item" t-on-click="onClickRefresh"
|
||||
t-att-disabled="state.loading">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-info stat-item" t-on-click="onClickDownload">
|
||||
<i class="fa fa-download me-1"></i>Export Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div t-if="state.projectData" class="kpi-grid">
|
||||
<!-- Total Hours -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-clock text-primary"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': getProjectKPIs().actualHours <= getProjectKPIs().totalHours,
|
||||
'negative': getProjectKPIs().actualHours > getProjectKPIs().totalHours
|
||||
}">
|
||||
<t t-if="getProjectKPIs().totalHours > 0">
|
||||
<t t-esc="Math.round((getProjectKPIs().actualHours / getProjectKPIs().totalHours) * 100)"/>%
|
||||
</t>
|
||||
<t t-else="">0%</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="Math.round(getProjectKPIs().actualHours)"/>h
|
||||
</div>
|
||||
<div class="kpi-label">Actual Hours</div>
|
||||
<div class="kpi-subtext">
|
||||
of <t t-esc="Math.round(getProjectKPIs().totalHours)"/>h estimated
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Budget Utilization -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-rupee-sign text-success"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': (getProjectKPIs().budgetUsed / (getProjectKPIs().totalBudget || 1) * 100) <= 80,
|
||||
'warning': (getProjectKPIs().budgetUsed / (getProjectKPIs().totalBudget || 1) * 100) <= 95,
|
||||
'negative': (getProjectKPIs().budgetUsed / (getProjectKPIs().totalBudget || 1) * 100) > 95
|
||||
}">
|
||||
<t t-if="getProjectKPIs().totalBudget > 0">
|
||||
<t t-esc="Math.round((getProjectKPIs().budgetUsed / getProjectKPIs().totalBudget) * 100)"/>%
|
||||
</t>
|
||||
<t t-else="">0%</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
₹<t t-esc="formatNumber(getProjectKPIs().budgetUsed)"/>
|
||||
</div>
|
||||
<div class="kpi-label">Budget Used</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-if="getProjectKPIs().totalBudget > 0">
|
||||
of ₹<t t-esc="formatNumber(getProjectKPIs().totalBudget)"/> total
|
||||
</t>
|
||||
<t t-else="">No budget set</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Completion -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-check-circle text-info"></i>
|
||||
</div>
|
||||
<div class="kpi-trend positive">
|
||||
<t t-esc="getProjectKPIs().completedTasks"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value">
|
||||
<t t-esc="Math.round(getProjectKPIs().completionRate)"/>%
|
||||
</div>
|
||||
<div class="kpi-label">Tasks Completed</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-esc="getProjectKPIs().completedTasks"/> of <t t-esc="getProjectKPIs().totalTasks"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overdue Tasks -->
|
||||
<div class="kpi-card glassmorphism">
|
||||
<div class="kpi-header">
|
||||
<div class="kpi-icon">
|
||||
<i class="fa fa-exclamation-triangle text-warning"></i>
|
||||
</div>
|
||||
<div class="kpi-trend" t-att-class="{
|
||||
'positive': getProjectKPIs().overdueTasks === 0,
|
||||
'negative': getProjectKPIs().overdueTasks > 0
|
||||
}">
|
||||
<t t-esc="getProjectKPIs().overdueTasks"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-value" t-att-class="{
|
||||
'text-success': getProjectKPIs().overdueTasks === 0,
|
||||
'text-danger': getProjectKPIs().overdueTasks > 0
|
||||
}">
|
||||
<t t-esc="getProjectKPIs().overdueTasks"/>
|
||||
</div>
|
||||
<div class="kpi-label">Overdue Tasks</div>
|
||||
<div class="kpi-subtext">
|
||||
<t t-if="getProjectKPIs().overdueTasks > 0">Needs attention</t>
|
||||
<t t-else="">All tasks on track</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Grid -->
|
||||
<div t-if="state.projectData" class="charts-grid">
|
||||
<!-- Row 1: Budget & Performance -->
|
||||
<div class="chart-container glassmorphism budget-dashboard">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-inr me-2"></i>Budget Overview</h5>
|
||||
<span class="chart-info">
|
||||
Total: ₹<t t-esc="formatNumber(state.budgetData.total_budget)"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 TOP MINI CHARTS -->
|
||||
<div class="budget-mini-grid">
|
||||
<div id="resourceBudgetMini"></div>
|
||||
<div id="assetBudgetMini"></div>
|
||||
<div id="usageBudgetMini"></div>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 MAIN CHART -->
|
||||
<div id="budgetMainChart" class="chart-content large"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-users me-2"></i>Employee Performance</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
<t t-esc="state.employeePerformance ? state.employeePerformance.length : 0"/> employees
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="employeePerformanceChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Task Progress & Time Variance -->
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-tasks me-2"></i>Overall Progress</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
<t t-esc="getProjectKPIs().completedTasks"/>/<t t-esc="getProjectKPIs().totalTasks"/> tasks
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="taskProgressChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-chart-bar me-2"></i>Time Variance</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Estimated vs Actual
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="timeVarianceChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="chart-container glassmorphism">
|
||||
<div class="chart-header">
|
||||
<h5><i class="fa fa-sitemap me-2"></i>Stage Distribution</h5>
|
||||
<div class="chart-actions">
|
||||
<span class="chart-info">
|
||||
Across all stages
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stageDistributionChart" class="chart-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employee Performance Table -->
|
||||
<div t-if="state.employeePerformance and state.employeePerformance.length > 0" class="performance-table glassmorphism">
|
||||
<div class="table-header">
|
||||
<h5><i class="fa fa-user-check me-2"></i>Employee Performance Details</h5>
|
||||
<div class="table-subtitle">Based on timesheet data and task estimates</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Employee</th>
|
||||
<th class="text-center">Estimated Hours</th>
|
||||
<th class="text-center">Actual Hours</th>
|
||||
<th class="text-center">Time Variance</th>
|
||||
<th class="text-center">On-Time Rate</th>
|
||||
<th class="text-center">Efficiency</th>
|
||||
<th class="text-center">Tasks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.employeePerformance.slice(0, 10)" t-as="emp" t-key="emp.employee_id">
|
||||
<td>
|
||||
<i class="fa fa-user me-2"></i>
|
||||
<t t-esc="emp.employee_name"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark">
|
||||
<t t-esc="Math.round(emp.total_estimated)"/>h
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark">
|
||||
<t t-esc="Math.round(emp.total_actual)"/>h
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-att-class="{
|
||||
'badge bg-success': emp.time_variance <= 0,
|
||||
'badge bg-warning': emp.time_variance > 0 && emp.time_variance <= 10,
|
||||
'badge bg-danger': emp.time_variance > 10
|
||||
}">
|
||||
<t t-if="emp.time_variance > 0">+</t>
|
||||
<t t-esc="emp.time_variance.toFixed(1)"/>%
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="progress" style="height: 20px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
t-att-style="'width: ' + emp.on_time_rate + '%;'"
|
||||
t-att-class="{
|
||||
'bg-success': emp.on_time_rate >= 80,
|
||||
'bg-warning': emp.on_time_rate >= 50 && emp.on_time_rate < 80,
|
||||
'bg-danger': emp.on_time_rate < 50
|
||||
}">
|
||||
<span class="progress-text">
|
||||
<t t-esc="emp.on_time_rate.toFixed(1)"/>%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-att-class="{
|
||||
'badge bg-success': emp.efficiency_rate >= 90,
|
||||
'badge bg-warning': emp.efficiency_rate >= 70 && emp.efficiency_rate < 90,
|
||||
'badge bg-danger': emp.efficiency_rate < 70
|
||||
}">
|
||||
<t t-esc="emp.efficiency_rate.toFixed(1)"/>%
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-info">
|
||||
<t t-esc="emp.tasks_count"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div t-if="state.loading" class="loading-overlay glassmorphism">
|
||||
<div class="loading-content">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
<div class="loading-text">Loading Dashboard Data...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Unavailable Message -->
|
||||
<div t-if="!state.loading and !state.projectData" class="no-data-message glassmorphism">
|
||||
<div class="text-center py-5">
|
||||
<i class="fa fa-chart-bar fa-4x text-muted mb-3"></i>
|
||||
<h4>No Project Data Available</h4>
|
||||
<p class="text-muted">This dashboard widget only works when viewing a Project record.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Button -->
|
||||
<div class="fab" t-on-click="onClickRefresh" title="Refresh Dashboard">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Fullscreen Dashboard Action -->
|
||||
<record id="action_project_dashboard_fullscreen" model="ir.actions.client">
|
||||
<field name="name">Project Dashboard</field>
|
||||
<field name="tag">project_dashboard</field>
|
||||
<field name="target">fullscreen</field>
|
||||
<field name="params" eval="{'model': 'project.project', 'res_id': False}"/>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item for Dashboard -->
|
||||
<menuitem id="menu_project_dashboard"
|
||||
name="Project Dashboard"
|
||||
parent="project.menu_project_management"
|
||||
action="action_project_dashboard_fullscreen"
|
||||
sequence="10"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_project_form_dashboard" model="ir.ui.view">
|
||||
<field name="name">project.project.form.dashboard</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Dashboard" invisible="not is_project_editor">
|
||||
<widget name="project_dashboard"/>
|
||||
</page>
|
||||
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_portfolio_form_dashboard" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.form.dashboard</field>
|
||||
<field name="model">project.portfolio</field>
|
||||
<field name="inherit_id" ref="project_task_timesheet_extended.view_project_portfolio_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Dashboard">
|
||||
<widget name="portfolio_dashboard"/>
|
||||
</page>
|
||||
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -38,14 +38,18 @@ Key Features:
|
|||
'wizards/internal_team_members_wizard.xml',
|
||||
'wizards/project_stage_update_wizard.xml',
|
||||
'wizards/task_reject_reason_wizard.xml',
|
||||
'wizards/project_cancel_hold_wizard.xml',
|
||||
'view/teams.xml',
|
||||
'view/project_attachments.xml',
|
||||
'view/project_roles_master.xml',
|
||||
'view/project_stages.xml',
|
||||
'view/task_stages.xml',
|
||||
'view/deployment_log.xml',
|
||||
'view/maintenance_support.xml',
|
||||
'view/project_closer.xml',
|
||||
'view/project_actual_costings.xml',
|
||||
'view/project.xml',
|
||||
'view/project_portfolio.xml',
|
||||
'view/project_task.xml',
|
||||
'view/timesheets.xml',
|
||||
'view/pro_task_gantt.xml',
|
||||
|
|
|
|||
|
|
@ -24,6 +24,18 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_show_budget_summary" model="ir.actions.server">
|
||||
<field name="name">Show/Hide Budget Summary</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="binding_model_id" ref="project.model_project_project"/>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.action_show_budget_summary()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_show_project_task_chatter" model="ir.actions.server">
|
||||
<field name="name">Show/Hide Chatter</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
|
|
@ -48,6 +60,32 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_cancel_button" model="ir.actions.server">
|
||||
<field name="name">Cancel Project</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="binding_model_id" ref="project.model_project_project"/>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.action_cancel_project()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hold_unhold_project" model="ir.actions.server">
|
||||
<field name="name">Hold/Un Hold</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="binding_model_id" ref="project.model_project_project"/>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.action_hold_unhold()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- <record id="action_reward_user" model="ir.actions.server">-->
|
||||
<!-- <field name="name">Reward User</field>-->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from . import teams
|
||||
from . import project_roles_master
|
||||
from . import project_attachments
|
||||
from . import project_sprint
|
||||
from . import task_documents
|
||||
from . import project_architecture_design
|
||||
|
|
@ -13,6 +14,9 @@ from . import deployment_log
|
|||
from . import maintenance_support
|
||||
from . import project_closer
|
||||
from . import project
|
||||
from . import project_actual_costing
|
||||
from . import project_portfolio
|
||||
from . import project_portfolio_dashboard
|
||||
from . import project_task
|
||||
from . import timesheets
|
||||
# from . import project_task_gantt
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ class DeploymentLog(models.Model):
|
|||
deployment_notes = fields.Text(string="Notes")
|
||||
|
||||
deployment_files_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
string="Deployment Files"
|
||||
'project.attachments',
|
||||
string="Deployment Files",
|
||||
domain=[],
|
||||
attachment=True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ class MaintenanceSupport(models.Model):
|
|||
|
||||
# Attachments
|
||||
maintenance_file_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'maintenance_support_attachment_rel',
|
||||
'project.attachments',
|
||||
'maintenance_support_project_attachment_rel',
|
||||
'support_id',
|
||||
'attachment_id',
|
||||
string="Files"
|
||||
'project_attachment_id',
|
||||
string="Files",
|
||||
attachment=True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,58 @@ class ProjectProject(models.Model):
|
|||
show_approval_button = fields.Boolean(compute="_compute_access_check")
|
||||
show_refuse_button = fields.Boolean(compute="_compute_access_check")
|
||||
show_back_button = fields.Boolean(compute="_compute_access_check")
|
||||
|
||||
show_approval_button_filter = fields.Boolean(
|
||||
string="Needs Approval",
|
||||
compute="_compute_show_approval_button_filter",
|
||||
search="_search_show_approval_button_filter"
|
||||
)
|
||||
|
||||
show_submission_button_filter = fields.Boolean(
|
||||
string="Needs To Submit",
|
||||
compute="_compute_show_submission_button_filter",
|
||||
search="_search_show_submission_button_filter"
|
||||
)
|
||||
|
||||
def _compute_show_submission_button_filter(self):
|
||||
for record in self:
|
||||
record.show_submission_button_filter = record.show_submission_button
|
||||
|
||||
def _search_show_submission_button_filter(self, operator, value):
|
||||
if operator not in ('=', '!=') or not isinstance(value, bool):
|
||||
return []
|
||||
|
||||
all_records = self.sudo().search([])
|
||||
matching_ids = []
|
||||
|
||||
for record in all_records:
|
||||
record._compute_access_check()
|
||||
if (record.show_submission_button == value and record.assign_approval_flow and record.begin_approval_processing) if operator == '=' else (record.show_submission_button != value):
|
||||
matching_ids.append(record.id)
|
||||
|
||||
return [('id', 'in', matching_ids)]
|
||||
|
||||
def _compute_show_approval_button_filter(self):
|
||||
"""Simply copy the value for display purposes"""
|
||||
for record in self:
|
||||
record.show_approval_button_filter = record.show_approval_button
|
||||
|
||||
def _search_show_approval_button_filter(self, operator, value):
|
||||
"""Search implementation"""
|
||||
# Same logic as above
|
||||
if operator not in ('=', '!=') or not isinstance(value, bool):
|
||||
return []
|
||||
|
||||
all_records = self.sudo().search([])
|
||||
matching_ids = []
|
||||
|
||||
for record in all_records:
|
||||
record._compute_access_check()
|
||||
if (record.show_approval_button == value and record.assign_approval_flow and record.begin_approval_processing) if operator == '=' else (record.show_approval_button != value):
|
||||
matching_ids.append(record.id)
|
||||
|
||||
return [('id', 'in', matching_ids)]
|
||||
|
||||
project_activity_log = fields.Html(string="Project Activity Log")
|
||||
project_scope = fields.Html(string="Scope", default=lambda self: """
|
||||
<h3>Scope Description</h3><br/><br/>
|
||||
|
|
@ -72,8 +124,8 @@ class ProjectProject(models.Model):
|
|||
showable_stage_ids = fields.Many2many('project.project.stage',compute='_compute_project_project_stages')
|
||||
|
||||
# fields:
|
||||
estimated_amount = fields.Float(string="Estimated Amount")
|
||||
total_budget_amount = fields.Float(string="Total Budget Amount", compute="_compute_total_budget", store=True)
|
||||
estimated_amount = fields.Float(string="Estimated planned Amount")
|
||||
total_planned_budget_amount = fields.Float(string="Total Estimated planned Budget Amount", compute="_compute_total_budget", store=True)
|
||||
|
||||
# Manpower
|
||||
resource_cost_ids = fields.One2many(
|
||||
|
|
@ -96,6 +148,24 @@ class ProjectProject(models.Model):
|
|||
string="Equipment Costs"
|
||||
)
|
||||
|
||||
can_edit_stage_in_approval = fields.Boolean(default=False)
|
||||
initial_estimated_resource_cost = fields.Float(string="Estimated Resource Cost", compute='_estimated_cost_planned')
|
||||
initial_estimated_material_cost = fields.Float(string="Estimated Material Cost",compute='_estimated_cost_planned')
|
||||
initial_estimated_equiipment_cost = fields.Float(string="Estimated Asset Cost",compute='_estimated_cost_planned')
|
||||
|
||||
@api.depends('resource_cost_ids.total_cost','material_cost_ids.total_cost','equipment_cost_ids.total_cost')
|
||||
def _estimated_cost_planned(self):
|
||||
for project in self:
|
||||
project.initial_estimated_resource_cost = sum(
|
||||
project.resource_cost_ids.mapped('total_cost')
|
||||
)
|
||||
project.initial_estimated_material_cost = sum(
|
||||
project.material_cost_ids.mapped('total_cost')
|
||||
)
|
||||
project.initial_estimated_equiipment_cost = sum(
|
||||
project.equipment_cost_ids.mapped('total_cost')
|
||||
)
|
||||
|
||||
architecture_design_ids = fields.One2many(
|
||||
"project.architecture.design",
|
||||
"project_id",
|
||||
|
|
@ -165,6 +235,49 @@ class ProjectProject(models.Model):
|
|||
lessons_learned = fields.Text(string="Lessons Learned")
|
||||
challenges_faced = fields.Text(string="Challenges Faced")
|
||||
future_recommendations = fields.Text(string="Future Recommendations")
|
||||
|
||||
project_state = fields.Selection([
|
||||
('active', 'Active'),
|
||||
('hold', 'On Hold'),
|
||||
('cancel', 'Cancelled'),
|
||||
], default='active', tracking=True)
|
||||
|
||||
cancel_reason = fields.Text(string="Cancel Reason", tracking=True)
|
||||
hold_reason = fields.Text(string="Hold Reason", tracking=True)
|
||||
privacy_visibility = fields.Selection(default="followers")
|
||||
|
||||
def action_hold_unhold(self):
|
||||
for project in self:
|
||||
if project.project_state == 'hold':
|
||||
project.write({'project_state': 'active'})
|
||||
else:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Hold Project',
|
||||
'res_model': 'project.cancel.hold.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_project_id': project.id,
|
||||
'default_action_type': 'hold',
|
||||
}
|
||||
}
|
||||
|
||||
def action_cancel_project(self):
|
||||
for project in self:
|
||||
if project.project_state == 'active':
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Cancel Project',
|
||||
'res_model': 'project.cancel.hold.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_project_id': project.id,
|
||||
'default_action_type': 'cancel',
|
||||
}
|
||||
}
|
||||
|
||||
@api.depends('require_sprint','project_stages','assign_approval_flow')
|
||||
def _compute_project_project_stages(self):
|
||||
for rec in self:
|
||||
|
|
@ -186,7 +299,7 @@ class ProjectProject(models.Model):
|
|||
@api.depends("resource_cost_ids.total_cost", "material_cost_ids.total_cost", "equipment_cost_ids.total_cost")
|
||||
def _compute_total_budget(self):
|
||||
for project in self:
|
||||
project.total_budget_amount = (
|
||||
project.total_planned_budget_amount = (
|
||||
sum(project.resource_cost_ids.mapped("total_cost"))
|
||||
+ sum(project.material_cost_ids.mapped("total_cost"))
|
||||
+ sum(project.equipment_cost_ids.mapped("total_cost"))
|
||||
|
|
@ -415,6 +528,7 @@ class ProjectProject(models.Model):
|
|||
def submit_project_for_approval(self):
|
||||
"""Submit project for current stage approval"""
|
||||
for project in self:
|
||||
project.sudo().can_edit_stage_in_approval = True
|
||||
project.sudo().approval_status = "submitted"
|
||||
current_stage = project.sudo().stage_id
|
||||
current_approval_timeline = project.sudo().project_stages.filtered(
|
||||
|
|
@ -450,6 +564,8 @@ class ProjectProject(models.Model):
|
|||
project.sudo()._post_to_project_channel(channel_message)
|
||||
|
||||
# Send notification
|
||||
|
||||
project.sudo().can_edit_stage_in_approval = False
|
||||
if responsible_user:
|
||||
project.sudo().message_post(
|
||||
body=activity_log,
|
||||
|
|
@ -460,7 +576,9 @@ class ProjectProject(models.Model):
|
|||
|
||||
def project_proceed_further(self):
|
||||
"""Advance project to next stage after approval"""
|
||||
for project in self:
|
||||
for project in self.sudo():
|
||||
|
||||
project.can_edit_stage_in_approval = True
|
||||
current_stage = project.stage_id
|
||||
next_stage = self.env["project.project.stage"].search([
|
||||
('sequence', '>', project.stage_id.sequence),
|
||||
|
|
@ -529,10 +647,12 @@ class ProjectProject(models.Model):
|
|||
_("Project %s completed and fully approved") % project.name
|
||||
)
|
||||
project.message_post(body=activity_log)
|
||||
project.can_edit_stage_in_approval = False
|
||||
|
||||
def reject_and_return(self, reason=None):
|
||||
"""Reject project at current stage with optional reason"""
|
||||
for project in self:
|
||||
for project in self.sudo():
|
||||
project.can_edit_stage_in_approval = True
|
||||
reason = reason or ""
|
||||
current_stage = project.stage_id
|
||||
project.approval_status = "reject"
|
||||
|
|
@ -577,9 +697,13 @@ class ProjectProject(models.Model):
|
|||
subtype_xmlid='mail.mt_comment'
|
||||
)
|
||||
|
||||
project.can_edit_stage_in_approval = False
|
||||
|
||||
def project_back_button(self):
|
||||
"""Revert project to previous stage"""
|
||||
for project in self:
|
||||
for project in self.sudo():
|
||||
|
||||
project.can_edit_stage_in_approval = True
|
||||
prev_stage = self.env["project.project.stage"].search([
|
||||
('sequence', '<', project.stage_id.sequence),
|
||||
('id', 'in', project.showable_stage_ids.ids)
|
||||
|
|
@ -608,6 +732,8 @@ class ProjectProject(models.Model):
|
|||
project.stage_id = prev_stage
|
||||
project.message_post(body=activity_log)
|
||||
|
||||
project.can_edit_stage_in_approval = False
|
||||
|
||||
def action_open_reject_wizard(self):
|
||||
"""Open rejection wizard for projects"""
|
||||
self.ensure_one()
|
||||
|
|
@ -761,6 +887,18 @@ class ProjectProject(models.Model):
|
|||
if project.discuss_channel_id:
|
||||
project._add_project_members_to_channel()
|
||||
|
||||
if any(field in vals for field in ['stage_id']):
|
||||
for project in self:
|
||||
if project.assign_approval_flow:
|
||||
if not project.can_edit_stage_in_approval:
|
||||
raise UserError(_(
|
||||
"This project uses Approval Flow.\n"
|
||||
"Stage cannot be changed from Kanban view.\n"
|
||||
"Please use the action buttons to move through stages."
|
||||
))
|
||||
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -812,7 +950,7 @@ class ProjectProject(models.Model):
|
|||
members are users who can have an access to
|
||||
the tasks related to this project."""
|
||||
)
|
||||
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True,
|
||||
user_id = fields.Many2one('res.users', string='Project Manager', default=False, tracking=True,
|
||||
domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_manager').user_ids.ids),('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],)
|
||||
|
||||
type_ids = fields.Many2many(default=lambda self: self._default_type_ids())
|
||||
|
|
@ -822,6 +960,8 @@ class ProjectProject(models.Model):
|
|||
task_estimated_hours = fields.Float(string="Task Estimated Hours", compute="_compute_task_estimated_hours", store=True)
|
||||
actual_hours = fields.Float(string="Actual Hours", compute="_compute_actual_hours", store=True)
|
||||
|
||||
|
||||
|
||||
@api.depends('task_ids.estimated_hours')
|
||||
def _compute_task_estimated_hours(self):
|
||||
for project in self:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,702 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.addons.test_convert.tests.test_env import record
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from markupsafe import Markup
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
|
||||
class ProjectProject(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
# Financial tracking fields
|
||||
enable_budget_summary = fields.Boolean(default=False)
|
||||
project_cost = fields.Float(
|
||||
string="Project Cost (Client)",
|
||||
help="Total cost quoted to the client for this project"
|
||||
)
|
||||
actual_cost = fields.Float(
|
||||
string="Actual Cost",
|
||||
compute="_compute_actual_cost",
|
||||
store=True,
|
||||
help="Total actual cost incurred for the project"
|
||||
)
|
||||
profit = fields.Float(
|
||||
string="Profit",
|
||||
compute="_compute_profit_loss",
|
||||
store=True,
|
||||
help="Project profit (Project Cost - Actual Cost)"
|
||||
)
|
||||
loss = fields.Float(
|
||||
string="Loss",
|
||||
compute="_compute_profit_loss",
|
||||
store=True,
|
||||
help="Project loss (Actual Cost - Project Cost)"
|
||||
)
|
||||
difference = fields.Float(
|
||||
string="Difference",
|
||||
compute="_compute_profit_loss",
|
||||
store=True,
|
||||
help="Difference between project cost and actual cost"
|
||||
)
|
||||
resource_actual_cost_ids = fields.One2many(
|
||||
'project.resource.actual.cost',
|
||||
'project_id',
|
||||
string="Resource Actual Costs"
|
||||
)
|
||||
|
||||
# External costs field
|
||||
external_cost_ids = fields.One2many(
|
||||
'project.external.cost',
|
||||
'project_id',
|
||||
string="External Costs"
|
||||
)
|
||||
total_external_costs = fields.Float(
|
||||
string="Actual External Costs",
|
||||
compute="_compute_total_external_costs",
|
||||
store=True
|
||||
)
|
||||
|
||||
total_resource_actual_costs = fields.Float(
|
||||
string="Manpower Cost",
|
||||
compute="_compute_total_manpower_costs",
|
||||
store=True
|
||||
)
|
||||
estimated_external_cost = fields.Monetary(
|
||||
string="Estimated External Cost",
|
||||
compute="_compute_estimated_external_cost",
|
||||
currency_field="currency_id",
|
||||
store=True
|
||||
)
|
||||
profit_percentage = fields.Float(
|
||||
string="Profit %",
|
||||
compute="_compute_profit_percentage",
|
||||
help="Profit as a percentage of the project cost"
|
||||
)
|
||||
|
||||
# Loss Percentage
|
||||
loss_percentage = fields.Float(
|
||||
string="Loss %",
|
||||
compute="_compute_loss_percentage",
|
||||
help="Loss as a percentage of the project cost"
|
||||
)
|
||||
|
||||
def action_show_budget_summary(self):
|
||||
for project in self:
|
||||
project.enable_budget_summary = not project.enable_budget_summary
|
||||
|
||||
@api.depends('profit', 'project_cost')
|
||||
def _compute_profit_percentage(self):
|
||||
for project in self:
|
||||
if project.project_cost:
|
||||
project.profit_percentage = (project.profit / project.project_cost) * 100
|
||||
else:
|
||||
project.profit_percentage = 0.0
|
||||
|
||||
@api.depends('loss', 'project_cost')
|
||||
def _compute_loss_percentage(self):
|
||||
for project in self:
|
||||
if project.project_cost:
|
||||
project.loss_percentage = (project.loss / project.project_cost) * 100
|
||||
else:
|
||||
project.loss_percentage = 0.0
|
||||
|
||||
@api.depends(
|
||||
'initial_estimated_material_cost',
|
||||
'initial_estimated_equiipment_cost'
|
||||
)
|
||||
def _compute_estimated_external_cost(self):
|
||||
for project in self:
|
||||
project.estimated_external_cost = (
|
||||
project.initial_estimated_material_cost +
|
||||
project.initial_estimated_equiipment_cost
|
||||
)
|
||||
|
||||
@api.depends('resource_actual_cost_ids.total_cost')
|
||||
def _compute_total_manpower_costs(self):
|
||||
for project in self:
|
||||
project.total_resource_actual_costs = sum(project.resource_actual_cost_ids.mapped('total_cost'))
|
||||
|
||||
@api.depends('project_cost', 'actual_cost')
|
||||
def _compute_profit_loss(self):
|
||||
for project in self:
|
||||
project.difference = project.project_cost - project.actual_cost
|
||||
if project.difference >= 0:
|
||||
project.profit = project.difference
|
||||
project.loss = 0.0
|
||||
else:
|
||||
project.profit = 0.0
|
||||
project.loss = -project.difference
|
||||
|
||||
@api.depends('external_cost_ids.total_cost')
|
||||
def _compute_total_external_costs(self):
|
||||
for project in self:
|
||||
project.total_external_costs = sum(project.external_cost_ids.mapped('total_cost'))
|
||||
|
||||
@api.depends('total_external_costs', 'total_resource_actual_costs')
|
||||
def _compute_actual_cost(self):
|
||||
for project in self:
|
||||
project.actual_cost = project.total_external_costs + project.total_resource_actual_costs
|
||||
|
||||
def action_update_resource_actual_costs(self):
|
||||
"""Update actual costs based on timesheets and contract information"""
|
||||
for project in self:
|
||||
# Get all users from project members
|
||||
project_members = project.members_ids
|
||||
|
||||
# Get all users who have timesheets in the project but are not in members_ids
|
||||
timesheet_users = self.env['res.users']
|
||||
timesheets = self.env['account.analytic.line'].search([
|
||||
('project_id', '=', project.id),
|
||||
('task_id', 'in', project.task_ids.ids)
|
||||
])
|
||||
|
||||
for ts in timesheets:
|
||||
if ts.user_id and ts.user_id not in project_members:
|
||||
timesheet_users |= ts.user_id
|
||||
|
||||
# Combine both sets of users
|
||||
all_users = project_members | timesheet_users
|
||||
|
||||
# For each user, compute the total cost
|
||||
for user in all_users:
|
||||
employee = user.employee_id
|
||||
if not employee:
|
||||
continue
|
||||
|
||||
# Get timesheets for this user in the project
|
||||
user_timesheets = timesheets.filtered(lambda ts: ts.user_id == user)
|
||||
total_hours = 0
|
||||
total_cost = 0.0
|
||||
|
||||
# Get all contracts for the employee that overlap with project dates
|
||||
project_start = project.date_start or fields.Date.today()
|
||||
project_end = project.date or fields.Date.today()
|
||||
|
||||
contracts = self.env['hr.contract'].search([
|
||||
('employee_id', '=', employee.id),
|
||||
('state', 'in', ['open','close']),
|
||||
], order='date_start asc')
|
||||
|
||||
# For each timesheet entry, find the applicable contract
|
||||
for ts in user_timesheets:
|
||||
ts_date = ts.date
|
||||
applicable_contract = None
|
||||
|
||||
# Find the contract that was active on the timesheet date
|
||||
for contract in contracts:
|
||||
if (contract.date_start <= ts_date and
|
||||
(not contract.date_end or contract.date_end >= ts_date)):
|
||||
applicable_contract = contract
|
||||
break
|
||||
|
||||
if applicable_contract and applicable_contract.wage:
|
||||
# Convert monthly wage to hourly rate
|
||||
# We assume 8 hours per day and 30 days per month -> 240 hours per month
|
||||
hourly_rate = applicable_contract.wage / 240
|
||||
cost = ts.unit_amount * hourly_rate
|
||||
total_cost += cost
|
||||
total_hours += ts.unit_amount
|
||||
else:
|
||||
# If no contract found, just track hours
|
||||
total_hours += ts.unit_amount
|
||||
|
||||
# Determine if this is a planned resource
|
||||
is_planned = user in project_members
|
||||
|
||||
# Check if a record already exists for this employee
|
||||
existing_record = project.resource_actual_cost_ids.filtered(lambda r: r.employee_id == employee)
|
||||
|
||||
if existing_record:
|
||||
# Update the existing record
|
||||
record = existing_record[0]
|
||||
record.write({
|
||||
'planned_resource': is_planned,
|
||||
'total_hours': total_hours,
|
||||
'total_cost': total_cost,
|
||||
})
|
||||
|
||||
# Update contract periods
|
||||
record.contract_period_ids.unlink()
|
||||
if total_hours > 0:
|
||||
record._create_contract_periods()
|
||||
else:
|
||||
# Create a new resource actual cost record
|
||||
self.env['project.resource.actual.cost'].create({
|
||||
'project_id': project.id,
|
||||
'employee_id': employee.id,
|
||||
'planned_resource': is_planned,
|
||||
'cost_based_on': 'timesheets', # Always set to timesheets initially
|
||||
'total_hours': total_hours,
|
||||
'total_cost': total_cost,
|
||||
})
|
||||
|
||||
# Add methods for external costs
|
||||
def action_create_external_cost(self):
|
||||
"""Create a new external cost for the project"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Create External Cost'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'project.external.cost.wizard',
|
||||
'view_mode': 'form',
|
||||
'context': {'default_project_id': self.id},
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_view_external_costs(self):
|
||||
"""View all external costs for the project"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('External Costs'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'project.external.cost',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('project_id', '=', self.id)],
|
||||
'context': {'default_project_id': self.id},
|
||||
}
|
||||
|
||||
|
||||
class ProjectResourceActualCost(models.Model):
|
||||
_name = 'project.resource.actual.cost'
|
||||
_description = 'Project Resource Actual Cost'
|
||||
|
||||
project_id = fields.Many2one(
|
||||
'project.project',
|
||||
string='Project',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Employee',
|
||||
required=True
|
||||
)
|
||||
planned_resource = fields.Boolean(
|
||||
string="Planned Resource",
|
||||
default=False,
|
||||
help="True if the resource was part of the planned project team"
|
||||
)
|
||||
cost_based_on = fields.Selection([
|
||||
("timesheets", "Timesheets"),
|
||||
("manual", "Manual")
|
||||
], default='manual', string="Cost Based On", required=True)
|
||||
estimated_hours = fields.Float(string="Assigned Estimated Hours", compute="_compute_employee_estimated_hours")
|
||||
total_hours = fields.Float(
|
||||
string='Total Hours',
|
||||
help="Total hours worked by the employee on this project"
|
||||
)
|
||||
total_cost = fields.Float(
|
||||
string='Total Cost',
|
||||
help="Total cost for the employee based on timesheets and contracts"
|
||||
)
|
||||
|
||||
# Contract period information
|
||||
contract_period_ids = fields.One2many(
|
||||
'project.resource.contract.period',
|
||||
'resource_cost_id',
|
||||
string='Contract Periods'
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'project_id',
|
||||
'employee_id',
|
||||
'project_id.task_ids.show_approval_flow',
|
||||
'project_id.task_ids.assignees_timelines.assigned_to',
|
||||
'project_id.task_ids.assignees_timelines.estimated_time_readonly',
|
||||
)
|
||||
def _compute_employee_estimated_hours(self):
|
||||
for rec in self:
|
||||
rec.estimated_hours = 0.0
|
||||
|
||||
if not rec.project_id or not rec.employee_id:
|
||||
continue
|
||||
|
||||
# 1️⃣ Filter tasks with approval flow
|
||||
tasks = rec.project_id.task_ids.filtered(
|
||||
lambda t: t.show_approval_flow and t.state not in ['1_canceled','04_waiting_normal']
|
||||
)
|
||||
# 2️⃣ Collect matching assignee timelines
|
||||
timelines = tasks.mapped('assignees_timelines').filtered(
|
||||
lambda tl: tl.assigned_to
|
||||
and tl.assigned_to == rec.employee_id.user_id
|
||||
)
|
||||
|
||||
# 3️⃣ Sum estimated hours
|
||||
rec.estimated_hours = sum(
|
||||
timelines.mapped('estimated_time')
|
||||
)
|
||||
|
||||
@api.depends('cost_based_on')
|
||||
def _compute_readonly_total_cost(self):
|
||||
for record in self:
|
||||
record.readonly_total_cost = record.cost_based_on == 'timesheets'
|
||||
|
||||
readonly_total_cost = fields.Boolean(
|
||||
compute='_compute_readonly_total_cost',
|
||||
invisible=True
|
||||
)
|
||||
|
||||
@api.depends('project_id','employee_id','total_cost')
|
||||
def _compute_display_name(self):
|
||||
for resource in self:
|
||||
resource.display_name = (f"{resource.project_id.name} ({resource.employee_id.name} ₹{resource.total_cost:.2f})") if resource.total_cost else (f"{resource.project_id.name} ({resource.employee_id.name})")
|
||||
|
||||
|
||||
# Override the create method to create contract periods
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
if record.cost_based_on == 'timesheets' and record.total_hours > 0:
|
||||
record._create_contract_periods()
|
||||
return records
|
||||
|
||||
def _create_contract_periods(self):
|
||||
"""Create contract period records for this resource cost"""
|
||||
self.ensure_one()
|
||||
|
||||
# Get all timesheet entries for the employee in the project
|
||||
timesheets = self.env['account.analytic.line'].search([
|
||||
('project_id', '=', self.project_id.id),
|
||||
('task_id', 'in', self.project_id.task_ids.ids),
|
||||
('employee_id', '=', self.employee_id.id)
|
||||
])
|
||||
|
||||
# Group timesheets by contract
|
||||
contract_timesheets = {}
|
||||
for ts in timesheets:
|
||||
ts_date = ts.date
|
||||
|
||||
# Find the contract that was active on the timesheet date
|
||||
project_start = self.project_id.date_start or fields.Date.today()
|
||||
project_end = self.project_id.date or fields.Date.today()
|
||||
|
||||
contracts = self.env['hr.contract'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('state', 'in', ['open','close']),
|
||||
], order='date_start asc')
|
||||
|
||||
applicable_contract = None
|
||||
for contract in contracts:
|
||||
if (contract.date_start <= ts_date and
|
||||
(not contract.date_end or contract.date_end >= ts_date)):
|
||||
applicable_contract = contract
|
||||
break
|
||||
|
||||
if applicable_contract:
|
||||
if applicable_contract not in contract_timesheets:
|
||||
contract_timesheets[applicable_contract] = []
|
||||
contract_timesheets[applicable_contract].append(ts)
|
||||
|
||||
# Create contract period records
|
||||
for contract, ts_list in contract_timesheets.items():
|
||||
total_hours = sum(ts.unit_amount for ts in ts_list)
|
||||
|
||||
# Calculate cost based on contract wage
|
||||
hourly_rate = contract.wage / 240 # 8 hours/day * 30 days/month
|
||||
total_cost = total_hours * hourly_rate
|
||||
|
||||
# Get the earliest and latest timesheet dates for this contract
|
||||
dates = [ts.date for ts in ts_list]
|
||||
start_date = min(dates)
|
||||
end_date = max(dates)
|
||||
|
||||
self.env['project.resource.contract.period'].create({
|
||||
'resource_cost_id': self.id,
|
||||
'contract_id': contract.id,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'hours_worked': total_hours,
|
||||
'cost_incurred': total_cost,
|
||||
})
|
||||
|
||||
def update_from_timesheets(self):
|
||||
"""Update this resource cost record from timesheets"""
|
||||
self.ensure_one()
|
||||
|
||||
# Get all timesheet entries for the employee in the project
|
||||
timesheets = self.env['account.analytic.line'].search([
|
||||
('project_id', '=', self.project_id.id),
|
||||
('task_id', 'in', self.project_id.task_ids.ids),
|
||||
('employee_id', '=', self.employee_id.id)
|
||||
])
|
||||
|
||||
total_hours = 0
|
||||
total_cost = 0.0
|
||||
|
||||
# Get all contracts for the employee that overlap with project dates
|
||||
project_start = self.project_id.date_start or fields.Date.today()
|
||||
project_end = self.project_id.date or fields.Date.today()
|
||||
|
||||
contracts = self.env['hr.contract'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('state', 'in', ['open','close']),
|
||||
], order='date_start asc')
|
||||
|
||||
# For each timesheet entry, find the applicable contract
|
||||
for ts in timesheets:
|
||||
ts_date = ts.date
|
||||
applicable_contract = None
|
||||
|
||||
# Find the contract that was active on the timesheet date
|
||||
for contract in contracts:
|
||||
if (contract.date_start <= ts_date and
|
||||
(not contract.date_end or contract.date_end >= ts_date)):
|
||||
applicable_contract = contract
|
||||
break
|
||||
|
||||
if applicable_contract and applicable_contract.wage:
|
||||
# Convert monthly wage to hourly rate
|
||||
# We assume 8 hours per day and 30 days per month -> 240 hours per month
|
||||
hourly_rate = applicable_contract.wage / 240
|
||||
cost = ts.unit_amount * hourly_rate
|
||||
total_cost += cost
|
||||
total_hours += ts.unit_amount
|
||||
else:
|
||||
# If no contract found, just track hours
|
||||
total_hours += ts.unit_amount
|
||||
|
||||
# Update the record
|
||||
self.write({
|
||||
'cost_based_on': 'timesheets',
|
||||
'total_hours': total_hours,
|
||||
'total_cost': total_cost,
|
||||
})
|
||||
|
||||
# Update contract periods
|
||||
self.contract_period_ids.unlink()
|
||||
if total_hours > 0:
|
||||
self._create_contract_periods()
|
||||
|
||||
|
||||
class ProjectResourceContractPeriod(models.Model):
|
||||
_name = 'project.resource.contract.period'
|
||||
_description = 'Project Resource Contract Period'
|
||||
|
||||
resource_cost_id = fields.Many2one(
|
||||
'project.resource.actual.cost',
|
||||
string='Resource Cost',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
contract_id = fields.Many2one(
|
||||
'hr.contract',
|
||||
string='Contract',
|
||||
required=True
|
||||
)
|
||||
start_date = fields.Date(
|
||||
string='Start Date',
|
||||
required=True
|
||||
)
|
||||
end_date = fields.Date(
|
||||
string='End Date'
|
||||
)
|
||||
hours_worked = fields.Float(
|
||||
string='Hours Worked'
|
||||
)
|
||||
cost_incurred = fields.Float(
|
||||
string='Cost Incurred'
|
||||
)
|
||||
|
||||
@api.depends('contract_id', 'hours_worked', 'cost_incurred')
|
||||
def _compute_display_name(self):
|
||||
for contract in self:
|
||||
if contract.hours_worked and contract.cost_incurred:
|
||||
hours = int(contract.hours_worked)
|
||||
minutes = int(round((contract.hours_worked - hours) * 60))
|
||||
time_str = f"{hours:02d}:{minutes:02d}"
|
||||
|
||||
contract.display_name = (
|
||||
f"{time_str} (Hours) ₹{contract.cost_incurred:.2f}"
|
||||
)
|
||||
else:
|
||||
contract.display_name = contract.contract_id.display_name
|
||||
|
||||
|
||||
class ProjectExternalCost(models.Model):
|
||||
_name = 'project.external.cost'
|
||||
_description = 'Project External Costs'
|
||||
_order = 'date_start desc, id desc'
|
||||
|
||||
project_id = fields.Many2one(
|
||||
'project.project',
|
||||
string='Project',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
name = fields.Char(
|
||||
string='Description',
|
||||
required=True
|
||||
)
|
||||
cost_type = fields.Selection([
|
||||
('hourly', 'Hourly Rate'),
|
||||
('daily', 'Daily Rate'),
|
||||
('fixed', 'Fixed Cost'),
|
||||
('material', 'Material Purchase'),
|
||||
('rental', 'Equipment Rental'),
|
||||
('service', 'External Service'),
|
||||
], string='Cost Type', required=True, default='fixed')
|
||||
|
||||
# Vendor information
|
||||
vendor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Vendor',
|
||||
domain="[('supplier_rank', '>', 0)]"
|
||||
)
|
||||
vendor_reference = fields.Char(
|
||||
string='Vendor Reference'
|
||||
)
|
||||
|
||||
# Cost calculation fields
|
||||
unit_price = fields.Float(
|
||||
string='Unit Price',
|
||||
required=True
|
||||
)
|
||||
quantity = fields.Float(
|
||||
string='Quantity',
|
||||
default=1.0,
|
||||
help="Number of hours, days, or units"
|
||||
)
|
||||
total_cost = fields.Float(
|
||||
string='Total Cost',
|
||||
compute='_compute_total_cost',
|
||||
store=True
|
||||
)
|
||||
|
||||
# Date fields for time-based costs
|
||||
date_start = fields.Date(
|
||||
string='Start Date',
|
||||
default=fields.Date.today
|
||||
)
|
||||
date_end = fields.Date(
|
||||
string='End Date'
|
||||
)
|
||||
|
||||
# Material specific fields
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product/Material'
|
||||
)
|
||||
uom_id = fields.Many2one(
|
||||
'uom.uom',
|
||||
string='Unit of Measure'
|
||||
)
|
||||
|
||||
# Document attachment
|
||||
invoice_id = fields.Many2one(
|
||||
'account.move',
|
||||
string='Vendor Bill'
|
||||
)
|
||||
attachment_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'project_external_cost_attachment_rel',
|
||||
'cost_id',
|
||||
'attachment_id',
|
||||
string='Attachments'
|
||||
)
|
||||
|
||||
# Status tracking
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('confirmed', 'Confirmed'),
|
||||
('billed', 'Billed'),
|
||||
('paid', 'Paid'),
|
||||
('cancelled', 'Cancelled'),
|
||||
], string='Status', default='draft', tracking=True)
|
||||
|
||||
notes = fields.Text(
|
||||
string='Notes'
|
||||
)
|
||||
|
||||
@api.depends('unit_price', 'quantity')
|
||||
def _compute_total_cost(self):
|
||||
for cost in self:
|
||||
cost.total_cost = cost.unit_price * cost.quantity
|
||||
|
||||
def action_confirm(self):
|
||||
"""Confirm the external cost"""
|
||||
self.write({'state': 'confirmed'})
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancel the external cost"""
|
||||
self.write({'state': 'cancelled'})
|
||||
|
||||
def action_mark_as_billed(self):
|
||||
"""Mark the cost as billed"""
|
||||
self.write({'state': 'billed'})
|
||||
|
||||
def action_mark_as_paid(self):
|
||||
"""Mark the cost as paid"""
|
||||
self.write({'state': 'paid'})
|
||||
|
||||
|
||||
class ProjectExternalCostWizard(models.TransientModel):
|
||||
_name = 'project.external.cost.wizard'
|
||||
_description = 'Project External Cost Wizard'
|
||||
|
||||
project_id = fields.Many2one(
|
||||
'project.project',
|
||||
string='Project',
|
||||
required=True
|
||||
)
|
||||
name = fields.Char(
|
||||
string='Description',
|
||||
required=True
|
||||
)
|
||||
cost_type = fields.Selection([
|
||||
('hourly', 'Hourly Rate'),
|
||||
('daily', 'Daily Rate'),
|
||||
('fixed', 'Fixed Cost'),
|
||||
('material', 'Material Purchase'),
|
||||
('rental', 'Equipment Rental'),
|
||||
('service', 'External Service'),
|
||||
], string='Cost Type', required=True, default='fixed')
|
||||
unit_price = fields.Float(
|
||||
string='Unit Price',
|
||||
required=True
|
||||
)
|
||||
quantity = fields.Float(
|
||||
string='Quantity',
|
||||
default=1.0
|
||||
)
|
||||
vendor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Vendor',
|
||||
domain="[('supplier_rank', '>', 0)]"
|
||||
)
|
||||
date_start = fields.Date(
|
||||
string='Start Date',
|
||||
default=fields.Date.today
|
||||
)
|
||||
date_end = fields.Date(
|
||||
string='End Date'
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Notes'
|
||||
)
|
||||
|
||||
def action_create_cost(self):
|
||||
"""Create the external cost"""
|
||||
self.ensure_one()
|
||||
cost_vals = {
|
||||
'project_id': self.project_id.id,
|
||||
'name': self.name,
|
||||
'cost_type': self.cost_type,
|
||||
'unit_price': self.unit_price,
|
||||
'quantity': self.quantity,
|
||||
'vendor_id': self.vendor_id.id,
|
||||
'date_start': self.date_start,
|
||||
'date_end': self.date_end,
|
||||
'notes': self.notes,
|
||||
}
|
||||
|
||||
# Set UOM based on cost type
|
||||
if self.cost_type == 'hourly':
|
||||
cost_vals['uom_id'] = self.env.ref('uom.product_uom_hour').id
|
||||
elif self.cost_type == 'daily':
|
||||
cost_vals['uom_id'] = self.env.ref('uom.product_uom_day').id
|
||||
|
||||
self.env['project.external.cost'].create(cost_vals)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from odoo import fields, models, api, _
|
||||
|
||||
class projectAttachments(models.Model):
|
||||
_name = 'project.attachments'
|
||||
|
||||
project_id = fields.Many2one("project.project", string="Project", ondelete="cascade")
|
||||
|
||||
file = fields.Binary(string="Attachment File", required=True)
|
||||
file_name = fields.Char(string="Filename")
|
||||
notes = fields.Text(string="Notes")
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
from odoo import models, fields, api, _
|
||||
from datetime import date, timedelta
|
||||
import json
|
||||
|
||||
|
||||
class ProjectPortfolio(models.Model):
|
||||
_name = 'project.portfolio'
|
||||
_description = 'Project Portfolio'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string='Portfolio Name', required=True)
|
||||
code = fields.Char(string='Code')
|
||||
|
||||
industry_id = fields.Many2one(
|
||||
'res.partner.industry',
|
||||
string='Industry'
|
||||
)
|
||||
|
||||
owner_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Portfolio Owner'
|
||||
)
|
||||
|
||||
description = fields.Text(string='Description')
|
||||
|
||||
project_ids = fields.One2many(
|
||||
'project.project',
|
||||
'portfolio_id',
|
||||
string='Projects'
|
||||
)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
default=lambda self: self.env.company,
|
||||
required=True
|
||||
)
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
project_count = fields.Integer(
|
||||
compute='_compute_project_count',
|
||||
string='Projects'
|
||||
)
|
||||
|
||||
# Budget Summary Fields
|
||||
total_estimated_budget = fields.Float(
|
||||
string="Total Planned Budget",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Sum of all estimated project budgets (internal estimates)"
|
||||
)
|
||||
|
||||
total_client_budget = fields.Float(
|
||||
string="Total Client Budget",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Sum of all client-approved budgets"
|
||||
)
|
||||
|
||||
total_actual_budget = fields.Float(
|
||||
string="Total Actual Budget",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Sum of all actual project costs incurred to date"
|
||||
)
|
||||
|
||||
# Variance calculations based on internal estimates
|
||||
budget_variance = fields.Float(
|
||||
string="Budget Variance (Est. vs Actual)",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Difference between estimated and actual budget"
|
||||
)
|
||||
budget_variance_percent = fields.Float(
|
||||
string="Budget Variance % (Est. vs Actual)",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Percentage difference between estimated and actual budget"
|
||||
)
|
||||
|
||||
# Variance calculations based on client budgets
|
||||
client_budget_variance = fields.Float(
|
||||
string="Client Budget Variance",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Difference between client budget and actual costs"
|
||||
)
|
||||
client_budget_variance_percent = fields.Float(
|
||||
string="Client Budget Variance %",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Percentage difference between client budget and actual costs"
|
||||
)
|
||||
|
||||
#variance calculations based on Client budgets vs Estimated Budgets
|
||||
planned_client_budget_variance = fields.Float(
|
||||
string="Planned Budget Variance",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Difference between client budget and actual costs"
|
||||
)
|
||||
planned_client_budget_variance_percent = fields.Float(
|
||||
string="Planned Budget Variance %",
|
||||
compute="_compute_budget_totals",
|
||||
store=True,
|
||||
help="Percentage difference between client budget and actual costs"
|
||||
)
|
||||
|
||||
budget_status = fields.Selection([
|
||||
('under', 'Under Budget'),
|
||||
('on_track', 'On Track'),
|
||||
('over', 'Over Budget')
|
||||
], compute="_compute_budget_totals", store=True)
|
||||
|
||||
# Financial KPIs
|
||||
total_resource_cost = fields.Float(
|
||||
compute="_compute_budget_totals",
|
||||
store=True
|
||||
)
|
||||
total_material_cost = fields.Float(
|
||||
compute="_compute_budget_totals",
|
||||
store=True
|
||||
)
|
||||
total_equipment_cost = fields.Float(
|
||||
compute="_compute_budget_totals",
|
||||
store=True
|
||||
)
|
||||
|
||||
# Profit/Loss Fields
|
||||
total_profit = fields.Float(
|
||||
string="Total Profit",
|
||||
compute="_compute_budget_totals",
|
||||
store=True
|
||||
)
|
||||
total_loss = fields.Float(
|
||||
string="Total Loss",
|
||||
compute="_compute_budget_totals",
|
||||
store=True
|
||||
)
|
||||
net_profit = fields.Float(
|
||||
string="Net Profit/Loss",
|
||||
compute="_compute_budget_totals",
|
||||
store=True
|
||||
)
|
||||
|
||||
roi_estimate = fields.Float(
|
||||
string="Estimation ROI (%)",
|
||||
compute="_compute_roi",
|
||||
store=True,
|
||||
help="Accuracy of internal estimates vs actual costs"
|
||||
)
|
||||
|
||||
planned_roi_estimate = fields.Float(
|
||||
string="Planned ROI (%)",
|
||||
compute="_compute_roi",
|
||||
store=True,
|
||||
help="Expected ROI based on client budget vs estimated costs"
|
||||
)
|
||||
|
||||
actual_roi_estimate = fields.Float(
|
||||
string="Actual ROI (%)",
|
||||
compute="_compute_roi",
|
||||
store=True,
|
||||
help="Actual ROI achieved based on client budget vs actual costs"
|
||||
)
|
||||
|
||||
# Dashboard Data
|
||||
dashboard_graph_data = fields.Text(
|
||||
string="Graph Data",
|
||||
compute="_compute_dashboard_data"
|
||||
)
|
||||
|
||||
# Employee Performance Summary
|
||||
employee_performance_ids = fields.One2many(
|
||||
'project.portfolio.employee.performance',
|
||||
'portfolio_id',
|
||||
string='Employee Performance'
|
||||
)
|
||||
|
||||
# Performance Metrics
|
||||
avg_time_variance = fields.Float(
|
||||
string="Average Time Variance",
|
||||
compute="_compute_performance_metrics",
|
||||
help="Average variance between estimated and actual hours"
|
||||
)
|
||||
on_time_completion_rate = fields.Float(
|
||||
string="On-Time Completion%",
|
||||
compute="_compute_performance_metrics",
|
||||
help="Percentage of tasks count that are completed on or before estimated time"
|
||||
)
|
||||
overall_efficiency = fields.Float(
|
||||
string="Overall Efficiency %",
|
||||
compute="_compute_performance_metrics",
|
||||
help="Overall project efficiency (Estimated/Actual hours)"
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'project_ids.estimated_amount', # Internal estimate
|
||||
'project_ids.project_cost', # Client-approved budget
|
||||
'project_ids.actual_cost', # Actual costs to date
|
||||
'project_ids.total_resource_actual_costs',
|
||||
'project_ids.total_external_costs',
|
||||
'project_ids.profit',
|
||||
'project_ids.loss',
|
||||
'project_ids.difference'
|
||||
)
|
||||
def _compute_budget_totals(self):
|
||||
for portfolio in self:
|
||||
projects = portfolio.project_ids.filtered(lambda p: p.active)
|
||||
|
||||
# Budget Summary
|
||||
portfolio.total_estimated_budget = sum(projects.mapped('estimated_amount'))
|
||||
portfolio.total_client_budget = sum(projects.mapped('project_cost'))
|
||||
portfolio.total_actual_budget = sum(projects.mapped('actual_cost'))
|
||||
portfolio.total_profit = sum(projects.mapped('profit'))
|
||||
portfolio.total_loss = sum(projects.mapped('loss'))
|
||||
portfolio.net_profit = portfolio.total_profit - portfolio.total_loss
|
||||
|
||||
# Internal Estimate Variance (Est. vs Actual)
|
||||
portfolio.budget_variance = portfolio.total_estimated_budget - portfolio.total_actual_budget
|
||||
if portfolio.total_estimated_budget:
|
||||
portfolio.budget_variance_percent = (
|
||||
(portfolio.budget_variance / portfolio.total_estimated_budget) * 100
|
||||
)
|
||||
else:
|
||||
portfolio.budget_variance_percent = 0.0
|
||||
|
||||
# Client Budget Variance (Client vs Actual)
|
||||
portfolio.client_budget_variance = portfolio.total_client_budget - portfolio.total_actual_budget
|
||||
if portfolio.total_client_budget:
|
||||
portfolio.client_budget_variance_percent = (
|
||||
(portfolio.client_budget_variance / portfolio.total_client_budget) * 100
|
||||
)
|
||||
else:
|
||||
portfolio.client_budget_variance_percent = 0.0
|
||||
|
||||
#planned Client Budget Variance (Client vs Est.)
|
||||
portfolio.planned_client_budget_variance = portfolio.total_client_budget - portfolio.total_estimated_budget
|
||||
if portfolio.total_actual_budget:
|
||||
portfolio.planned_client_budget_variance_percent = (
|
||||
(portfolio.planned_client_budget_variance / portfolio.total_client_budget) * 100
|
||||
)
|
||||
else:
|
||||
portfolio.planned_client_budget_variance_percent = 0.0
|
||||
|
||||
# Status based on internal estimates
|
||||
if portfolio.budget_variance_percent > 5:
|
||||
portfolio.budget_status = 'under' # under cost → good
|
||||
elif -5 <= portfolio.budget_variance_percent <= 5:
|
||||
portfolio.budget_status = 'on_track'
|
||||
else:
|
||||
portfolio.budget_status = 'over' # over cost → bad
|
||||
|
||||
# Cost split
|
||||
portfolio.total_resource_cost = sum(
|
||||
projects.mapped('total_resource_actual_costs')
|
||||
)
|
||||
portfolio.total_material_cost = 0.0 # deprecated
|
||||
portfolio.total_equipment_cost = 0.0
|
||||
|
||||
@api.depends('total_estimated_budget', 'total_actual_budget', 'total_client_budget')
|
||||
def _compute_roi(self):
|
||||
for portfolio in self:
|
||||
# Estimation Accuracy: How accurate were our internal estimates?
|
||||
if portfolio.total_actual_budget:
|
||||
portfolio.roi_estimate = (
|
||||
((portfolio.total_estimated_budget - portfolio.total_actual_budget)
|
||||
/ portfolio.total_actual_budget) * 100
|
||||
)
|
||||
else:
|
||||
portfolio.roi_estimate = 0.0
|
||||
|
||||
# Planned ROI: What ROI did we expect to achieve?
|
||||
if portfolio.total_estimated_budget:
|
||||
portfolio.planned_roi_estimate = (
|
||||
((portfolio.total_client_budget - portfolio.total_estimated_budget)
|
||||
/ portfolio.total_estimated_budget) * 100
|
||||
)
|
||||
else:
|
||||
portfolio.planned_roi_estimate = 0.0
|
||||
|
||||
# Actual ROI: What ROI did we actually achieve?
|
||||
if portfolio.total_actual_budget:
|
||||
portfolio.actual_roi_estimate = (
|
||||
((portfolio.total_client_budget - portfolio.total_actual_budget)
|
||||
/ portfolio.total_actual_budget) * 100
|
||||
)
|
||||
else:
|
||||
portfolio.actual_roi_estimate = 0.0
|
||||
|
||||
def _compute_project_count(self):
|
||||
for rec in self:
|
||||
rec.project_count = len(rec.project_ids)
|
||||
|
||||
@api.depends(
|
||||
'project_ids',
|
||||
'project_ids.task_ids',
|
||||
'project_ids.task_ids.assignees_timelines'
|
||||
)
|
||||
def _compute_performance_metrics(self):
|
||||
for portfolio in self:
|
||||
# Get all tasks in portfolio projects
|
||||
tasks = self.env['project.task'].search([
|
||||
('project_id', 'in', portfolio.project_ids.ids),
|
||||
('show_approval_flow', '=', True)
|
||||
])
|
||||
|
||||
total_estimated = 0
|
||||
total_actual = 0
|
||||
completed_on_time = 0
|
||||
total_tasks = len(tasks)
|
||||
|
||||
for task in tasks:
|
||||
# Get estimated hours from assignee timelines
|
||||
estimated_hours = sum(task.assignees_timelines.mapped('estimated_time'))
|
||||
|
||||
# Get actual hours from timesheets
|
||||
actual_hours = sum(self.env['account.analytic.line'].search([
|
||||
('task_id', '=', task.id)
|
||||
]).mapped('unit_amount'))
|
||||
|
||||
total_estimated += estimated_hours
|
||||
total_actual += actual_hours
|
||||
|
||||
# Check if task was completed on time
|
||||
if actual_hours <= estimated_hours or estimated_hours == 0:
|
||||
completed_on_time += 1
|
||||
|
||||
# Calculate metrics
|
||||
if total_estimated > 0:
|
||||
portfolio.avg_time_variance = ((total_actual - total_estimated) / total_estimated) * 100
|
||||
else:
|
||||
portfolio.avg_time_variance = 0.0
|
||||
|
||||
if total_tasks > 0:
|
||||
portfolio.on_time_completion_rate = (completed_on_time / total_tasks) * 100
|
||||
else:
|
||||
portfolio.on_time_completion_rate = 0.0
|
||||
|
||||
if total_actual > 0:
|
||||
portfolio.overall_efficiency = (total_estimated / total_actual) * 100
|
||||
else:
|
||||
portfolio.overall_efficiency = 0.0
|
||||
|
||||
def _compute_dashboard_data(self):
|
||||
"""Compute JSON data for dashboard graphs"""
|
||||
for portfolio in self:
|
||||
projects = portfolio.project_ids.filtered(lambda p: p.active)
|
||||
|
||||
# Budget vs Actual Data
|
||||
budget_data = []
|
||||
for project in projects:
|
||||
budget_data.append({
|
||||
'name': project.name,
|
||||
'estimated': project.estimated_amount,
|
||||
'client_budget': project.project_cost,
|
||||
'actual': project.actual_cost,
|
||||
'profit': project.profit,
|
||||
'loss': project.loss
|
||||
})
|
||||
|
||||
# Cost Breakdown Data
|
||||
cost_data = {
|
||||
'manpower': portfolio.total_resource_cost,
|
||||
'external': sum(projects.mapped('total_external_costs')),
|
||||
'other': 0
|
||||
}
|
||||
|
||||
# Performance Data
|
||||
performance_data = {
|
||||
'avg_time_variance': portfolio.avg_time_variance,
|
||||
'on_time_rate': portfolio.on_time_completion_rate,
|
||||
'efficiency': portfolio.overall_efficiency
|
||||
}
|
||||
|
||||
# Combine all data
|
||||
dashboard_data = {
|
||||
'budget_data': budget_data,
|
||||
'cost_data': cost_data,
|
||||
'performance_data': performance_data,
|
||||
'summary': {
|
||||
'total_estimated': portfolio.total_estimated_budget,
|
||||
'total_client_budget': portfolio.total_client_budget,
|
||||
'total_actual': portfolio.total_actual_budget,
|
||||
'net_profit': portfolio.net_profit,
|
||||
'roi': portfolio.roi_estimate,
|
||||
'budget_variance': portfolio.budget_variance,
|
||||
'client_budget_variance': portfolio.client_budget_variance
|
||||
}
|
||||
}
|
||||
|
||||
portfolio.dashboard_graph_data = json.dumps(dashboard_data)
|
||||
|
||||
def action_update_employee_performance(self):
|
||||
"""Update employee performance data based on timesheets and estimates
|
||||
(only for projects with privacy_visibility = 'followers')
|
||||
"""
|
||||
AAL = self.env['account.analytic.line']
|
||||
Performance = self.env['project.portfolio.employee.performance']
|
||||
|
||||
for portfolio in self:
|
||||
# Clear existing performance records
|
||||
portfolio.employee_performance_ids.unlink()
|
||||
|
||||
# 🔹 Only consider follower-visible projects
|
||||
projects = portfolio.project_ids.filtered(
|
||||
lambda p: p.privacy_visibility == 'followers'
|
||||
)
|
||||
|
||||
if not projects:
|
||||
continue
|
||||
|
||||
# Fetch relevant timesheets
|
||||
timesheet_lines = AAL.sudo().search([
|
||||
('project_id', 'in', projects.ids),
|
||||
('task_id', '!=', False),
|
||||
('employee_id', '!=', False),
|
||||
])
|
||||
employees_data = {}
|
||||
task_ids_list = []
|
||||
for line in timesheet_lines:
|
||||
employee = line.employee_id
|
||||
task = line.task_id
|
||||
|
||||
emp_vals = employees_data.setdefault(employee.id, {
|
||||
'employee_id': employee.id,
|
||||
'total_estimated_hours': 0.0,
|
||||
'total_actual_hours': 0.0,
|
||||
'tasks_completed': 0,
|
||||
'tasks_on_time': 0,
|
||||
'total_tasks': 0,
|
||||
})
|
||||
|
||||
# 🔹 Actual hours
|
||||
emp_vals['total_actual_hours'] += line.unit_amount
|
||||
|
||||
if not task:
|
||||
continue
|
||||
|
||||
estimated_hours = 0.0
|
||||
|
||||
# 🔹 Estimate from assignee timelines
|
||||
if not task.is_generic and task.show_approval_flow and task.id not in task_ids_list:
|
||||
timelines = task.assignees_timelines.filtered(
|
||||
lambda t: t.assigned_to == employee.user_id
|
||||
)
|
||||
if task.assignees_timelines and timelines and task.id not in task_ids_list:
|
||||
estimated_hours = sum(timelines.mapped('estimated_time'))
|
||||
elif task.id not in task_ids_list:
|
||||
estimated_hours = task.estimated_hours or line.unit_amount
|
||||
# 🔹 Fallback to task estimate
|
||||
if not estimated_hours and task.id not in task_ids_list:
|
||||
estimated_hours = task.estimated_hours or line.unit_amount
|
||||
|
||||
emp_vals['total_estimated_hours'] += estimated_hours
|
||||
emp_vals['total_tasks'] += 1
|
||||
emp_vals['tasks_completed'] += 1
|
||||
|
||||
# 🔹 On-time check
|
||||
if estimated_hours == 0 or line.unit_amount <= estimated_hours:
|
||||
emp_vals['tasks_on_time'] += 1
|
||||
task_ids_list.append(task.id)
|
||||
|
||||
# 🔹 Create performance records
|
||||
for emp_vals in employees_data.values():
|
||||
total_est = emp_vals['total_estimated_hours']
|
||||
total_act = emp_vals['total_actual_hours']
|
||||
|
||||
time_variance = (
|
||||
((total_act - total_est) / total_act) * 100
|
||||
if total_est > 0 and total_act > total_est
|
||||
else 0.0
|
||||
)
|
||||
|
||||
on_time_rate = (
|
||||
(emp_vals['tasks_on_time'] / emp_vals['total_tasks']) * 100
|
||||
if emp_vals['total_tasks'] > 0
|
||||
else 0.0
|
||||
)
|
||||
|
||||
efficiency = (
|
||||
(total_est / total_act) * 100
|
||||
if total_act else 0.0
|
||||
)
|
||||
|
||||
Performance.create({
|
||||
'portfolio_id': portfolio.id,
|
||||
'employee_id': emp_vals['employee_id'],
|
||||
'total_estimated_hours': total_est,
|
||||
'total_actual_hours': total_act,
|
||||
'time_variance_percent': time_variance,
|
||||
'on_time_completion_rate': on_time_rate,
|
||||
'tasks_completed': emp_vals['tasks_completed'],
|
||||
'efficiency_rate': efficiency,
|
||||
})
|
||||
|
||||
def action_view_budget_analysis(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Budget Analysis'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'project.portfolio.dashboard',
|
||||
'view_mode': 'graph,pivot',
|
||||
'domain': [('portfolio_id', '=', self.id)],
|
||||
'context': {'search_default_group_by_project': 1}
|
||||
}
|
||||
|
||||
def action_view_employee_performance(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Employee Performance'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'project.portfolio.employee.performance',
|
||||
'view_mode': 'list,graph,pivot',
|
||||
'domain': [('portfolio_id', '=', self.id)],
|
||||
'context': {
|
||||
'default_portfolio_id': self.id,
|
||||
'search_default_group_by_employee': 1
|
||||
}
|
||||
}
|
||||
|
||||
def action_view_projects(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': 'Projects',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'project.project',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('portfolio_id', '=', self.id)],
|
||||
'context': {
|
||||
'default_portfolio_id': self.id,
|
||||
'search_default_groupby_stage_id': 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
portfolio_id = fields.Many2one(
|
||||
'project.portfolio',
|
||||
string='Portfolio'
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ProjectPortfolioEmployeePerformance(models.Model):
|
||||
_name = 'project.portfolio.employee.performance'
|
||||
_description = 'Project Portfolio Employee Performance'
|
||||
_order = 'time_variance_percent desc'
|
||||
|
||||
portfolio_id = fields.Many2one(
|
||||
'project.portfolio',
|
||||
string='Portfolio',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Employee',
|
||||
required=True
|
||||
)
|
||||
department_id = fields.Many2one(
|
||||
'hr.department',
|
||||
string='Department',
|
||||
related='employee_id.department_id',
|
||||
store=True
|
||||
)
|
||||
job_id = fields.Many2one(
|
||||
'hr.job',
|
||||
string='Job Position',
|
||||
related='employee_id.job_id',
|
||||
store=True
|
||||
)
|
||||
|
||||
# Performance Metrics
|
||||
total_estimated_hours = fields.Float(
|
||||
string='Total Estimated Hours'
|
||||
)
|
||||
total_actual_hours = fields.Float(
|
||||
string='Total Actual Hours'
|
||||
)
|
||||
time_variance = fields.Float(
|
||||
string='Time Variance (Hours)',
|
||||
compute='_compute_time_variance',
|
||||
store=True
|
||||
)
|
||||
time_variance_percent = fields.Float(
|
||||
string='Time Variance %'
|
||||
)
|
||||
on_time_completion_rate = fields.Float(
|
||||
string='On-Time Completion %'
|
||||
)
|
||||
tasks_completed = fields.Integer(
|
||||
string='Tasks Completed'
|
||||
)
|
||||
efficiency_rate = fields.Float(
|
||||
string='Efficiency Rate %',
|
||||
help='Estimated hours / Actual hours * 100'
|
||||
)
|
||||
|
||||
performance_status = fields.Selection([
|
||||
('excellent', 'Excellent (On Time)'),
|
||||
('good', 'Good (Slight Delay)'),
|
||||
('average', 'Average (Moderate Delay)'),
|
||||
('poor', 'Poor (Significant Delay)'),
|
||||
('critical', 'Critical (Major Delay)')
|
||||
], string='Performance Status',
|
||||
compute='_compute_performance_status',
|
||||
store=True,
|
||||
help="Performance classification based on time variance percentage:\n"
|
||||
"- Excellent: 0% or less (Completed on or before estimated time)\n"
|
||||
"- Good: Up to 10% delay\n"
|
||||
"- Average: 11% to 25% delay\n"
|
||||
"- Poor: 26% to 50% delay\n"
|
||||
"- Critical: More than 50% delay"
|
||||
)
|
||||
|
||||
@api.depends('time_variance_percent')
|
||||
def _compute_performance_status(self):
|
||||
for record in self:
|
||||
variance = record.time_variance_percent
|
||||
if variance <= 0:
|
||||
record.performance_status = 'excellent'
|
||||
elif variance <= 10:
|
||||
record.performance_status = 'good'
|
||||
elif variance <= 25:
|
||||
record.performance_status = 'average'
|
||||
elif variance <= 50:
|
||||
record.performance_status = 'poor'
|
||||
else:
|
||||
record.performance_status = 'critical'
|
||||
|
||||
@api.depends('total_estimated_hours', 'total_actual_hours')
|
||||
def _compute_time_variance(self):
|
||||
for record in self:
|
||||
record.time_variance = record.total_actual_hours - record.total_estimated_hours
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
from odoo import models, fields, api, _
|
||||
|
||||
class ProjectPortfolioBudgetOverview(models.Model):
|
||||
_name = 'project.portfolio.budget.overview'
|
||||
_description = 'Project Portfolio Budget Overview'
|
||||
_auto = False
|
||||
|
||||
portfolio_id = fields.Many2one('project.portfolio')
|
||||
project_id = fields.Many2one('project.project')
|
||||
|
||||
estimated_budget = fields.Float(string='Estimated Budget')
|
||||
client_budget = fields.Float(string='Client Budget')
|
||||
actual_cost = fields.Float(string='Actual Cost')
|
||||
estimated_variance = fields.Float(string='Est. vs Actual Diff')
|
||||
estimated_variance_percent = fields.Float(string='Est. vs Actual %')
|
||||
client_variance = fields.Float(string='Client vs Actual Diff')
|
||||
client_variance_percent = fields.Float(string='Client vs Actual %')
|
||||
|
||||
@property
|
||||
def _table_query(self):
|
||||
return """
|
||||
SELECT
|
||||
row_number() OVER () AS id,
|
||||
pp.id AS portfolio_id,
|
||||
prj.id AS project_id,
|
||||
prj.estimated_amount,
|
||||
prj.project_cost,
|
||||
prj.actual_cost,
|
||||
(prj.estimated_amount - prj.actual_cost) AS estimated_variance,
|
||||
CASE
|
||||
WHEN prj.estimated_amount > 0
|
||||
THEN ((prj.estimated_amount - prj.actual_cost)
|
||||
/ prj.estimated_amount) * 100
|
||||
ELSE 0
|
||||
END AS estimated_variance_percent,
|
||||
(prj.project_cost - prj.actual_cost) AS client_variance,
|
||||
CASE
|
||||
WHEN prj.project_cost > 0
|
||||
THEN ((prj.project_cost - prj.actual_cost)
|
||||
/ prj.project_cost) * 100
|
||||
ELSE 0
|
||||
END AS client_variance_percent
|
||||
FROM project_portfolio pp
|
||||
JOIN project_project prj
|
||||
ON prj.portfolio_id = pp.id
|
||||
"""
|
||||
|
||||
|
||||
class ProjectPortfolioDashboard(models.Model):
|
||||
_name = 'project.portfolio.dashboard'
|
||||
_description = 'Project Portfolio Dashboard'
|
||||
_auto = False
|
||||
|
||||
portfolio_id = fields.Many2one('project.portfolio')
|
||||
project_id = fields.Many2one('project.project')
|
||||
|
||||
estimated_budget = fields.Float(string='Estimated Budget')
|
||||
client_budget = fields.Float(string='Client Budget')
|
||||
actual_cost = fields.Float(string='Actual Cost')
|
||||
|
||||
resource_cost = fields.Float(string='Manpower Cost')
|
||||
external_cost = fields.Float(string='External Cost')
|
||||
|
||||
profit = fields.Float(string='Profit')
|
||||
loss = fields.Float(string='Loss')
|
||||
|
||||
estimated_variance = fields.Float(string='Est. vs Actual Diff')
|
||||
estimated_variance_percent = fields.Float(string='Est. vs Actual %')
|
||||
client_variance = fields.Float(string='Client vs Actual Diff')
|
||||
client_variance_percent = fields.Float(string='Client vs Actual %')
|
||||
|
||||
# Performance Fields
|
||||
estimated_hours = fields.Float(string='Estimated Hours')
|
||||
actual_hours = fields.Float(string='Actual Hours')
|
||||
time_variance_percent = fields.Float(string='Time Variance %')
|
||||
|
||||
@property
|
||||
def _table_query(self):
|
||||
return """
|
||||
SELECT
|
||||
row_number() OVER () AS id,
|
||||
pp.id AS portfolio_id,
|
||||
prj.id AS project_id,
|
||||
|
||||
prj.estimated_amount,
|
||||
prj.project_cost,
|
||||
prj.actual_cost,
|
||||
|
||||
prj.total_resource_actual_costs AS resource_cost,
|
||||
prj.total_external_costs AS external_cost,
|
||||
|
||||
prj.profit,
|
||||
prj.loss,
|
||||
(prj.estimated_amount - prj.actual_cost) AS estimated_variance,
|
||||
(prj.project_cost - prj.actual_cost) AS client_variance,
|
||||
|
||||
CASE
|
||||
WHEN prj.estimated_amount > 0
|
||||
THEN ((prj.estimated_amount - prj.actual_cost) / prj.estimated_amount) * 100
|
||||
ELSE 0
|
||||
END AS estimated_variance_percent,
|
||||
|
||||
CASE
|
||||
WHEN prj.project_cost > 0
|
||||
THEN ((prj.project_cost - prj.actual_cost) / prj.project_cost) * 100
|
||||
ELSE 0
|
||||
END AS client_variance_percent,
|
||||
|
||||
-- Performance metrics
|
||||
COALESCE((
|
||||
SELECT SUM(tl.estimated_time)
|
||||
FROM project_task pt
|
||||
JOIN project_task_time_lines tl ON tl.task_id = pt.id
|
||||
WHERE pt.project_id = prj.id
|
||||
AND pt.show_approval_flow = true
|
||||
), 0) AS estimated_hours,
|
||||
|
||||
COALESCE((
|
||||
SELECT SUM(aal.unit_amount)
|
||||
FROM project_task pt
|
||||
JOIN account_analytic_line aal ON aal.task_id = pt.id
|
||||
WHERE pt.project_id = prj.id
|
||||
), 0) AS actual_hours,
|
||||
|
||||
CASE
|
||||
WHEN COALESCE((
|
||||
SELECT SUM(tl.estimated_time)
|
||||
FROM project_task pt
|
||||
JOIN project_task_time_lines tl ON tl.task_id = pt.id
|
||||
WHERE pt.project_id = prj.id
|
||||
AND pt.show_approval_flow = true
|
||||
), 0) > 0
|
||||
THEN ((COALESCE((
|
||||
SELECT SUM(aal.unit_amount)
|
||||
FROM project_task pt
|
||||
JOIN account_analytic_line aal ON aal.task_id = pt.id
|
||||
WHERE pt.project_id = prj.id
|
||||
), 0) - COALESCE((
|
||||
SELECT SUM(tl.estimated_time)
|
||||
FROM project_task pt
|
||||
JOIN project_task_time_lines tl ON tl.task_id = pt.id
|
||||
WHERE pt.project_id = prj.id
|
||||
AND pt.show_approval_flow = true
|
||||
), 0)) / COALESCE((
|
||||
SELECT SUM(tl.estimated_time)
|
||||
FROM project_task pt
|
||||
JOIN project_task_time_lines tl ON tl.task_id = pt.id
|
||||
WHERE pt.project_id = prj.id
|
||||
AND pt.show_approval_flow = true
|
||||
), 0)) * 100
|
||||
ELSE 0
|
||||
END AS time_variance_percent
|
||||
|
||||
FROM project_portfolio pp
|
||||
JOIN project_project prj ON prj.portfolio_id = pp.id
|
||||
WHERE prj.active = true
|
||||
"""
|
||||
|
|
@ -43,6 +43,7 @@ class ProjectRole(models.Model):
|
|||
ROLE_LEVELS,
|
||||
string='Authority Level',
|
||||
required=True,
|
||||
default='administrative',
|
||||
help="Structured authority level of the role"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ class projectTask(models.Model):
|
|||
estimated_hours = fields.Float(
|
||||
string="Estimated Hours",
|
||||
compute="_compute_estimated_hours",
|
||||
inverse="_inverse_estimated_hours",
|
||||
store=True
|
||||
store=True,
|
||||
readonly=False
|
||||
)
|
||||
has_supervisor_access = fields.Boolean(compute="_compute_has_supervisor_access")
|
||||
actual_hours = fields.Float(
|
||||
|
|
@ -60,6 +60,90 @@ class projectTask(models.Model):
|
|||
compute="_compute_deadline_warning",
|
||||
string="Deadline Warning"
|
||||
)
|
||||
allowed_employee_ids = fields.Many2many(
|
||||
'hr.employee',
|
||||
compute='_compute_allowed_employee_ids',
|
||||
store=False,
|
||||
)
|
||||
assignee_domain_ids = fields.Many2many(
|
||||
'res.users',
|
||||
compute='_compute_assignee_domain',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'project_id',
|
||||
'project_id.privacy_visibility',
|
||||
'project_id.message_partner_ids',
|
||||
)
|
||||
def _compute_assignee_domain(self):
|
||||
Users = self.env['res.users']
|
||||
all_internal_users = Users.search([('share', '=', False)])
|
||||
|
||||
for task in self:
|
||||
# no project → all internal
|
||||
if not task.project_id:
|
||||
task.assignee_domain_ids = all_internal_users
|
||||
continue
|
||||
|
||||
# # GENERIC → all internal
|
||||
# if getattr(task, 'is_generic', False):
|
||||
# task.assignee_domain_ids = all_internal_users
|
||||
# continue
|
||||
|
||||
# PRIVATE → invited users only
|
||||
if task.project_id.privacy_visibility == 'followers':
|
||||
task.assignee_domain_ids = (
|
||||
task.project_id.message_partner_ids
|
||||
.mapped('user_ids')
|
||||
.filtered(lambda u: not u.share)
|
||||
)
|
||||
else:
|
||||
# INTERNAL / PUBLIC
|
||||
task.assignee_domain_ids = all_internal_users
|
||||
|
||||
@api.depends(
|
||||
'is_generic',
|
||||
'user_ids',
|
||||
'project_id',
|
||||
'project_id.privacy_visibility',
|
||||
'project_id.message_partner_ids',
|
||||
)
|
||||
def _compute_allowed_employee_ids(self):
|
||||
Employee = self.env['hr.employee']
|
||||
|
||||
for task in self:
|
||||
employees = Employee.browse()
|
||||
|
||||
# 1️⃣ GENERIC TASK
|
||||
if task.is_generic and task.project_id:
|
||||
project = task.project_id
|
||||
|
||||
# 🔐 Private → followers only
|
||||
if project.privacy_visibility == 'followers':
|
||||
users = (
|
||||
project.message_partner_ids
|
||||
.mapped('user_ids')
|
||||
.filtered(lambda u: u and not u.share)
|
||||
)
|
||||
# 🌍 Internal / Public → all internal users
|
||||
else:
|
||||
users = self.env['res.users'].search([
|
||||
('share', '=', False),
|
||||
('active', '=', True),
|
||||
])
|
||||
|
||||
employees = users.mapped('employee_id').filtered(lambda e: e)
|
||||
|
||||
# 2️⃣ NORMAL TASK → task assignees only
|
||||
else:
|
||||
employees = (
|
||||
task.user_ids
|
||||
.mapped('employee_id')
|
||||
.filtered(lambda e: e)
|
||||
)
|
||||
|
||||
task.allowed_employee_ids = employees
|
||||
|
||||
@api.depends('suggested_deadline', 'date_deadline','timelines_requested','show_approval_flow')
|
||||
def _compute_deadline_warning(self):
|
||||
|
|
@ -297,15 +381,6 @@ class projectTask(models.Model):
|
|||
if task.show_approval_flow:
|
||||
task.estimated_hours = sum(task.assignees_timelines.mapped('estimated_time'))
|
||||
|
||||
def _inverse_estimated_hours(self):
|
||||
"""Allow editing only if approval flow is disabled."""
|
||||
for task in self:
|
||||
# Only check after record is created
|
||||
if not task.id:
|
||||
continue
|
||||
|
||||
if not task.show_approval_flow:
|
||||
task.write({'estimated_hours': task.estimated_hours})
|
||||
@api.depends('timesheet_ids.unit_amount')
|
||||
def _compute_actual_hours(self):
|
||||
for task in self:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from odoo import api, fields, models
|
|||
class ProjectProject(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
begin_approval_processing = fields.Boolean(default=False)
|
||||
is_initiation_user = fields.Boolean(compute='_compute_stage_access')
|
||||
is_planning_user = fields.Boolean(compute='_compute_stage_access')
|
||||
is_development_user = fields.Boolean(compute='_compute_stage_access')
|
||||
|
|
@ -14,32 +15,48 @@ class ProjectProject(models.Model):
|
|||
is_architecture_user = fields.Boolean(compute='_compute_stage_access')
|
||||
is_project_editor = fields.Boolean(compute='_compute_stage_access')
|
||||
|
||||
|
||||
def action_begin_approval_processing(self):
|
||||
for project in self:
|
||||
project.begin_approval_processing = True
|
||||
|
||||
def _compute_stage_access(self):
|
||||
user = self.env.user
|
||||
|
||||
for project in self:
|
||||
flows = self.env['project.stages.approval.flow'].search([
|
||||
('project_id', '=', project.id)
|
||||
])
|
||||
|
||||
stages = flows.filtered(
|
||||
lambda f:
|
||||
f.assigned_to == user
|
||||
or f.approval_by == user
|
||||
or user in f.involved_users
|
||||
or user.has_group('project.group_project_manager')
|
||||
).mapped('stage_id.name')
|
||||
project_editor = False
|
||||
if (project.user_id and user == project.user_id) or (project.project_sponsor and user == project.project_sponsor):
|
||||
if (project.user_id and user == project.user_id) or user.has_group('project.group_project_manager'):
|
||||
project_editor = True
|
||||
|
||||
|
||||
project.is_initiation_user = 'Initiation' in stages
|
||||
project.is_planning_user = 'Planning' in stages
|
||||
project.is_development_user = 'Development' in stages
|
||||
project.is_testing_user = 'Testing' in stages
|
||||
project.is_deployment_user = 'Deployment' in stages
|
||||
project.is_maintenance_user = 'Maintenance & Support' in stages
|
||||
project.is_closure_user = 'Closer' in stages
|
||||
project.is_architecture_user = 'Architecture & Design' in stages
|
||||
project.is_project_editor = project_editor
|
||||
|
||||
if project.assign_approval_flow:
|
||||
flows = self.env['project.stages.approval.flow'].search([
|
||||
('project_id', '=', project.id)
|
||||
])
|
||||
|
||||
stages = flows.filtered(
|
||||
lambda f:
|
||||
f.assigned_to == user
|
||||
or f.approval_by == user
|
||||
or user in f.involved_users
|
||||
or user.has_group('project.group_project_manager')
|
||||
).mapped('stage_id.name')
|
||||
|
||||
project.is_initiation_user = 'Initial' in stages and project.begin_approval_processing
|
||||
project.is_planning_user = 'Planning' in stages and project.begin_approval_processing
|
||||
project.is_development_user = 'Development' in stages and project.begin_approval_processing
|
||||
project.is_testing_user = 'Testing & QA' in stages and project.begin_approval_processing
|
||||
project.is_deployment_user = 'Deployment' in stages and project.begin_approval_processing
|
||||
project.is_maintenance_user = 'Maintenance & Support' in stages and project.begin_approval_processing
|
||||
project.is_closure_user = 'Closer' in stages and project.begin_approval_processing
|
||||
project.is_architecture_user = 'Architecture & Design' in stages and project.begin_approval_processing
|
||||
else:
|
||||
project.is_initiation_user = True
|
||||
project.is_planning_user = True
|
||||
project.is_development_user = True
|
||||
project.is_testing_user = True
|
||||
project.is_deployment_user = True
|
||||
project.is_maintenance_user = True
|
||||
project.is_closure_user = True
|
||||
project.is_architecture_user = True
|
||||
|
||||
|
|
|
|||
|
|
@ -34,5 +34,6 @@ class TaskStages(models.Model):
|
|||
'default_team_id': self.team_id.id if self.team_id else False,
|
||||
'default_approval_by': self.approval_by if self.approval_by else False,
|
||||
'default_fold': self.fold,
|
||||
'default_involved_user_ids': [(6,0,self.involved_user_ids.ids)]
|
||||
},
|
||||
}
|
||||
|
|
@ -3,7 +3,24 @@ internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_pro
|
|||
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
|
||||
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
|
||||
|
||||
access_project_portfolio_employee_performance_user,project.portfolio.employee.performance.user,model_project_portfolio_employee_performance,base.group_user,1,1,1,1
|
||||
|
||||
access_project_portfolio_dashboard,project.portfolio.dashboard,model_project_portfolio_dashboard,base.group_user,1,0,0,0
|
||||
access_project_portfolio_dashboard_manager,project.portfolio.dashboard,model_project_portfolio_dashboard,project.group_project_manager,1,0,0,0
|
||||
|
||||
access_project_portfolio_budget_overview,project.portfolio.budget.overview,model_project_portfolio_budget_overview,base.group_user,1,0,0,0
|
||||
access_project_portfolio_budget_overview_manager,project.portfolio.budget.overview,model_project_portfolio_budget_overview,project.group_project_manager,1,0,0,0
|
||||
|
||||
access_project_attachments_users,project.attachments.users,model_project_attachments,base.group_user,1,1,1,1
|
||||
|
||||
access_project_cancel_hold_wizard_supervisor,access.project.cancel.hold.wizard.supervisor,model_project_cancel_hold_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
|
||||
access_project_cancel_hold_wizard_manager,access.project.cancel.hold.wizard.manager,model_project_cancel_hold_wizard,project.group_project_manager,1,1,1,1
|
||||
|
||||
access_project_role_user,project.role.user,model_project_role,base.group_user,1,0,0,0
|
||||
|
||||
access_project_portfolio_user,project.portfolio.user,model_project_portfolio,base.group_user,1,1,1,0
|
||||
access_project_portfolio_manager,project.portfolio.manager,model_project_portfolio,project.group_project_manager,1,1,1,1
|
||||
|
||||
access_project_role_manager,project.role.manager,model_project_role,project.group_project_manager,1,1,1,1
|
||||
|
||||
access_project_sprint_user,access.project.sprint.user,model_project_sprint,project.group_project_user,1,1,1,1
|
||||
|
|
@ -63,3 +80,13 @@ access_project_deployment_log_user,access.project.deployment.log.user,model_proj
|
|||
access_project_maintenance_support_user,access.project.maintenance.support.user,model_project_maintenance_support,base.group_user,1,1,1,1
|
||||
|
||||
access_project_closure_document_user,access.project.closure.document.user,model_project_closure_document,base.group_user,1,1,1,1
|
||||
|
||||
|
||||
access_project_external_cost_user,project.external.cost.user,model_project_external_cost,base.group_user,1,1,1,0
|
||||
access_project_external_cost_manager,project.external.cost.manager,model_project_external_cost,project.group_project_manager,1,1,1,1
|
||||
access_project_external_cost_wizard_user,project.external.cost.wizard.user,model_project_external_cost_wizard,base.group_user,1,1,1,0
|
||||
access_project_external_cost_wizard_manager,project.external.cost.wizard.manager,model_project_external_cost_wizard,project.group_project_manager,1,1,1,1
|
||||
access_project_resource_actual_cost_user,project.resource.actual.cost.user,model_project_resource_actual_cost,base.group_user,1,0,0,0
|
||||
access_project_resource_actual_cost_manager,project.resource.actual.cost.manager,model_project_resource_actual_cost,project.group_project_manager,1,1,1,1
|
||||
access_project_resource_contract_period_user,project.resource.contract.period.user,model_project_resource_contract_period,base.group_user,1,0,0,0
|
||||
access_project_resource_contract_period_manager,project.resource.contract.period.manager,model_project_resource_contract_period,project.group_project_manager,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -31,14 +31,15 @@
|
|||
create="true"
|
||||
mode="list,form">
|
||||
<list editable="bottom">
|
||||
<field name="name" column_invisible="1"/>
|
||||
<field name="datas" filename="name" widget="60%"/>
|
||||
<field name="mimetype" widget="30%"/>
|
||||
<field name="file_name" column_invisible="1"/>
|
||||
<field name="file" filename="file_name" widget="60%"/>
|
||||
<field name="notes" widget="30%"/>
|
||||
</list>
|
||||
<form string="Deployment File">
|
||||
<form string="Maintenance File">
|
||||
<group>
|
||||
<field name="datas" filename="name"/>
|
||||
<field name="mimetype"/>
|
||||
<field name="file_name" invisible="1"/>
|
||||
<field name="file" filename="file_name"/>
|
||||
<field name="notes"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@
|
|||
</group>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='partner_id']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='user_id']" position="after">
|
||||
<field name="project_lead" widget="many2one_avatar_user"/>
|
||||
</xpath>
|
||||
|
|
@ -39,29 +43,37 @@
|
|||
|
||||
</group>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//form/header/button[@name='action_open_share_project_wizard']" position="before">
|
||||
<button type="object" name="action_begin_approval_processing"
|
||||
string="Begin Approval Flow"
|
||||
class="oe_highlight"
|
||||
invisible="not assign_approval_flow or begin_approval_processing or not is_project_editor"/>
|
||||
</xpath>
|
||||
<xpath expr="//form/header" position="inside">
|
||||
<button type="object" name="submit_project_for_approval"
|
||||
string="Submit for Approval"
|
||||
class="oe_highlight"
|
||||
invisible="not assign_approval_flow or not show_submission_button"/>
|
||||
invisible="not assign_approval_flow or not show_submission_button or not begin_approval_processing"/>
|
||||
|
||||
<button type="object" name="project_proceed_further"
|
||||
string="Approve & Proceed"
|
||||
class="oe_highlight"
|
||||
invisible="not assign_approval_flow or not show_approval_button"/>
|
||||
invisible="not assign_approval_flow or not show_approval_button or not begin_approval_processing"/>
|
||||
|
||||
<button type="object" name="action_open_reject_wizard"
|
||||
string="Reject"
|
||||
class="oe_highlight"
|
||||
invisible="not assign_approval_flow or not show_refuse_button"/>
|
||||
invisible="not assign_approval_flow or not show_refuse_button or not begin_approval_processing"/>
|
||||
|
||||
<button type="object" name="project_back_button"
|
||||
string="Go Back"
|
||||
class="oe_highlight"
|
||||
invisible="not assign_approval_flow or not show_back_button"/>
|
||||
invisible="not assign_approval_flow or not show_back_button or not begin_approval_processing"/>
|
||||
|
||||
</xpath>
|
||||
<xpath expr="//form" position="inside">
|
||||
<field name="begin_approval_processing" invisible="1"/>
|
||||
<field name="showable_stage_ids" invisible="1"/>
|
||||
<field name="assign_approval_flow" invisible="1"/>
|
||||
<field name="manager_level_edit_access" invisible="1"/>
|
||||
|
|
@ -70,10 +82,15 @@
|
|||
<field name="show_refuse_button" invisible="1"/>
|
||||
<field name="show_back_button" invisible="1"/>
|
||||
<field name="is_project_editor" invisible="1"/>
|
||||
<field name="enable_budget_summary" invisible="1"/>
|
||||
<field name="can_edit_stage_in_approval" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="invisible">assign_approval_flow and not begin_approval_processing</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//page[@name='settings']" position="attributes">
|
||||
<attribute name="invisible">not is_project_editor</attribute>
|
||||
|
|
@ -142,85 +159,8 @@
|
|||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='description']" position="attributes">
|
||||
<attribute name="invisible">not is_initiation_user</attribute>
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<page name="description" position="inside">
|
||||
<group>
|
||||
<field name="project_vision"
|
||||
placeholder="Eg: Build a mobile app that allows users to order groceries and track delivery in real time."
|
||||
readonly="not manager_level_edit_access"/>
|
||||
</group>
|
||||
<group string="Requirements Document">
|
||||
|
||||
<div class="o_row" style="align-items: flex-start;">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
<div class="o_col" style="width: 70%;">
|
||||
|
||||
<!-- HTML field (visible when NO file uploaded) -->
|
||||
<field name="description"
|
||||
widget="html" force_save="1" readonly="not manager_level_edit_access"
|
||||
invisible="requirement_file" placeholder="The system should allow user login,
|
||||
Users should be able to add items to a cart."/>
|
||||
|
||||
<!-- PDF Viewer (visible when file exists) -->
|
||||
<field name="requirement_file"
|
||||
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
||||
invisible="not requirement_file"/>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
||||
|
||||
<!-- Upload button (visible when NO file exists) -->
|
||||
<field name="requirement_file"
|
||||
widget="binary" force_save="1"
|
||||
filename="requirement_file_name"
|
||||
invisible="requirement_file or not manager_level_edit_access"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</group>
|
||||
|
||||
|
||||
<!-- ===================== -->
|
||||
<!-- FEASIBILITY ASSESSMENT -->
|
||||
<!-- ===================== -->
|
||||
<group string="Feasibility Assessment">
|
||||
|
||||
<div class="o_row" style="align-items: flex-start;">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
<div class="o_col" style="width: 70%;">
|
||||
|
||||
<!-- HTML field -->
|
||||
<field name="feasibility_html"
|
||||
widget="html" force_save="1"
|
||||
readonly="not manager_level_edit_access"
|
||||
invisible="feasibility_file"
|
||||
placeholder="Check whether the project is technically, financially, and operationally possible."/>
|
||||
|
||||
<!-- PDF Viewer -->
|
||||
<field name="feasibility_file"
|
||||
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
||||
invisible="not feasibility_file"/>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
||||
|
||||
<!-- Upload Field -->
|
||||
<field name="feasibility_file"
|
||||
widget="binary" force_save="1"
|
||||
filename="feasibility_file_name"
|
||||
invisible="feasibility_file or not manager_level_edit_access"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</group>
|
||||
</page>
|
||||
<xpath expr="//page[@name='settings']" position="inside">
|
||||
<group>
|
||||
<group name="group_sprint_requirement_management" string="Project Sprint" col="1"
|
||||
|
|
@ -236,16 +176,10 @@
|
|||
</group>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//sheet" position="inside">
|
||||
<xpath expr="//div[@name='button_box']" position="after">
|
||||
<widget name="web_ribbon" title="Rejected" bg_color="text-bg-danger"
|
||||
invisible="approval_status != 'reject'"/>
|
||||
<widget name="web_ribbon" title="Rejected" invisible="approval_status != 'submitted'"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Project Activity Log" invisible="not assign_approval_flow or not show_project_chatter">
|
||||
<field name="project_activity_log" widget="html" options="{'sanitize': False}" readonly="1"
|
||||
force_save="1"/>
|
||||
</page>
|
||||
<widget name="web_ribbon" title="Submitted" invisible="approval_status != 'submitted'"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='date_start']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
|
|
@ -255,6 +189,90 @@
|
|||
</xpath>
|
||||
|
||||
<xpath expr="//sheet/notebook/page[@name='settings']" position="after">
|
||||
<page string="Project Activity Log" invisible="not assign_approval_flow or not show_project_chatter">
|
||||
<field name="project_activity_log" widget="html" options="{'sanitize': False}" readonly="1"
|
||||
force_save="1"/>
|
||||
</page>
|
||||
<page name="initiation" string="Initial" invisible="not is_initiation_user">
|
||||
<group>
|
||||
<field name="project_vision"
|
||||
placeholder="Eg: Build a mobile app that allows users to order groceries and track delivery in real time."
|
||||
readonly="not manager_level_edit_access"/>
|
||||
</group>
|
||||
<group string="Requirements Document">
|
||||
|
||||
<div class="o_row" style="align-items: flex-start;">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
<div class="o_col" style="width: 70%;">
|
||||
|
||||
<!-- HTML field (visible when NO file uploaded) -->
|
||||
<field name="description"
|
||||
widget="html" force_save="1" readonly="not manager_level_edit_access"
|
||||
invisible="requirement_file" placeholder="The system should allow user login,
|
||||
Users should be able to add items to a cart."/>
|
||||
|
||||
<!-- PDF Viewer (visible when file exists) -->
|
||||
<field name="requirement_file" filename="requirement_file_name"
|
||||
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
||||
invisible="not requirement_file"/>
|
||||
<field name="requirement_file_name" force_save="1" invisible="1"/>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
||||
|
||||
<!-- Upload button (visible when NO file exists) -->
|
||||
<field name="requirement_file"
|
||||
widget="binary" force_save="1"
|
||||
filename="requirement_file_name"
|
||||
invisible="requirement_file or not manager_level_edit_access"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</group>
|
||||
|
||||
|
||||
<!-- ===================== -->
|
||||
<!-- FEASIBILITY ASSESSMENT -->
|
||||
<!-- ===================== -->
|
||||
<group string="Feasibility Assessment">
|
||||
|
||||
<div class="o_row" style="align-items: flex-start;">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
<div class="o_col" style="width: 70%;">
|
||||
|
||||
<!-- HTML field -->
|
||||
<field name="feasibility_html"
|
||||
widget="html" force_save="1"
|
||||
readonly="not manager_level_edit_access"
|
||||
invisible="feasibility_file"
|
||||
placeholder="Check whether the project is technically, financially, and operationally possible."/>
|
||||
|
||||
<!-- PDF Viewer -->
|
||||
<field name="feasibility_file" filename="feasibility_file_name"
|
||||
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
||||
invisible="not feasibility_file"/>
|
||||
<field name="feasibility_file_name" force_save="1" invisible="1"/>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
||||
|
||||
<!-- Upload Field -->
|
||||
<field name="feasibility_file"
|
||||
widget="binary" force_save="1"
|
||||
filename="feasibility_file_name"
|
||||
invisible="feasibility_file or not manager_level_edit_access"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page name="planning" string="Planning (Budget & Deadlines)" invisible="not is_planning_user">
|
||||
<group>
|
||||
|
||||
|
|
@ -314,7 +332,7 @@
|
|||
<group string="Budget Planning">
|
||||
<group>
|
||||
<field name="estimated_amount"/>
|
||||
<field name="total_budget_amount" readonly="1"/>
|
||||
<field name="total_planned_budget_amount" readonly="1"/>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
|
|
@ -353,10 +371,10 @@
|
|||
</page>
|
||||
|
||||
<!-- EQUIPMENT TAB -->
|
||||
<page string="Equipments/Others">
|
||||
<page string="Asset Rental">
|
||||
<field name="equipment_cost_ids">
|
||||
<list editable="bottom">
|
||||
<field name="equipment_name" string="Equip/Others" width="30%"/>
|
||||
<field name="equipment_name" string="Asset" width="30%"/>
|
||||
<field name="duration_hours" width="20%"/>
|
||||
<field name="hourly_rate" width="20%"/>
|
||||
<field name="total_cost" readonly="1" width="20%"/>
|
||||
|
|
@ -367,6 +385,85 @@
|
|||
</notebook>
|
||||
</group>
|
||||
</page>
|
||||
<page name="budget_summary" string="Budget Summary" invisible="not is_project_editor or not enable_budget_summary">
|
||||
<group>
|
||||
<group>
|
||||
<field name="project_cost"/>
|
||||
<field name="actual_cost"/>
|
||||
<field name="estimated_amount" readonly="1" force_save="1" string="Total Planned Amount"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="profit"/>
|
||||
<field name="loss"/>
|
||||
<field name="difference"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="internal_resource_costs" string="Manpower">
|
||||
<field name="resource_actual_cost_ids">
|
||||
<list editable="bottom" decoration-warning="not planned_resource">
|
||||
<field name="project_id" column_invisible="1"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="planned_resource"/>
|
||||
<field name="cost_based_on"/>
|
||||
<field name="estimated_hours" widget="timesheet_uom" string="Assigned Task Hours"/>
|
||||
<field name="total_hours" widget="timesheet_uom" readonly="cost_based_on != 'manual'"/>
|
||||
<field name="total_cost" readonly="cost_based_on != 'manual'"/>
|
||||
<field name="contract_period_ids" widget="many2many_tags"/>
|
||||
<field name="readonly_total_cost" column_invisible="1"/>
|
||||
<button name="update_from_timesheets" type="object" icon="fa-refresh"
|
||||
invisible="cost_based_on != 'timesheets'"
|
||||
string="Update"/>
|
||||
</list>
|
||||
</field>
|
||||
<div class="oe_clear">
|
||||
<button name="action_update_resource_actual_costs" type="object"
|
||||
string="Update Actual Cost" class="oe_highlight"/>
|
||||
</div>
|
||||
<group class="oe_subtotal_footer oe_right">
|
||||
<field name="total_resource_actual_costs" widget="monetary" string="Actual Manpower Cost"/>
|
||||
<field name="initial_estimated_resource_cost" widget="monetary" string="Estimated Manpower Cost"/>
|
||||
</group>
|
||||
</page>
|
||||
<page name="external_costs" string="External Costs">
|
||||
<field name="external_cost_ids">
|
||||
<list editable="bottom" decoration-info="state=='draft'"
|
||||
decoration-success="state=='paid'" decoration-warning="state=='billed'">
|
||||
<field name="name"/>
|
||||
<field name="cost_type"/>
|
||||
<field name="vendor_id"/>
|
||||
<field name="unit_price"/>
|
||||
<field name="quantity"/>
|
||||
<field name="total_cost" sum="Total"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="state"/>
|
||||
<button name="action_confirm" type="object" icon="fa-check"
|
||||
attrs="{'invisible': [('state', '!=', 'draft')]}" title="Confirm"/>
|
||||
<button name="action_mark_as_billed" type="object" icon="fa-file-text-o"
|
||||
attrs="{'invisible': [('state', '!=', 'confirmed')]}"
|
||||
title="Mark as Billed"/>
|
||||
<button name="action_mark_as_paid" type="object" icon="fa-money"
|
||||
attrs="{'invisible': [('state', '!=', 'billed')]}" title="Mark as Paid"/>
|
||||
<button name="action_cancel" type="object" icon="fa-times"
|
||||
attrs="{'invisible': [('state', 'in', ['paid', 'cancelled'])]}"
|
||||
title="Cancel"/>
|
||||
</list>
|
||||
</field>
|
||||
<div class="oe_clear">
|
||||
<button name="action_create_external_cost" type="object"
|
||||
string="Add External Cost" class="oe_highlight"/>
|
||||
<button name="action_view_external_costs" type="object"
|
||||
string="View All External Costs"/>
|
||||
</div>
|
||||
<group class="oe_subtotal_footer oe_right">
|
||||
<field name="total_external_costs" widget="monetary"/>
|
||||
<field name="estimated_external_cost" widget="monetary"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
</notebook>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//sheet/notebook" position="inside">
|
||||
<page name="architecture_design" string="Architecture & Design" invisible="not is_architecture_user">
|
||||
|
|
@ -592,14 +689,15 @@
|
|||
create="true"
|
||||
mode="list,form">
|
||||
<list editable="bottom">
|
||||
<field name="name" column_invisible="1"/>
|
||||
<field name="datas" filename="name" widget="60%"/>
|
||||
<field name="mimetype" widget="30%"/>
|
||||
<field name="file_name" column_invisible="1"/>
|
||||
<field name="file" filename="file_name" widget="60%"/>
|
||||
<field name="notes" widget="30%"/>
|
||||
</list>
|
||||
<form string="Deployment File">
|
||||
<group>
|
||||
<field name="datas" filename="name"/>
|
||||
<field name="mimetype"/>
|
||||
<field name="file_name" invisible="1"/>
|
||||
<field name="file" filename="file_name"/>
|
||||
<field name="notes"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
|
|
@ -639,8 +737,50 @@
|
|||
</group>
|
||||
|
||||
</page>
|
||||
<page string="Cancel / Hold" invisible="project_state in ['active']">
|
||||
<group>
|
||||
<field name="project_state" readonly="1"/>
|
||||
</group>
|
||||
<group string="Cancel Details" invisible="project_state != 'cancel'">
|
||||
<field name="cancel_reason" widget="html" readonly="1"/>
|
||||
</group>
|
||||
<group string="Hold Details" invisible="project_state != 'hold'">
|
||||
<field name="hold_reason" widget="html" readonly="1"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
</xpath>
|
||||
<xpath expr="//sheet" position="before">
|
||||
<div class="alert alert-warning text-center" role="alert"
|
||||
invisible="project_state != 'hold'">
|
||||
<i class="fa fa-pause me-2"/>
|
||||
<strong>THIS PROJECT IS CURRENTLY ON HOLD</strong>
|
||||
</div>
|
||||
<div class="alert alert-warning text-center" role="alert"
|
||||
invisible="project_state != 'cancel'">
|
||||
<i class="fa fa-pause me-2"/>
|
||||
<strong>THIS PROJECT IS CANCELED</strong>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//form/header" position="attributes">
|
||||
<attribute name="invisible">project_state != 'active'</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_project_search_view_inherit" model="ir.ui.view">
|
||||
<field name="name">project.project.search.view.inherit</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.view_project_project_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//search" position="inside">
|
||||
<filter name="approval_filter"
|
||||
string="Pending Approval"
|
||||
domain="[('show_approval_button_filter', '=', True)]"/>
|
||||
<filter name="submission_filter"
|
||||
string="Pending Submissions"
|
||||
domain="[('show_submission_button_filter','=',True)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -746,5 +886,15 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<!-- <record id="project_view_kanban_inherit" model="ir.ui.view">-->
|
||||
<!-- <field name="name">project.view.kanban.inherit</field>-->
|
||||
<!-- <field name="model">project.project</field>-->
|
||||
<!-- <field name="inherit_id" ref="project.view_project_kanban"/>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <xpath expr="//kanban" position="attributes">-->
|
||||
<!-- <attribute name="context">{'view_type':'kanban'}</attribute>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="project_resource_actual_cost_form_view" model="ir.ui.view">
|
||||
<field name="name">project.resource.actual.cost.form</field>
|
||||
<field name="model">project.resource.actual.cost</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Resource Actual Cost">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="project_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="planned_resource"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="cost_based_on"/>
|
||||
<field name="total_hours" widget="timesheet_uom" readonly="1"/>
|
||||
<field name="total_cost" readonly="cost_based_on == 'timesheets'"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Contract Periods">
|
||||
<field name="contract_period_ids">
|
||||
<list>
|
||||
<field name="contract_id"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="hours_worked" widget="timesheet_uom"/>
|
||||
<field name="cost_incurred"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_external_cost_list_view" model="ir.ui.view">
|
||||
<field name="name">project.external.cost.list</field>
|
||||
<field name="model">project.external.cost</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="External Costs" decoration-info="state == 'draft'"
|
||||
decoration-success="state == 'paid'" decoration-warning="state == 'billed'">
|
||||
<field name="project_id"/>
|
||||
<field name="name"/>
|
||||
<field name="cost_type"/>
|
||||
<field name="vendor_id"/>
|
||||
<field name="unit_price"/>
|
||||
<field name="quantity"/>
|
||||
<field name="total_cost" sum="Total"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="state"/>
|
||||
<button name="action_confirm" type="object" icon="fa-check" invisible="state != 'draft'" string="Confirm"/>
|
||||
<button name="action_mark_as_billed" type="object" icon="fa-file-text-o" invisible="state != 'confirmed'" string="Mark as Billed"/>
|
||||
<button name="action_mark_as_paid" type="object" icon="fa-money" invisible="state != 'billed'" string="Mark as Paid"/>
|
||||
<button name="action_cancel" type="object" icon="fa-times" invisible="state in ['paid', 'cancelled']" string="Cancel"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- External Cost Form View -->
|
||||
<record id="project_external_cost_form_view" model="ir.ui.view">
|
||||
<field name="name">project.external.cost.form</field>
|
||||
<field name="model">project.external.cost</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="External Cost">
|
||||
<header>
|
||||
<button name="action_confirm" type="object" string="Confirm" invisible="state != 'draft'" class="btn-primary"/>
|
||||
<button name="action_mark_as_billed" type="object" string="Mark as Billed" invisible="state != 'confirmed'"/>
|
||||
<button name="action_mark_as_paid" type="object" string="Mark as Paid" invisible="state != 'billed'" class="btn-primary"/>
|
||||
<button name="action_cancel" type="object" string="Cancel" invisible="state in ['paid', 'cancelled']"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,billed,paid"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="project_id"/>
|
||||
<field name="name"/>
|
||||
<field name="cost_type"/>
|
||||
<field name="vendor_id"/>
|
||||
<field name="vendor_reference"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="unit_price"/>
|
||||
<field name="quantity"/>
|
||||
<field name="total_cost"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end" invisible="cost_type not in ['hourly','daily']"/>
|
||||
<field name="invoice_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Details">
|
||||
<group>
|
||||
<field name="product_id"/>
|
||||
<field name="uom_id"/>
|
||||
<field name="notes"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Attachments">
|
||||
<field name="attachment_ids" widget="many2many_binary"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- External Cost Wizard Form View -->
|
||||
<record id="project_external_cost_wizard_form_view" model="ir.ui.view">
|
||||
<field name="name">project.external.cost.wizard.form</field>
|
||||
<field name="model">project.external.cost.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create External Cost">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="name"/>
|
||||
<field name="cost_type"/>
|
||||
<field name="vendor_id"/>
|
||||
<field name="unit_price"/>
|
||||
<field name="quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end" invisible="cost_type not in ['hourly', 'daily']"/>
|
||||
<field name="notes"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_create_cost" type="object" string="Create" class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Actions -->
|
||||
<record id="action_project_external_cost" model="ir.actions.act_window">
|
||||
<field name="name">External Costs</field>
|
||||
<field name="res_model">project.external.cost</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No external costs found
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<menuitem id="menu_project_external_cost"
|
||||
name="External Costs"
|
||||
parent="project.menu_main_pm"
|
||||
action="action_project_external_cost"
|
||||
sequence="30"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="project_attachments_list" model="ir.ui.view">
|
||||
<field name="name">project.attachment.list</field>
|
||||
<field name="model">project.attachments</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="file_name" column_invisible="1"/>
|
||||
<field name="file" filename="file_name" widget="60%"/>
|
||||
<field name="notes" widget="30%"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_attachments_form" model="ir.ui.view">
|
||||
<field name="name">project.attachment.form</field>
|
||||
<field name="model">project.attachments</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="file_name" invisible="1"/>
|
||||
<field name="file" filename="file_name"/>
|
||||
<field name="notes"/>
|
||||
</group>
|
||||
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,513 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<!-- Project Portfolio List View -->
|
||||
<record id="view_project_portfolio_list" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.list</field>
|
||||
<field name="model">project.portfolio</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="industry_id"/>
|
||||
<field name="owner_id"/>
|
||||
<field name="company_id"/>
|
||||
<field name="budget_status" widget="badge" decoration-success="budget_status == 'under'"
|
||||
decoration-warning="budget_status == 'on_track'" decoration-danger="budget_status == 'over'"/>
|
||||
<field name="total_estimated_budget" widget="monetary"/>
|
||||
<field name="total_client_budget" widget="monetary"/>
|
||||
<field name="total_actual_budget" widget="monetary"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Project Portfolio Form View -->
|
||||
<record id="view_project_portfolio_form" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.form</field>
|
||||
<field name="model">project.portfolio</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_budget_analysis" type="object"
|
||||
class="oe_stat_button" icon="fa-bar-chart-o" invisible="project_count == 0">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
Budget Analysis
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_employee_performance" type="object"
|
||||
class="oe_stat_button" icon="fa-users" invisible="project_count == 0">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
Team Performance
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_projects" type="object"
|
||||
class="oe_stat_button" icon="fa-tasks">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="project_count" widget="integer"/>
|
||||
</span>
|
||||
<span class="o_stat_text">Projects</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_update_employee_performance" type="object"
|
||||
class="oe_stat_button" icon="fa-refresh" invisible="project_count == 0">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Refresh Performance</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name"/>
|
||||
</h1>
|
||||
<field name="budget_status" widget="statusbar" statusbar_visible="under,on_track,over"
|
||||
statusbar_colors='{"under":"success","on_track":"warning","over":"danger"}' invisible="project_count == 0"/>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="industry_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="owner_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Budget Summary Section -->
|
||||
<div invisible="project_count == 0">
|
||||
<h2>Budget Summary</h2>
|
||||
</div>
|
||||
|
||||
<group invisible="project_count == 0">
|
||||
<group>
|
||||
<field name="total_client_budget" widget="monetary"/>
|
||||
<field name="total_estimated_budget" widget="monetary"/>
|
||||
<field name="total_actual_budget" widget="monetary"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_profit" widget="monetary"/>
|
||||
<field name="total_loss" widget="monetary"/>
|
||||
<field name="net_profit" widget="monetary"/>
|
||||
</group>
|
||||
</group>
|
||||
<group invisible="project_count == 0">
|
||||
<group>
|
||||
<group string="Client Budget vs Est.">
|
||||
<field name="planned_client_budget_variance" widget="monetary"/>
|
||||
<field name="planned_client_budget_variance_percent" options="{'digits': [16, 2]}"/>
|
||||
<field name="planned_roi_estimate" options="{'digits': [16, 2]}"/>
|
||||
</group>
|
||||
<group string="Estimated vs Actual">
|
||||
<field name="budget_variance" widget="monetary"/>
|
||||
<field name="budget_variance_percent" options="{'digits': [16, 2]}"/>
|
||||
<field name="roi_estimate" options="{'digits': [16, 2]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Client Budget vs Actual">
|
||||
<field name="client_budget_variance" widget="monetary"/>
|
||||
<field name="client_budget_variance_percent" options="{'digits': [16, 2]}"/>
|
||||
<field name="actual_roi_estimate" options="{'digits': [16, 2]}"/>
|
||||
</group>
|
||||
<group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Performance Summary Section -->
|
||||
<div invisible="project_count == 0">
|
||||
<h2>Performance Metrics</h2>
|
||||
</div>
|
||||
<group invisible="project_count == 0">
|
||||
<group>
|
||||
<field name="avg_time_variance" options="{'digits': [16, 2]}"/>
|
||||
<field name="on_time_completion_rate" options="{'digits': [16, 2]}"/>
|
||||
<field name="overall_efficiency" options="{'digits': [16, 2]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_resource_cost" widget="monetary"/>
|
||||
<field name="total_material_cost" widget="monetary"/>
|
||||
<field name="total_equipment_cost" widget="monetary"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Projects" invisible="project_count == 0">
|
||||
<field name="project_ids">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="user_id"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="estimated_amount" widget="monetary"/>
|
||||
<field name="project_cost" widget="monetary"/>
|
||||
<field name="actual_cost" widget="monetary"/>
|
||||
<field name="profit" widget="monetary"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
<page string="Budget Dashboard" invisible="project_count == 0">
|
||||
<div class="o_container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<field name="project_ids">
|
||||
<list string="Project Budget Overview">
|
||||
<field name="name"/>
|
||||
<field name="estimated_amount" widget="monetary"/>
|
||||
<field name="project_cost" widget="monetary"/>
|
||||
<field name="actual_cost" widget="monetary"/>
|
||||
<field name="profit" widget="monetary"/>
|
||||
<field name="profit_percentage" options="{'digits': [16, 2]}"/>
|
||||
<field name="loss" widget="monetary"/>
|
||||
<field name="loss_percentage" options="{'digits': [16, 2]}"/>
|
||||
</list>
|
||||
</field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</page>
|
||||
|
||||
<page string="Employee Performance" invisible="project_count == 0">
|
||||
<field name="employee_performance_ids">
|
||||
<list>
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="total_estimated_hours" widget="timesheet_uom"
|
||||
options="{'digits': [16, 2]}"/>
|
||||
<field name="total_actual_hours" widget="timesheet_uom"
|
||||
options="{'digits': [16, 2]}"/>
|
||||
<field name="time_variance" widget="timesheet_uom" options="{'digits': [16, 2]}"/>
|
||||
<field name="time_variance_percent" options="{'digits': [16, 2]}"/>
|
||||
<field name="on_time_completion_rate" options="{'digits': [16, 2]}"/>
|
||||
<field name="performance_status" widget="badge"
|
||||
decoration-success="performance_status == 'excellent'"
|
||||
decoration-warning="performance_status == 'good'"
|
||||
decoration-info="performance_status == 'average'"
|
||||
decoration-danger="performance_status in ['poor','critical']"/>
|
||||
<field name="efficiency_rate" options="{'digits': [16, 2]}"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
<page string="Description">
|
||||
<field name="description"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Project Portfolio Kanban View -->
|
||||
<record id="view_project_portfolio_kanban" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.kanban</field>
|
||||
<field name="model">project.portfolio</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_small_column">
|
||||
<field name="name"/>
|
||||
<field name="industry_id"/>
|
||||
<field name="owner_id"/>
|
||||
<field name="project_ids"/>
|
||||
<field name="budget_status"/>
|
||||
<field name="total_estimated_budget"/>
|
||||
<field name="total_client_budget"/>
|
||||
<field name="total_actual_budget"/>
|
||||
<field name="budget_variance"/>
|
||||
<field name="budget_variance_percent"/>
|
||||
<field name="roi_estimate"/>
|
||||
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_card">
|
||||
<div class="o_kanban_header">
|
||||
<strong>
|
||||
<field name="name"/>
|
||||
</strong>
|
||||
<div>
|
||||
<field name="budget_status" widget="badge"
|
||||
decoration-success="budget_status == 'under'"
|
||||
decoration-warning="budget_status == 'on_track'"
|
||||
decoration-danger="budget_status == 'over'"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_kanban_content">
|
||||
<div t-if="record.industry_id.raw_value">
|
||||
<span class="fa fa-industry me-1"/>
|
||||
<field name="industry_id"/>
|
||||
</div>
|
||||
|
||||
<div t-if="record.owner_id.raw_value" class="mt-1">
|
||||
<span class="fa fa-user me-1"/>
|
||||
<field name="owner_id"/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<span class="fa fa-folder-open me-1"/>
|
||||
<span>
|
||||
<t t-esc="record.project_ids.count"/>
|
||||
Projects
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<span class="fa fa-money me-1"/>
|
||||
<span>
|
||||
Est: ₹
|
||||
<t t-esc="Math.round(record.total_estimated_budget.raw_value || 0)"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<span class="fa fa-handshake-o me-1"/>
|
||||
<span>
|
||||
Client: ₹
|
||||
<t t-esc="Math.round(record.total_client_budget.raw_value || 0)"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<span class="fa fa-line-chart me-1"/>
|
||||
<span>
|
||||
ROI:<t t-esc="Math.round(record.roi_estimate.raw_value || 0)"/>%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Employee Performance Views -->
|
||||
<record id="view_project_portfolio_employee_performance_list" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.employee.performance.list</field>
|
||||
<field name="model">project.portfolio.employee.performance</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="total_estimated_hours" widget="timesheet_uom" options="{'digits': [16, 2]}"/>
|
||||
<field name="total_actual_hours" widget="timesheet_uom" options="{'digits': [16, 2]}"/>
|
||||
<field name="time_variance" widget="timesheet_uom" options="{'digits': [16, 2]}"/>
|
||||
<field name="time_variance_percent" options="{'digits': [16, 2]}"/>
|
||||
<field name="performance_status" widget="badge"
|
||||
decoration-success="performance_status == 'excellent'"
|
||||
decoration-warning="performance_status == 'good'"
|
||||
decoration-info="performance_status == 'average'"
|
||||
decoration-danger="performance_status in ['poor','critical']"/>
|
||||
<field name="on_time_completion_rate" options="{'digits': [16, 2]}"/>
|
||||
<field name="efficiency_rate" options="{'digits': [16, 2]}"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Employee Performance Search View -->
|
||||
<record id="view_project_portfolio_employee_performance_search" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.employee.performance.search</field>
|
||||
<field name="model">project.portfolio.employee.performance</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="performance_status"/>
|
||||
<filter name="excellent_performers" string="Excellent Performers"
|
||||
domain="[('performance_status', '=', 'excellent')]"/>
|
||||
<filter name="needs_attention" string="Needs Attention"
|
||||
domain="[('performance_status', 'in', ['poor', 'critical'])]"/>
|
||||
<separator/>
|
||||
<filter name="group_by_employee" string="Employee" context="{'group_by': 'employee_id'}"/>
|
||||
<filter name="group_by_department" string="Department" context="{'group_by': 'department_id'}"/>
|
||||
<filter name="group_by_status" string="Performance Status"
|
||||
context="{'group_by': 'performance_status'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Graph Views -->
|
||||
<record id="view_project_portfolio_dashboard_graph_budget" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.dashboard.graph.budget</field>
|
||||
<field name="model">project.portfolio.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Budget Comparison" type="bar">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="estimated_budget" type="measure"/>
|
||||
<field name="client_budget" type="measure"/>
|
||||
<field name="actual_cost" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_portfolio_dashboard_graph_variance" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.dashboard.graph.variance</field>
|
||||
<field name="model">project.portfolio.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Budget Variance" type="bar">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="estimated_variance_percent" type="measure"/>
|
||||
<field name="client_variance_percent" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_portfolio_dashboard_graph_cost" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.dashboard.graph.cost</field>
|
||||
<field name="model">project.portfolio.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Cost Breakdown" type="pie">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="resource_cost" type="measure"/>
|
||||
<field name="external_cost" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_portfolio_dashboard_graph_performance" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.dashboard.graph.performance</field>
|
||||
<field name="model">project.portfolio.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Project Performance" type="bar">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="time_variance_percent" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_portfolio_employee_performance_graph" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.employee.performance.graph</field>
|
||||
<field name="model">project.portfolio.employee.performance</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Employee Performance" type="bar">
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="time_variance_percent" type="measure"/>
|
||||
<field name="on_time_completion_rate" type="measure"/>
|
||||
<field name="efficiency_rate" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Actions -->
|
||||
<record id="action_project_portfolio" model="ir.actions.act_window">
|
||||
<field name="name">Project Portfolios</field>
|
||||
<field name="res_model">project.portfolio</field>
|
||||
<field name="view_mode">list,kanban,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first portfolio
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_portfolio_employee_performance" model="ir.actions.act_window">
|
||||
<field name="name">Employee Performance</field>
|
||||
<field name="res_model">project.portfolio.employee.performance</field>
|
||||
<field name="view_mode">list,graph,pivot</field>
|
||||
<field name="search_view_id" ref="view_project_portfolio_employee_performance_search"/>
|
||||
<field name="context">{'search_default_group_by_employee': 1}</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Structure -->
|
||||
<menuitem id="menu_project_portfolio_root"
|
||||
name="Portfolios"
|
||||
parent="project.menu_main_pm"
|
||||
sequence="30"
|
||||
groups="project.group_project_manager"/>
|
||||
|
||||
<menuitem id="menu_project_portfolio"
|
||||
name="Project Portfolios"
|
||||
parent="menu_project_portfolio_root"
|
||||
action="action_project_portfolio"/>
|
||||
|
||||
<menuitem id="menu_project_portfolio_performance"
|
||||
name="Team Performance"
|
||||
parent="menu_project_portfolio_root"
|
||||
action="action_project_portfolio_employee_performance"/>
|
||||
|
||||
<!-- Inherit Project Form -->
|
||||
<record id="view_project_form_inherit_portfolio" model="ir.ui.view">
|
||||
<field name="name">project.project.form.inherit.portfolio</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- <xpath expr="//div[@name='button_box']" position="inside">-->
|
||||
<!-- <button name="action_view_budget_analysis" type="object"-->
|
||||
<!-- class="oe_stat_button" icon="fa-bar-chart-o"-->
|
||||
<!-- attrs="{'invisible': [('portfolio_id', '=', False)]}">-->
|
||||
<!-- <div class="o_field_widget o_stat_info">-->
|
||||
<!-- <span class="o_stat_value">Budget</span>-->
|
||||
<!-- <span class="o_stat_text">Analysis</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </button>-->
|
||||
<!-- </xpath>-->
|
||||
|
||||
<xpath expr="//group" position="inside">
|
||||
<field name="portfolio_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Budget Overview Views -->
|
||||
<record id="view_project_portfolio_budget_overview_list" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.budget.overview.list</field>
|
||||
<field name="model">project.portfolio.budget.overview</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="project_id"/>
|
||||
<field name="estimated_budget" widget="monetary"/>
|
||||
<field name="client_budget" widget="monetary"/>
|
||||
<field name="actual_cost" widget="monetary"/>
|
||||
<field name="estimated_variance" widget="monetary"/>
|
||||
<field name="estimated_variance_percent" options="{'digits': [16, 2]}"/>
|
||||
<field name="client_variance" widget="monetary"/>
|
||||
<field name="client_variance_percent" options="{'digits': [16, 2]}"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_portfolio_budget_overview_graph" model="ir.ui.view">
|
||||
<field name="name">project.portfolio.budget.overview.graph</field>
|
||||
<field name="model">project.portfolio.budget.overview</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Budget Overview" type="bar">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="estimated_budget" type="measure"/>
|
||||
<field name="client_budget" type="measure"/>
|
||||
<field name="actual_cost" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="action_project_portfolio_budget_overview" model="ir.actions.act_window">
|
||||
<field name="name">Budget Overview</field>
|
||||
<field name="res_model">project.portfolio.budget.overview</field>
|
||||
<field name="view_mode">list,graph,pivot</field>
|
||||
<field name="context">{'search_default_group_by_project': 1}</field>
|
||||
</record>
|
||||
<record id="action_project_portfolio_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Project Dashboard</field>
|
||||
<field name="res_model">project.portfolio.dashboard</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
<field name="context">{'search_default_group_by_project': 1}</field>
|
||||
<!-- <field name="view_ids" eval="[-->
|
||||
<!-- (5, 0, 0),-->
|
||||
<!-- (0, 0, {'view_mode': 'graph', 'view_id': ref('view_project_portfolio_dashboard_graph_budget')}),-->
|
||||
<!-- (0, 0, {'view_mode': 'graph', 'view_id': ref('view_project_portfolio_dashboard_graph_variance')}),-->
|
||||
<!-- (0, 0, {'view_mode': 'graph', 'view_id': ref('view_project_portfolio_dashboard_graph_cost')}),-->
|
||||
<!-- (0, 0, {'view_mode': 'graph', 'view_id': ref('view_project_portfolio_dashboard_graph_performance')}),-->
|
||||
<!-- (0, 0, {'view_mode': 'pivot'})-->
|
||||
<!-- ]"/>-->
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<field name="name" placeholder="Role Name..."/>
|
||||
</h1>
|
||||
<h2>
|
||||
<field name="role_level" readonly="1"/>
|
||||
<field name="role_level" readonly="0"/>
|
||||
</h2>
|
||||
</div>
|
||||
<group>
|
||||
|
|
@ -139,15 +139,12 @@
|
|||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem id="menu_project_role_root"
|
||||
name="Project Roles"
|
||||
sequence="65"
|
||||
parent="project.menu_main_pm"/>
|
||||
<menuitem id="menu_project_role"
|
||||
name="Roles"
|
||||
action="action_project_role"
|
||||
parent="menu_project_role_root"
|
||||
sequence="10"/>
|
||||
parent="project.menu_project_config"
|
||||
sequence="100"/>
|
||||
|
||||
|
||||
|
||||
<record id="project_project_stage_list" model="ir.ui.view">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="project_task_form_kanban" model="ir.ui.view">
|
||||
<field name="name">project.task.kanban.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban" position="attributes">
|
||||
<attribute name="on_create"></attribute>
|
||||
<attribute name="quick_create_view"></attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="project_task_form_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
|
|
@ -14,6 +24,29 @@
|
|||
<strong>THIS TASK IS CURRENTLY PAUSED</strong>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='user_ids']" position="attributes">
|
||||
<attribute name="domain">[('id', 'in', assignee_domain_ids)]</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='timesheet_ids']" position="attributes">
|
||||
<attribute name="context">
|
||||
{
|
||||
'default_task_id': id,
|
||||
'default_employee_id': False
|
||||
}
|
||||
</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='timesheet_ids']//field[@name='employee_id']"
|
||||
position="attributes">
|
||||
<attribute name="domain">
|
||||
[('id', 'in', parent.allowed_employee_ids)]
|
||||
</attribute>
|
||||
</xpath>
|
||||
<!-- <xpath expr="//field[@name='timesheet_ids']//field[@name='stage_id']"-->
|
||||
<!-- position="attributes">-->
|
||||
<!-- <attribute name="domain">-->
|
||||
<!-- [('assigned_user_ids.employee_id', '=', employee_id)]-->
|
||||
<!-- </attribute>-->
|
||||
<!-- </xpath>-->
|
||||
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="after">
|
||||
<group>
|
||||
<h1><field name="sequence_name" readonly="1"/></h1>
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@
|
|||
<field name="name">Internal Teams</field>
|
||||
<field name="res_model">internal.teams</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="domain">[('parent_id', '=', False)]</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ from . import roles_user_assign_wizard
|
|||
from . import internal_team_members_wizard
|
||||
from . import project_stage_update_wizard
|
||||
from . import task_reject_reason_wizard
|
||||
from . import project_cancel_hold_wizard
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class ProjectCancelHoldWizard(models.TransientModel):
|
||||
_name = 'project.cancel.hold.wizard'
|
||||
_description = 'Project Cancel / Hold Wizard'
|
||||
|
||||
|
||||
project_id = fields.Many2one('project.project', required=True)
|
||||
action_type = fields.Selection([
|
||||
('cancel', 'Cancel'),
|
||||
('hold', 'Hold'),
|
||||
], required=True)
|
||||
|
||||
|
||||
reason = fields.Text(string="Reason", required=True)
|
||||
|
||||
|
||||
def action_confirm(self):
|
||||
for wizard in self:
|
||||
if wizard.action_type == 'cancel':
|
||||
wizard.project_id.sudo().write({
|
||||
'project_state': 'cancel',
|
||||
'cancel_reason': f"{wizard.reason}",
|
||||
})
|
||||
else:
|
||||
wizard.project_id.sudo().write({
|
||||
'project_state': 'hold',
|
||||
'hold_reason': f"{wizard.reason}",
|
||||
})
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="project_cancel_hold_wizard_form" model="ir.ui.view">
|
||||
<field name="name">project.cancel.hold.wizard.form</field>
|
||||
<field name="model">project.cancel.hold.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="action_type" invisible="1"/>
|
||||
<field name="reason" placeholder="Enter the reason ..."/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Confirm" type="object" name="action_confirm" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_project_cancel_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Cancel Project</field>
|
||||
<field name="res_model">project.cancel.hold.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="action_project_hold_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Hold Project</field>
|
||||
<field name="res_model">project.cancel.hold.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -15,7 +15,7 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
('project_lead', 'Project Lead / Manager')
|
||||
], readonly=False)
|
||||
fold = fields.Boolean(string='Folded in Kanban', readonly=False)
|
||||
involved_user_ids = fields.Many2many('res.users', domain="[('id','in',related_user_ids)]")
|
||||
involved_user_ids = fields.Many2many('res.users')
|
||||
related_user_ids = fields.Many2many(related="team_id.all_members_ids")
|
||||
|
||||
|
||||
|
|
@ -24,7 +24,8 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
def onchange_team_id(self):
|
||||
for rec in self:
|
||||
if rec.team_id and rec.team_id.all_members_ids:
|
||||
rec.involved_user_ids = [(6,0,rec.team_id.all_members_ids.ids)]
|
||||
if rec.team_id != rec.stage_id.team_id:
|
||||
rec.involved_user_ids = [(6,0,rec.team_id.all_members_ids.ids)]
|
||||
else:
|
||||
rec.involved_user_ids = [(5, 0)]
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
old_stage = self.stage_id
|
||||
|
||||
# Check if stage with same properties exists
|
||||
existing_stage = self.env['project.task.type'].search([
|
||||
stages = self.env['project.task.type'].search([
|
||||
('name', '=', self.name),
|
||||
('team_id', '=', self.team_id.id),
|
||||
('approval_by', '=', self.approval_by),
|
||||
|
|
@ -43,8 +44,16 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
('involved_user_ids','=',self.involved_user_ids.ids)
|
||||
], limit=1)
|
||||
|
||||
existing_stage = stages.filtered(
|
||||
lambda s: set(s.involved_user_ids.ids) == set(self.involved_user_ids.ids)
|
||||
)[:1]
|
||||
|
||||
if existing_stage:
|
||||
existing_stage.sudo().write({
|
||||
'project_ids': [(4, self.project_id.id)]
|
||||
})
|
||||
new_stage = existing_stage
|
||||
|
||||
else:
|
||||
# Instead of copy(), create a clean new record without '(copy)'
|
||||
new_stage = self.env['project.task.type'].create({
|
||||
|
|
@ -53,7 +62,8 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
'approval_by': self.approval_by ,
|
||||
'fold': self.fold,
|
||||
'sequence': old_stage.sequence, # optional: keep same order
|
||||
'involved_user_ids': [(6,0,self.involved_user_ids.ids)]
|
||||
'involved_user_ids': [(6,0,self.involved_user_ids.ids)],
|
||||
'project_ids':[(6,0,self.project_id.ids)]
|
||||
})
|
||||
|
||||
# If new_stage is different from old_stage → update references
|
||||
|
|
@ -61,6 +71,9 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
# Update project type_ids
|
||||
type_ids = project.type_ids.ids
|
||||
if old_stage.id in type_ids:
|
||||
old_stage.sudo().write({
|
||||
'project_ids':[(3,self.project_id.id)]
|
||||
})
|
||||
type_ids.remove(old_stage.id)
|
||||
if new_stage.id not in type_ids:
|
||||
type_ids.append(new_stage.id)
|
||||
|
|
|
|||