Compare commits

...

12 commits

35 changed files with 896 additions and 0 deletions

View file

@ -0,0 +1,3 @@
from . import models
from . import wizard

View 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,
}

View 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>

View file

@ -0,0 +1,4 @@
from . import document
from . import document_revision
from . import document_revision_sequence
from . import workflow

View 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()

View 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

View file

@ -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.'))

View 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',
}

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_document_revision_user Document Revision Access model_documents_revision documents.group_documents_user 1 1 1 1
3 access_document_revision_manager Document Revision Manager Access model_documents_revision documents.group_documents_manager 1 1 1 1
4 access_document_revision_wizard_user Document Revision Wizard Access model_documents_revision_wizard documents.group_documents_user 1 1 1 1
5 access_document_revision_wizard_manager Document Revision Wizard Manager Access model_documents_revision_wizard documents.group_documents_manager 1 1 1 1

View 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>

View file

@ -0,0 +1 @@
from . import document_revision_wizard

View 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,
})

View 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>

View file

@ -0,0 +1,2 @@
from . import controllers
from . import models

View 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,
}

View file

@ -0,0 +1 @@
from . import portal

View 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

View file

@ -0,0 +1 @@
from . import documents

View 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')

View 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&amp;id=%s&amp;action=%s&amp;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}}&amp;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>

View file

@ -0,0 +1,3 @@
from . import models
from . import wizard
from . import controllers

View 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,
}

View file

@ -0,0 +1 @@
from . import main

View 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

View 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>

View file

@ -0,0 +1,2 @@
from . import project
from . import document

View 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')

View 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),
])

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_documents_request_approval_wizard access.documents.request_approval_wizard model_project_documents_approval_wizard documents.group_documents_user 1 1 1 0

View file

@ -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);

View 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>

View 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>

View file

@ -0,0 +1 @@
from . import request_approvals_wizard

View 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.'))

View 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>