mirror of
https://github.com/OCA/knowledge.git
synced 2025-07-22 12:06:57 -06:00
[IMP][9.0] Change Requests and workflow improvements on documents (#155)
* [IMP] Refactor document_page_approval to always use states, and a few code improvements * [IMP] Add QWeb report to print document pages * Categories don't save content * FIX Last Contributor (uid and date). Use related fields instead of computed where possible. Fix search views, store some fields to make them searchable, added filters * Add api.depends on computed diff
This commit is contained in:
parent
17d757fd95
commit
96256b9872
@ -21,7 +21,7 @@
|
||||
|
||||
{
|
||||
'name': 'Document Page',
|
||||
'version': '9.0.1.0.1',
|
||||
'version': '9.0.2.0.0',
|
||||
'category': 'Knowledge Management',
|
||||
'author': 'OpenERP SA, Odoo Community Association (OCA)',
|
||||
'images': ['images/category_list.png', 'images/create_category.png',
|
||||
@ -37,6 +37,10 @@
|
||||
'wizard/document_page_create_menu.xml',
|
||||
'wizard/document_page_show_diff.xml',
|
||||
'views/document_page.xml',
|
||||
'views/document_page_category.xml',
|
||||
'views/document_page_history.xml',
|
||||
'views/document_page_assets.xml',
|
||||
'views/report_document_page.xml',
|
||||
'security/document_page_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
|
@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="wiki_wiki_main" model="document.page">
|
||||
<field name="name">The Odoo wiki</field>
|
||||
<field name="tags">help, quick start, wiki, formatting</field>
|
||||
<field name="minor_edit">0</field>
|
||||
<field name="index">1</field>
|
||||
<field name="summary">Initial Page</field>
|
||||
<field name="content">== The Odoo wiki ==
|
||||
|
||||
[[File:https://www.odoo.com/openerp_website/static/src/img/logo_transparent_198px.png Odoo]]
|
||||
|
||||
The Odoo wiki allows you to manage your enterprise's contents using wiki
|
||||
restructured texts. This module provides a collaborative way to manage internal
|
||||
FAQs, quality manuals, technical references, etc.
|
||||
|
||||
==Keypoints==
|
||||
* Same formating style than MediaWiki,
|
||||
* Any number of wiki group for different purposes,
|
||||
* Detailed history on all pages,
|
||||
* Integrated with the document management system.
|
||||
|
||||
==Why you should use the OpenERP integrated wiki than a separate wiki system ?==
|
||||
* Allows links to any document of the system,
|
||||
* Uses the access controls of OpenERP for uniq access rights management,
|
||||
* Use it to describe projects, tasks, products,
|
||||
* Integrated with customer portal to provide restricted external accesses,
|
||||
* Linked to users processes for quality manuals.
|
||||
|
||||
==To get more information==
|
||||
* [[Basic Wiki Editing]]
|
||||
* [[Wiki Documentation]]
|
||||
* [http://openerp.com The OpenERP website]
|
||||
|
||||
|
||||
</field>
|
||||
<field name="parent_id" ref="wiki_groups_wikiformatting0"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
@ -8,7 +8,7 @@
|
||||
<record id="demo_category1" model="document.page">
|
||||
<field name="name">OpenERP Features</field>
|
||||
<field name="type">category</field>
|
||||
<field name="content">
|
||||
<field name="template">
|
||||
Summary of the feature
|
||||
|
||||
Long explanation
|
||||
|
12
document_page/migrations/9.0.2.0.0/post-migration.py
Normal file
12
document_page/migrations/9.0.2.0.0/post-migration.py
Normal file
@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Ivan Todorovich <ivan.todorovich@gmail.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
def migrate(cr, version): # pragma: no cover
|
||||
# Set all pre-existing categories template to its content
|
||||
cr.execute("""
|
||||
UPDATE document_page
|
||||
SET template = content
|
||||
WHERE type = 'category'
|
||||
""")
|
@ -18,10 +18,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
import logging
|
||||
from openerp import models, fields, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from openerp import models, fields, api
|
||||
|
||||
|
||||
class DocumentPage(models.Model):
|
||||
@ -54,18 +52,47 @@ class DocumentPage(models.Model):
|
||||
)
|
||||
|
||||
content = fields.Text(
|
||||
"Content"
|
||||
"Content",
|
||||
compute='_compute_content',
|
||||
inverse='_inverse_content',
|
||||
search='_search_content',
|
||||
required=True,
|
||||
)
|
||||
|
||||
# no-op computed field
|
||||
summary = fields.Char(
|
||||
help='Describe the changes made',
|
||||
compute=lambda x: x,
|
||||
inverse=lambda x: x,
|
||||
)
|
||||
|
||||
template = fields.Html(
|
||||
"Template",
|
||||
help="Template that will be used as a content template "
|
||||
"for all new page of this category.",
|
||||
)
|
||||
|
||||
# deprecated - should be removed on 10.0
|
||||
# left here because some modules might still need it
|
||||
display_content = fields.Text(
|
||||
string='Displayed Content',
|
||||
compute='_get_display_content'
|
||||
compute='_compute_display_content'
|
||||
)
|
||||
|
||||
history_head = fields.Many2one(
|
||||
'document.page.history',
|
||||
'HEAD',
|
||||
compute='_compute_history_head',
|
||||
store=True,
|
||||
auto_join=True,
|
||||
)
|
||||
|
||||
history_ids = fields.One2many(
|
||||
'document.page.history',
|
||||
'page_id',
|
||||
'History'
|
||||
'History',
|
||||
order='create_date DESC',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
menu_id = fields.Many2one(
|
||||
@ -74,85 +101,88 @@ class DocumentPage(models.Model):
|
||||
readonly=True
|
||||
)
|
||||
|
||||
create_date = fields.Datetime(
|
||||
"Created on",
|
||||
content_date = fields.Datetime(
|
||||
'Last Contribution Date',
|
||||
related='history_head.create_date',
|
||||
store=True,
|
||||
index=True,
|
||||
readonly=True
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
create_uid = fields.Many2one(
|
||||
content_uid = fields.Many2one(
|
||||
'res.users',
|
||||
'Author',
|
||||
'Last Contributor',
|
||||
related='history_head.create_uid',
|
||||
store=True,
|
||||
index=True,
|
||||
readonly=True
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
write_date = fields.Datetime(
|
||||
"Modification Date",
|
||||
index=True,
|
||||
readonly=True)
|
||||
|
||||
write_uid = fields.Many2one(
|
||||
'res.users',
|
||||
"Last Contributor",
|
||||
index=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
def _get_page_index(self, page, link=True):
|
||||
@api.multi
|
||||
def _get_page_index(self, link=True):
|
||||
"""Return the index of a document."""
|
||||
self.ensure_one()
|
||||
index = []
|
||||
for subpage in page.child_ids:
|
||||
index += ["<li>" + self._get_page_index(subpage) +
|
||||
"</li>"]
|
||||
for subpage in self.child_ids:
|
||||
index += ["<li>" + subpage._get_page_index() + "</li>"]
|
||||
r = ''
|
||||
if link:
|
||||
r = '<a href="#id=%s">%s</a>' % (page.id, page.name)
|
||||
r = '<a href="#id=%s">%s</a>' % (self.id, self.name)
|
||||
|
||||
if index:
|
||||
r += "<ul>" + "".join(index) + "</ul>"
|
||||
return r
|
||||
|
||||
def _get_display_content(self):
|
||||
"""Return the content of a document."""
|
||||
for page in self:
|
||||
if page.type == "category":
|
||||
display_content = self._get_page_index(page, link=False)
|
||||
else:
|
||||
display_content = page.content
|
||||
page.display_content = display_content
|
||||
|
||||
@api.onchange("parent_id")
|
||||
def do_set_content(self):
|
||||
"""We Set it the right content to the new parent."""
|
||||
if self.parent_id and not self.content:
|
||||
if self.parent_id.type == "category":
|
||||
self.content = self.parent_id.content
|
||||
|
||||
def create_history(self, page_id, content):
|
||||
"""Create the first history of a newly created document."""
|
||||
history = self.env['document.page.history']
|
||||
return history.create({
|
||||
"content": content,
|
||||
"page_id": page_id
|
||||
})
|
||||
@api.multi
|
||||
@api.depends('content')
|
||||
def _compute_display_content(self):
|
||||
# @deprecated, simply use content
|
||||
for rec in self:
|
||||
rec.display_content = rec.content
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
"""Write the content and set the history."""
|
||||
result = super(DocumentPage, self).write(vals)
|
||||
content = vals.get('content')
|
||||
if content:
|
||||
for page in self:
|
||||
self.create_history(page.id, content)
|
||||
return result
|
||||
@api.depends('history_head', 'history_ids')
|
||||
def _compute_content(self):
|
||||
for rec in self:
|
||||
if rec.type == 'category':
|
||||
rec.content = rec._get_page_index(link=False)
|
||||
else:
|
||||
if rec.history_head:
|
||||
rec.content = rec.history_head.content
|
||||
else:
|
||||
# html widget's default, so it doesn't trigger ghost save
|
||||
rec.content = '<p><br></p>'
|
||||
|
||||
@api.model
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def create(self, vals):
|
||||
"""Create the first history of a document."""
|
||||
page_id = super(DocumentPage, self).create(vals)
|
||||
content = vals.get('content')
|
||||
if content:
|
||||
self.create_history(page_id.id, content)
|
||||
return page_id
|
||||
@api.multi
|
||||
def _inverse_content(self):
|
||||
for rec in self:
|
||||
if rec.type == 'content':
|
||||
rec._create_history({
|
||||
'content': rec.content,
|
||||
'summary': rec.summary,
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def _search_content(self, operator, value):
|
||||
return [('history_head.content', operator, value)]
|
||||
|
||||
@api.multi
|
||||
@api.depends('history_ids')
|
||||
def _compute_history_head(self):
|
||||
for rec in self:
|
||||
if rec.history_ids:
|
||||
rec.history_head = rec.history_ids[0]
|
||||
|
||||
@api.multi
|
||||
def _create_history(self, vals):
|
||||
self.ensure_one()
|
||||
history = self.env['document.page.history']
|
||||
vals['page_id'] = self.id
|
||||
return history.create(vals)
|
||||
|
||||
@api.onchange("parent_id")
|
||||
def _onchange_parent_id(self):
|
||||
"""We Set it the right content to the new parent."""
|
||||
if not self.content or self.content == '<p><br></p>':
|
||||
if self.parent_id and self.parent_id.type == "category":
|
||||
self.content = self.parent_id.template
|
||||
|
@ -18,11 +18,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
import logging
|
||||
import difflib
|
||||
from openerp import models, fields, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from openerp import models, fields, api, _
|
||||
|
||||
|
||||
class DocumentPageHistory(models.Model):
|
||||
@ -31,24 +28,40 @@ class DocumentPageHistory(models.Model):
|
||||
_name = "document.page.history"
|
||||
_description = "Document Page History"
|
||||
_order = 'id DESC'
|
||||
_rec_name = "create_date"
|
||||
|
||||
page_id = fields.Many2one('document.page', 'Page')
|
||||
page_id = fields.Many2one('document.page', 'Page', ondelete='cascade')
|
||||
summary = fields.Char('Summary', index=True)
|
||||
content = fields.Text("Content")
|
||||
create_date = fields.Datetime("Date")
|
||||
create_uid = fields.Many2one('res.users', "Modified By")
|
||||
diff = fields.Text(compute='_compute_diff')
|
||||
|
||||
@api.multi
|
||||
@api.depends('content', 'page_id.history_ids')
|
||||
def _compute_diff(self):
|
||||
"""Shows a diff between this version and the previous version"""
|
||||
history = self.env['document.page.history']
|
||||
for rec in self:
|
||||
prev = history.search([
|
||||
('page_id', '=', rec.page_id.id),
|
||||
('create_date', '<', rec.create_date)],
|
||||
limit=1,
|
||||
order='create_date DESC')
|
||||
if prev:
|
||||
rec.diff = self.getDiff(prev.id, rec.id)
|
||||
else:
|
||||
rec.diff = self.getDiff(False, rec.id)
|
||||
|
||||
@api.model
|
||||
def getDiff(self, v1, v2):
|
||||
"""Return the difference between two version of document version."""
|
||||
text1 = self.browse(v1).content
|
||||
text2 = self.browse(v2).content
|
||||
line1 = line2 = ''
|
||||
if text1:
|
||||
line1 = text1.splitlines(1)
|
||||
if text2:
|
||||
line2 = text2.splitlines(1)
|
||||
if (not line1 and not line2) or (line1 == line2):
|
||||
text1 = v1 and self.browse(v1).content or ''
|
||||
text2 = v2 and self.browse(v2).content or ''
|
||||
# Include line breaks to make it more readable
|
||||
# TODO: consider using a beautify library directly on the content
|
||||
text1 = text1.replace('</p><p>', '</p>\r\n<p>')
|
||||
text2 = text2.replace('</p><p>', '</p>\r\n<p>')
|
||||
line1 = text1.splitlines(1)
|
||||
line2 = text2.splitlines(1)
|
||||
if line1 == line2:
|
||||
return _('There are no changes in revisions.')
|
||||
else:
|
||||
diff = difflib.HtmlDiff()
|
||||
@ -58,3 +71,11 @@ class DocumentPageHistory(models.Model):
|
||||
"Revision-{}".format(v2),
|
||||
context=True
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
result = []
|
||||
for rec in self:
|
||||
name = "%s #%i" % (rec.page_id.name, rec.id)
|
||||
result.append((rec.id, name))
|
||||
return result
|
||||
|
@ -1,8 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- Defined in Knowledge. Change it's name -->
|
||||
<record id="base.group_document_user" model="res.groups">
|
||||
<field name="name">User (Read only)</field>
|
||||
</record>
|
||||
|
||||
<record id="group_document_editor" model="res.groups">
|
||||
<field name="name">Editor</field>
|
||||
<field name="category_id" ref="knowledge.module_category_knowledge_management"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_document_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_document_manager" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="knowledge.module_category_knowledge_management"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_document_editor'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
@ -1,4 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
document_page_all,document.page,model_document_page,,1,0,0,0
|
||||
document_page,document.page,model_document_page,base.group_user,1,1,1,1
|
||||
document_page_history,document.page.history,model_document_page_history,base.group_user,1,0,1,0
|
||||
document_page_user,document.page user,model_document_page,base.group_document_user,1,0,0,0
|
||||
document_page_history_user,document.page.history user,model_document_page_history,base.group_document_user,1,0,0,0
|
||||
document_page_editor,document.page editor,model_document_page,group_document_editor,1,1,1,0
|
||||
document_page_history_editor,document.page.history editor,model_document_page_history,group_document_editor,1,1,1,0
|
||||
document_page_manager,document.page manager,model_document_page,group_document_manager,1,1,1,1
|
||||
document_page_history_manager,document.page.history manager,model_document_page_history,group_document_manager,1,1,1,0
|
||||
|
|
@ -1,12 +1,29 @@
|
||||
.oe_form_editable .oe_document_page {
|
||||
display: none;
|
||||
table.diff {
|
||||
font-family: Courier;
|
||||
border: medium;
|
||||
}
|
||||
|
||||
table.diff {font-family:Courier; border:medium;}
|
||||
.diff_header {background-color:#e0e0e0}
|
||||
td.diff_header {text-align:right}
|
||||
.diff_next {background-color:#c0c0c0}
|
||||
.diff_add {background-color:#aaffaa}
|
||||
.diff_chg {background-color:#ffff77}
|
||||
.diff_sub {background-color:#ffaaaa}
|
||||
table.diff .diff_header {
|
||||
background-color: #e0e0e0
|
||||
}
|
||||
|
||||
table.diff td.diff_header {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
table.diff .diff_next {
|
||||
background-color:#c0c0c0
|
||||
}
|
||||
|
||||
table.diff .diff_add {
|
||||
background-color:#aaffaa
|
||||
}
|
||||
|
||||
table.diff .diff_chg {
|
||||
background-color:#ffff77
|
||||
}
|
||||
|
||||
table.diff .diff_sub {
|
||||
background-color:#ffaaaa
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_document_page, test_document_page_history
|
||||
from . import test_document_page
|
||||
from . import test_document_page_create_menu
|
||||
from . import test_document_page_show_diff
|
||||
|
@ -4,34 +4,37 @@ from openerp.tests import common
|
||||
|
||||
|
||||
class TestDocumentPage(common.TransactionCase):
|
||||
"""document_page test class."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestDocumentPage, self).setUp()
|
||||
self.page_obj = self.env['document.page']
|
||||
self.history_obj = self.env['document.page.history']
|
||||
self.category1 = self.env.ref('document_page.demo_category1')
|
||||
self.page1 = self.env.ref('document_page.demo_page1')
|
||||
|
||||
def test_page_creation(self):
|
||||
"""Test page creation."""
|
||||
parent_page = self.env.ref('document_page.demo_category1')
|
||||
|
||||
self.assertEqual(parent_page.name, 'OpenERP Features')
|
||||
|
||||
record = self.env['document.page'].create({
|
||||
'name': 'Test Page1',
|
||||
'parent_id': parent_page.id,
|
||||
page = self.page_obj.create({
|
||||
'name': 'Test Page 1',
|
||||
'parent_id': self.category1.id,
|
||||
'content': 'Test content'
|
||||
})
|
||||
self.assertEqual(record.name, 'Test Page1')
|
||||
self.assertEqual(page.content, 'Test content')
|
||||
self.assertEqual(len(page.history_ids), 1)
|
||||
page.content = 'New content for Demo Page'
|
||||
self.assertEqual(len(page.history_ids), 2)
|
||||
|
||||
def test_category_display_content(self):
|
||||
"""Test category display content."""
|
||||
page = self.env.ref('document_page.demo_category1')
|
||||
self.assertTrue(page.display_content.find('Demo') > 1)
|
||||
def test_category_template(self):
|
||||
page = self.page_obj.create({
|
||||
'name': 'Test Page 2',
|
||||
'parent_id': self.category1.id,
|
||||
})
|
||||
page._onchange_parent_id()
|
||||
self.assertEqual(page.content, self.category1.template)
|
||||
|
||||
def test_page_display_content(self):
|
||||
"""Test page display content."""
|
||||
page = self.env.ref('document_page.demo_page1')
|
||||
self.assertTrue(page.display_content.find('Demo') > 1)
|
||||
|
||||
def test_page_do_set_content(self):
|
||||
"""Test page set content."""
|
||||
page = self.env.ref('document_page.demo_page1')
|
||||
page.content = None
|
||||
page.do_set_content()
|
||||
self.assertTrue(page.display_content.find('Summary') == 1)
|
||||
def test_page_history_diff(self):
|
||||
page = self.page_obj.create({
|
||||
'name': 'Test Page 3',
|
||||
'content': 'Test content'
|
||||
})
|
||||
page.content = 'New content'
|
||||
self.assertIsNotNone(page.history_ids[0].diff)
|
||||
|
@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from openerp.tests import common
|
||||
|
||||
|
||||
class TestDocumentPageHistory(common.TransactionCase):
|
||||
"""document_page_history test class."""
|
||||
|
||||
def test_page_history_demo_page1(self):
|
||||
"""Test page history demo page1."""
|
||||
page = self.env.ref('document_page.demo_page1')
|
||||
page.content = 'Test content updated'
|
||||
history_document = self.env['document.page.history']
|
||||
history_pages = history_document.search([('page_id', '=', page.id)])
|
||||
active_ids = [i.id for i in history_pages]
|
||||
|
||||
result = history_document.getDiff(active_ids[0], active_ids[0])
|
||||
self.assertEqual(result, 'There are no changes in revisions.')
|
||||
|
||||
result = history_document.getDiff(active_ids[0], active_ids[1])
|
||||
self.assertNotEqual(result, 'There are no changes in revisions.')
|
@ -19,7 +19,7 @@ class TestDocumentPageShowDiff(common.TransactionCase):
|
||||
self.assertTrue(
|
||||
show_diff_object.with_context(
|
||||
active_ids=[i.id for i in history_pages]
|
||||
).get_diff()
|
||||
)._get_diff()
|
||||
)
|
||||
|
||||
page.write({'content': 'Text content updated'})
|
||||
@ -30,7 +30,7 @@ class TestDocumentPageShowDiff(common.TransactionCase):
|
||||
with self.assertRaises(Exception) as context:
|
||||
show_diff_object.with_context(
|
||||
active_ids=[i.id for i in history_pages]
|
||||
).get_diff()
|
||||
)._get_diff()
|
||||
|
||||
self.assertTrue(_("Select one or maximum two history revisions!")
|
||||
in context.exception)
|
||||
|
@ -1,10 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<menuitem name="Pages"
|
||||
id="menu_wiki"
|
||||
parent="knowledge.menu_document"
|
||||
sequence="20" />
|
||||
|
||||
|
||||
<!-- wiki tree view -->
|
||||
<record id="view_wiki_tree_children" model="ir.ui.view">
|
||||
@ -15,8 +12,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Document Page">
|
||||
<field name="name"/>
|
||||
<field name="write_uid"/>
|
||||
<field name="write_date"/>
|
||||
<field name="content_uid"/>
|
||||
<field name="content_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@ -30,8 +27,8 @@
|
||||
<field name="name"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="create_uid" invisible="1"/>
|
||||
<field name="write_uid"/>
|
||||
<field name="write_date"/>
|
||||
<field name="content_uid"/>
|
||||
<field name="content_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@ -47,31 +44,32 @@
|
||||
<h1><field name="name" placeholder="Name"/></h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="parent_id"
|
||||
string="Category"
|
||||
context="{'default_type':'category'}"/>
|
||||
<field name="parent_id" string="Category" context="{'default_type':'category'}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="write_uid"
|
||||
groups="base.group_no_one"/>
|
||||
<field name="write_date"
|
||||
groups="base.group_no_one"/>
|
||||
<field name="menu_id"
|
||||
groups="base.group_no_one"/>
|
||||
<field name="content_uid"/>
|
||||
<field name="content_date"/>
|
||||
<field name="menu_id" readonly="1" attrs="{'invisible': [('menu_id','=',False)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Content" class="oe_edit_only" />
|
||||
<field name="content"
|
||||
placeholder="e.g. Once upon a time..."
|
||||
class="oe_edit_only"
|
||||
widget="html" />
|
||||
<separator string="Last Content" />
|
||||
<div class="oe_document_page">
|
||||
<field name="display_content"
|
||||
widget="html"
|
||||
class="oe_view_only"
|
||||
options='{"safe": True}' />
|
||||
</div>
|
||||
<notebook>
|
||||
<page name="content" string="Content">
|
||||
<label for="summary" class="oe_edit_only" />
|
||||
<field name="summary" placeholder="eg: Changed ... for ..." class="oe_edit_only" />
|
||||
<label for="content" class="oe_edit_only"/>
|
||||
<field name="content" widget="html" placeholder="e.g. Once upon a time..." required="1" options="{'safe': True}"/>
|
||||
</page>
|
||||
<page name="history" string="History">
|
||||
<field name="history_ids">
|
||||
<tree>
|
||||
<field name="id"/>
|
||||
<field name="create_date"/>
|
||||
<field name="summary"/>
|
||||
<field name="create_uid"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
@ -88,12 +86,7 @@
|
||||
<form string="Document Page">
|
||||
<field name="type" invisible="1"/>
|
||||
<h1><field name="name" placeholder="Name"/></h1>
|
||||
<div class="oe_document_page">
|
||||
<field name="display_content"
|
||||
widget="html"
|
||||
class="oe_view_only"
|
||||
options='{"safe": True}' />
|
||||
</div>
|
||||
<field name="content" class="oe_view_only" required="1" options='{"safe": True}'/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
@ -104,90 +97,19 @@
|
||||
<field name="model">document.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Document Page">
|
||||
<field name="name"
|
||||
string="Content"
|
||||
filter_domain="['|', ('name','ilike',self), ('content','ilike',self)]"/>
|
||||
<field name="write_uid"/>
|
||||
<field name="name" string="Content" filter_domain="['|', ('name','ilike',self), ('content','ilike',self)]"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="content_uid"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Document Type"
|
||||
domain="[]"
|
||||
context="{'group_by':'parent_id'}" />
|
||||
<filter string="Author"
|
||||
domain="[]"
|
||||
context="{'group_by':'create_uid'}" />
|
||||
<filter string="Last Contributor"
|
||||
domain="[]"
|
||||
context="{'group_by':'write_uid'}" />
|
||||
<filter string="Category" context="{'group_by':'parent_id'}" />
|
||||
<filter string="Author" context="{'group_by':'create_uid'}" />
|
||||
<filter string="Last Contributor" context="{'group_by':'content_uid'}" />
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Category Views -->
|
||||
<record id="view_category_form" model="ir.ui.view">
|
||||
<field name="name">document.page.category.form</field>
|
||||
<field name="model">document.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Category">
|
||||
<sheet>
|
||||
<field name="type" invisible="1"/>
|
||||
<h1><field name="name" placeholder="Name"/></h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="parent_id" string="Category"
|
||||
context="{'default_type':'category'}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="write_uid"
|
||||
groups="base.group_no_one"/>
|
||||
<field name="write_date"
|
||||
groups="base.group_no_one"/>
|
||||
<field name="menu_id"
|
||||
groups="base.group_no_one"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Template" name="template">
|
||||
<div>
|
||||
<label for="content"
|
||||
string="Template that will be used as a content template for all new page of this category."/>
|
||||
</div>
|
||||
<field name="content"
|
||||
placeholder="e.g. Once upon a time..."
|
||||
widget="html" />
|
||||
</page>
|
||||
<page string="Documents" name="documents">
|
||||
<div class="oe_document_page">
|
||||
<field name="display_content"
|
||||
widget="html"
|
||||
class="oe_view_only"
|
||||
options='{"safe": True}' />
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
<field name="message_ids" widget="mail_thread"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_category_tree" model="ir.ui.view">
|
||||
<field name="name">document.page.category.tree</field>
|
||||
<field name="model">document.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Categories">
|
||||
<field name="name"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="create_uid" invisible="1"/>
|
||||
<field name="write_uid"/>
|
||||
<field name="write_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- page action -->
|
||||
<record id="action_page" model="ir.actions.act_window">
|
||||
@ -205,103 +127,35 @@
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_page_view_tree" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="0" />
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="view_wiki_tree"/>
|
||||
<field name="act_window_id" ref="action_page"/>
|
||||
</record>
|
||||
|
||||
<record id="action_page_view_form" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="5" />
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_wiki_form"/>
|
||||
<field name="act_window_id" ref="action_page"/>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
<menuitem id="menu_wiki"
|
||||
name="Pages"
|
||||
parent="knowledge.menu_document"
|
||||
sequence="20" />
|
||||
|
||||
|
||||
<menuitem id="menu_page"
|
||||
parent="menu_wiki"
|
||||
name="Pages"
|
||||
action="action_page"
|
||||
sequence="10" />
|
||||
name="Pages"
|
||||
parent="menu_wiki"
|
||||
action="action_page"
|
||||
sequence="10" />
|
||||
|
||||
<record id="action_category" model="ir.actions.act_window">
|
||||
<field name="name">Category</field>
|
||||
<field name="res_model">document.page</field>
|
||||
<field name="domain">[('type','=','category')]</field>
|
||||
<field name="context">{'default_type': 'category'}</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_category_tree"/>
|
||||
<field name="search_view_id" ref="view_wiki_filter"/>
|
||||
</record>
|
||||
<record id="action_category_view_tree" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="0" />
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="view_category_tree"/>
|
||||
<field name="act_window_id" ref="action_category"/>
|
||||
</record>
|
||||
<record id="action_category_view_form" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="5" />
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_category_form"/>
|
||||
<field name="act_window_id" ref="action_category"/>
|
||||
</record>
|
||||
<menuitem id="menu_category"
|
||||
parent="menu_wiki"
|
||||
name="Categories"
|
||||
action="action_category"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- History Tree view -->
|
||||
<record model="ir.ui.view" id="view_wiki_history_tree">
|
||||
<field name="name">document.page.history.tree</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Document History">
|
||||
<field name="create_date"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="page_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<!-- History Form view -->
|
||||
<record model="ir.ui.view" id="wiki_history_form">
|
||||
<field name="name">document.page.history.form</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Document Page History" version="7.0">
|
||||
<label for="page_id" class="oe_edit_only"/>
|
||||
<h1><field name="page_id" select="1" /></h1>
|
||||
<label for="create_date" class="oe_edit_only"/>
|
||||
<field name="create_date" readonly="1"/>
|
||||
<label for="content" class="oe_edit_only"/>
|
||||
<field name="content" colspan="4"
|
||||
widget="html" options='{"safe": True}'/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- History Action -->
|
||||
<record model="ir.actions.act_window" id="action_history">
|
||||
<field name="name">Page history</field>
|
||||
<field name="res_model">document.page.history</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_page_history"
|
||||
parent="menu_wiki"
|
||||
name="Pages history"
|
||||
action="action_history"
|
||||
sequence="30"
|
||||
groups="base.group_no_one" />
|
||||
|
||||
<act_window
|
||||
id="action_related_page_history"
|
||||
context="{'search_default_page_id': [active_id], 'default_page_id': active_id}"
|
||||
domain="[('page_id','=',active_id)]"
|
||||
name="Page History"
|
||||
res_model="document.page.history"
|
||||
src_model="document.page"/>
|
||||
|
||||
<act_window
|
||||
id="action_related_page_create_menu"
|
||||
|
10
document_page/views/document_page_assets.xml
Normal file
10
document_page/views/document_page_assets.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="assets_backend" name="document_page assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/document_page/static/src/css/document_page.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
90
document_page/views/document_page_category.xml
Executable file
90
document_page/views/document_page_category.xml
Executable file
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Category Views -->
|
||||
<record id="view_category_form" model="ir.ui.view">
|
||||
<field name="name">document.page.category.form</field>
|
||||
<field name="model">document.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Category">
|
||||
<sheet>
|
||||
<field name="type" invisible="1"/>
|
||||
<h1><field name="name" placeholder="Name"/></h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="parent_id" string="Category" context="{'default_type':'category'}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="write_uid" groups="base.group_no_one"/>
|
||||
<field name="write_date" groups="base.group_no_one"/>
|
||||
<field name="menu_id" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Template" name="template">
|
||||
<field name="template" placeholder="e.g. Once upon a time..." />
|
||||
</page>
|
||||
<page string="Documents" name="documents">
|
||||
<field name="content" widget="html" class="oe_view_only" options='{"safe": True}' />
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
<field name="message_ids" widget="mail_thread"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_category_tree" model="ir.ui.view">
|
||||
<field name="name">document.page.category.tree</field>
|
||||
<field name="model">document.page</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Categories">
|
||||
<field name="name"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="create_uid" invisible="1"/>
|
||||
<field name="write_uid"/>
|
||||
<field name="write_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- category Action -->
|
||||
<record id="action_category" model="ir.actions.act_window">
|
||||
<field name="name">Category</field>
|
||||
<field name="res_model">document.page</field>
|
||||
<field name="domain">[('type','=','category')]</field>
|
||||
<field name="context">{'default_type': 'category'}</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_category_tree"/>
|
||||
<field name="search_view_id" ref="view_wiki_filter"/>
|
||||
</record>
|
||||
|
||||
<record id="action_category_view_tree" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="0" />
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="view_category_tree"/>
|
||||
<field name="act_window_id" ref="action_category"/>
|
||||
</record>
|
||||
|
||||
<record id="action_category_view_form" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="5" />
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_category_form"/>
|
||||
<field name="act_window_id" ref="action_category"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_category"
|
||||
parent="menu_wiki"
|
||||
name="Categories"
|
||||
action="action_category"
|
||||
sequence="20"/>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
90
document_page/views/document_page_history.xml
Executable file
90
document_page/views/document_page_history.xml
Executable file
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- History Tree view -->
|
||||
<record model="ir.ui.view" id="view_wiki_history_tree">
|
||||
<field name="name">document.page.history.tree</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Document History">
|
||||
<field name="id"/>
|
||||
<field name="page_id"/>
|
||||
<field name="summary"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="create_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- History Search view -->
|
||||
<record id="view_wiki_history_filter" model="ir.ui.view">
|
||||
<field name="name">document.page.history.search</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Document Page History">
|
||||
<field name="page_id"/>
|
||||
<field name="content"/>
|
||||
<field name="create_uid"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter name="group_author" string="Author" context="{'group_by':'create_uid'}" />
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- History Form view -->
|
||||
<record model="ir.ui.view" id="wiki_history_form">
|
||||
<field name="name">document.page.history.form</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Document Page History">
|
||||
<sheet>
|
||||
<h1><field name="page_id"/></h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="create_uid" readonly="1"/>
|
||||
<field name="create_date" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="content" string="Content">
|
||||
<label for="summary"/>
|
||||
<field name="summary" placeholder="eg: Changed ... for ..."/>
|
||||
<label for="content"/>
|
||||
<field name="content" widget="html" placeholder="e.g. Once upon a time..." options="{'safe': True}"/>
|
||||
</page>
|
||||
<page name="diff" string="Changes">
|
||||
<field name="diff" widget="html" style="overflow-x: scroll" />
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- History Action -->
|
||||
<record model="ir.actions.act_window" id="action_history">
|
||||
<field name="name">Page history</field>
|
||||
<field name="res_model">document.page.history</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_page_history"
|
||||
parent="menu_wiki"
|
||||
name="Pages history"
|
||||
action="action_history"
|
||||
sequence="30"
|
||||
groups="base.group_no_one" />
|
||||
|
||||
<act_window id="action_related_page_history"
|
||||
context="{'search_default_page_id': [active_id], 'default_page_id': active_id}"
|
||||
domain="[('page_id','=',active_id)]"
|
||||
name="Page History"
|
||||
res_model="document.page.history"
|
||||
src_model="document.page"/>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
34
document_page/views/report_document_page.xml
Executable file
34
document_page/views/report_document_page.xml
Executable file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<template id="report_documentpage_doc">
|
||||
<t t-call="report.external_layout">
|
||||
|
||||
<div class="page">
|
||||
<h1 t-field="doc.display_name" />
|
||||
<div t-raw="doc.display_content" />
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_documentpage">
|
||||
<t t-call="report.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="document_page.report_documentpage_doc" />
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
|
||||
<report
|
||||
id="report_document_page"
|
||||
string="Document Page"
|
||||
model="document.page"
|
||||
report_type="qweb-pdf"
|
||||
file="document_page.report_documentpage"
|
||||
name="document_page.report_documentpage"
|
||||
/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
##############################################################################
|
||||
from openerp import models, fields, _
|
||||
from openerp import exceptions
|
||||
from openerp.exceptions import UserError
|
||||
|
||||
|
||||
class DocumentPageShowDiff(models.TransientModel):
|
||||
@ -27,33 +27,24 @@ class DocumentPageShowDiff(models.TransientModel):
|
||||
|
||||
_name = 'wizard.document.page.history.show_diff'
|
||||
|
||||
def get_diff(self):
|
||||
def _get_diff(self):
|
||||
"""Return the Difference between two document."""
|
||||
history = self.env["document.page.history"]
|
||||
ids = self.env.context.get('active_ids', [])
|
||||
|
||||
diff = ""
|
||||
diff = False
|
||||
if len(ids) == 2:
|
||||
if ids[0] > ids[1]:
|
||||
diff = history.getDiff(ids[1], ids[0])
|
||||
else:
|
||||
diff = history.getDiff(ids[0], ids[1])
|
||||
elif len(ids) == 1:
|
||||
old = history.browse(ids[0])
|
||||
nids = history.search(
|
||||
[('page_id', '=', old.page_id.id)],
|
||||
order='id DESC',
|
||||
limit=1
|
||||
)
|
||||
diff = history.getDiff(ids[0], nids.id)
|
||||
diff = history.browse(ids[0]).diff
|
||||
else:
|
||||
raise exceptions.Warning(
|
||||
_("Select one or maximum two history revisions!")
|
||||
)
|
||||
raise UserError(
|
||||
_("Select one or maximum two history revisions!"))
|
||||
return diff
|
||||
|
||||
diff = fields.Text(
|
||||
'Diff',
|
||||
readonly=True,
|
||||
default=get_diff
|
||||
default=_get_diff,
|
||||
)
|
||||
|
@ -8,13 +8,9 @@
|
||||
<field name="model">wizard.document.page.history.show_diff</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Difference" version="7.0">
|
||||
<field name="diff"
|
||||
widget="html"
|
||||
options='{"safe": True}' />
|
||||
<field name="diff" widget="html" class="overflow-x: scroll" />
|
||||
<footer>
|
||||
<button string="Close"
|
||||
class="oe_link"
|
||||
special="cancel" />
|
||||
<button string="Close" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
|
@ -19,3 +19,4 @@
|
||||
#
|
||||
##############################################################################
|
||||
from . import models
|
||||
from .hooks import post_init_hook, uninstall_hook
|
||||
|
@ -21,13 +21,12 @@
|
||||
|
||||
{
|
||||
'name': 'Document Page Approval',
|
||||
'version': '9.0.1.0.0',
|
||||
'version': '9.0.2.0.0',
|
||||
"author": "Savoir-faire Linux,Odoo Community Association (OCA)",
|
||||
"website": "http://www.savoirfairelinux.com",
|
||||
"license": "AGPL-3",
|
||||
'category': 'Knowledge Management',
|
||||
'depends': [
|
||||
'knowledge',
|
||||
'document_page',
|
||||
'mail',
|
||||
],
|
||||
@ -40,6 +39,8 @@
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'post_init_hook': 'post_init_hook',
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
'images': [
|
||||
'images/category.png',
|
||||
'images/page_history_list.png',
|
||||
|
@ -1,15 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Allow user to make upgrade-proof customizations to email template -->
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- If user wants to make upgrade-proof customizations to email templates, he should edit ir.model.data and check noupdate himself -->
|
||||
<data>
|
||||
|
||||
<record id="email_template_new_draft_need_approval" model="mail.template">
|
||||
<field name="name">Automated new draft need approval Notification Mail</field>
|
||||
<field name="email_from">${object.create_uid.company_id.email or 'noreply@localhost.com'}</field>
|
||||
<field name="subject">New version of "${object.page_id.name}" to approve</field>
|
||||
<field name="email_to">${object.get_approvers_email}</field>
|
||||
<field name="subject">New version of ${object.display_name} needs your approval</field>
|
||||
<field name="model_id" ref="model_document_page_history"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="lang">${object.create_uid.partner_id.lang}</field>
|
||||
@ -17,13 +15,29 @@
|
||||
<![CDATA[
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>The page "${object.page_id.name}" has been modified and need your approval.</p>
|
||||
<p>${object.create_uid.name} submited a new Change Request for <b>${object.page_id.name}</b> and it needs your approval.</p>
|
||||
|
||||
<p>You can review the new version here : <a href="${object.get_page_url}">${object.get_page_url}</a></p>
|
||||
<h1><a href="${object.page_url}">${object.display_name}</a></h1>
|
||||
<p>
|
||||
<b>Modified by:</b> ${object.create_uid.name}<br/>
|
||||
<b>Date:</b> ${object.create_date}<br>
|
||||
</p>
|
||||
|
||||
% if object.summary:
|
||||
<h3>Summary</h3>
|
||||
<p>${object.summary}</p>
|
||||
% endif
|
||||
|
||||
<h3>Diff</h3>
|
||||
<div style="overflow-x:scroll; font-size:0.85em; margin-bottom:2em;">
|
||||
${object.diff|safe}
|
||||
</div>
|
||||
|
||||
<p>Have a great day.</p>
|
||||
|
||||
<p>Have a great day.<br/>
|
||||
--<br/>
|
||||
Odoo</p>]]>
|
||||
<p>Odoo</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
22
document_page_approval/hooks.py
Normal file
22
document_page_approval/hooks.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Ivan Todorovich (<ivan.todorovich@gmail.com>)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
def post_init_hook(cr, registry): # pragma: no cover
|
||||
# Set all pre-existing pages history to approved
|
||||
cr.execute("""
|
||||
UPDATE document_page_history
|
||||
SET state='approved',
|
||||
approved_uid=create_uid,
|
||||
approved_date=create_date
|
||||
WHERE state IS NULL
|
||||
""")
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry): # pragma: no cover
|
||||
# Remove unapproved pages
|
||||
cr.execute(
|
||||
"DELETE FROM document_page_history "
|
||||
"WHERE state != 'approved'"
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Ivan Todorovich <ivan.todorovich@gmail.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
def migrate(cr, version): # pragma: no cover
|
||||
# Set all pre-existing pages history to approved
|
||||
cr.execute("""
|
||||
UPDATE document_page_history
|
||||
SET state='approved',
|
||||
approved_uid=create_uid,
|
||||
approved_date=create_date
|
||||
WHERE state IS NULL
|
||||
""")
|
@ -20,6 +20,7 @@
|
||||
##############################################################################
|
||||
|
||||
from openerp import models, fields, api
|
||||
from ast import literal_eval
|
||||
|
||||
|
||||
class DocumentPageApproval(models.Model):
|
||||
@ -27,104 +28,126 @@ class DocumentPageApproval(models.Model):
|
||||
|
||||
_inherit = 'document.page'
|
||||
|
||||
@api.multi
|
||||
def _get_display_content(self):
|
||||
"""Display the content of document."""
|
||||
for page in self:
|
||||
content = ""
|
||||
if page.type == "category":
|
||||
content = self._get_page_index(page, link=False)
|
||||
else:
|
||||
history = self.env['document.page.history']
|
||||
if self.is_approval_required(page):
|
||||
history_ids = history.search(
|
||||
[
|
||||
('page_id', '=', page.id),
|
||||
('state', '=', 'approved')
|
||||
],
|
||||
limit=1,
|
||||
order='create_date DESC'
|
||||
)
|
||||
content = history_ids.content
|
||||
else:
|
||||
content = page.content
|
||||
page.display_content = content
|
||||
|
||||
@api.multi
|
||||
def _get_approved_date(self):
|
||||
"""Return the approved date of a document."""
|
||||
for page in self:
|
||||
approved_date = False
|
||||
if self.is_approval_required(page):
|
||||
history = self.env['document.page.history']
|
||||
history_ids = history.search(
|
||||
[
|
||||
('page_id', '=', page.id),
|
||||
('state', '=', 'approved')
|
||||
],
|
||||
limit=1,
|
||||
order='create_date DESC'
|
||||
)
|
||||
approved_date = history_ids.approved_date
|
||||
page.approved_date = approved_date
|
||||
|
||||
@api.multi
|
||||
def _get_approved_uid(self):
|
||||
"""Return the user's id of the approved user."""
|
||||
for page in self:
|
||||
approved_uid = False
|
||||
if self.is_approval_required(page):
|
||||
history = self.env['document.page.history']
|
||||
history_ids = history.search(
|
||||
[
|
||||
('page_id', '=', page.id),
|
||||
('state', '=', 'approved')
|
||||
],
|
||||
limit=1,
|
||||
order='create_date DESC'
|
||||
)
|
||||
approved_uid = history_ids.approved_uid.id
|
||||
page.approved_uid = approved_uid
|
||||
|
||||
@api.multi
|
||||
def _is_parent_approval_required(self):
|
||||
"""Check if the document required approval base on his parrent."""
|
||||
for page in self:
|
||||
page.is_parent_approval_required = self.is_approval_required(page)
|
||||
|
||||
def is_approval_required(self, page):
|
||||
"""Check if a document required approval."""
|
||||
if page:
|
||||
res = page.approval_required
|
||||
res = res or self.is_approval_required(page.parent_id)
|
||||
else:
|
||||
res = False
|
||||
return res
|
||||
|
||||
display_content = fields.Text(
|
||||
compute=_get_display_content,
|
||||
string='Displayed Content'
|
||||
history_ids = fields.One2many(
|
||||
order='approved_date DESC',
|
||||
domain=[('state', '=', 'approved')],
|
||||
)
|
||||
|
||||
approved_date = fields.Datetime(
|
||||
compute=_get_approved_date,
|
||||
string="Approved Date"
|
||||
'Approved Date',
|
||||
related='history_head.approved_date',
|
||||
store=True,
|
||||
index=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
approved_uid = fields.Many2one(
|
||||
'res.users',
|
||||
compute=_get_approved_uid,
|
||||
string="Approved By",
|
||||
'Approved by',
|
||||
related='history_head.approved_uid',
|
||||
store=True,
|
||||
index=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
approval_required = fields.Boolean("Require approval")
|
||||
|
||||
is_parent_approval_required = fields.Boolean(
|
||||
compute=_is_parent_approval_required,
|
||||
string="parent approval"
|
||||
approval_required = fields.Boolean(
|
||||
'Require approval',
|
||||
help='Require approval for changes on this page or its child pages.',
|
||||
)
|
||||
|
||||
approver_gid = fields.Many2one(
|
||||
"res.groups",
|
||||
"Approver group"
|
||||
"Approver group",
|
||||
help='Users must also belong to the Approvers group',
|
||||
)
|
||||
|
||||
is_approval_required = fields.Boolean(
|
||||
'Approval required',
|
||||
help='If true, changes of this page require approval',
|
||||
compute='_compute_is_approval_required',
|
||||
)
|
||||
|
||||
am_i_approver = fields.Boolean(
|
||||
compute='_compute_am_i_approver'
|
||||
)
|
||||
|
||||
approver_group_ids = fields.Many2many(
|
||||
'res.groups',
|
||||
string='Approver groups',
|
||||
help='Groups that can approve changes to this document',
|
||||
compute='_compute_approver_group_ids',
|
||||
)
|
||||
|
||||
has_changes_pending_approval = fields.Boolean(
|
||||
compute='_compute_has_changes_pending_approval',
|
||||
string='Has changes pending approval'
|
||||
)
|
||||
|
||||
@api.multi
|
||||
@api.depends('approval_required', 'parent_id.is_approval_required')
|
||||
def _compute_is_approval_required(self):
|
||||
"""Check if the document required approval based on his parents."""
|
||||
for page in self:
|
||||
res = page.approval_required
|
||||
if page.parent_id:
|
||||
res = res or page.parent_id.is_approval_required
|
||||
page.is_approval_required = res
|
||||
|
||||
@api.multi
|
||||
@api.depends('approver_gid', 'parent_id.approver_group_ids')
|
||||
def _compute_approver_group_ids(self):
|
||||
"""Compute the approver groups based on his parents."""
|
||||
for page in self:
|
||||
res = page.approver_gid
|
||||
if page.parent_id:
|
||||
res = res | page.parent_id.approver_group_ids
|
||||
page.approver_group_ids = res
|
||||
|
||||
@api.multi
|
||||
@api.depends('is_approval_required', 'approver_group_ids')
|
||||
def _compute_am_i_approver(self):
|
||||
"""Check if the current user can approve changes to this page."""
|
||||
for rec in self:
|
||||
rec.am_i_approver = rec.can_user_approve_this_page(self.env.user)
|
||||
|
||||
@api.multi
|
||||
def can_user_approve_this_page(self, user):
|
||||
"""Check if a user can approve this page."""
|
||||
self.ensure_one()
|
||||
# if it's not required, anyone can approve
|
||||
if not self.is_approval_required:
|
||||
return True
|
||||
# to approve, you must have approver rights
|
||||
approver_group_id = self.env.ref(
|
||||
'document_page_approval.group_document_approver_user')
|
||||
if approver_group_id not in user.groups_id:
|
||||
return False
|
||||
# and belong to at least one of the approver_groups (if any is set)
|
||||
if not self.approver_group_ids:
|
||||
return True
|
||||
return len(user.groups_id & self.approver_group_ids) > 0
|
||||
|
||||
@api.multi
|
||||
def _compute_has_changes_pending_approval(self):
|
||||
history = self.env['document.page.history']
|
||||
for rec in self:
|
||||
changes = history.search_count([
|
||||
('page_id', '=', rec.id),
|
||||
('state', '=', 'to approve')])
|
||||
rec.has_changes_pending_approval = (changes > 0)
|
||||
|
||||
@api.multi
|
||||
def _create_history(self, vals):
|
||||
res = super(DocumentPageApproval, self)._create_history(vals)
|
||||
res.signal_workflow('document_page_auto_confirm')
|
||||
|
||||
@api.multi
|
||||
def action_changes_pending_approval(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('document_page_approval.action_change_requests')
|
||||
action = action.read()[0]
|
||||
context = literal_eval(action['context'])
|
||||
context['search_default_page_id'] = self.id
|
||||
context['default_page_id'] = self.id
|
||||
action['context'] = context
|
||||
return action
|
||||
|
@ -28,117 +28,122 @@ from openerp import models, fields, api
|
||||
class DocumentPageHistoryWorkflow(models.Model):
|
||||
"""Useful to manage edition's workflow on a document."""
|
||||
|
||||
_inherit = 'document.page.history'
|
||||
_name = 'document.page.history'
|
||||
_inherit = ['document.page.history', 'mail.thread']
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('to approve', 'Pending Approval'),
|
||||
('approved', 'Approved'),
|
||||
('cancelled', 'Cancelled')],
|
||||
'Status',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
approved_date = fields.Datetime(
|
||||
'Approved Date',
|
||||
)
|
||||
|
||||
approved_uid = fields.Many2one(
|
||||
'res.users',
|
||||
'Approved by',
|
||||
)
|
||||
|
||||
is_approval_required = fields.Boolean(
|
||||
related='page_id.is_approval_required',
|
||||
string="Approval required",
|
||||
)
|
||||
|
||||
am_i_owner = fields.Boolean(
|
||||
compute='_compute_am_i_owner'
|
||||
)
|
||||
|
||||
am_i_approver = fields.Boolean(
|
||||
related='page_id.am_i_approver'
|
||||
)
|
||||
|
||||
page_url = fields.Text(
|
||||
compute='_compute_page_url',
|
||||
string="URL",
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def page_approval_draft(self):
|
||||
"""Set a document state as draft and notified the reviewers."""
|
||||
"""Set a change request as draft"""
|
||||
self.write({'state': 'draft'})
|
||||
|
||||
@api.multi
|
||||
def page_approval_to_approve(self):
|
||||
"""Set a change request as to approve"""
|
||||
self.write({'state': 'to approve'})
|
||||
template = self.env.ref(
|
||||
'document_page_approval.email_template_new_draft_need_approval')
|
||||
for page in self:
|
||||
if page.is_parent_approval_required:
|
||||
template.send_mail(page.id, force_send=True)
|
||||
return True
|
||||
approver_gid = self.env.ref(
|
||||
'document_page_approval.group_document_approver_user')
|
||||
for rec in self:
|
||||
if rec.is_approval_required:
|
||||
guids = [g.id for g in rec.page_id.approver_group_ids]
|
||||
users = self.env['res.users'].search([
|
||||
('groups_id', 'in', guids),
|
||||
('groups_id', 'in', approver_gid.id)])
|
||||
rec.message_subscribe_users([u.id for u in users])
|
||||
rec.message_post_with_template(template.id)
|
||||
|
||||
@api.multi
|
||||
def page_approval_approved(self):
|
||||
"""Set a document state as approve."""
|
||||
message_obj = self.env['mail.message']
|
||||
"""Set a change request as approved."""
|
||||
self.write({
|
||||
'state': 'approved',
|
||||
'approved_date': datetime.now().strftime(
|
||||
DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
'approved_uid': self.env.uid
|
||||
})
|
||||
# Notify followers a new version is available
|
||||
for page_history in self:
|
||||
subtype = self.env.ref('mail.mt_comment')
|
||||
message_obj.create(
|
||||
{'res_id': page_history.page_id.id,
|
||||
'model': 'document.page',
|
||||
'subtype_id': subtype.id,
|
||||
'body': _('New version of the document %s'
|
||||
' approved.') % page_history.page_id.name
|
||||
}
|
||||
for rec in self:
|
||||
# Trigger computed field update
|
||||
rec.page_id._compute_history_head()
|
||||
# Notify state change
|
||||
rec.message_post(
|
||||
subtype='mt_comment',
|
||||
body=_(
|
||||
'Change request has been approved by %s.'
|
||||
) % (self.env.user.name)
|
||||
)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _can_user_approve_page(self):
|
||||
"""Check if a user cas approve the page."""
|
||||
user = self.env.user
|
||||
for page in self:
|
||||
page.can_user_approve_page = page.can_user_approve_this_page(
|
||||
page.page_id,
|
||||
user
|
||||
# Notify followers a new version is available
|
||||
rec.page_id.message_post(
|
||||
subtype='mt_comment',
|
||||
body=_(
|
||||
'New version of the document %s approved.'
|
||||
) % (rec.page_id.name)
|
||||
)
|
||||
|
||||
def can_user_approve_this_page(self, page, user):
|
||||
"""Check if a user can approved the page."""
|
||||
if page:
|
||||
res = page.approver_gid in user.groups_id
|
||||
res = res or self.can_user_approve_this_page(page.parent_id, user)
|
||||
else:
|
||||
res = False
|
||||
return res
|
||||
@api.multi
|
||||
def page_approval_cancelled(self):
|
||||
"""Set a change request as cancelled."""
|
||||
self.write({'state': 'cancelled'})
|
||||
for rec in self:
|
||||
rec.message_post(
|
||||
subtype='mt_comment',
|
||||
body=_(
|
||||
'Change request <b>%s</b> has been cancelled by %s.'
|
||||
) % (rec.display_name, self.env.user.name)
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def get_approvers_guids(self):
|
||||
"""Return the approvers group."""
|
||||
res = {}
|
||||
for page in self:
|
||||
res[page.id] = self.get_approvers_guids_for_page(page.page_id)
|
||||
return res
|
||||
|
||||
def get_approvers_guids_for_page(self, page):
|
||||
"""Return the approvers group for a page."""
|
||||
if page:
|
||||
if page.approver_gid:
|
||||
res = [page.approver_gid.id]
|
||||
else:
|
||||
res = []
|
||||
res.extend(self.get_approvers_guids_for_page(page.parent_id))
|
||||
else:
|
||||
res = []
|
||||
|
||||
return res
|
||||
def _compute_am_i_owner(self):
|
||||
"""Check if current user is the owner"""
|
||||
for rec in self:
|
||||
rec.am_i_owner = (rec.create_uid == self.env.user)
|
||||
|
||||
@api.multi
|
||||
def _get_approvers_email(self):
|
||||
"""Get the approvers email."""
|
||||
for page in self:
|
||||
emails = ''
|
||||
guids = self.get_approvers_guids()
|
||||
uids = [i.id for i in self.env['res.users'].search([
|
||||
('groups_id', 'in', guids[page.id])
|
||||
])]
|
||||
users = self.env['res.users'].browse(uids)
|
||||
|
||||
for user in users:
|
||||
if user.email:
|
||||
emails += user.email
|
||||
emails += ','
|
||||
else:
|
||||
empl = self.env['hr.employee'].search([
|
||||
('login', '=', user.login)
|
||||
])
|
||||
if empl.work_email:
|
||||
emails += empl.work_email
|
||||
emails += ','
|
||||
|
||||
page.get_approvers_email = emails[:-1]
|
||||
|
||||
@api.multi
|
||||
def _get_page_url(self):
|
||||
"""Get the page url."""
|
||||
def _compute_page_url(self):
|
||||
"""Compute the page url."""
|
||||
for page in self:
|
||||
base_url = self.env['ir.config_parameter'].get_param(
|
||||
'web.base.url',
|
||||
default='http://localhost:8069'
|
||||
)
|
||||
|
||||
page.get_page_url = (
|
||||
page.page_url = (
|
||||
'{}/web#db={}&id={}&view_type=form&'
|
||||
'model=document.page.history').format(
|
||||
base_url,
|
||||
@ -146,37 +151,18 @@ class DocumentPageHistoryWorkflow(models.Model):
|
||||
page.id
|
||||
)
|
||||
|
||||
state = fields.Selection(
|
||||
[('draft', 'Draft'), ('approved', 'Approved')],
|
||||
'Status',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
approved_date = fields.Datetime("Approved Date")
|
||||
|
||||
approved_uid = fields.Many2one(
|
||||
'res.users',
|
||||
"Approved By"
|
||||
)
|
||||
|
||||
is_parent_approval_required = fields.Boolean(
|
||||
related='page_id.is_parent_approval_required',
|
||||
string="parent approval",
|
||||
store=False
|
||||
)
|
||||
|
||||
can_user_approve_page = fields.Boolean(
|
||||
compute=_can_user_approve_page,
|
||||
string="can user approve this page",
|
||||
store=False
|
||||
)
|
||||
get_approvers_email = fields.Text(
|
||||
compute=_get_approvers_email,
|
||||
string="get all approvers email",
|
||||
store=False
|
||||
)
|
||||
get_page_url = fields.Text(
|
||||
compute=_get_page_url,
|
||||
string="URL",
|
||||
store=False
|
||||
)
|
||||
@api.multi
|
||||
def _compute_diff(self):
|
||||
"""Shows a diff between this version and the previous version"""
|
||||
history = self.env['document.page.history']
|
||||
for rec in self:
|
||||
domain = [
|
||||
('page_id', '=', rec.page_id.id),
|
||||
('state', '=', 'approved')]
|
||||
if rec.approved_date:
|
||||
domain.append(('approved_date', '<', rec.approved_date))
|
||||
prev = history.search(domain, limit=1, order='approved_date DESC')
|
||||
if prev:
|
||||
rec.diff = self.getDiff(prev.id, rec.id)
|
||||
else:
|
||||
rec.diff = self.getDiff(False, rec.id)
|
||||
|
@ -1,9 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="group_document_approver_user" model="res.groups">
|
||||
<field name="name">Document approver</field>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
<field name="name">Approver</field>
|
||||
<field name="category_id" ref="knowledge.module_category_knowledge_management"/>
|
||||
<field name="implied_ids" eval="[(4, ref('document_page.group_document_editor'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="document_page.group_document_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('group_document_approver_user'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
@ -1,2 +1 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
document_page_history,document.page.history,model_document_page_history,group_document_approver_user,1,1,1,0
|
||||
|
|
@ -1,2 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_document_page_approval, test_document_page_history_workflow
|
||||
from . import test_document_page_approval
|
||||
|
@ -3,35 +3,123 @@ from openerp.tests import common
|
||||
|
||||
|
||||
class TestDocumentPageApproval(common.TransactionCase):
|
||||
"""Test document page approval model."""
|
||||
|
||||
def test_get_display_content(self):
|
||||
"""Test page display content."""
|
||||
# Check content of a category
|
||||
category = self.env['document.page'].search([
|
||||
('name', '=', 'OpenERP Features')
|
||||
])
|
||||
def setUp(self):
|
||||
super(TestDocumentPageApproval, self).setUp()
|
||||
self.page_obj = self.env['document.page']
|
||||
self.history_obj = self.env['document.page.history']
|
||||
# demo
|
||||
self.category1 = self.env.ref('document_page.demo_category1')
|
||||
self.page1 = self.env.ref('document_page.demo_page1')
|
||||
self.approver_gid = self.env.ref(
|
||||
'document_page_approval.group_document_approver_user')
|
||||
# demo_approval
|
||||
self.category2 = self.page_obj.create({
|
||||
'name': 'This category requires approval',
|
||||
'type': 'category',
|
||||
'approval_required': True,
|
||||
'approver_gid': self.approver_gid.id,
|
||||
})
|
||||
self.page2 = self.page_obj.create({
|
||||
'name': 'This page requires approval',
|
||||
'parent_id': self.category2.id,
|
||||
'content': 'This content will require approval'
|
||||
})
|
||||
|
||||
self.assertIsNotNone(category.display_content, 'a category')
|
||||
def test_approval_required(self):
|
||||
page = self.page2
|
||||
self.assertTrue(page.is_approval_required)
|
||||
self.assertTrue(page.has_changes_pending_approval)
|
||||
self.assertEqual(len(page.history_ids), 0)
|
||||
|
||||
# Check content of a page
|
||||
pages = self.env['document.page'].search([
|
||||
('parent_id', '=', category.id)
|
||||
])
|
||||
def test_change_request_approve(self):
|
||||
page = self.page2
|
||||
chreq = self.history_obj.search([
|
||||
('page_id', '=', page.id),
|
||||
('state', '!=', 'approved')
|
||||
])[0]
|
||||
|
||||
# It should automatically be in 'to approve' state
|
||||
self.assertEqual(chreq.state, 'to approve')
|
||||
self.assertNotEqual(chreq.content, page.content)
|
||||
|
||||
# who_am_i
|
||||
self.assertTrue(chreq.am_i_owner)
|
||||
self.assertTrue(chreq.am_i_approver)
|
||||
|
||||
# approve
|
||||
chreq.signal_workflow('page_approval_approve')
|
||||
self.assertEqual(chreq.state, 'approved')
|
||||
self.assertEqual(chreq.content, page.content)
|
||||
|
||||
# new changes should create change requests
|
||||
page.write({'content': 'New content'})
|
||||
self.assertNotEqual(page.content, 'New content')
|
||||
chreq = self.history_obj.search([
|
||||
('page_id', '=', page.id),
|
||||
('state', '!=', 'approved')
|
||||
])[0]
|
||||
chreq.signal_workflow('page_approval_approve')
|
||||
self.assertEqual(page.content, 'New content')
|
||||
|
||||
def test_change_request_auto_approve(self):
|
||||
page = self.page1
|
||||
self.assertFalse(page.is_approval_required)
|
||||
page.write({'content': 'New content'})
|
||||
self.assertEqual(page.content, 'New content')
|
||||
|
||||
def test_change_request_from_scratch(self):
|
||||
page = self.page2
|
||||
|
||||
# aprove everything
|
||||
self.history_obj.search([
|
||||
('page_id', '=', page.id),
|
||||
('state', '!=', 'approved')
|
||||
]).signal_workflow('page_approval_approve')
|
||||
|
||||
# new change request from scrath
|
||||
chreq = self.history_obj.create({
|
||||
'page_id': page.id,
|
||||
'summary': 'Changed something',
|
||||
'content': 'New content',
|
||||
})
|
||||
|
||||
self.assertEqual(chreq.state, 'draft')
|
||||
self.assertNotEqual(page.content, chreq.content)
|
||||
self.assertNotEqual(page.approved_date, chreq.approved_date)
|
||||
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
|
||||
|
||||
chreq.signal_workflow('page_approval_to_approve')
|
||||
self.assertEqual(chreq.state, 'to approve')
|
||||
self.assertNotEqual(page.content, chreq.content)
|
||||
self.assertNotEqual(page.approved_date, chreq.approved_date)
|
||||
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
|
||||
|
||||
chreq.signal_workflow('page_approval_cancel')
|
||||
self.assertEqual(chreq.state, 'cancelled')
|
||||
self.assertNotEqual(page.content, chreq.content)
|
||||
self.assertNotEqual(page.approved_date, chreq.approved_date)
|
||||
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
|
||||
|
||||
chreq.signal_workflow('page_approval_reopen')
|
||||
self.assertEqual(chreq.state, 'draft')
|
||||
self.assertNotEqual(page.content, chreq.content)
|
||||
self.assertNotEqual(page.approved_date, chreq.approved_date)
|
||||
self.assertNotEqual(page.approved_uid, chreq.approved_uid)
|
||||
|
||||
chreq.signal_workflow('page_approval_approve')
|
||||
self.assertEqual(chreq.state, 'approved')
|
||||
self.assertEqual(page.content, chreq.content)
|
||||
self.assertEqual(page.approved_date, chreq.approved_date)
|
||||
self.assertEqual(page.approved_uid, chreq.approved_uid)
|
||||
|
||||
def test_get_approvers_guids(self):
|
||||
"""Get approver guids."""
|
||||
page = self.page2
|
||||
self.assertTrue(len(page.approver_group_ids) > 0)
|
||||
|
||||
def test_get_page_url(self):
|
||||
"""Test if page url exist."""
|
||||
pages = self.env['document.page.history'].search([])
|
||||
page = pages[0]
|
||||
self.assertIsNotNone(page.display_content, 'Page content')
|
||||
|
||||
# Check if approval is required
|
||||
self.assertTrue(page.is_approval_required(page) ==
|
||||
category.approval_required)
|
||||
|
||||
# Check content of an approval page
|
||||
page.approval_required = True
|
||||
|
||||
self.assertIsNotNone(page.display_content, 'Page content')
|
||||
|
||||
# Check if approval is required
|
||||
self.assertTrue(page.is_approval_required(page))
|
||||
|
||||
# Check if parent approval is required
|
||||
self.assertTrue(page.is_parent_approval_required)
|
||||
self.assertIsNotNone(page.page_url)
|
||||
|
@ -1,67 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp.tests import common
|
||||
# Import logger
|
||||
import logging
|
||||
|
||||
# Get the logger
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestDocumentPageHistoryWorkflow(common.TransactionCase):
|
||||
"""Test document page history workflow."""
|
||||
|
||||
def test_can_user_approve_this_page(self):
|
||||
"""Test if a user can approve this page."""
|
||||
category = self.env.ref('document_page.demo_category1')
|
||||
category.approval_required = True
|
||||
category.approver_gid = self.env.ref(
|
||||
'document_page_approval.group_document_approver_user')
|
||||
|
||||
page = self.env['document.page'].create({
|
||||
'name': 'Test Page10',
|
||||
'content': 'A difficult test',
|
||||
'parent_id': category.id
|
||||
})
|
||||
|
||||
history = self.env['document.page.history'].search(
|
||||
[
|
||||
('page_id', '=', page.id)
|
||||
],
|
||||
limit=1,
|
||||
order='create_date DESC'
|
||||
)
|
||||
|
||||
self.assertTrue(history.can_user_approve_page)
|
||||
|
||||
def test_get_approvers_guids(self):
|
||||
"""Get approver guids."""
|
||||
category = self.env.ref('document_page.demo_category1')
|
||||
category.approval_required = True
|
||||
pages = self.env['document.page.history'].search([
|
||||
('page_id', '=', category.id)
|
||||
])
|
||||
page = pages[0]
|
||||
approvers_guid = page.get_approvers_guids()
|
||||
self.assertTrue(len(approvers_guid) > 0)
|
||||
|
||||
def test_get_approvers_email(self):
|
||||
"""Get approver email."""
|
||||
category = self.env.ref('document_page.demo_category1')
|
||||
category.approval_required = True
|
||||
pages = self.env['document.page.history'].search([
|
||||
('page_id', '=', category.id)
|
||||
])
|
||||
page = pages[0]
|
||||
_logger.info("Email: " + str(page.get_approvers_email))
|
||||
self.assertIsNotNone(page.get_approvers_email)
|
||||
|
||||
def test_get_page_url(self):
|
||||
"""Test if page url exist."""
|
||||
category = self.env.ref('document_page.demo_category1')
|
||||
category.approval_required = True
|
||||
pages = self.env['document.page.history'].search([
|
||||
('page_id', '=', category.id)
|
||||
])
|
||||
page = pages[0]
|
||||
_logger.info("Page: " + str(page.get_page_url))
|
||||
self.assertIsNotNone(page.get_page_url)
|
@ -1,58 +1,110 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<!-- History Form View -->
|
||||
<record id="wiki_history_form_inherit" model="ir.ui.view">
|
||||
<field name="name">document.page.history.form</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="inherit_id"
|
||||
ref="document_page.wiki_history_form"/>
|
||||
<field name="inherit_id" ref="document_page.wiki_history_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form/label[@for='page_id']"
|
||||
position="before">
|
||||
<header attrs="{'invisible':[('is_parent_approval_required','=',False)]}">
|
||||
<span attrs="{'invisible':[('can_user_approve_page','=',False)]}">
|
||||
<button name="page_approval_approve"
|
||||
string="Approve"
|
||||
states="draft" />
|
||||
</span>
|
||||
<field name="state"
|
||||
widget="statusbar"
|
||||
statusbar_visible="draft,approved"/>
|
||||
<field name="is_parent_approval_required"
|
||||
invisible="1" />
|
||||
<field name="can_user_approve_page"
|
||||
invisible="1" />
|
||||
|
||||
<sheet position="before">
|
||||
<header>
|
||||
<!-- draft -> to approve -->
|
||||
<button name="page_approval_to_approve" string="Send to Review" class="oe_highlight"
|
||||
attrs="{'invisible':['|','|',('is_approval_required','=',False),('am_i_owner','=',False),('state', 'not in', ['draft'])]}"/>
|
||||
<!-- approve if i am approver -->
|
||||
<button name="page_approval_approve" string="Approve" class="oe_highlight"
|
||||
attrs="{'invisible':['|','|',('is_approval_required','=',False),('am_i_approver','=',False),('state','not in',['draft','to approve'])]}"/>
|
||||
<!-- approve if it's not required and i am owner -->
|
||||
<button name="page_approval_approve" string="Approve" class="oe_highlight"
|
||||
attrs="{'invisible':['|','|',('is_approval_required','=',True),('am_i_owner','=',False),('state','not in',['draft', 'to approve'])]}"/>
|
||||
<!-- cancel if i am owner or approver -->
|
||||
<button name="page_approval_cancel" string="Cancel"
|
||||
attrs="{'invisible':['|','&',('am_i_owner','=',False),('am_i_approver','=',False),('state','not in',['draft','to approve'])]}"/>
|
||||
<!-- reopen if i am owner or approver -->
|
||||
<button name="page_approval_reopen" string="Back to draft"
|
||||
attrs="{'invisible':['|','&',('am_i_owner','=',False),('am_i_approver','=',False),('state','not in',['cancelled'])]}"/>
|
||||
<field name="am_i_owner" invisible="1"/>
|
||||
<field name="am_i_approver" invisible="1"/>
|
||||
<field name="is_approval_required" invisible="1"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,approved" />
|
||||
</header>
|
||||
</sheet>
|
||||
<xpath expr="//field[@name='create_uid']/parent::group" position="after">
|
||||
<group>
|
||||
<field name="approved_uid" readonly="1" attrs="{'invisible':[('state','not in',['approved'])]}"/>
|
||||
<field name="approved_date" readonly="1" attrs="{'invisible':[('state','not in',['approved'])]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<field name="content" position="attributes">
|
||||
<attribute name="attrs">{'readonly': [('state', 'not in', ['draft'])]}</attribute>
|
||||
</field>
|
||||
<sheet position="after">
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
<field name="message_ids" widget="mail_thread"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Page Form View -->
|
||||
<record id="wiki_form_inherit" model="ir.ui.view">
|
||||
<field name="name">document.page.form</field>
|
||||
<field name="model">document.page</field>
|
||||
<field name="inherit_id"
|
||||
ref="document_page.view_wiki_form" />
|
||||
<field name="inherit_id" ref="document_page.view_wiki_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="display_content"
|
||||
position="before">
|
||||
<group class="oe_read_only">
|
||||
<field name="approved_date" />
|
||||
<field name="approved_uid" />
|
||||
</group>
|
||||
|
||||
<sheet position="before">
|
||||
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
|
||||
attrs="{'invisible': [('has_changes_pending_approval','=',False)]}">
|
||||
This document has <b>Changes Pending Approval</b>. You are viewing the last approved content.
|
||||
</div>
|
||||
<div class="alert alert-warning oe_edit_only" role="alert" style="margin-bottom:0px;"
|
||||
attrs="{'invisible': [('is_approval_required','=',False)]}">
|
||||
This document requires approval. If edited, you will create a new <b>Change Request</b>.
|
||||
</div>
|
||||
<field name="is_approval_required" invisible="1"/>
|
||||
<field name="has_changes_pending_approval" invisible="1"/>
|
||||
</sheet>
|
||||
|
||||
<field name="type" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="action_changes_pending_approval"
|
||||
string="Change Requests" type="object"
|
||||
attrs="{'invisible':[('has_changes_pending_approval','=',False)]}" icon="fa-edit"/>
|
||||
</div>
|
||||
</field>
|
||||
|
||||
<field name="content_uid" position="after">
|
||||
<field name="approved_uid"/>
|
||||
</field>
|
||||
|
||||
<field name="content_date" position="replace">
|
||||
<field name="approved_date"/>
|
||||
</field>
|
||||
|
||||
<field name="history_ids" position="inside">
|
||||
<tree>
|
||||
<field name="id"/>
|
||||
<field name="approved_date"/>
|
||||
<field name="summary"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="approved_uid"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Page Menu Form View -->
|
||||
<record id="view_wiki_menu_form_inherit" model="ir.ui.view">
|
||||
<field name="name">document.page.form</field>
|
||||
<field name="model">document.page</field>
|
||||
<field name="inherit_id"
|
||||
ref="document_page.view_wiki_menu_form" />
|
||||
<field name="inherit_id" ref="document_page.view_wiki_menu_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="display_content"
|
||||
position="before">
|
||||
<group class="oe_read_only"
|
||||
attrs="{'invisible':[('type','!=','content')]}">
|
||||
<field name="content" position="before">
|
||||
<group class="oe_read_only" attrs="{'invisible':[('type','!=','content')]}">
|
||||
<field name="approved_date" />
|
||||
<field name="approved_uid" />
|
||||
</group>
|
||||
@ -60,11 +112,11 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Catgory Form View -->
|
||||
<record id="view_category_form_inherit" model="ir.ui.view">
|
||||
<field name="name">document.page.category.form</field>
|
||||
<field name="model">document.page</field>
|
||||
<field name="inherit_id"
|
||||
ref="document_page.view_category_form" />
|
||||
<field name="inherit_id" ref="document_page.view_category_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="parent_id" position="after">
|
||||
<field name="approval_required"/>
|
||||
@ -74,18 +126,63 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- History Tree view -->
|
||||
<record id="view_wiki_history_tree_inherit" model="ir.ui.view">
|
||||
<field name="name">document.page.history.tree</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="inherit_id"
|
||||
ref="document_page.view_wiki_history_tree" />
|
||||
<field name="inherit_id" ref="document_page.view_wiki_history_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="page_id" position="after">
|
||||
<field name="state"
|
||||
attrs="{'invisible':[('is_parent_approval_required','=',False)]}"/>
|
||||
<field name="is_parent_approval_required"
|
||||
invisible="1" />
|
||||
</field>
|
||||
<tree position="attributes">
|
||||
<attribute name="decoration-info">state=='draft'</attribute>
|
||||
<attribute name="decoration-primary">state=='to approve'</attribute>
|
||||
<attribute name="decoration-muted">state=='cancelled'</attribute>
|
||||
</tree>
|
||||
<tree position="inside">
|
||||
<field name="state"/>
|
||||
<field name="approved_uid"/>
|
||||
<field name="approved_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- History Search view -->
|
||||
<record id="view_wiki_history_filter" model="ir.ui.view">
|
||||
<field name="name">document.page.history.search</field>
|
||||
<field name="model">document.page.history</field>
|
||||
<field name="inherit_id" ref="document_page.view_wiki_history_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="page_id" position="before">
|
||||
<field name="state"/>
|
||||
</field>
|
||||
<field name="create_uid" position="after">
|
||||
<filter name="draft" string="Draft" domain="[('state','=','draft')]"/>
|
||||
<filter name="pending" string="Pending Approval" domain="[('state','=','to approve')]"/>
|
||||
<filter name="approved" string="Approved" domain="[('state','=','approved')]"/>
|
||||
<filter name="cancelled" string="Cancelled" domain="[('state','=','cancelled')]"/>
|
||||
</field>
|
||||
<filter name="group_author" position="before">
|
||||
<filter name="group_state" string="State" context="{'group_by':'state'}" />
|
||||
</filter>
|
||||
<filter name="group_author" position="after">
|
||||
<filter name="group_approver" string="Approver" context="{'group_by':'approved_uid'}" />
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Change Requests Action -->
|
||||
<record model="ir.actions.act_window" id="action_change_requests">
|
||||
<field name="name">Change Requests</field>
|
||||
<field name="res_model">document.page.history</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_state':'to approve'}</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_page_change_requests"
|
||||
name="Change Requests"
|
||||
parent="document_page.menu_wiki"
|
||||
action="action_change_requests"
|
||||
sequence="25"
|
||||
groups="document_page.group_document_editor" />
|
||||
</odoo>
|
||||
|
@ -6,33 +6,98 @@
|
||||
<field name="on_create">True</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.activity" id="act_approved">
|
||||
<field name="wkf_id"
|
||||
ref="wkf_document_page_history_aproval" />
|
||||
<field name="name">approved</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">page_approval_approved()</field>
|
||||
<field name="flow_stop">True</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.activity" id="act_draft">
|
||||
<field name="wkf_id"
|
||||
ref="wkf_document_page_history_aproval" />
|
||||
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
|
||||
<field name="flow_start">True</field>
|
||||
<field name="name">draft</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">page_approval_draft()</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.activity" id="act_to_approve">
|
||||
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
|
||||
<field name="name">to approve</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">page_approval_to_approve()</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.activity" id="act_approved">
|
||||
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
|
||||
<field name="name">approved</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">page_approval_approved()</field>
|
||||
<field name="flow_stop">True</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.activity" id="act_cancelled">
|
||||
<field name="wkf_id" ref="wkf_document_page_history_aproval" />
|
||||
<field name="name">cancelled</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">page_approval_cancelled()</field>
|
||||
</record>
|
||||
|
||||
<!-- Transitions -->
|
||||
<record model="workflow.transition" id="tdr">
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_to_approve"/>
|
||||
<field name="condition">am_i_owner</field>
|
||||
<field name="signal">page_approval_to_approve</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.transition" id="tda">
|
||||
<field name="act_from" ref="act_draft" />
|
||||
<field name="act_to" ref="act_approved" />
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_approved"/>
|
||||
<field name="condition">am_i_approver</field>
|
||||
<field name="signal">page_approval_approve</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.transition" id="tra">
|
||||
<field name="act_from" ref="act_to_approve"/>
|
||||
<field name="act_to" ref="act_approved"/>
|
||||
<field name="condition">am_i_approver</field>
|
||||
<field name="signal">page_approval_approve</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.transition" id="tad">
|
||||
<field name="act_from" ref="act_approved" />
|
||||
<field name="act_to" ref="act_draft" />
|
||||
<field name="act_from" ref="act_approved"/>
|
||||
<field name="act_to" ref="act_draft"/>
|
||||
<field name="condition">am_i_approver</field>
|
||||
<field name="signal">edit</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.transition" id="tdc">
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_cancelled"/>
|
||||
<field name="condition">am_i_owner</field>
|
||||
<field name="signal">page_approval_cancel</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.transition" id="trc">
|
||||
<field name="act_from" ref="act_to_approve"/>
|
||||
<field name="act_to" ref="act_cancelled"/>
|
||||
<field name="condition">am_i_owner or am_i_approver</field>
|
||||
<field name="signal">page_approval_cancel</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.transition" id="tcd">
|
||||
<field name="act_from" ref="act_cancelled"/>
|
||||
<field name="act_to" ref="act_draft"/>
|
||||
<field name="condition">am_i_owner or am_i_approver</field>
|
||||
<field name="signal">page_approval_reopen</field>
|
||||
</record>
|
||||
|
||||
<!-- Automatic Transitions for change requests created directly from documents -->
|
||||
<record model="workflow.transition" id="tda_auto">
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_approved"/>
|
||||
<field name="condition">not is_approval_required</field>
|
||||
<field name="signal">document_page_auto_confirm</field>
|
||||
</record>
|
||||
|
||||
<record model="workflow.transition" id="tdr_auto">
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_to_approve"/>
|
||||
<field name="condition">is_approval_required</field>
|
||||
<field name="signal">document_page_auto_confirm</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
Loading…
Reference in New Issue
Block a user