@ -0,0 +1,46 @@ |
|||
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg |
|||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html |
|||
:alt: License: LGPL-3 |
|||
|
|||
Document Management |
|||
=================== |
|||
The Document Management module provide a quick access to create, share and delete. |
|||
The module requires beautiful Soup python library |
|||
|
|||
License |
|||
------- |
|||
General Public License, Version 3 (LGPL v3). |
|||
(https://www.odoo.com/documentation/user/17.0/legal/licenses/licenses.html) |
|||
|
|||
Company |
|||
------- |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
|||
|
|||
Credits |
|||
------- |
|||
* Developers: (V16) Mohamed Savad, Gokul PI, Megha AP, Javid, Nisiya, Farhana Jahan PT |
|||
(V17) Gayathri V, |
|||
(V18) Mruthul Raj, |
|||
* Contact: odoo@cybrosys.com |
|||
|
|||
Contacts |
|||
-------- |
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Bug Tracker |
|||
----------- |
|||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. |
|||
|
|||
Maintainer |
|||
========== |
|||
.. image:: https://cybrosys.com/images/logo.png |
|||
:target: https://cybrosys.com |
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
|
|||
For support and more information, please visit `Our Website <https://cybrosys.com/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
|||
@ -0,0 +1,24 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import controllers |
|||
from . import models |
|||
from . import wizards |
|||
@ -0,0 +1,91 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
{ |
|||
'name': 'Document Management', |
|||
'version': '18.0.1.0.0', |
|||
'category': 'Document Management', |
|||
'summary': 'The Document Management module to access document tools', |
|||
'description': 'The Document Management module provides a quick access to ' |
|||
'create, share and delete.', |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': 'https://cybrosys.com', |
|||
'depends': ['mail', 'website', 'hr'], |
|||
'data': [ |
|||
'security/enhanced_document_management_groups.xml', |
|||
'security/ir.model.access.csv', |
|||
'data/document_data.xml', |
|||
'data/ir_cron_data.xml', |
|||
'views/document_tag_views.xml', |
|||
'views/document_request_views.xml', |
|||
'views/document_request_template_views.xml', |
|||
'views/document_delete_trash_views.xml', |
|||
'views/document_lock_views.xml', |
|||
'views/document_workspace_views.xml', |
|||
'views/document_file_views.xml', |
|||
'views/document_portal_templates.xml', |
|||
'views/document_request_wizard_view.xml', |
|||
'views/outgoing_request_document_view.xml', |
|||
'views/portal_document_templates.xml', |
|||
'views/document_trash_views.xml', |
|||
'views/document_request_portal_templates.xml', |
|||
'views/document_request_portal_form.xml', |
|||
'views/document_template_request_portal_templates.xml', |
|||
'wizards/document_request_accept_view.xml', |
|||
'wizards/document_request_reject_view.xml', |
|||
'views/incoming_request_document_views.xml', |
|||
'views/res_config_settings_views.xml', |
|||
'views/enhanced_document_management_menu.xml', |
|||
'reports/document_download.xml', |
|||
'wizards/document_url_view.xml', |
|||
'wizards/document_share_view.xml', |
|||
'wizards/work_space_views.xml', |
|||
], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'enhanced_document_management/static/src/css/kanban.css', |
|||
'enhanced_document_management/static/src/css/style.css', |
|||
'enhanced_document_management/static/src/xml/KanbanController.xml', |
|||
'enhanced_document_management/static/src/xml/document_preview_template.xml', |
|||
'enhanced_document_management/static/src/js/kanban_record.js', |
|||
'enhanced_document_management/static/src/js/kanban_renderer.js', |
|||
'enhanced_document_management/static/src/js/kanbancontroller.js', |
|||
'enhanced_document_management/static/src/js/document_preview_action.js', |
|||
'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js', |
|||
'https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js', |
|||
'https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.6/dist/jquery.fancybox.min.css', |
|||
'https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.6/dist/jquery.fancybox.min.js' |
|||
], |
|||
'web.assets_frontend': [ |
|||
'enhanced_document_management/static/src/js/portal.js', |
|||
] |
|||
}, |
|||
'external_dependencies': { |
|||
'python': ['bs4'] |
|||
}, |
|||
'images': ['static/description/banner.jpg'], |
|||
'license': 'LGPL-3', |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': True, |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import document_file |
|||
from . import document_portal_view |
|||
from . import document_request_portal |
|||
from . import document_templates_request |
|||
from . import website_customer_portal |
|||
from . import website_documents_upload |
|||
@ -0,0 +1,78 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import io |
|||
import zipfile |
|||
from odoo import http |
|||
from odoo.http import request |
|||
|
|||
|
|||
class DocumentFile(http.Controller): |
|||
"""Http Controller to create sharable view for selected documents """ |
|||
|
|||
@http.route('/web/content/share/', type='http', auth='public', |
|||
website='True') |
|||
def document_share(self, **kwargs): |
|||
"""Share a document and prepare the context for rendering. |
|||
:param kwargs: A dictionary containing the 'unique' key for |
|||
identifying the document to share. |
|||
:type kwargs: Dict |
|||
:return: Rendered document_share_preview |
|||
template with the prepared context. |
|||
:rtype: Http.Response""" |
|||
folder_ids = request.env['document.share'].sudo().search([ |
|||
('unique_id', '=', kwargs.get('unique'))]) |
|||
context = ({ |
|||
'doc_id': document.id, |
|||
'doc_name': document.name, |
|||
'doc_extension': document.extension, |
|||
'doc_owner': document.user_id, |
|||
'doc_date': document.date, |
|||
'doc_url': document.content_url, |
|||
} for document in folder_ids.document_ids) |
|||
return http.request.render( |
|||
'enhanced_document_management.document_share_preview', {'context': context} |
|||
) |
|||
|
|||
@http.route("/get/document", type="http", auth='public', |
|||
website='True') |
|||
def download_zip(self): |
|||
""" |
|||
HTTP controller to download selected files as a ZIP archive. |
|||
This controller takes a list of document IDs as input, |
|||
creates a ZIP archive |
|||
containing those documents, and sends it as a response for download. |
|||
:return: HTTP response containing the ZIP archive. |
|||
:rtype: Http.Response |
|||
""" |
|||
param_value = request.params.get('param') |
|||
param_list = eval(param_value) |
|||
zip_data = io.BytesIO() |
|||
with zipfile.ZipFile(zip_data, 'w', zipfile.ZIP_DEFLATED) as zipf: |
|||
for doc in request.env['document.file'].sudo().browse(param_list): |
|||
if doc.content_type != "url": |
|||
zipf.write(doc.attachment_id._full_path( |
|||
doc.attachment_id.store_fname), doc.attachment_id.name) |
|||
headers = [ |
|||
('Content-Type', 'application/zip'), |
|||
('Content-Disposition', http.content_disposition('archive.zip')), |
|||
] |
|||
return request.make_response(zip_data.getvalue(), headers) |
|||
@ -0,0 +1,93 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import http |
|||
from odoo.http import request |
|||
|
|||
|
|||
class DocumentPortalView(http.Controller): |
|||
@http.route('/my/documents', type="http", auth="user", website=True) |
|||
def document_in_portal(self): |
|||
"""Http controller to all user document from portal |
|||
:return Http response with all Documents data""" |
|||
document_ids = request.env['document.file'].search([ |
|||
('user_id.id', '=', request.uid) |
|||
]) |
|||
extensions = set(item.extension for item in document_ids) |
|||
groups = [[rec for rec in document_ids if rec.extension == item |
|||
] for item in extensions] |
|||
return request.render("enhanced_document_management.portal_my_documents", |
|||
{ |
|||
'extensions': extensions, |
|||
'document_ids': groups, |
|||
'page_name': 'document', |
|||
}) |
|||
|
|||
@http.route('/my/document_request', type="http", auth="user", |
|||
website=True) |
|||
def document_request_in_portal(self): |
|||
"""Http controller to access user requests for document from portal |
|||
:return Http response with all Documents data""" |
|||
request_ids = request.env['request.document'].search([ |
|||
('user_id.id', '=', request.uid), |
|||
('state', '=', 'requested')]) |
|||
context = [{ |
|||
'id': item.id, |
|||
'needed_doc': item.needed_doc, |
|||
'workspace_id': [item.workspace_id.id, item.workspace_id.name], |
|||
'requested_by_id': [item.requested_by_id.id, |
|||
item.requested_by_id.name], |
|||
'user_id': [item.user_id.id, item.user_id.name], |
|||
'date': item.create_date.date() |
|||
} for item in request_ids] |
|||
return request.render("enhanced_document_management.portal_my_document_request", |
|||
{ |
|||
'requests': context, |
|||
'page_name': 'document_requests', |
|||
}) |
|||
|
|||
@http.route('/my/documents/<model("document.file"):doc>', type="http", |
|||
auth="user", website=True) |
|||
def document_view(self, doc): |
|||
""" |
|||
Http controller to access document from portal |
|||
:param doc: primary key of a record |
|||
:return Http response with the selected Documents data |
|||
""" |
|||
url = f"""{request.httprequest.host_url[:-1]}/web/content/ |
|||
{doc.attachment_id.id}/{doc.name}""" |
|||
context = { |
|||
'page_name': 'document', |
|||
'document': True, |
|||
'name': doc.name, |
|||
'id': doc.id, |
|||
'owner': doc.user_id, |
|||
'attachment_id': doc.attachment_id.id, |
|||
'brochure_url': doc.brochure_url, |
|||
'workspace_id': doc.workspace_id.name, |
|||
'date': doc.date, |
|||
'url': url, |
|||
'partner_id': doc.partner_id.name, |
|||
'extension': doc.extension, |
|||
'preview': doc.preview, |
|||
'content_url': doc.content_url, |
|||
} |
|||
return request.render("enhanced_document_management.portal_my_document_view", context) |
|||
@ -0,0 +1,51 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo.http import request |
|||
from odoo import http |
|||
from odoo.addons.portal.controllers.portal import CustomerPortal |
|||
|
|||
|
|||
class DocumentRequestCount(CustomerPortal): |
|||
"""Controller handling document request counts and downloads.""" |
|||
def _prepare_home_portal_values(self, counters): |
|||
"""Prepare home portal values including document request count. |
|||
Args:counters (dict): Dictionary of counters. |
|||
Returns:Dictionary of values for the home portal.""" |
|||
vals = super()._prepare_home_portal_values(counters) |
|||
if 'doc_req_count' in counters: |
|||
vals['doc_req_count'] = request.env[ |
|||
'document.template.request'].sudo().search_count( |
|||
[]) |
|||
return vals |
|||
|
|||
@http.route(['/document_request/download/<int:order_id>'], type='http', |
|||
auth="public", website=True) |
|||
def portal_document_download(self, order_id): |
|||
"""Route to handle downloading of requested documents. |
|||
Args:order_id (int): ID of the requested document. |
|||
Returns:HTTP response for the requested document download.""" |
|||
document_request = (request.env['document.template.request']. |
|||
browse(order_id)) |
|||
return self._show_report(model=document_request, |
|||
report_type='pdf', |
|||
report_ref='enhanced_document_management.action_download_doc', |
|||
download=True) |
|||
@ -0,0 +1,140 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import http |
|||
from odoo.http import request |
|||
|
|||
|
|||
class DocumentRequest(http.Controller): |
|||
|
|||
@http.route(['/document_request'], type='http', auth="user", website=True) |
|||
def get_document_request(self): |
|||
"""This route is called whenever the user clicks on the |
|||
'Document Request' menu on the website. |
|||
It checks if the user is an internal user and retrieves |
|||
their document requests, if authorized. |
|||
:return: HTTP response containing the 'my_document_request' |
|||
template with relevant data for the user's document requests. |
|||
""" |
|||
if request.env.user.has_group( |
|||
'enhanced_document_management.view_all_document'): |
|||
values = {'document_requests': request.env[ |
|||
'document.template.request'].sudo().search( |
|||
[('employee_id', '=', request.env.user.employee_id.id)]), |
|||
'document_req_count': request.env[ |
|||
'document.template.request'].sudo(). |
|||
search_count( |
|||
[('employee_id', '=', request.env.user.employee_id.id)])} |
|||
return request.render( |
|||
"enhanced_document_management.document_request_template", |
|||
values) |
|||
else: |
|||
# Return an error message or redirect to an unauthorized page |
|||
return "You are not authorized to access this page." |
|||
|
|||
@http.route(['/document_request/all'], type='http', auth="user", |
|||
website=True) |
|||
def get_all_document_request(self): |
|||
""" This route is called whenever the user clicks on 'My' menu""" |
|||
values = { |
|||
'document_requests': request.env['document.template.request']. |
|||
sudo().search([]), 'document_req_count': request.env[ |
|||
'document.template.request'].sudo().search_count([])} |
|||
return request.render("enhanced_document_management.document_request_template", |
|||
values) |
|||
|
|||
@http.route(['/document_request/draft'], type='http', auth="user", |
|||
website=True) |
|||
def get_draft_document_request(self): |
|||
""" This route is called whenever the user clicks on 'New' menu""" |
|||
values = {'document_requests': request.env[ |
|||
'document.template.request'].sudo().search( |
|||
[('state', '=', 'new')]), |
|||
'document_req_count': request.env[ |
|||
'document.template.request'].sudo().search_count( |
|||
[('state', '=', 'new')])} |
|||
return request.render("enhanced_document_management.document_request_template", |
|||
values) |
|||
|
|||
@http.route(['/document_request/document_approval'], type='http', |
|||
auth="user", |
|||
website=True) |
|||
def get_approval_document_request(self): |
|||
""" This route is called whenever the user clicks on |
|||
'Document Approval' menu""" |
|||
values = {'document_requests': request.env[ |
|||
'document.template.request'].sudo().search( |
|||
[('state', '=', 'document_approval')]), |
|||
'document_req_count': request.env[ |
|||
'document.template.request'].sudo().search_count( |
|||
[('state', '=', 'document_approval')])} |
|||
return request.render("enhanced_document_management.document_request_template", |
|||
values) |
|||
|
|||
@http.route(['/document_request/new'], type='http', auth="user", |
|||
website=True) |
|||
def document_request_form(self): |
|||
""" This route is called whenever the user clicks on |
|||
'Document Request' menu in website""" |
|||
values = { |
|||
'employees': request.env['hr.employee'].sudo().search([]), |
|||
'users': request.env['res.users'].sudo().search([]), |
|||
'templates': request.env[ |
|||
'document.request.template'].sudo().search( |
|||
[]), |
|||
} |
|||
return request.render("enhanced_document_management.document_request_form", |
|||
values) |
|||
|
|||
@http.route(['/document_request/submit'], type='http', auth="user", |
|||
website=True) |
|||
def document_request_submit(self, **kwargs): |
|||
""" This route is called whenever the user hits the 'Submit' button |
|||
after filling the Service Request Form.""" |
|||
request_template = request.env['document.request.template'].browse( |
|||
int(kwargs.get('document_request_template_id'))) |
|||
request.env['document.template.request'].sudo().create({ |
|||
'document_id': request_template.id, |
|||
'manager_id': request_template.manager_id, |
|||
'template': request_template.template, |
|||
'stamp': request_template.stamp, |
|||
}) |
|||
return request.render( |
|||
'enhanced_document_management.document_request_submit_template') |
|||
|
|||
@http.route('/document_request/templates', type='json', auth="public") |
|||
def get_document_request_template(self, **kw): |
|||
"""This route is called whenever a template is selected to render |
|||
that template in UI""" |
|||
return request.env['document.request.template'].browse( |
|||
int(kw.get('template_id'))).template |
|||
|
|||
@http.route(['/document_request/details/request/<int:req_id>'], |
|||
type='http', auth="user", website=True) |
|||
def get_document_request_details(self, req_id): |
|||
""" This route is called whenever the user clicks on |
|||
'Document Request' menu in website""" |
|||
values = { |
|||
'document_request': request.env[ |
|||
'document.template.request'].sudo().browse( |
|||
req_id)} |
|||
return request.render("enhanced_document_management.document_request_details", |
|||
values) |
|||
@ -0,0 +1,48 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo.addons.portal.controllers.portal import CustomerPortal |
|||
from odoo.http import request |
|||
|
|||
|
|||
class WebsiteCustomerPortal(CustomerPortal): |
|||
""" functon : Prepare portal values, datas are searched from document.file |
|||
:return document count, request count""" |
|||
|
|||
def _prepare_home_portal_values(self, counters): |
|||
""" |
|||
Prepare the values for the home portal page. |
|||
:param counters(dict): A dictionary containing various counters for |
|||
different features on the portal. |
|||
:return: A dictionary containing the prepared values for the home |
|||
portal page. |
|||
""" |
|||
values = (super(WebsiteCustomerPortal, self). |
|||
_prepare_home_portal_values(counters)) |
|||
if 'document_count' in counters: |
|||
values['document_count'] = request.env[ |
|||
'document.file'].sudo().search_count([ |
|||
('user_id.id', '=', request.uid)]) |
|||
values['request_count'] = request.env[ |
|||
'request.document'].sudo().search_count([ |
|||
('user_id.id', '=', request.uid), |
|||
('state', '=', 'requested')]) |
|||
return values |
|||
@ -0,0 +1,98 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import base64 |
|||
from odoo import fields |
|||
from odoo import http |
|||
from odoo.http import request |
|||
|
|||
|
|||
class WebsiteDocumentsUpload(http.Controller): |
|||
"""Controller for accept document upload form submission""" |
|||
|
|||
@http.route('/website/documents', type="http", auth="user", |
|||
website=True, csrf=False) |
|||
def website_docs(self, **post): |
|||
""" |
|||
Function : website form submit controller, |
|||
it creates a record in document.file |
|||
:param post: form-data |
|||
:return: redirect |
|||
""" |
|||
val_list = { |
|||
'name': post['file'].filename, |
|||
'attachment': base64.b64encode(post['file'].read()), |
|||
'workspace_id': int(post['workspace']), |
|||
'date': fields.Date.today(), |
|||
'user_id': request.uid, |
|||
'description': post['reason'], |
|||
'security': 'private', |
|||
'extension': post['file'].filename.split(".")[ |
|||
len(post['file'].filename.split(".")) - 1] |
|||
} |
|||
if post['security'] == 'Private': |
|||
val_list['security'] = 'private' |
|||
else: |
|||
val_list['security'] = 'managers_and_owner' |
|||
file_id = request.env['document.file'].create(val_list) |
|||
file_id.action_upload_document() |
|||
return request.redirect("/my/documents") |
|||
|
|||
@http.route('/website/documents_request', type="http", auth="user", |
|||
website=True, csrf=False) |
|||
def website_docs_request(self, **post): |
|||
""" |
|||
Function : website form submit controller for requested documents, |
|||
it creates a record in document.file |
|||
:param post: form-data |
|||
:return: redirect to /my/document_request |
|||
""" |
|||
request_id = request.env['request.document'].browse( |
|||
int(post['rec_id'])) |
|||
file_id = request.env['document.file'].sudo().create({ |
|||
'name': post['file'].filename, |
|||
'attachment': base64.b64encode(post['file'].read()), |
|||
'workspace_id': int(post['workspace']), |
|||
'date': fields.Date.today(), |
|||
'user_id': request.uid, |
|||
'description': post['reason'], |
|||
'security': 'specific_users', |
|||
'user_ids': [post['requested_by_id']] if post['requested_by_id'] else [], |
|||
'extension': post['file'].filename.split(".")[ |
|||
len(post['file'].filename.split(".")) - 1] |
|||
}) |
|||
file_id.action_upload_document() |
|||
request_id.state = 'accepted' |
|||
return request.redirect("/my/document_request") |
|||
|
|||
@http.route('/website/documents_request_reject', type="http", |
|||
auth="user", website=True, csrf=False) |
|||
def document_request_reject(self, **post): |
|||
""" |
|||
Function accept document reject and update document.request |
|||
:param post: form-data |
|||
:return: redirect to /my/document_request |
|||
""" |
|||
request_id = request.env['request.document'].browse( |
|||
int(post['req_id'])) |
|||
request_id.state = 'rejected' |
|||
request_id.reject_reason = post['reason'] |
|||
return request.redirect("/my/document_request") |
|||
@ -0,0 +1,19 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<data noupdate="1"> |
|||
<!-- default document workspace --> |
|||
<record id="document_workspace_default" model="document.workspace"> |
|||
<field name="name">My Workspace</field> |
|||
<field name="display_name">My Workspace</field> |
|||
<field name="description">The workspace here is the default</field> |
|||
</record> |
|||
<!-- Adding sequence for insurance--> |
|||
<record id="document_request_sequence" model="ir.sequence"> |
|||
<field name="name">Document Details</field> |
|||
<field name="code">document.request</field> |
|||
<field name="prefix">DOC/REQ</field> |
|||
<field name="padding">3</field> |
|||
<field name="company_id" eval="False"/> |
|||
</record> |
|||
</data> |
|||
</odoo> |
|||
@ -0,0 +1,23 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<odoo noupdate="1"> |
|||
<!-- Create a schedule action for file delete while module installation --> |
|||
<record id="ir_cron_schedule_delete_file" model="ir.cron"> |
|||
<field name="name">Document Delete</field> |
|||
<field name="model_id" ref="enhanced_document_management.model_document_trash"/> |
|||
<field name="state">code</field> |
|||
<field name="code">model.delete_doc()</field> |
|||
<field name="user_id" ref="base.user_root"/> |
|||
<field name="interval_number">1</field> |
|||
<field name="interval_type">days</field> |
|||
</record> |
|||
<!-- Create a schedule action for removed file that inside trash while module installation --> |
|||
<record id="ir_cron_auto_delete_file" model="ir.cron"> |
|||
<field name="name">Auto Document Delete</field> |
|||
<field name="model_id" ref="enhanced_document_management.model_document_file"/> |
|||
<field name="state">code</field> |
|||
<field name="code">model.auto_delete_doc()</field> |
|||
<field name="user_id" ref="base.user_root"/> |
|||
<field name="interval_number">1</field> |
|||
<field name="interval_type">days</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,7 @@ |
|||
## Module <enhanced_document_management> |
|||
|
|||
#### 26.02.2025 |
|||
#### Version 17.0.1.0.0 |
|||
#### ADD |
|||
|
|||
- Initial commit for Document Management |
|||
@ -0,0 +1,31 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import document_delete_trash |
|||
from . import document_file |
|||
from . import document_lock |
|||
from . import document_template_request |
|||
from . import document_tag |
|||
from . import document_request_template |
|||
from . import document_trash |
|||
from . import document_workspace |
|||
from . import request_document |
|||
from . import res_config_settings |
|||
@ -0,0 +1,84 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class DocumentDeleteTrash(models.Model): |
|||
_name = "document.delete.trash" |
|||
_description = "document permanent delete or move to trash" |
|||
|
|||
document_file_id = fields.Many2one("document.file", |
|||
string='document file id', |
|||
help='file if of document to delete') |
|||
|
|||
def document_move_trash(self): |
|||
""" |
|||
Move a document to the trash if it is not locked, otherwise return an |
|||
action to unlock the document. |
|||
This method checks if a document is locked. If it is not locked, it |
|||
moves the document to the trash. |
|||
If the document is locked, it returns an action to unlock the document. |
|||
:return: Action to move to the trash or unlock the document. |
|||
:rtype: dict or None |
|||
""" |
|||
if not self.document_file_id.is_locked: |
|||
self.document_file_id.document_file_delete( |
|||
self.document_file_id.id) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Lock', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_move_trash_view_form').id, |
|||
'target': 'new', |
|||
'context': { |
|||
'default_document_file_id': self.document_file_id.id, |
|||
} |
|||
} |
|||
|
|||
def document_permanent_delete(self): |
|||
""" |
|||
Permanently delete a document if it is not locked, or return an action |
|||
to unlock the document. |
|||
This method checks if a document is locked. If it is not locked, it |
|||
permanently deletes the document. |
|||
If the document is locked, it returns an action to unlock the document. |
|||
:return: Action to permanently delete or unlock the document. |
|||
:rtype: dict or None |
|||
""" |
|||
if not self.document_file_id.is_locked: |
|||
self.document_file_id.unlink() |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Lock', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_delete_view_form').id, |
|||
'target': 'new', |
|||
'context': { |
|||
'default_document_file_id': self.document_file_id.id, |
|||
} |
|||
} |
|||
@ -0,0 +1,721 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class DocumentFile(models.Model): |
|||
""" Model used to store documents, perform document related functions """ |
|||
_name = 'document.file' |
|||
_inherit = ['mail.thread', 'mail.activity.mixin'] |
|||
_description = 'Documents' |
|||
|
|||
name = fields.Char(string="Name", help="Document name") |
|||
attachment = fields.Binary(string='File', help="Document data") |
|||
date = fields.Datetime(string='Date', help="Document create date") |
|||
workspace_id = fields.Many2one('document.workspace', |
|||
string='Workspace', |
|||
required=True, help="workspace name") |
|||
security = fields.Selection( |
|||
selection=[ |
|||
('private', 'Private'), |
|||
('managers_and_owner', 'Managers & Owner'), |
|||
('specific_users', 'Specific Users') |
|||
], default='managers_and_owner', string="Security", |
|||
help="""Privet : only the uploaded user can view |
|||
Managers & Owner : Document shared with Managers """) |
|||
user_id = fields.Many2one('res.users', string='Owner', |
|||
default=lambda self: self.env.user, |
|||
help="""Document owner name, if the document |
|||
belongs to a specific partner""") |
|||
brochure_url = fields.Char(string="URL", help="Document sharable URL") |
|||
extension = fields.Char(string='Extension', help="""Document extension, |
|||
helps to determine the file type""") |
|||
priority = fields.Selection( |
|||
[('0', 'None'), ('1', 'Favorite')], |
|||
string="Priority", help="Favorite button") |
|||
activity_ids = fields.One2many('mail.activity', string='Activities', |
|||
help="List of activity ids") |
|||
attachment_id = fields.Many2one('ir.attachment', ondelete='restrict', |
|||
help="Attached item") |
|||
content_url = fields.Char(string='Content Url', |
|||
help='content url') |
|||
content_type = fields.Selection( |
|||
selection=[('file', 'File'), ('url', 'Url')], |
|||
help="Document content type", string="Content type") |
|||
preview = fields.Char(string='Preview', help="Preview URL") |
|||
active = fields.Boolean(string='Active', default=True, |
|||
help="document is active or not") |
|||
deleted_date = fields.Date(string="Deleted Date", |
|||
help="Document auto delete date") |
|||
mimetype = fields.Char(string='Mime Type', help="Document mimetype") |
|||
description = fields.Text(string='Description', |
|||
help="Write the description here") |
|||
user_ids = fields.Many2many('res.users', string="User", |
|||
help="Document related users") |
|||
partner_id = fields.Many2one('res.partner', string="Partner", |
|||
help="Document related") |
|||
auto_delete = fields.Boolean(string='Auto Delete', default=False, |
|||
help="Document delete status") |
|||
days = fields.Integer(string='Days', help="auto delete in days") |
|||
trash = fields.Boolean(string='Trash', help="To specify deleted items") |
|||
delete_date = fields.Date(string='Date Delete', readonly=True, store=True) |
|||
file_url = fields.Char(string='File URL', help="""it store url while adding |
|||
an url document""") |
|||
size = fields.Char(string='Size', compute='_compute_size', |
|||
help='computed size of document') |
|||
is_locked = fields.Boolean(string="Lock document", |
|||
help="To make document lock", |
|||
default=False) |
|||
document_file_id = fields.Many2one('document.file', string='Document File', |
|||
help="Add document here") |
|||
google_drive_file_key = fields.Char(string='Google Drive file id', |
|||
help='id of file from google drive') |
|||
one_drive_file_key = fields.Char(string='One Drive file id', |
|||
help='id of file from one drive') |
|||
is_crm_install = fields.Boolean(string="is crm installed", |
|||
help="field to check crm installed or not") |
|||
is_project_install = fields.Boolean(string="is project installed", |
|||
help="field to check project installed " |
|||
"or not") |
|||
document_tag_id = fields.Many2one('document.tag', string='Tags') |
|||
|
|||
def auto_delete_doc(self): |
|||
"""Function to delete document automatically using schedule action""" |
|||
self.search([ |
|||
('auto_delete', '=', True), |
|||
('delete_date', '<=', fields.Date.today())]).unlink() |
|||
|
|||
@api.depends('attachment_id') |
|||
def _compute_size(self): |
|||
"""Function is used to fetch the file size of an attachment""" |
|||
for rec in self: |
|||
rec.size = str(rec.attachment_id.file_size / 1000) + ' Kb' |
|||
|
|||
def action_upload_document(self): |
|||
"""Function it works while uploading a file, and it adds some basic |
|||
information about the file""" |
|||
# important to maintain extension and name as different |
|||
attachment_id = self.env['ir.attachment'].sudo().create({ |
|||
'name': self.name, |
|||
'datas': self.attachment, |
|||
'res_model': 'document.file', |
|||
'res_id': self.id, |
|||
'public': True, |
|||
}) |
|||
self.sudo().write({ |
|||
'name': self.name, |
|||
'date': fields.Date.today(), |
|||
'user_id': self.env.uid, |
|||
'extension': self.name.split(".")[len(self.name.split(".")) - 1], |
|||
'content_url': |
|||
f"/web/content/{attachment_id.id}/{self.name}", |
|||
'mimetype': attachment_id.mimetype, |
|||
'attachment_id': attachment_id.id, |
|||
'brochure_url': attachment_id.local_url, |
|||
'is_crm_install': True if self.env[ |
|||
'ir.module.module'].sudo().search( |
|||
[('name', '=', 'crm')]).state == 'installed' else False, |
|||
'is_project_install': True if self.env[ |
|||
'ir.module.module'].sudo().search( |
|||
[('name', '=', 'project')]).state == 'installed' else False |
|||
}) |
|||
if self.env.context.get('active_model') == "request.document": |
|||
self.env['request.document'].search( |
|||
[('id', '=', self.env.context.get('active_id'))]).write({ |
|||
'state': 'accepted' |
|||
}) |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'reload' |
|||
} |
|||
|
|||
@api.model |
|||
def action_document_request_upload_document(self, *args): |
|||
""" |
|||
Upload a document and add basic information about the file to the |
|||
database.This function is used to upload a file and create a |
|||
corresponding database record with basic information about the |
|||
uploaded file, such as name, date, extension, and more. |
|||
:param args: A tuple containing a dictionary with information about |
|||
the uploaded file. |
|||
:type args: Tuple, |
|||
:return: None |
|||
""" |
|||
attachment_id = self.env['ir.attachment'].sudo().create({ |
|||
'name': args[0]['file_name'], |
|||
'datas': args[0]['file'], |
|||
'res_model': 'document.file', |
|||
'res_id': self.id, |
|||
'public': True, |
|||
}) |
|||
base_url = (self.env['ir.config_parameter'].sudo(). |
|||
get_param('web.base.url')) |
|||
self.sudo().create({ |
|||
'attachment': attachment_id.datas, |
|||
'workspace_id': args[0]['workspace_id'] if args[0][ |
|||
'workspace_id'] else 1, |
|||
'name': args[0]['file_name'], |
|||
'date': fields.Datetime.today().now(), |
|||
'user_id': self.env.uid, |
|||
'extension': args[0]['file_name'].split(".")[ |
|||
len(args[0]['file_name'].split(".")) - 1], |
|||
'content_url': f"""{base_url}/web/content/{attachment_id.id}/ |
|||
{args[0]['file_name']}""", |
|||
'mimetype': attachment_id.mimetype, |
|||
'attachment_id': attachment_id.id, |
|||
'is_crm_install': True if self.env[ |
|||
'ir.module.module'].sudo().search( |
|||
[('name', '=', 'crm')]).state == 'installed' else False, |
|||
'is_project_install': True if self.env[ |
|||
'ir.module.module'].sudo().search( |
|||
[('name', '=', 'project')]).state == 'installed' else False |
|||
}) |
|||
|
|||
@api.model |
|||
def download_zip_function(self, document_selected): |
|||
""" |
|||
Download selected documents as a ZIP archive. |
|||
This function generates a URL to download the selected documents |
|||
as a ZIP archive. |
|||
:param document_selected: A list of document IDs to be included in |
|||
the ZIP archive. |
|||
:type document_selected: list |
|||
:return: Action to open url a for downloading the ZIP archive. |
|||
""" |
|||
url = "/get/document?param=" + str(document_selected) |
|||
return { |
|||
'type': 'ir.actions.act_url', |
|||
'url': url, |
|||
'target': 'new', |
|||
} |
|||
|
|||
@api.model |
|||
def document_file_delete(self, doc_ids): |
|||
""" |
|||
Delete documents and move them to the document.trash. |
|||
This function is responsible for deleting documents and creating |
|||
corresponding records |
|||
in the 'document.trash' table to keep track of the deleted documents. |
|||
:param doc_ids: A list of document IDs to be deleted and moved |
|||
to the trash. |
|||
:type doc_ids: list |
|||
:return: None |
|||
""" |
|||
for docs in self.browse(doc_ids): |
|||
self.env['document.trash'].create({ |
|||
'name': docs.name, |
|||
'attachment': docs.attachment, |
|||
'document_create_date': docs.date, |
|||
'workspace_id': docs.workspace_id.id, |
|||
'user_id': docs.user_id.id, |
|||
'brochure_url': docs.brochure_url, |
|||
'extension': docs.extension, |
|||
'priority': docs.priority, |
|||
'attachment_id': docs.attachment_id.id, |
|||
'content_url': docs.content_url, |
|||
'content_type': docs.content_type, |
|||
'preview': docs.preview, |
|||
'active': docs.active, |
|||
'deleted_date': fields.Date.today(), |
|||
'mimetype': docs.mimetype, |
|||
'description': docs.description, |
|||
'security': docs.security, |
|||
'user_ids': docs.user_ids.ids, |
|||
'partner_id': docs.partner_id.id, |
|||
'days': docs.days, |
|||
'file_url': docs.file_url, |
|||
}) |
|||
docs.unlink() |
|||
|
|||
@api.model |
|||
def document_file_archive(self, documents_selected): |
|||
""" |
|||
Archive documents and toggle their active status or delete date. |
|||
This function is used to archive documents. Depending on the current |
|||
state of |
|||
the documents, it either toggles the 'active' status to mark them as |
|||
archived |
|||
or clears the 'delete_date' and sets them as active again. |
|||
:param documents_selected: document ID to be archived. |
|||
:type documents_selected: int |
|||
:return: None |
|||
""" |
|||
for docs in self.browse(documents_selected): |
|||
|
|||
if docs.active: |
|||
docs.active = False |
|||
elif docs.delete_date: |
|||
docs.delete_date = False |
|||
docs.active = True |
|||
else: |
|||
docs.active = True |
|||
|
|||
@api.model |
|||
def on_mail_document(self, doc_ids): |
|||
""" |
|||
Open a new email compose window to send selected documents |
|||
as attachments.This function opens a new email compose window with |
|||
selected documents as attachments.The 'doc_ids' parameter |
|||
contains a list of document IDs to be attached to the email. |
|||
:param doc_ids: A list of document IDs to be attached to the email. |
|||
:type doc_ids: list |
|||
:return: Action to open a new email compose window. |
|||
""" |
|||
default_composition_mode = self.env.context.get( |
|||
'default_composition_mode', |
|||
self.env.context.get('composition_mode', 'comment')) |
|||
compose_ctx = dict( |
|||
default_composition_mode=default_composition_mode, |
|||
default_model='document.file', |
|||
default_res_ids=doc_ids, |
|||
mail_tz=self.env.user.tz, |
|||
default_attachment_ids=self.browse(doc_ids).mapped( |
|||
'attachment_id').ids |
|||
) |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Sent document', |
|||
'view_mode': 'form', |
|||
'res_model': 'mail.compose.message', |
|||
'views': [(False, 'form')], |
|||
'view_id': False, |
|||
'target': 'new', |
|||
'context': compose_ctx, |
|||
} |
|||
|
|||
@api.model |
|||
def action_btn_create_task(self, doc): |
|||
""" |
|||
Create a task based on a document if the 'project' module is installed. |
|||
This function checks if the 'project' module is installed and, if so, |
|||
creates a task with the name of the document. It also associates the |
|||
document's attachment with the task. |
|||
:param doc: The document for which a task should be created. |
|||
:type doc: recordset |
|||
:return: True if the task is created, False if the 'project' module |
|||
is not installed. |
|||
:rtype: bool |
|||
""" |
|||
module_id = self.env['ir.module.module'].sudo().search( |
|||
[('name', '=', 'project')]) |
|||
if module_id.state == 'installed': |
|||
for rec in self.browse(doc): |
|||
task_id = self.env['project.task'].create({ |
|||
'name': rec['name'] |
|||
}) |
|||
rec.attachment_id.res_model = 'project.task' |
|||
rec.attachment_id.res_id = task_id |
|||
return True |
|||
return False |
|||
|
|||
@api.model |
|||
def action_btn_create_lead(self, doc): |
|||
""" |
|||
Create a CRM lead based on a document if the 'crm' module is installed. |
|||
This function checks if the 'crm' module is installed and, if so, |
|||
creates a lead with the name of the document. It also associates |
|||
the document's attachment with the lead. |
|||
:param doc: The document for which a CRM lead should be created. |
|||
:type doc: recordset |
|||
:return: True if the lead is created, False if the 'crm' module is |
|||
not installed. |
|||
:rtype: bool |
|||
""" |
|||
module_id = self.env['ir.module.module'].sudo().search( |
|||
[('name', '=', 'crm')]) |
|||
if module_id.state == 'installed': |
|||
for rec in self.browse(doc): |
|||
lead_id = self.env['crm.lead'].create({ |
|||
'name': rec['name'] |
|||
}) |
|||
rec.attachment_id.res_model = 'crm.lead' |
|||
rec.attachment_id.res_id = lead_id |
|||
return True |
|||
return False |
|||
|
|||
@api.model |
|||
def delete_doc(self): |
|||
""" |
|||
Delete documents from the trash based on the configured |
|||
retention period.This function deletes documents from the |
|||
'document.trash' table based on the configured retention period |
|||
specified in the 'enhanced_document_management.trash' parameter.It checks |
|||
the 'deleted_date' of each document in the trash and deletes those |
|||
that have reached or exceeded the retention |
|||
period. |
|||
:return: None |
|||
""" |
|||
limit = self.env['ir.config_parameter'].sudo().get_param( |
|||
'enhanced_document_management.trash') |
|||
for rec in self.env['document.trash'].search( |
|||
[('deleted_date', '!=', False)]): |
|||
delta = fields.Date.today() - rec.deleted_date |
|||
if delta.days == limit: |
|||
rec.unlink() |
|||
|
|||
def click_share(self): |
|||
""" |
|||
Share a single document by generating a shareable URL or requesting a |
|||
password for locked documents. |
|||
This function allows users to share a single document. |
|||
If the document is not locked,it generates a shareable URL using the |
|||
'document.share' model. If the document is locked, |
|||
it opens a window to prompt the user to enter a password for access. |
|||
:return: Shareable URL or action to enter a password. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
if not self.browse(document_id).is_locked: |
|||
return self.env['document.share'].create_url([document_id]) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Please Enter Password', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_share_view_form').id, |
|||
'target': 'new', |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
def click_create_lead(self): |
|||
""" |
|||
Create a CRM lead from the document's dropdown menu. |
|||
This method allows users to create a CRM lead from a document if the |
|||
document is not locked. |
|||
If the document is locked, it prompts the user to enter a password. |
|||
:return: Action to create a lead or a notification to install the CRM |
|||
module. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
if not self.browse(document_id).is_locked: |
|||
result = self.action_btn_create_lead(document_id) |
|||
if not result: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Install CRM Module to use this function", |
|||
'type': 'info', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Please Enter Password', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_lead_view_form').id, |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
def click_create_task(self): |
|||
""" |
|||
Create a task from the document's dropdown menu. |
|||
This method allows users to create a task from a document if |
|||
the document is not locked. |
|||
If the document is locked, it prompts the user to enter a password. |
|||
:return: Action to create a task or a notification to install the |
|||
Project module. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
if not self.browse(document_id).is_locked: |
|||
result = self.action_btn_create_task(document_id) |
|||
if not result: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Install Project Module to use this " |
|||
"function", |
|||
'type': 'info', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Please Enter Password', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_task_view_form').id, |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
def click_create_mail(self): |
|||
""" |
|||
Create an email from the document's dropdown menu. |
|||
This method allows users to create an email from a document if the |
|||
document is not locked. |
|||
If the document is locked, it prompts the user to enter a password. |
|||
:return: Action to create an email or a window to enter a password. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
if not self.browse(document_id).is_locked: |
|||
return self.on_mail_document([document_id]) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Please Enter Password', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_mail_view_form').id, |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
def click_copy_move(self): |
|||
""" |
|||
Copy or move a document from one workspace to another using the |
|||
document's dropdown.His method allows users with appropriate |
|||
permissions to copy or move a document from one workspace to another. |
|||
It checks if the document is locked and if the user has the |
|||
'enhanced_document_management.view_all_document' group. If the user has permission |
|||
and the document is not locked, it opens a window for copying the |
|||
document. If the document is locked, it prompts the user to enter a |
|||
password. If the user does not have |
|||
permission, it displays a notification. |
|||
:return: Action to copy or move a document, or a notification for |
|||
permission denied. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
user_group = self.env.user.has_group( |
|||
'enhanced_document_management.view_all_document') |
|||
if user_group: |
|||
if not self.browse(document_id).is_locked: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'copy', |
|||
'res_model': 'work.space', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'views': [[False, 'form']], |
|||
'context': { |
|||
'default_doc_ids': [document_id] |
|||
} |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Please Enter Password', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_copy_view_form').id, |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "You don't have permission to " |
|||
"perform this action", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def click_document_archive(self): |
|||
"""Archive a document from the document's dropdown menu. |
|||
This method allows users to archive a document if the document is not |
|||
locked. |
|||
If the document is locked, it prompts the user to enter a password. |
|||
:return: None or action to enter a password. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
if not self.browse(document_id).is_locked: |
|||
self.document_file_archive(document_id) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Please Enter Password', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_archive_view_form').id, |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
def click_document_delete(self): |
|||
""" |
|||
Delete a document from the document's dropdown menu. |
|||
|
|||
This method allows users to delete a document if they have the |
|||
'enhanced_document_management.view_all_document' group. It opens a window for |
|||
users to choose between moving the document to the trash or |
|||
permanently deleting it. |
|||
:return: Action to choose between moving to trash or permanent |
|||
deletion. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
user_group = self.env.user.has_group( |
|||
'enhanced_document_management.view_all_document') |
|||
if user_group: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Choose One', |
|||
'res_model': 'document.delete.trash', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_delete_trash_view_form').id, |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
def click_document_lock(self): |
|||
""" |
|||
Lock a document from the document's dropdown menu. |
|||
This method allows users to lock a document by opening a window where |
|||
they can set a password for the document. Once locked, |
|||
the document can only be accessed with the provided password. |
|||
:return: Action to set a password and lock the document. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
document = self.env.ref('enhanced_document_management.document_lock_view_form') |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Lock', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'view_id': document.id, |
|||
'target': 'new', |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
def click_document_unlock(self): |
|||
""" |
|||
Unlock a document from the document's dropdown menu. |
|||
This method allows users to unlock a locked document by opening a |
|||
window where they can enter |
|||
the document's password for access. |
|||
:return: Action to enter the document's password and unlock it. |
|||
""" |
|||
document_id = self.env.context.get('document_id', False) |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Unlock', |
|||
'res_model': 'document.lock', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'view_id': self.env.ref( |
|||
'enhanced_document_management.document_lock_unlock_view_form').id, |
|||
'context': { |
|||
'default_document_file_id': document_id, |
|||
} |
|||
} |
|||
|
|||
@api.model |
|||
def get_documents_list(self, *args): |
|||
"""Get a list of documents to open the document viewer. |
|||
This method retrieves information about a specific document and a |
|||
list of other documents that can be viewed in the document viewer. |
|||
It provides details such as the document's name, download URL, |
|||
extension, and more. It also checks if a document is viewable based |
|||
on its extension and whether it's locked. |
|||
:param args: A list of arguments, with the first argument being the |
|||
document ID to retrieve. |
|||
:type args: list |
|||
:return: Information about the specified document and a list of other |
|||
viewable documents. |
|||
:rtype: tuple""" |
|||
document = self.browse(int(args[0])) |
|||
attachment = ({ |
|||
'defaultSource': document.brochure_url, |
|||
'displayName': document.name, |
|||
'downloadUrl': document.brochure_url, |
|||
'extension': document.extension, |
|||
'filename': document.name, |
|||
'id': document.id, |
|||
'originThreadLocalId': 'document.file,' + str( |
|||
document.id), |
|||
'uid': str(document.create_uid.id), |
|||
'isPdf': True if document.extension == 'pdf' else False, |
|||
'isImage': True if document.mimetype == 'image/jpeg' else False, |
|||
'isViewable': True if not document.is_locked and ( |
|||
document.mimetype == 'image/jpeg' or document.extension == |
|||
'pdf') else False, |
|||
'mimetype': document.mimetype, |
|||
'urlRoute': "/web/image/" + str(document.attachment_id.id), |
|||
}) |
|||
attachment_list = [{ |
|||
'defaultSource': record.brochure_url, |
|||
'displayName': str(record.name), |
|||
'downloadUrl': record.brochure_url, |
|||
'extension': record.extension, |
|||
'filename': record.name, |
|||
'id': record.id, |
|||
'originThreadLocalId': 'document.file,' + str( |
|||
record.id), |
|||
'uid': str(record.create_uid.id), |
|||
'isPdf': True if record.extension == 'pdf' else False, |
|||
'isImage': True if record.mimetype == 'image/jpeg' else False, |
|||
'isViewable': True if not record.is_locked and ( |
|||
record.mimetype == 'image/jpeg' or record.extension == |
|||
'pdf') else False, |
|||
'mimetype': record.mimetype, |
|||
'urlRoute': "/web/image/" + str(record.attachment_id.id), |
|||
} for record in self.search([]) if record.extension == 'pdf' or |
|||
record.mimetype == 'image/jpeg'] |
|||
return attachment, attachment_list |
|||
|
|||
@api.model |
|||
def get_document_count(self, *args): |
|||
""" |
|||
Get the count of documents in a workspace and all documents. |
|||
This method counts the number of documents in a specific workspace |
|||
and the total number of documents across all workspaces. |
|||
:param args: A list of arguments, with the first argument being the |
|||
workspace ID. |
|||
:type args: list |
|||
:return: A tuple containing the count of documents in the specified |
|||
workspace and the count of all documents. |
|||
:rtype: tuple |
|||
""" |
|||
document_count_in_workspace = self.search_count( |
|||
[('workspace_id', '=', args[0])]) |
|||
all_document_count = self.sudo().search_count([]) |
|||
return document_count_in_workspace, all_document_count |
|||
@ -0,0 +1,424 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import hashlib |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class DocumentLock(models.Model): |
|||
"""Model used to lock documents and do other functions |
|||
for locked documents""" |
|||
_name = 'document.lock' |
|||
_description = 'Document Lock' |
|||
_rec_name = 'document_file_id' |
|||
|
|||
document_file_id = fields.Many2one('document.file', string='document file', |
|||
help="id of document file") |
|||
password = fields.Char(string='password', required=True, |
|||
help="Password to lock document") |
|||
is_lock = fields.Boolean(string="Lock", |
|||
help='field for document is lock or not') |
|||
|
|||
def lock_doc(self): |
|||
""" |
|||
Lock a document. |
|||
This method allows authorized users to lock a document by setting a |
|||
password for it.It checks if the user has the |
|||
'enhanced_document_management.view_all_document' group. If the user has permission, |
|||
it hashes the provided password and sets the 'is_lock' flag to True for |
|||
both the document and its associated file. |
|||
:return: None or a notification if the user does not have permission. |
|||
""" |
|||
if self.env.user.has_group('enhanced_document_management.view_all_document'): |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest(), |
|||
'is_lock': True |
|||
|
|||
}) |
|||
self.document_file_id.is_locked = True |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "You dont have permission to" |
|||
" lock the documents", |
|||
'type': 'info', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def unlock_doc(self): |
|||
""" |
|||
Unlock a document. |
|||
This method allows users to unlock a locked document by verifying the |
|||
provided password.It hashes the provided password and checks if it |
|||
matches the stored password for the document. If the passwords match, |
|||
the document is unlocked, and the associated lock records are removed. |
|||
If the passwords do not match, an incorrect password |
|||
notification is displayed. |
|||
:return: None or a notification if the password is incorrect. |
|||
:rtype: None or dict |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
self.document_file_id.is_locked = False |
|||
self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").unlink() |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_share(self): |
|||
""" |
|||
Share a locked document. |
|||
This method allows users to share a locked document by verifying the |
|||
provided password.It hashes the provided password and checks if it |
|||
matches the stored password for the document. If the passwords match, |
|||
the document is shared using the 'document.share' model. |
|||
If the passwords do not match, an incorrect password notification is |
|||
displayed. |
|||
:return: URL for sharing the document or a notification |
|||
if the password is incorrect. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
return self.env['document.share'].create_url( |
|||
[self.document_file_id.id]) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_download(self): |
|||
""" |
|||
Download a locked document. |
|||
This method allows users to download a locked document by verifying |
|||
the provided password.It hashes the provided password and checks |
|||
if it matches the stored password for the document. If the passwords |
|||
match, the document is downloaded using 'ir.actions.act_url'. |
|||
If the passwords do not match, an incorrect password notification is |
|||
displayed. |
|||
:return: Action to download the document or a notification |
|||
if the password is incorrect. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
document_url = self.env.context.get('document_url', False) |
|||
return { |
|||
'type': 'ir.actions.act_url', |
|||
'url': document_url + '?download=true', |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_create_lead(self): |
|||
""" |
|||
Create a lead for a locked document. |
|||
This method allows users to create a lead for a locked document by |
|||
verifying the provided password. |
|||
It hashes the provided password and checks if it matches the stored |
|||
password for the document. If the passwords match, it calls the |
|||
'action_btn_create_lead' method of the associated document file |
|||
to create a lead. If the passwords do not match, an incorrect |
|||
password notification is displayed. |
|||
:return: Action to create a lead, a notification for CRM module |
|||
installation,or a notification for an incorrect password. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
result = self.document_file_id.action_btn_create_lead( |
|||
self.document_file_id.id) |
|||
if not result: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Install CRM Module to use this function", |
|||
'type': 'info', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_create_task(self): |
|||
""" |
|||
Create a task for a locked document. |
|||
This method allows users to create a task for a locked document by |
|||
verifying the provided password.It hashes the provided password and |
|||
checks if it matches the stored password for the document. If the |
|||
passwords match, it calls the 'action_btn_create_task' method of the |
|||
associated document file to create a task. If the passwords do not |
|||
match, an incorrect password notification is displayed. |
|||
:return: Action to create a task, a notification for Project module |
|||
installation,or a notification for an incorrect password. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
result = self.document_file_id.action_btn_create_task( |
|||
self.document_file_id.id) |
|||
if not result: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Install Project Module to use this " |
|||
"function", |
|||
'type': 'info', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_lock_mail(self): |
|||
""" |
|||
Create mail for locked documents.This method allows users to create a |
|||
mail for a locked document by verifying the provided password.It |
|||
hashes the provided password and checks if it matches the stored |
|||
password for the document. If the passwords match, it calls the |
|||
'on_mail_document' method of the associated document file to create a |
|||
mail. If the passwords do not match, an incorrect password notification |
|||
is displayed. |
|||
:return: Action to create a mail or a notification for an incorrect |
|||
password. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
return self.document_file_id.on_mail_document( |
|||
[self.document_file_id.id]) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_copy_mail(self): |
|||
""" |
|||
Copy or move locked documents. |
|||
This method allows users to copy or move locked documents from one |
|||
workspace to another by verifying the provided password. It hashes the |
|||
provided password and checks if it matches the stored password |
|||
for the document. If the passwords match, it opens a window to copy or |
|||
move the document. If the passwords do not match, an incorrect |
|||
password notification is displayed. |
|||
:return: Action to copy or move documents or a notification for an |
|||
incorrect password. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'copy', |
|||
'res_model': 'work.space', |
|||
'view_mode': 'form', |
|||
'target': 'new', |
|||
'views': [[False, 'form']], |
|||
'context': { |
|||
'default_doc_ids': [self.document_file_id.id] |
|||
} |
|||
} |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_lock_archive(self): |
|||
""" |
|||
Archive locked documents after verifying the password. |
|||
This method archives locked documents by checking the provided password |
|||
against the stored password.If the passwords match, it archives the |
|||
document using the 'document_file_archive' method of the associated |
|||
document file. If not, it displays an incorrect password notification. |
|||
:return: Archive action or incorrect password notification. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
self.document_file_id.document_file_archive( |
|||
self.document_file_id.id) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_move_to_trash(self): |
|||
""" |
|||
Move locked documents to the trash folder after password verification. |
|||
This method moves locked documents to the trash folder by verifying the |
|||
provided password against the stored password. If the passwords match, |
|||
it performs the document deletion using the 'document_file_delete' |
|||
method of the associated document file. If not, it displays an |
|||
incorrect password notification. |
|||
:return: Trash action or incorrect password notification. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.env['document.lock'].search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], |
|||
order="id desc").password: |
|||
self.document_file_id.document_file_delete( |
|||
self.document_file_id.id) |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
|
|||
def document_delete_permanent(self): |
|||
""" |
|||
Permanently delete locked documents after password verification. |
|||
This method permanently deletes locked documents by verifying the |
|||
provided password against the stored password. If the passwords match, |
|||
it deletes the document using the 'unlink' method of the associated |
|||
document file. If not, it displays an incorrect password notification. |
|||
:return: Deletion action or incorrect password notification. |
|||
""" |
|||
password = hashlib.sha256(self.password.encode()).hexdigest() |
|||
self.write({ |
|||
'password': hashlib.sha256(self.password.encode()).hexdigest() |
|||
}) |
|||
if password == self.search( |
|||
[('document_file_id', '=', self.document_file_id.id), |
|||
('is_lock', '=', True)], order="id desc").password: |
|||
self.document_file_id.unlink() |
|||
else: |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'display_notification', |
|||
'params': { |
|||
'message': "Incorrect Password", |
|||
'type': 'danger', |
|||
'sticky': False, |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class DocumentRequestTemplate(models.Model): |
|||
"""Model representing document request templates.""" |
|||
_name = "document.request.template" |
|||
_description = "Document request template" |
|||
_inherit = ['mail.thread', 'mail.activity.mixin'] |
|||
|
|||
name = fields.Char(string="Template name", |
|||
required=True, |
|||
help="name of template") |
|||
user_ids = fields.Many2many('res.users', string="User", help="Choose User", |
|||
compute='_get_managers') |
|||
company_id = fields.Many2one('res.company', string='Company', |
|||
help='choose company',default=lambda self: self.env.company) |
|||
manager_id = fields.Many2one("res.users", string="Managers", |
|||
help="Choose Manager", required=True) |
|||
stamp = fields.Image(string="Stamp", max_width=170, max_height=170, |
|||
help="Add your stamp") |
|||
template = fields.Html(string="Template", help="Add the template here") |
|||
|
|||
@api.depends('name') |
|||
def _get_managers(self): |
|||
"""function to get user's with document request template |
|||
manager group""" |
|||
self.user_ids = self.manager_id.search([]).filtered( |
|||
lambda managers: managers.has_group( |
|||
'enhanced_document_management.view_all_document')) |
|||
@ -0,0 +1,33 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class DocumentTag(models.Model): |
|||
_name = "document.tag" |
|||
_description ="Document tag" |
|||
|
|||
name = fields.Char(string="name",required=True) |
|||
|
|||
_sql_constraints = [ |
|||
('name_uniq', 'unique (name)', "Tag name already exists!"), |
|||
] |
|||
@ -0,0 +1,113 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import api, fields, models, _ |
|||
|
|||
|
|||
class DocumentTemplateRequest(models.Model): |
|||
"""Model representing document requests.""" |
|||
_name = "document.template.request" |
|||
_description = "Document Request" |
|||
_rec_name = "display_name" |
|||
_inherit = 'mail.thread' |
|||
|
|||
display_name = fields.Char(string="Display Name", |
|||
help='display name of document request',require=True ) |
|||
document_id = fields.Many2one('document.request.template', |
|||
help="Selected document template", |
|||
string="Document", required=True) |
|||
user_id = fields.Many2one('res.users', |
|||
string="User", help="User creating the request", |
|||
default=lambda self: self.env.user) |
|||
manager_id = fields.Many2one('res.users', string="Manager", |
|||
help="Manager of the document", |
|||
related="document_id.manager_id") |
|||
requested_date = fields.Date(string="Requested Date", |
|||
help="Date when the request was made", |
|||
default=fields.Date.today()) |
|||
template = fields.Html(string="Template", |
|||
help="HTML template content") |
|||
stamp = fields.Image(string="Image", |
|||
help="Image associated with the document") |
|||
create_user_avatar = fields.Binary(help="Users image", |
|||
string="Author", |
|||
related='create_uid.image_1920') |
|||
state = fields.Selection( |
|||
[('new', 'New'), |
|||
('document_approval', 'Document Approval'), |
|||
('approved', 'Approved')], default='new', help="Avatar of the author", |
|||
tracking=True, string="State") |
|||
employee_id = fields.Many2one('hr.employee', string="Employee") |
|||
|
|||
@api.model_create_multi |
|||
def create(self, vals_list): |
|||
"""Super the create function in document.template.request to generate |
|||
sequences""" |
|||
for vals in vals_list: |
|||
if not vals.get('display_name') or vals['display_name'] == _( |
|||
'New'): |
|||
vals['display_name'] = self.env['ir.sequence'].next_by_code( |
|||
'document.template.request') or _('New') |
|||
return super().create(vals_list) |
|||
|
|||
def action_sent_document_approval(self): |
|||
"""Function to change the state to 'document_approval'.""" |
|||
self.write({'state': 'document_approval'}) |
|||
|
|||
def action_approve(self): |
|||
"""Function to change the state to 'approved'.""" |
|||
self.write({'state': 'approved'}) |
|||
|
|||
def action_preview_document(self): |
|||
"""Function to preview the document |
|||
Return: |
|||
client action to view preview of document |
|||
""" |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'preview_document', |
|||
'params': { |
|||
'body_html': self.template, |
|||
'stamp': self.stamp |
|||
} |
|||
} |
|||
|
|||
def action_download_document(self): |
|||
"""method to download the document""" |
|||
data = {'template': self.template, |
|||
'image': self.stamp} |
|||
return self.env.ref( |
|||
'enhanced_document_management.action_download_doc').report_action( |
|||
None, |
|||
data=data) |
|||
|
|||
def _get_report_base_filename(self): |
|||
"""Method to return document's name""" |
|||
self.ensure_one() |
|||
return f'{self.document_id.name}' |
|||
|
|||
@api.onchange('document_id') |
|||
def _onchange_document(self): |
|||
"""Onchange function to set the template and stamp based on the |
|||
document template selected""" |
|||
if self.document_id: |
|||
self.template = self.document_id.template |
|||
self.stamp = self.document_id.stamp |
|||
@ -0,0 +1,191 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import api, fields, models, _ |
|||
|
|||
|
|||
class DocumentTrash(models.Model): |
|||
"""Module to store deleted documents for a specific time, |
|||
then it automatically""" |
|||
_name = "document.trash" |
|||
_description = "Document Trash" |
|||
|
|||
name = fields.Char(string="Name", help="Document name") |
|||
attachment = fields.Binary(string="File", readonly=True, |
|||
help="Document data") |
|||
document_create_date = fields.Datetime(string="Date", |
|||
help="Document create date") |
|||
workspace_id = fields.Many2one( |
|||
"document.workspace", string="Workspace", required=True, |
|||
help="workspace name" |
|||
) |
|||
user_id = fields.Many2one( |
|||
"res.users", |
|||
string="Owner", |
|||
default=lambda self: self.env.user, |
|||
help="""owner name, if the document belongs to a specific user""", |
|||
) |
|||
brochure_url = fields.Char(string="URL", store=True, |
|||
help="Document sharable URL") |
|||
extension = fields.Char(string="Extension", |
|||
help="helps to determine the file type") |
|||
priority = fields.Selection( |
|||
selection=[("0", "None"), ("1", "Favorite")], |
|||
string="Priority", |
|||
help="Favorite button", |
|||
) |
|||
attachment_id = fields.Many2one( |
|||
"ir.attachment", |
|||
string="Attachment", |
|||
help="Used to access datas without search function", |
|||
) |
|||
content_url = fields.Char( |
|||
string="Content Url", help="It store the URL for url type documents" |
|||
) |
|||
content_type = fields.Selection( |
|||
[("file", "File"), ("url", "Url")], |
|||
help="Document content type", |
|||
string="Content type", |
|||
) |
|||
preview = fields.Char( |
|||
string="Preview", help="Used to show a preview for URL file type" |
|||
) |
|||
active = fields.Boolean( |
|||
string="Active", default=True, help="It specify archived file" |
|||
) |
|||
days = fields.Integer(string="Days", help="auto delete in days") |
|||
deleted_date = fields.Date(string="Deleted Date", help="File deleted date") |
|||
mimetype = fields.Char(string="Mime Type", help="Document mimetype") |
|||
description = fields.Text(string="Description", help="Short description") |
|||
security = fields.Selection( |
|||
string="Security", |
|||
selection=[ |
|||
("private", "Private"), |
|||
("managers_and_owner", "Managers & Owner"), |
|||
("specific_users", "Specific Users"), |
|||
], |
|||
default="managers_and_owner", |
|||
help="""Privet : only the uploaded user can view |
|||
Managers & Owner : Document shared with Managers """, |
|||
) |
|||
user_ids = fields.Many2many( |
|||
"res.users", help="Can access the documents", string="User Access" |
|||
) |
|||
partner_id = fields.Many2one( |
|||
"res.partner", help="Document related partner name", |
|||
string="Related Partner" |
|||
) |
|||
auto_delete = fields.Boolean( |
|||
string="Auto Delete", default=False, help="Document delete status" |
|||
) |
|||
delete_date = fields.Date( |
|||
string="Date Delete", |
|||
readonly=True, |
|||
help="Used to calculate file remove date from trash", |
|||
) |
|||
file_url = fields.Char( |
|||
string="File URL", help="""it store url while adding an url document""" |
|||
) |
|||
size = fields.Char(string="Size", help="it store size of the document") |
|||
company_id = fields.Many2one( |
|||
related='workspace_id.company_id', string='Company', |
|||
help="Company Name") |
|||
|
|||
def delete_doc(self): |
|||
"""Function to delete all the documents after the trash date""" |
|||
trash_limit = ( |
|||
self.env["ir.config_parameter"] |
|||
.sudo() |
|||
.get_param("document_management.trash") |
|||
) |
|||
if trash_limit: |
|||
for rec in self.search([]): |
|||
if fields.Date.today() == fields.Date.add( |
|||
rec.deleted_date, days=int(trash_limit) |
|||
): |
|||
rec.unlink() |
|||
|
|||
def action_restore_document(self): |
|||
""" |
|||
Restore a previously deleted document from the trash. |
|||
This function restores a deleted document by creating a new record in |
|||
the 'document.file' model with the same attributes as the deleted |
|||
document. It then unlinks the deleted document and returns to the |
|||
'Trash' view. |
|||
:return: Window action to view the 'Trash' or restore the document. |
|||
:rtype: dict |
|||
""" |
|||
doc_id = self.env['document.file'].create({ |
|||
'name': self.name, |
|||
'extension': self.extension, |
|||
'attachment': self.attachment, |
|||
'date': fields.Date.today(), |
|||
'workspace_id': self.workspace_id.id, |
|||
'user_id': self.user_id.id, |
|||
'content_type': self.content_type, |
|||
'brochure_url': self.brochure_url, |
|||
'active': self.active, |
|||
'mimetype': self.mimetype, |
|||
'description': self.description, |
|||
'content_url': self.content_url, |
|||
'user_ids': self.user_ids.ids, |
|||
'partner_id': self.partner_id, |
|||
'days': self.days, |
|||
}) |
|||
attachment_id = self.env['ir.attachment'].sudo().create( |
|||
{'name': self.name, |
|||
'datas': self.attachment, |
|||
'res_model': 'document.file', |
|||
'res_id': self.id, |
|||
} |
|||
) |
|||
doc_id.attachment_id = attachment_id.id |
|||
self.unlink() |
|||
return { |
|||
'name': _('Trash'), |
|||
'target': 'main', |
|||
'view_mode': 'tree,form', |
|||
'res_model': 'document.trash', |
|||
'type': 'ir.actions.act_window', |
|||
} |
|||
|
|||
@api.onchange('days') |
|||
def _onchange_days(self): |
|||
""" |
|||
Set the delete date for a record based on the specified number of days. |
|||
This function calculates and sets the delete date for a record by |
|||
adding the specified number of days to the current date. |
|||
The record will be automatically deleted on the calculated delete date. |
|||
:return: None |
|||
""" |
|||
self.write({'delete_date': fields.Date.add(fields.Date.today(), |
|||
days=self.days)}) |
|||
|
|||
def auto_delete_doc(self): |
|||
""" |
|||
Automatically delete documents based on a schedule action. |
|||
This function searches for documents marked for automatic deletion |
|||
(auto_delete=True) and with a delete date less than or equal to the |
|||
current date. It then deletes these documents from the system. |
|||
:return: None |
|||
""" |
|||
self.search([('auto_delete', '=', True), |
|||
('delete_date', '<=', fields.Date.today())]).unlink() |
|||
@ -0,0 +1,88 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class DocumentWorkspace(models.Model): |
|||
""" Model to store document workspace """ |
|||
_name = 'document.workspace' |
|||
_description = 'Document Workspace' |
|||
_inherit = 'mail.thread' |
|||
|
|||
name = fields.Char(string='Name', required=True, |
|||
help="Name of the WorkSpace.") |
|||
display_name = fields.Char(string='Workspace', |
|||
compute='_compute_display_name', |
|||
help="Name of the workSpace.") |
|||
company_id = fields.Many2one('res.company', string='Company', |
|||
help="WorkSpace belongs to this company",default=lambda self: self.env.company) |
|||
description = fields.Text(string='Description', |
|||
help="Description about the workSpace") |
|||
document_count = fields.Integer(compute='_compute_document_count', |
|||
string='Document Count', |
|||
help="Number of documents uploaded " |
|||
"under this workSpace") |
|||
privacy_visibility = fields.Selection([ |
|||
('followers', 'Invited internal users (private)'), |
|||
('employees', 'All internal users'), ], |
|||
string='Visibility', required=True, |
|||
default='employees', |
|||
help='- Invited internal users: when following a workspace, internal ' |
|||
'users will get access to all of its documents without ' |
|||
'distinction \n\n' |
|||
'All internal users: all internal users can access the ' |
|||
'workspace and all of its documents without distinction.\n\n') |
|||
google_drive_folder_id = fields.Char( |
|||
string='Google drive folder id', |
|||
help='Id of workspace in google drive if created', |
|||
copy=False, readonly=True) |
|||
onedrive_folder_id = fields.Char( |
|||
string='One drive folder id', |
|||
help='Id of workspace in one drive if created', |
|||
copy=False, readonly=True) |
|||
|
|||
def button_view_document(self): |
|||
""" |
|||
Open the Kanban view of associated documents. |
|||
This function opens the Kanban view displaying documents associated |
|||
with the current workspace. |
|||
:return: Action to open the Kanban view |
|||
""" |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'res_model': 'document.file', |
|||
'name': self.name, |
|||
'view_mode': 'kanban,form', |
|||
'view_type': 'form', |
|||
'target': 'current', |
|||
'domain': [('workspace_id', '=', self.id)] |
|||
} |
|||
|
|||
def _compute_document_count(self): |
|||
""" |
|||
Calculate the number of documents associated with this workspace. |
|||
This function computes the count of documents that belong to the |
|||
current workspace. |
|||
""" |
|||
for record in self: |
|||
record.document_count = self.env['document.file'].search_count( |
|||
[('workspace_id', '=', self.id)]) |
|||
@ -0,0 +1,111 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import _, api, fields, models |
|||
|
|||
|
|||
class RequestDocument(models.Model): |
|||
""" module to store document requests """ |
|||
_name = 'request.document' |
|||
_description = 'Request document from user' |
|||
_rec_name = 'display_name' |
|||
_inherit = ['mail.thread', 'mail.activity.mixin'] |
|||
|
|||
display_name = fields.Char(string="Display Name", |
|||
help='display name document request',required=True) |
|||
user_id = fields.Many2one('res.users', string='User', help="Choose User", |
|||
required=True) |
|||
requested_by_id = fields.Many2one('res.users', |
|||
help="User who created request", |
|||
string="Requested User", |
|||
default=lambda self: self.env.user, |
|||
readonly=True) |
|||
needed_doc = fields.Text(string='Document Needed', required=True, |
|||
help="Document needed by requestor") |
|||
workspace_id = fields.Many2one('document.workspace', |
|||
string='Workspace', |
|||
required=True, |
|||
help="Select the workspace associated with" |
|||
" this item.") |
|||
manager_id = fields.Many2one('res.users', string='Manager', |
|||
help="Select Manager") |
|||
workspace = fields.Char(related='workspace_id.name', |
|||
string='Workspace Name', |
|||
help='Workspace name') |
|||
reject_reason = fields.Text(string='Reason', help="Reason for rejection") |
|||
state = fields.Selection(selection=[('draft', 'Draft'), |
|||
('requested', 'Requested'), |
|||
('accepted', 'Accepted'), |
|||
('rejected', 'Rejected')], |
|||
default='draft', string="State", |
|||
help="Choose the current state of the item: Requested, Accepted, or " |
|||
"Rejected.") |
|||
|
|||
def action_send_document_request(self): |
|||
""" function to send document request through email """ |
|||
self.state = 'requested' |
|||
user_id = self.env['res.users'].browse(self.env.uid) |
|||
mail_content = f'Hello <br/> {user_id.name} Requested Document <br/>' \ |
|||
f'{self.needed_doc}' |
|||
main_content = { |
|||
'subject': _('Document Request'), |
|||
'body_html': mail_content, |
|||
'email_to': self.user_id.partner_id.email, |
|||
} |
|||
self.env['mail.mail'].sudo().create(main_content).send() |
|||
|
|||
@api.model |
|||
def get_request(self): |
|||
""" |
|||
Function to fetch all requests for the currently logged-in user. |
|||
This function retrieves all requests related to the current user from |
|||
the 'request.document' model and formats |
|||
the data into a list of dictionaries containing relevant information |
|||
about each request. |
|||
Returns: |
|||
list of dict: A list of dictionaries containing information |
|||
about the requests. |
|||
""" |
|||
request_ids = self.env['request.document'].search( |
|||
[('user_id', '=', self.env.uid)]) |
|||
context = [{ |
|||
'request_id': rec.id, |
|||
'user_id': rec.user_id.name, |
|||
'manager_id': rec.manager_id.name, |
|||
'needed_doc': rec.needed_doc, |
|||
'workspace': rec.workspace, |
|||
'workspace_id': rec.workspace_id.id, |
|||
} for rec in request_ids] |
|||
return context |
|||
|
|||
@api.model |
|||
def get_wizard_view(self, view_id): |
|||
""" Method is used to get the wizard view xml id for wizard """ |
|||
return self.env.ref(view_id).id |
|||
|
|||
@api.model_create_multi |
|||
def create(self, vals_list): |
|||
""" Super the create function to generate sequences for document.request""" |
|||
for vals in vals_list: |
|||
if not vals.get('display_name') or vals['display_name'] == _('New'): |
|||
vals['display_name'] = self.env['ir.sequence'].next_by_code( |
|||
'document.request') or _('New') |
|||
return super().create(vals_list) |
|||
@ -0,0 +1,76 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Mruthul Raj(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class ResConfigSettings(models.TransientModel): |
|||
""" Inherited res.config.settings to add trash limit field""" |
|||
_inherit = 'res.config.settings' |
|||
|
|||
trash = fields.Integer( |
|||
string='Trash Limit', |
|||
default=30, |
|||
help="set the time limit for the deleted files", |
|||
config_parameter='enhanced_document_management.trash') |
|||
|
|||
@api.model_create_multi |
|||
def create(self, vals_list): |
|||
"""Create method to set scheduled action's interval number for |
|||
Google Drive and OneDrive synchronization. |
|||
This method is used to create or update the interval number of |
|||
scheduled actions for Google Drive and OneDrive |
|||
synchronization based on the provided configuration values. |
|||
Args: |
|||
vals_list (list): List of dictionaries containing values to |
|||
create the configuration settings. |
|||
Returns: |
|||
RecordSet: The created configuration settings record.""" |
|||
self.is_crm_installed(vals_list) |
|||
self.is_project_installed(vals_list) |
|||
res = super(ResConfigSettings, self).create(vals_list) |
|||
return res |
|||
|
|||
def is_crm_installed(self, vals_list): |
|||
""" |
|||
Set 'is_crm_install' field for all 'document.file' records. |
|||
Args: |
|||
vals_list (list): List of dictionaries containing values. |
|||
Returns: |
|||
None |
|||
""" |
|||
records = self.env['document.file'].search([]) |
|||
is_crm_install = vals_list[0].get('module_crm') |
|||
for record in records: |
|||
record.write({'is_crm_install': is_crm_install}) |
|||
|
|||
def is_project_installed(self, vals_list): |
|||
""" |
|||
Set 'is_project_install' field for all 'document.file' records. |
|||
Args: |
|||
vals_list (list): List of dictionaries containing values. |
|||
Returns: |
|||
None |
|||
""" |
|||
records = self.env['document.file'].search([]) |
|||
is_project_install = vals_list[0].get('module_project') |
|||
for record in records: |
|||
record.write({'is_project_install': is_project_install}) |
|||
@ -0,0 +1,26 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<odoo> |
|||
<!-- An action to download a document template in PDF format--> |
|||
<record id="action_download_doc" model="ir.actions.report"> |
|||
<field name="name">Document Download</field> |
|||
<field name="model">document.template.request</field> |
|||
<field name="report_type">qweb-pdf</field> |
|||
<field name="report_name">enhanced_document_management.document_download</field> |
|||
<field name="report_file">enhanced_document_management.document_download</field> |
|||
</record> |
|||
<!-- Template for requested document --> |
|||
<template id="document_download"> |
|||
<t t-call="web.html_container"> |
|||
<t t-call="web.external_layout"> |
|||
<t t-foreach="docs" t-as="o"> |
|||
<img t-if='o.stamp' t-attf-src="data:image/png;base64,{{o.stamp}}" |
|||
alt="Logo" style="margin-left:75%;"/> |
|||
<t t-raw="o.template"/> |
|||
</t> |
|||
<img t-if='image' t-attf-src="data:image/png;base64,{{image}}" |
|||
alt="Logo" style="margin-left:75%;"/> |
|||
<t t-if="template" t-raw="template"/> |
|||
</t> |
|||
</t> |
|||
</template> |
|||
</odoo> |
|||
@ -0,0 +1,102 @@ |
|||
<odoo> |
|||
<!-- Define a module category for Document Management --> |
|||
<record id="module_document_module" model="ir.module.category"> |
|||
<field name="name">Document Management</field> |
|||
<field name="description">Access for Document module</field> |
|||
<field name="sequence">50</field> |
|||
</record> |
|||
<!-- Define a group 'User' --> |
|||
<record id="view_own_document" model="res.groups"> |
|||
<field name="name">User</field> |
|||
<field name="category_id" ref="module_document_module"/> |
|||
</record> |
|||
<!-- Define a group 'Manager' with implied 'User' group and associated users --> |
|||
<record id="view_all_document" model="res.groups"> |
|||
<field name="name">Manager</field> |
|||
<field name="category_id" ref="module_document_module"/> |
|||
<field name="implied_ids" eval="[(4, ref('view_own_document'))]"/> |
|||
<field name="users" eval="[(4, ref('base.user_admin'))]"/> |
|||
</record> |
|||
<!-- Define an access rule 'Document Manager' --> |
|||
<record id="document_workspace_rule_manager" model="ir.rule"> |
|||
<field name="name">Document Manager</field> |
|||
<field name="model_id" ref="model_document_workspace"/> |
|||
<field name="domain_force">['|',('privacy_visibility', '=', |
|||
'followers'), ('privacy_visibility', '=', 'employees')] |
|||
</field> |
|||
<field name="groups" eval="[(4, ref('view_all_document'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Define an access rule 'View visible Workspace' --> |
|||
<record id="document_workspace_rule_own" model="ir.rule"> |
|||
<field name="name">View visible Workspace</field> |
|||
<field name="model_id" ref="model_document_workspace"/> |
|||
<field name="domain_force">['|',('privacy_visibility', '=', |
|||
'employees'),('message_partner_ids', 'in', [user.partner_id.id])] |
|||
</field> |
|||
<field name="groups" eval="[(4, ref('view_own_document'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="False"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Define an access rule 'Document File Visibility' --> |
|||
<record id="document_file_rule_visibility" model="ir.rule"> |
|||
<field name="name">Document File Visibility</field> |
|||
<field name="model_id" ref="model_document_file"/> |
|||
<field name="domain_force">['|',('workspace_id.privacy_visibility', |
|||
'=', 'employees'),('workspace_id.message_partner_ids', 'in', |
|||
[user.partner_id.id])] |
|||
</field> |
|||
<field name="groups" eval="[(4, ref('view_own_document'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="False"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Define an access rule 'View Own Request' --> |
|||
<record id="request_document_rule_own" model="ir.rule"> |
|||
<field name="name">View Own Request</field> |
|||
<field name="model_id" ref="model_request_document"/> |
|||
<field name="domain_force"> |
|||
['|',('requested_by_id', '=', user.id), |
|||
('user_id', '=', user.id)] |
|||
</field> |
|||
<field name="groups" eval="[(4, ref('view_own_document'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Define an access rule 'View All Request' --> |
|||
<record id="request_document_rule_all" model="ir.rule"> |
|||
<field name="name">View All Request</field> |
|||
<field name="model_id" ref="model_request_document"/> |
|||
<field name="domain_force">[(1, '=', 1)]</field> |
|||
<field name="groups" eval="[(4, ref('view_all_document'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Set default groups for the base.default_user --> |
|||
<record id="base.default_user" model="res.users"> |
|||
<field name="groups_id" |
|||
eval="[(4,ref('enhanced_document_management.view_own_document'))]"/> |
|||
</record> |
|||
<!-- Access rule for document templates based on multi-company settings.--> |
|||
<record id="document_request_template_rule_comp" model="ir.rule"> |
|||
<field name="name">Document template multi-company</field> |
|||
<field name="model_id" ref="model_document_request_template"/> |
|||
<field name="domain_force">['|', ('company_id', 'parent_of', company_ids), ('company_id', '=', False)] |
|||
</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,113 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<odoo> |
|||
<!-- Record rule for Document Manager he can create/update his record--> |
|||
<record id="document_management_rule_manager" model="ir.rule"> |
|||
<field name="name">Document Manager</field> |
|||
<field name="model_id" ref="model_document_file"/> |
|||
<field name="domain_force">['|', '|', ('security', '=', 'specific_users'), |
|||
('security', '=', 'managers_and_owner'), '&', |
|||
('security', '=', 'private'), |
|||
('user_id', '=', user.id)]</field> |
|||
<field name="groups" eval="[(4, ref('document_management_group_manager'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Record rule for Document Manager he can create/update record with security --> |
|||
<record id="document_management_rule_user" model="ir.rule"> |
|||
<field name="name">View Own Document</field> |
|||
<field name="model_id" ref="model_document_file"/> |
|||
<field name="domain_force">['|', '|', '|', '|', ('user_ids', 'in', user.id), '&', |
|||
('security', '=', 'specific_users'), |
|||
('user_ids', '=', False), '&', |
|||
('security', '=', 'private'), |
|||
('user_id', '=', user.id), '&', |
|||
('security', '=', 'managers_and_owner'), |
|||
('user_id', '=', user.id), '&', |
|||
('user_ids', 'not in', user.id), |
|||
('user_id', '=', user.id)]</field> |
|||
<field name="groups" eval="[(4, ref('document_management_group_user'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="False"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Record rule for Document user he can create/update his record --> |
|||
<record id="document_management_rule_own" model="ir.rule"> |
|||
<field name="name">View Own Request</field> |
|||
<field name="model_id" ref="model_request_document"/> |
|||
<field name="domain_force"> |
|||
['|',('requested_by', '=', user.id), |
|||
('user_id', '=', user.id)] |
|||
</field> |
|||
<field name="groups" eval="[(4, ref('document_management_group_user'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
<!-- Record rule for Document manager he can create/update record that shared with him --> |
|||
<record id="document_management_rule_all" model="ir.rule"> |
|||
<field name="name">View All Request</field> |
|||
<field name="model_id" ref="model_request_document"/> |
|||
<field name="domain_force">[(1, '=', 1)]</field> |
|||
<field name="groups" eval="[(4, ref('document_management_group_manager'))]"/> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
|
|||
<!-- Record role for multy-company environment --> |
|||
<record id="document_file_multy_company" model="ir.rule"> |
|||
<field name="name">Documents multy-company</field> |
|||
<field name="model_id" ref="model_document_file"/> |
|||
<field name="domain_force">['|',('company_id', '=',False),('company_id', 'in', company_ids)]</field> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
|
|||
<!-- Record role for multy-company environment --> |
|||
<record id="document_workspace_multy_company" model="ir.rule"> |
|||
<field name="name">Document workspace multy-company</field> |
|||
<field name="model_id" ref="model_document_workspace"/> |
|||
<field name="domain_force">['|',('company_id', '=',False),('company_id', 'in', company_ids)]</field> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
|
|||
<!-- Record role for multy-company environment --> |
|||
<record id="document_trash_multy_company" model="ir.rule"> |
|||
<field name="name">Document trash multy-company</field> |
|||
<field name="model_id" ref="model_document_trash"/> |
|||
<field name="domain_force">['|',('company_id', '=',False),('company_id', 'in', company_ids)]</field> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
|
|||
<!-- Record role for multy-company environment --> |
|||
<record id="document_requests_multy_company" model="ir.rule"> |
|||
<field name="name">Document requests multy-company</field> |
|||
<field name="model_id" ref="model_request_document"/> |
|||
<field name="domain_force">['|',('company_id', '=',False),('company_id', 'in', company_ids)]</field> |
|||
<field name="perm_read" eval="True"/> |
|||
<field name="perm_write" eval="True"/> |
|||
<field name="perm_create" eval="True"/> |
|||
<field name="perm_unlink" eval="True"/> |
|||
<field name="active" eval="True"/> |
|||
</record> |
|||
</odoo> |
|||
|
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 738 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 911 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 600 B |
|
After Width: | Height: | Size: 673 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 905 B |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 767 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 697 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 776 KiB |
|
After Width: | Height: | Size: 48 KiB |