Compare commits
12 commits
18.0
...
documents-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e85c8655f | ||
|
|
a987afd5d3 | ||
|
|
f0f92cf483 | ||
|
|
1379fe609d | ||
|
|
6c88f4a2d0 | ||
|
|
a30e89a777 | ||
|
|
9a4d145537 | ||
|
|
e1576aba18 | ||
|
|
e4f5ae123a | ||
|
|
981af372f6 | ||
|
|
679e29f4da | ||
|
|
ee7882ea69 |
35 changed files with 896 additions and 0 deletions
3
bemade_document_versions/__init__.py
Normal file
3
bemade_document_versions/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
||||
35
bemade_document_versions/__manifest__.py
Normal file
35
bemade_document_versions/__manifest__.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) September 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Document Versions',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Adds document revisions alongside Documents',
|
||||
'category': 'Document Management',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['documents', 'mail'],
|
||||
'data': ['security/ir.model.access.csv',
|
||||
'data/document_revision_data.xml',
|
||||
'views/document_views.xml',
|
||||
'wizard/document_revision_wizard.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
18
bemade_document_versions/data/document_revision_data.xml
Normal file
18
bemade_document_versions/data/document_revision_data.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.sequence" id="document_revision_sequence_default">
|
||||
<field name="name">Default Document Revision Sequence</field>
|
||||
<field name="code">document.revision</field>
|
||||
<field name="implementation">standard</field>
|
||||
<field name="prefix">DOC/</field>
|
||||
<field name="padding">2</field>
|
||||
</record>
|
||||
<record id="document_revision_workflow_rule" model="documents.workflow.rule">
|
||||
<field name="sequence">0</field>
|
||||
<field name="name">Revise</field>
|
||||
<field name="domain_folder_id" ref="documents.documents_internal_folder"/>
|
||||
<field name="create_model">documents.revision</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
4
bemade_document_versions/models/__init__.py
Normal file
4
bemade_document_versions/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from . import document
|
||||
from . import document_revision
|
||||
from . import document_revision_sequence
|
||||
from . import workflow
|
||||
99
bemade_document_versions/models/document.py
Normal file
99
bemade_document_versions/models/document.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
from odoo import models, fields, _, api, Command
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
_inherit = 'documents.document'
|
||||
|
||||
""" In order to implement named revisions, we hook into the already-existing version
|
||||
tracking that documents.document records implement. The field previous_attachmen_ids
|
||||
is already used to contain ir.attachment records that have been replaced after a
|
||||
write call. What we are adding here is the concept of tying a revision number/name
|
||||
to the current revision and keeping track of the same thing for previous revisions.
|
||||
|
||||
This is implemented in the documents.revision record.
|
||||
"""
|
||||
|
||||
current_revision_id = fields.Many2one('documents.revision', 'Current Revision')
|
||||
revision_ids = fields.One2many('documents.revision', 'document_id')
|
||||
revision_sequence = fields.Many2one('ir.sequence',
|
||||
'Revision Sequence', )
|
||||
|
||||
def write(self, vals):
|
||||
# When a document already has an attachment and we are passed a new
|
||||
# attachment_id in vals, we need to update the revision at least for the
|
||||
# current revision and maybe for the one that was superseded.
|
||||
recs_to_update = self.filtered(lambda r: r.attachment_id)
|
||||
super().write(vals)
|
||||
if 'attachment_id' in vals:
|
||||
recs_to_update.update_revision()
|
||||
|
||||
|
||||
def update_revision(self):
|
||||
for rec in self:
|
||||
if not rec.revision_sequence:
|
||||
rec.set_up_revisions()
|
||||
|
||||
# TODO : finish writing this method
|
||||
|
||||
def set_up_revisions(self, initial_revision_name: str = None,
|
||||
sequence_prefix: str = None,
|
||||
sequence_suffix: str = None, sequence_padding: int = None):
|
||||
"""
|
||||
Helper method to set up revisions, no matter how we got into tracking them.
|
||||
:param initial_revision_name str: The name to give to the initial revision. By
|
||||
default, this will be the next name from the sequence.
|
||||
:param sequence_prefix: The prefix to use for the new revision sequence.
|
||||
:param sequence_suffix: The suffix to use for the new revision sequence.
|
||||
:return None:
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.revision_sequence = self.revision_sequence or self._create_rev_sequence(
|
||||
sequence_prefix, sequence_suffix, sequence_padding)
|
||||
self.revision_ids = self.revision_ids or [Command.set(
|
||||
[self._create_first_revision().id])]
|
||||
|
||||
|
||||
def _create_rev_sequence(self, sequence_prefix: str = None,
|
||||
sequence_suffix: str = None, sequence_padding: int = None):
|
||||
"""
|
||||
Helper method to create a new sequence for a document that we are just starting
|
||||
to track revisions for.
|
||||
|
||||
:param sequence_prefix: The prefix to use for the sequence.
|
||||
:param sequence_suffix: The suffix to use for the sequence.
|
||||
:param sequence_padding: The padding to use for the sequence.
|
||||
:return: The created ir.sequence record.
|
||||
"""
|
||||
return self.env['ir.sequence'].create({
|
||||
'document_id': self.id,
|
||||
'name': f'sequence_doc{self.id}',
|
||||
'prefix': sequence_prefix,
|
||||
'suffix': sequence_suffix,
|
||||
'padding': sequence_padding,
|
||||
'implementation': 'standard',
|
||||
})
|
||||
|
||||
|
||||
def _create_first_revision(self):
|
||||
"""
|
||||
Creates a new documents.revision record to associate to this Document
|
||||
|
||||
:param name: Optional name to use for the newly created revision.
|
||||
:return: a single, new documents.revision record
|
||||
"""
|
||||
self.ensure_one()
|
||||
name = self.revision_sequence.next_by_id()
|
||||
return self.env['documents.revision'].create({
|
||||
'document_id': self.id,
|
||||
'name': name,
|
||||
'attachment_id': self.attachment_id.id,
|
||||
})
|
||||
|
||||
|
||||
def get_next_revision_name(self):
|
||||
"""
|
||||
:return str: The name of the next revision in the sequence or None if revision
|
||||
tracking is not yet set up.
|
||||
"""
|
||||
return self.revision_sequence.predict_next_id()
|
||||
36
bemade_document_versions/models/document_revision.py
Normal file
36
bemade_document_versions/models/document_revision.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from odoo import models, fields, _, api
|
||||
|
||||
|
||||
class DocumentRevision(models.Model):
|
||||
_name = "documents.revision"
|
||||
_description = "Document Revision"
|
||||
_sql_constraints = [
|
||||
('name_document_id_unique', 'unique (name,document_id)',
|
||||
'The revision name must be unique for each document.')]
|
||||
|
||||
name = fields.Char()
|
||||
document_id = fields.Many2one('documents.document', required=True)
|
||||
attachment_id = fields.Many2one('ir.attachment', required=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""
|
||||
Creates one or more new document revisions. Each revision created replaces the
|
||||
ir.attachment tied to the document with the one provided in this revision. It
|
||||
also supersedes the previous revision, updating the document's
|
||||
current_revision_id and carrying a link to the previous revision in
|
||||
previous_revision_id.
|
||||
|
||||
:param vals_list: Dictionary or list of dictionaries
|
||||
:return:
|
||||
"""
|
||||
res = super().create(vals_list)
|
||||
for rec in res:
|
||||
# When we create a new revision, we need to replace the attachment linked
|
||||
# to the document to keep it on the latest version.
|
||||
rec.document_id.attachment_id = rec.attachment_id
|
||||
# Then we update the current revision to this new revision and link
|
||||
# back to the previous revisions
|
||||
rec.previous_revision_id = rec.document_id.current_revision_id
|
||||
rec.document_id.current_revision_id = rec
|
||||
return res
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
from odoo import models, fields, api, _
|
||||
|
||||
class DocumentRevisionSequence(models.Model):
|
||||
_inherit = 'ir.sequence'
|
||||
# Would prefer to inherit and make a new model, but the way ir.sequence is
|
||||
# implemented has a lot of hard coded references to "ir.sequence" for all the
|
||||
# database operations.
|
||||
|
||||
document_id = fields.Many2one('documents.document', 'Document',
|
||||
ondelete='cascade')
|
||||
|
||||
def predict_next_id(self, sequence_date=None):
|
||||
self.check_access_rights('read')
|
||||
return self._predict_next(sequence_date=sequence_date)
|
||||
|
||||
def _predict_next(self, sequence_date=None):
|
||||
if not self.use_date_range:
|
||||
return self.get_next_char(self.number_next_actual)
|
||||
raise NotImplementedError(
|
||||
_('_predict_next is not implemented for date sequences.'))
|
||||
28
bemade_document_versions/models/workflow.py
Normal file
28
bemade_document_versions/models/workflow.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from odoo import models, fields, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class WorkflowActionRuleRevision(models.Model):
|
||||
_inherit = ['documents.workflow.rule']
|
||||
|
||||
create_model = fields.Selection(selection_add=[('documents.revision', "Revision")])
|
||||
|
||||
def create_record(self, documents=None):
|
||||
rv = super().create_record(documents=documents)
|
||||
if self.create_model == 'documents.revision':
|
||||
if len(documents) != 1:
|
||||
raise UserError(_('Document revisions must be added for one and '
|
||||
'only one document at a time.'))
|
||||
ctx = {'document_id': documents[0].id}
|
||||
ctx.update(self._context)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.revision.wizard',
|
||||
'name': 'New Revision',
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
'views': [(self.env.ref(
|
||||
'bemade_document_versions.document_revision_wizard_view_form').id,
|
||||
'form')],
|
||||
'view_mode': 'form',
|
||||
}
|
||||
5
bemade_document_versions/security/ir.model.access.csv
Normal file
5
bemade_document_versions/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_document_revision_user,Document Revision Access,model_documents_revision,documents.group_documents_user,1,1,1,1
|
||||
access_document_revision_manager,Document Revision Manager Access,model_documents_revision,documents.group_documents_manager,1,1,1,1
|
||||
access_document_revision_wizard_user,Document Revision Wizard Access,model_documents_revision_wizard,documents.group_documents_user,1,1,1,1
|
||||
access_document_revision_wizard_manager,Document Revision Wizard Manager Access,model_documents_revision_wizard,documents.group_documents_manager,1,1,1,1
|
||||
|
12
bemade_document_versions/views/document_views.xml
Normal file
12
bemade_document_versions/views/document_views.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="action_upload_revision" model="ir.actions.act_window">
|
||||
<field name="name">Upload Document Revision</field>
|
||||
<field name="res_model">documents.revision.wizard</field>
|
||||
<field name="binding_model_id" ref="documents.model_documents_document"/>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="binding_view_types">list,form,kanban</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
bemade_document_versions/wizard/__init__.py
Normal file
1
bemade_document_versions/wizard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import document_revision_wizard
|
||||
72
bemade_document_versions/wizard/document_revision_wizard.py
Normal file
72
bemade_document_versions/wizard/document_revision_wizard.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from odoo import models, fields, _, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class DocumentRevisionWizard(models.TransientModel):
|
||||
_name = 'documents.revision.wizard'
|
||||
_description = 'Allows the creation of new document revisions'
|
||||
|
||||
document_id = fields.Many2one('documents.document', 'Document')
|
||||
document_name = fields.Char()
|
||||
file = fields.Binary('File to upload')
|
||||
revision_name = fields.Char(required=True, readonly=True)
|
||||
revision_sequence = fields.Many2one('ir.sequence', required=True)
|
||||
revision_sequence_prefix = fields.Char(related='revision_sequence.prefix',
|
||||
readonly=False)
|
||||
revision_sequence_suffix = fields.Char(related='revision_sequence.suffix',
|
||||
readonly=False)
|
||||
revision_sequence_padding = fields.Integer(related='revision_sequence.padding',
|
||||
readonly=False)
|
||||
|
||||
def default_get(self, fields_list):
|
||||
ctx = self._context
|
||||
vals = {}
|
||||
if 'document_id' not in ctx:
|
||||
raise UserError(
|
||||
_('You must select a document for which you are creating a revision.'))
|
||||
document = self.env['documents.document'].browse(ctx.get('document_id'))
|
||||
vals['document_name'] = document.name
|
||||
vals['document_id'] = document.id
|
||||
sequence = document.revision_sequence or self.env.ref(
|
||||
'bemade_document_versions.document_revision_sequence_default')
|
||||
if document.revision_sequence:
|
||||
vals['revision_name'] = document.get_next_revision_name()
|
||||
else:
|
||||
vals['revision_name'] = sequence.get_next_char(0)
|
||||
vals['revision_sequence_prefix'] = sequence.prefix
|
||||
vals['revision_sequence_suffix'] = sequence.suffix
|
||||
vals['revision_sequence_padding'] = sequence.padding
|
||||
return vals
|
||||
|
||||
@api.depends('document_id', 'document_id.revision_sequence',
|
||||
'revision_sequence')
|
||||
def action_upload_revision(self):
|
||||
for wizard in self:
|
||||
if not wizard.file:
|
||||
raise UserError(_('You must upload a file.'))
|
||||
wizard.revision_sequence.write({
|
||||
'prefix': wizard.revision_sequence_prefix,
|
||||
'suffix': wizard.revision_sequence_suffix,
|
||||
'padding': wizard.revision_sequence_padding,
|
||||
})
|
||||
prev_attachment = wizard.document_id.attachment_id
|
||||
attachment = self.env['ir.attachment'].with_context(
|
||||
{'no_document': True}).create({
|
||||
'name': prev_attachment.name,
|
||||
'datas': wizard.file,
|
||||
'res_model': prev_attachment.res_model,
|
||||
'res_id': prev_attachment.res_id,
|
||||
'company_id': prev_attachment.company_id.id,
|
||||
'public': prev_attachment.public,
|
||||
})
|
||||
if not wizard.document_id.revision_sequence:
|
||||
wizard.document_id.set_up_revisions(wizard.revision_sequence_prefix,
|
||||
wizard.revision_sequence_suffix,
|
||||
wizard.revision_sequence_padding)
|
||||
else:
|
||||
self.env['documents.revision'].create({
|
||||
'document_id': wizard.document_id.id,
|
||||
'attachment_id': attachment.id,
|
||||
'name': wizard.document_id.revision_sequence.next_by_id(),
|
||||
'attachment_id': attachment.id,
|
||||
})
|
||||
32
bemade_document_versions/wizard/document_revision_wizard.xml
Normal file
32
bemade_document_versions/wizard/document_revision_wizard.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="document_revision_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">bemade_document_versions.document.revision.wizard.form</field>
|
||||
<field name="model">documents.revision.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<field name="document_id" invisible="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="File">
|
||||
<field name="file"/>
|
||||
<field name="revision_name"/>
|
||||
</group>
|
||||
<group string="Sequence Numbering Setup">
|
||||
<field name="revision_sequence_prefix"/>
|
||||
<field name="revision_sequence_suffix"/>
|
||||
<field name="revision_sequence_padding"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Upload" name="action_upload_revision" type="object"
|
||||
class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel" class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2
bemade_documents_portal/__init__.py
Normal file
2
bemade_documents_portal/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
32
bemade_documents_portal/__manifest__.py
Normal file
32
bemade_documents_portal/__manifest__.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) September 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Documents Portal Base',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Adds documents to the front-end portal.',
|
||||
'category': 'Document Management',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['documents', 'portal', 'mail_enterprise', 'im_livechat'],
|
||||
'data': ['views/document_portal_templates.xml'],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_documents_portal/controllers/__init__.py
Normal file
1
bemade_documents_portal/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import portal
|
||||
88
bemade_documents_portal/controllers/portal.py
Normal file
88
bemade_documents_portal/controllers/portal.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.http import request, route
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo import _
|
||||
|
||||
|
||||
class DocumentCustomerPortal(CustomerPortal):
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
rtn = super()._prepare_home_portal_values(counters)
|
||||
domain = self._prepare_documents_domain()
|
||||
rtn['documents_count'] = request.env['documents.document'].search_count(domain)
|
||||
return rtn
|
||||
|
||||
@route('/my/documents', type='http', auth='user', website=True)
|
||||
def portal_my_documents(self, **kwargs):
|
||||
values = self._prepare_portal_layout_values()
|
||||
Documents = request.env['documents.document']
|
||||
domain = self._prepare_documents_domain()
|
||||
documents_count = Documents.search_count(domain)
|
||||
documents = Documents.search(domain)
|
||||
values.update({
|
||||
'documents_count': documents_count,
|
||||
'documents': documents.sudo(),
|
||||
'default_url': '/my/documents',
|
||||
'page_name': 'my_documents',
|
||||
})
|
||||
return request.render("bemade_documents_portal.portal_my_documents", values)
|
||||
|
||||
def _prepare_documents_domain(self):
|
||||
"""Helper method intended to be overridden for future modules."""
|
||||
partner = request.env.user.partner_id
|
||||
user = request.env.user
|
||||
return ['|',
|
||||
('partner_id', '=', partner.id),
|
||||
('owner_id', '=', user.id),
|
||||
]
|
||||
|
||||
def _render_record_template(self, values):
|
||||
""" Override this method to apply a different template for a single document
|
||||
record on the portal. """
|
||||
return request.render("bemade_documents_portal.document_portal_template", values)
|
||||
|
||||
@route('/my/documents/<int:document_id>', type='http', auth='user', website=True)
|
||||
def portal_document_page(self, document_id, download=False, **kwargs):
|
||||
document = request.env['documents.document'].browse(document_id)
|
||||
if not document:
|
||||
raise MissingError(_('This document does not exist.'))
|
||||
if download:
|
||||
return self._download_attachment(document)
|
||||
values = {
|
||||
'document': document,
|
||||
'page_name': 'my_documents',
|
||||
'action': document._get_portal_return_action(),
|
||||
}
|
||||
return self._render_record_template(values)
|
||||
|
||||
def _download_attachment(self, document):
|
||||
partner = request.env.user.partner_id
|
||||
if partner and self._check_portal_access(document, partner):
|
||||
attachment = document.attachment_id.sudo()
|
||||
headers = [
|
||||
('content-type', attachment.mimetype),
|
||||
('content-length', attachment.file_size),
|
||||
('content-disposition', f'attachment; filename="{document.name}"')
|
||||
]
|
||||
return request.make_response(attachment.raw, headers)
|
||||
|
||||
def _check_portal_access(self, document, partner) -> bool:
|
||||
"""
|
||||
Helper method to determine if a given partner has access to a document.
|
||||
|
||||
This method is intended to be overridden should further access rights be granted.
|
||||
|
||||
Note that this method does NOT replace the user-level ACL verification and is
|
||||
instead used to bypass these ACL checks when portal users are trying to download
|
||||
a document.
|
||||
|
||||
In overriding, one should generally use the following form::
|
||||
|
||||
new_condition = ...
|
||||
return super()._check_portal_access or new_condition
|
||||
|
||||
:param document: The document in question.
|
||||
:param partner: The res_partner to check for portal access.
|
||||
:return: True if the partner should have access to the document, False otherwise.
|
||||
"""
|
||||
return partner == document.partner_id
|
||||
1
bemade_documents_portal/models/__init__.py
Normal file
1
bemade_documents_portal/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import documents
|
||||
17
bemade_documents_portal/models/documents.py
Normal file
17
bemade_documents_portal/models/documents.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
_name = 'documents.document'
|
||||
_inherit = ['documents.document', 'portal.mixin']
|
||||
|
||||
def _compute_access_url(self):
|
||||
super()._compute_access_url()
|
||||
for document in self:
|
||||
document.access_url = f'/my/documents/{document.id}'
|
||||
|
||||
def _get_portal_return_action(self):
|
||||
""" Return the action used to display documents when returning from customer
|
||||
portal."""
|
||||
self.ensure_one()
|
||||
return self.env.ref('documents.document_action')
|
||||
112
bemade_documents_portal/views/document_portal_templates.xml
Normal file
112
bemade_documents_portal/views/document_portal_templates.xml
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="portal_my_home" inherit_id="portal.portal_my_home">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Documents</t>
|
||||
<t t-set="url">/my/documents</t>
|
||||
<t t-set="placeholder_count">documents_count</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="portal_my_documents" name="My Documents">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="documents" t-as="document">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-att-href="document.get_portal_url()">
|
||||
<t t-esc="document.name"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template id="document_portal_template" name="Document Portal Template">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="o_portal_fullwidth_alert"
|
||||
groups="documents.group_documents_user">
|
||||
<t t-call="portal.portal_back_in_edit_mode">
|
||||
<t t-set="backend_url"
|
||||
t-value="'/web#model=%s&id=%s&action=%s&view_type=form' % (document._name, document.id, action.id)"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_record_layout">
|
||||
<t t-set="card_header">
|
||||
<div class="row no-gutters">
|
||||
<h5 class="mb-1 mb-md-0">
|
||||
<span t-field="document.name"/>
|
||||
</h5>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="card_body">
|
||||
<!-- Main Document Contents -->
|
||||
<div id="document_content"
|
||||
class="col-12 col-lg justify-content-end w-100 h-100">
|
||||
<div t-if="'image' in document.mimetype"
|
||||
class="o_attachment_preview_img">
|
||||
<img id="attachment_img"
|
||||
class="img img-fluid d-block"
|
||||
t-attf-src="/documents/content/{{document.id}}"/>
|
||||
</div>
|
||||
<iframe t-if="document.mimetype == 'application/pdf'"
|
||||
class="mb48 w-100 min-vh-100"
|
||||
t-attf-src="/web/static/lib/pdfjs/web/viewer.html?file=/documents/content/{{document.id}}&filename={{document.name}}"/>
|
||||
<ul class="list-group list-group-flush flex-wrap flex-row flex-lg-column">
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<a class="btn btn-secondary btn-block o_download_btn"
|
||||
t-att-href="document.get_portal_url(download=True)">
|
||||
Download</a>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">File Size:
|
||||
<t t-call="documents.format_file_size"/>
|
||||
</strong>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">File Type:
|
||||
<t t-esc="document.mimetype"/>
|
||||
</strong>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">Attachment Type:
|
||||
<t t-esc="document.attachment_type"/>
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Chatter -->
|
||||
<div id="document_communication" class="card-body">
|
||||
<h2>History</h2>
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="object" t-value="document"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="portal_breadcrumbs" inherit_id="portal.portal_breadcrumbs">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li t-if="page_name == 'my_documents'"
|
||||
t-attf-class="breadcrumb-item #{'active ' if not document else ''}">
|
||||
<a t-if="document"
|
||||
t-attf-href="/my/documents?{{ keep_query() }}">Documents</a>
|
||||
<t t-else="">Documents</t>
|
||||
</li>
|
||||
<li t-if="document" class="breadcrumb-item active" t-esc="document.name">
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
3
bemade_project_documents/__init__.py
Normal file
3
bemade_project_documents/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
from . import controllers
|
||||
50
bemade_project_documents/__manifest__.py
Normal file
50
bemade_project_documents/__manifest__.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) September 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Project Documents',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Improved workflow for project documents.',
|
||||
'description': """Adds multiple workflow items to project documents, including:
|
||||
* Approval stages
|
||||
* Document versions with revision numbers
|
||||
* Ability to request document approval from a partner
|
||||
""",
|
||||
'category': '',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['documents',
|
||||
'bemade_documents_portal',
|
||||
'documents_project',
|
||||
],
|
||||
'data': ['views/project_views.xml',
|
||||
'wizard/request_approvals_wizard.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'bemade_project_documents/static/src/js/documents_controller_patch.js'
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'bemade_project_documents/static/src/xml/documents_views.xml'
|
||||
]
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_project_documents/controllers/__init__.py
Normal file
1
bemade_project_documents/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
17
bemade_project_documents/controllers/main.py
Normal file
17
bemade_project_documents/controllers/main.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from odoo.addons.documents.controllers.main import ShareRoute
|
||||
from odoo.http import route, request
|
||||
import json
|
||||
|
||||
|
||||
class DocumentsController(ShareRoute):
|
||||
|
||||
@route('/documents/upload_attachment', type='http', methods=['POST'], auth='user')
|
||||
def upload_document(self, folder_id, ufile, tag_ids, res_model, res_id,
|
||||
document_id=False, partner_id=False, owner_id=False):
|
||||
res = super().upload_document(folder_id, ufile, tag_ids, document_id, partner_id,
|
||||
owner_id)
|
||||
res_data = json.loads(res.data)
|
||||
if 'ids' in res_data and res_model and res_id:
|
||||
docs = request.env['documents.document'].browse(res_data['ids'])
|
||||
docs.write({'res_model': res_model, 'res_id': res_id})
|
||||
return res
|
||||
11
bemade_project_documents/data/mail_templates.xml
Normal file
11
bemade_project_documents/data/mail_templates.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="mail_template_document_approval_request" model="mail.template">
|
||||
<field name="name">Document Approval Request</field>
|
||||
<field name="model_id" ref="model_documents_document"/>
|
||||
<field name="subject">Document Approval Request {{ object.name != False and ': ' + object.name or '' }} </field>
|
||||
<field name="email_to">{{ }}</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
2
bemade_project_documents/models/__init__.py
Normal file
2
bemade_project_documents/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import project
|
||||
from . import document
|
||||
8
bemade_project_documents/models/document.py
Normal file
8
bemade_project_documents/models/document.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
_inherit = 'documents.document'
|
||||
|
||||
external_approver_ids = fields.Many2many('res.partner')
|
||||
|
||||
15
bemade_project_documents/models/project.py
Normal file
15
bemade_project_documents/models/project.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
document_ids = fields.One2many('documents.document',
|
||||
compute='_compute_document_ids')
|
||||
|
||||
def _compute_document_ids(self):
|
||||
for project in self:
|
||||
project.document_ids = self.env['documents.document'].search([
|
||||
('res_model', '=', 'project.project'),
|
||||
('res_id', '=', project.id),
|
||||
])
|
||||
2
bemade_project_documents/security/ir.model.access.csv
Normal file
2
bemade_project_documents/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_documents_request_approval_wizard,access.documents.request_approval_wizard,model_project_documents_approval_wizard,documents.group_documents_user,1,1,1,0
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
const DocumentsListController = require('documents.DocumentsListController');
|
||||
const DocumentsKanbanController = require('documents.DocumentsKanbanController');
|
||||
const DocumentsControllerMixin = require('documents.controllerMixin');
|
||||
|
||||
import {patch} from 'web.utils';
|
||||
|
||||
const prototype_addins = {
|
||||
_onClickRequestApprovals: function (ev) {
|
||||
ev.preventDefault();
|
||||
const context = this.model.get(this.handle, {raw: true}).getContext();
|
||||
this.do_action('bemade_project_documents.action_request_approval_form', {
|
||||
additional_context: {
|
||||
default_document_ids: this._selectedRecordIds,
|
||||
},
|
||||
on_close: () => this.reload(),
|
||||
});
|
||||
},
|
||||
_makeFileUpload({ recordId }) {
|
||||
const context = this.model.get(this.handle, {raw: true}).getContext();
|
||||
return Object.assign({
|
||||
res_model: context.default_res_model || false,
|
||||
res_id: context.default_res_id || false,
|
||||
}, this._super(...arguments));
|
||||
},
|
||||
_makeFileUploadFormDataKeys({ recordId }) {
|
||||
const context = this.model.get(this.handle, {raw: true}).getContext();
|
||||
return Object.assign({
|
||||
res_model: context && context.default_res_model,
|
||||
res_id: context && context.default_res_id,
|
||||
}, this._super(...arguments));
|
||||
},
|
||||
};
|
||||
|
||||
const patch_name = "bemade_project_documents.DocumentsControllerPatch";
|
||||
|
||||
patch(DocumentsControllerMixin, patch_name, {
|
||||
events: _.extend({}, DocumentsControllerMixin.events, {
|
||||
'click .o_documents_kanban_request_approvals': '_onClickRequestApprovals',
|
||||
}),
|
||||
});
|
||||
patch(DocumentsListController.prototype, patch_name, prototype_addins);
|
||||
patch(DocumentsKanbanController.prototype, patch_name, prototype_addins);
|
||||
14
bemade_project_documents/static/src/xml/documents_views.xml
Normal file
14
bemade_project_documents/static/src/xml/documents_views.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="DocumentViews.buttons"
|
||||
t-inherit="documents.DocumentsViews.buttons" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('o_documents_kanban_share_domain')]"
|
||||
position="after">
|
||||
<button type="button" title="Request approvals"
|
||||
groups="project.group_project_manager"
|
||||
class="btn btn-secondary o_documents_kanban_request_approvals">
|
||||
Request Approvals
|
||||
</button>
|
||||
</xpath>
|
||||
</div>
|
||||
</templates>
|
||||
35
bemade_project_documents/views/project_views.xml
Normal file
35
bemade_project_documents/views/project_views.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="project_action_view_documents" model="ir.actions.act_window">
|
||||
<field name="name">Documents</field>
|
||||
<field name="res_model">documents.document</field>
|
||||
<field name="view_mode">tree,kanban</field>
|
||||
<field name="search_view_id" ref="documents.document_view_search"/>
|
||||
<field name="domain">
|
||||
[('res_id', '=', active_id), ('res_model', '=', 'project.project')]
|
||||
</field>
|
||||
<field name="context">{
|
||||
'default_res_id': active_id,
|
||||
'default_res_model': 'project.project',
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No documents found. Let's create one!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_project_kanban_inherit" model="ir.ui.view">
|
||||
<field name="inherit_id" ref="project.view_project_kanban"/>
|
||||
<field name="name">project_documents.project.view.kanban</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//a[@name='attachment_tree_view']/.." position="replace">
|
||||
<div role="menuitem">
|
||||
<a name="%(project_action_view_documents)d" type="action">Documents</a>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
1
bemade_project_documents/wizard/__init__.py
Normal file
1
bemade_project_documents/wizard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import request_approvals_wizard
|
||||
24
bemade_project_documents/wizard/request_approvals_wizard.py
Normal file
24
bemade_project_documents/wizard/request_approvals_wizard.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class RequestDocumentApprovalsWizard(models.TransientModel):
|
||||
_name = 'project_documents.approval.wizard'
|
||||
|
||||
document_ids = fields.Many2many('documents.document', string='Documents')
|
||||
partner_ids = fields.Many2many('res.partner', string="Recipients",
|
||||
help="""Contacts who will receive a request to
|
||||
approve the document.""")
|
||||
request_template = fields.Many2one('mail.template')
|
||||
|
||||
def default_get(self, fields_list):
|
||||
if 'request_template' not in fields_list:
|
||||
self.request_template = self.env.ref()
|
||||
|
||||
def request_approvals(self):
|
||||
self._validate_partners_have_emails()
|
||||
|
||||
def _validate_partners_have_emails(self):
|
||||
for wizard in self:
|
||||
if any([not p.email for p in wizard.partner_ids]):
|
||||
raise ValidationError(_('Each partner must have an email address.'))
|
||||
51
bemade_project_documents/wizard/request_approvals_wizard.xml
Normal file
51
bemade_project_documents/wizard/request_approvals_wizard.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="documents_view_list_inherit" model="ir.ui.view">
|
||||
<field name="inherit_id" ref="documents.documents_view_list"/>
|
||||
<field name="model">documents.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree[@js_class='documents_list']" position="attributes">
|
||||
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="documents_request_approval_form_view" model="ir.ui.view">
|
||||
<field name="name">Request Approvals</field>
|
||||
<field name="model">project_documents.approval.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<label for="partner_ids"/>
|
||||
<field name="partner_ids" domain="[('is_company', '=', False)]">
|
||||
<tree editable="bottom">
|
||||
<field name="name" colspan="3"/>
|
||||
<field name="email"/>
|
||||
<field name="title"/>
|
||||
</tree>
|
||||
</field>
|
||||
<label for="document_ids"/>
|
||||
<field name="document_ids" readonly="True"/>
|
||||
<footer>
|
||||
<button name="request_approvals" type="object"
|
||||
string="Request" class="btn btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary"
|
||||
special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="action_request_approval_form">
|
||||
<field name="name">Request Approvals</field>
|
||||
<field name="res_model">project_documents.approval.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="context">{
|
||||
'form_view_ref': 'bemade_project_documents.documents_request_approval_form_view',
|
||||
'default_res_model': context.get('default_res_model', False),
|
||||
'default_res_id': context.get('default_res_id', False)
|
||||
}</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
Loading…
Reference in a new issue