Browse Source

[ADD] base_kanban_stage: Stage model and abstract logic

* Add Kanban-compatible stage model base.kanban.stage
* Add views, menu items, actions, and access controls needed to manage base.kanban.stage records
* Add abstract model base.kanban.abstract that other models can inherit from to gain Kanban stage functionality
* Add base Kanban view base_kanban_abstract_view_kanban, which can be customized as needed for use with models that inherit from base.kanban.abstract
* Add model base.kanban.abstract.tester, which is needed for base.kanban.abstract unit tests
12.0
Ted Salmon 7 years ago
committed by ahenriquez
parent
commit
deb8969486
  1. 108
      base_kanban_stage/README.rst
  2. 5
      base_kanban_stage/__init__.py
  3. 23
      base_kanban_stage/__openerp__.py
  4. 6
      base_kanban_stage/models/__init__.py
  5. 112
      base_kanban_stage/models/base_kanban_abstract.py
  6. 84
      base_kanban_stage/models/base_kanban_stage.py
  7. 3
      base_kanban_stage/security/ir.model.access.csv
  8. BIN
      base_kanban_stage/static/description/icon.png
  9. 6
      base_kanban_stage/tests/__init__.py
  10. 73
      base_kanban_stage/tests/test_base_kanban_abstract.py
  11. 50
      base_kanban_stage/tests/test_base_kanban_stage.py
  12. 64
      base_kanban_stage/views/base_kanban_abstract.xml
  13. 72
      base_kanban_stage/views/base_kanban_stage.xml

108
base_kanban_stage/README.rst

@ -0,0 +1,108 @@
.. image:: https://img.shields.io/badge/licence-lgpl--3-blue.svg
:target: http://www.gnu.org/licenses/LGPL-3.0-standalone.html
:alt: License: LGPL-3
======================
Kanban - Stage Support
======================
This module provides a stage model compatible with Kanban views and the
standard views needed to manage these stages. It also provides the
``base.kanban.abstract`` model, which can be inherited to add support for
Kanban views with stages to any other model. Lastly, it includes a base Kanban
view that can be extended as needed.
Installation
============
To install this module, simply follow the standard install process.
Configuration
=============
No configuration is needed or possible.
Usage
=====
* Inherit from ``base.kanban.abstract`` to add Kanban stage functionality to
the child model:
.. code-block:: python
class MyModel(models.Model):
_name = 'my.model'
_inherit = 'base.kanban.abstract'
* Extend the provided base Kanban view (``base_kanban_abstract_view_kanban``)
as needed by the child model. The base view has four ``name`` attributes
intended to provide convenient XPath access to different parts of the Kanban
card. They are ``card_dropdown_menu``, ``card_header``, ``card_body``, and
``card_footer``:
.. code-block:: xml
<record id="my_model_view_kanban" model="ir.ui.view">
<field name="name">My Model - Kanban View</field>
<field name="model">my.model</field>
<field name="inherit_id" ref="base_kanban_stage.base_kanban_abstract_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='card_header']">
<!-- Add header content here -->
</xpath>
<xpath expr="//div[@name='card_body']">
<!-- Add body content here -->
</xpath>
</field>
</record>
* To manage stages, go to Settings > Technical > Kanban > Stages.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/162/9.0
Known Issues / Roadmap
======================
* The grouping logic used by ``base.kanban.abstract`` currently does not
support additional domains and alternate sort orders
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smash it by providing detailed and welcomed
feedback.
Credits
=======
Images
------
* Odoo Community Association:
`Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Dave Lasley <dave@laslabs.com>
* Oleg Bulkin <obulkin@laslabs.com>
* Daniel Reis
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit http://odoo-community.org.

5
base_kanban_stage/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import models

23
base_kanban_stage/__openerp__.py

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
'name': 'Kanban - Stage Support',
'summary': 'Provides stage model and abstract logic for inheritance',
'version': '9.0.1.0.0',
'author': "LasLabs, Odoo Community Association (OCA)",
'category': 'base',
'depends': [
'web_kanban',
],
'website': 'https://laslabs.com',
'license': 'LGPL-3',
'data': [
'security/ir.model.access.csv',
'views/base_kanban_abstract.xml',
'views/base_kanban_stage.xml',
],
'installable': True,
'application': False,
}

6
base_kanban_stage/models/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import base_kanban_abstract
from . import base_kanban_stage

112
base_kanban_stage/models/base_kanban_abstract.py

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp import api, fields, models
class BaseKanbanAbstract(models.AbstractModel):
'''Inherit from this class to add support for Kanban stages to your model.
All public properties are preceded with kanban_ in order to isolate from
child models, with the exception of stage_id, which is a required field in
the Kanban widget and must be defined as such.'''
_name = 'base.kanban.abstract'
_order = 'kanban_priority desc, kanban_sequence'
_group_by_full = {
'stage_id': lambda s, *a, **k: s._read_group_stage_ids(*a, **k),
}
kanban_sequence = fields.Integer(
default=10,
index=True,
help='Order of record in relation to other records in the same Kanban'
' stage and with the same priority',
)
kanban_priority = fields.Selection(
selection=[('0', 'Normal'), ('5', 'Medium'), ('10', 'High')],
index=True,
default='0',
help='The priority of the record (shown as stars in Kanban views)',
)
stage_id = fields.Many2one(
string='Kanban Stage',
comodel_name='base.kanban.stage',
track_visibility='onchange',
index=True,
copy=False,
help='The Kanban stage that this record is currently in',
default=lambda s: s._default_stage_id(),
domain=lambda s: [('res_model.model', '=', s._name)],
)
kanban_user_id = fields.Many2one(
string='Assigned To',
comodel_name='res.users',
index=True,
track_visibility='onchange',
help='User that the record is currently assigned to',
)
kanban_color = fields.Integer(
string='Color Index',
help='Color index to be used for the record\'s Kanban card',
)
kanban_legend_priority = fields.Text(
string='Priority Explanation',
related='stage_id.legend_priority',
help='Explanation text to help users understand how the priority/star'
' mechanism applies to this record (depends on current stage)',
)
kanban_legend_blocked = fields.Text(
string='Special Handling Explanation',
related='stage_id.legend_blocked',
help='Explanation text to help users understand how the special'
' handling status applies to this record (depends on current'
' stage)',
)
kanban_legend_done = fields.Text(
string='Ready Explanation',
related='stage_id.legend_done',
help='Explanation text to help users understand how the ready'
' status applies to this record (depends on current stage)',
)
kanban_legend_normal = fields.Text(
string='Normal Handling Explanation',
related='stage_id.legend_normal',
help='Explanation text to help users understand how the normal'
' handling status applies to this record (depends on current'
' stage)',
)
kanban_status = fields.Selection(
selection=[
('normal', 'Normal Handling'),
('done', 'Ready'),
('blocked', 'Special Handling'),
],
string='Kanban Status',
default='normal',
track_visibility='onchange',
required=True,
copy=False,
help='A record can have one of several Kanban statuses, which are used'
' to indicate whether there are any special situations affecting'
' it. The exact meaning of each status is allowed to vary based'
' on the stage the record is in but they are roughly as follow:\n'
'* Normal Handling: Default status, no special situations\n'
'* Ready: Ready to transition to the next stage\n'
'* Special Handling: Blocked in some way (e.g. must be handled by'
' a specific user)\n'
)
@api.model
def _default_stage_id(self):
return self.env['base.kanban.stage']
@api.multi
def _read_group_stage_ids(
self, domain=None, read_group_order=None, access_rights_uid=None
):
stage_model = self.env['base.kanban.stage'].sudo(access_rights_uid)
stages = stage_model.search([('res_model.model', '=', self._name)])
names = [(r.id, r.display_name) for r in stages]
fold = {r.id: r.fold for r in stages}
return names, fold

84
base_kanban_stage/models/base_kanban_stage.py

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp import api, fields, models
class BaseKanbanStage(models.Model):
_name = 'base.kanban.stage'
_description = 'Kanban Stage'
_order = 'res_model, sequence'
name = fields.Char(
string='Stage Name',
translate=True,
required=True,
help='Displayed as the header for this stage in Kanban views',
)
description = fields.Text(
translate=True,
help='Short description of the stage\'s meaning/purpose',
)
sequence = fields.Integer(
default=1,
required=True,
index=True,
help='Order of stage in relation to other stages available for the'
' same model',
)
legend_priority = fields.Text(
string='Priority Explanation',
translate=True,
default='Mark a card as medium or high priority (one or two stars) to'
' indicate that it should be escalated ahead of others with'
' lower priority/star counts.',
help='Explanation text to help users understand how the priority/star'
' mechanism applies to this stage',
)
legend_blocked = fields.Text(
string='Special Handling Explanation',
translate=True,
default='Give a card the special handling status to indicate that it'
' requires handling by a special user or subset of users.',
help='Explanation text to help users understand how the special'
' handling status applies to this stage',
)
legend_done = fields.Text(
string='Ready Explanation',
translate=True,
default='Mark a card as ready when it has been fully processed.',
help='Explanation text to help users understand how the ready status'
' applies to this stage',
)
legend_normal = fields.Text(
string='Normal Handling Explanation',
translate=True,
default='This is the default status and indicates that a card can be'
' processed by any user working this queue.',
help='Explanation text to help users understand how the normal'
' handling status applies to this stage',
)
fold = fields.Boolean(
string='Collapse?',
help='Determines whether this stage will be collapsed down in Kanban'
' views',
)
res_model = fields.Many2one(
string='Associated Model',
comodel_name='ir.model',
required=True,
index=True,
help='The model that this Kanban stage will be used for',
domain=[('transient', '=', False)],
default=lambda s: s._default_res_model(),
)
@api.model
def _default_res_model(self):
'''Useful when creating stages from a Kanban view for another model'''
action_id = self.env.context.get('params', {}).get('action')
action = self.env['ir.actions.act_window'].browse(action_id)
default_model = action.res_model
if default_model != self._name:
return self.env['ir.model'].search([('model', '=', default_model)])

3
base_kanban_stage/security/ir.model.access.csv

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_user,Kanban Stage - User Access,model_base_kanban_stage,base.group_user,1,0,0,0
access_manager,Kanban Stage - Manager Access,model_base_kanban_stage,base.group_erp_manager,1,1,1,1

BIN
base_kanban_stage/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

6
base_kanban_stage/tests/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_base_kanban_abstract
from . import test_base_kanban_stage

73
base_kanban_stage/tests/test_base_kanban_abstract.py

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp import models
from openerp.tests.common import TransactionCase
class BaseKanbanAbstractTester(models.Model):
_name = 'base.kanban.abstract.tester'
_inherit = 'base.kanban.abstract'
class TestBaseKanbanAbstract(TransactionCase):
def setUp(self):
super(TestBaseKanbanAbstract, self).setUp()
BaseKanbanAbstractTester._build_model(self.registry, self.cr)
self.test_model = self.env[BaseKanbanAbstractTester._name]
test_model_type = self.env['ir.model'].create({
'model': BaseKanbanAbstractTester._name,
'name': 'Kanban Abstract - Test Model',
'state': 'base',
})
test_stage_1 = self.env['base.kanban.stage'].create({
'name': 'Test Stage 1',
'res_model': test_model_type.id,
})
test_stage_2 = self.env['base.kanban.stage'].create({
'name': 'Test Stage 2',
'res_model': test_model_type.id,
'fold': True,
})
self.id_1 = test_stage_1.id
self.id_2 = test_stage_2.id
def test_read_group_stage_ids_base_case(self):
'''It should return a structure with the proper content'''
self.assertEqual(
self.test_model._read_group_stage_ids(),
(
[(self.id_1, 'Test Stage 1'), (self.id_2, 'Test Stage 2')],
{self.id_1: False, self.id_2: True},
)
)
def test_read_group_stage_ids_correct_associated_model(self):
'''It should only return info for stages with right associated model'''
stage_model = self.env['ir.model'].search([
('model', '=', 'base.kanban.stage'),
])
self.env['base.kanban.stage'].create({
'name': 'Test Stage 3',
'res_model': stage_model.id,
})
self.assertEqual(
self.test_model._read_group_stage_ids(),
(
[(self.id_1, 'Test Stage 1'), (self.id_2, 'Test Stage 2')],
{self.id_1: False, self.id_2: True},
)
)
def test_default_stage_id(self):
''' It should return an empty RecordSet '''
self.assertEqual(
self.env['base.kanban.abstract']._default_stage_id(),
self.env['base.kanban.stage']
)

50
base_kanban_stage/tests/test_base_kanban_stage.py

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp.tests.common import TransactionCase
class TestBaseKanbanStage(TransactionCase):
def test_default_res_model_no_params(self):
'''It should return empty ir.model Recordset if no params in context'''
test_stage = self.env['base.kanban.stage'].with_context({})
res_model = test_stage._default_res_model()
self.assertFalse(res_model)
self.assertEqual(res_model._name, 'ir.model')
def test_default_res_model_no_action(self):
'''It should return empty ir.model Recordset if no action in params'''
test_stage = self.env['base.kanban.stage'].with_context(params={})
res_model = test_stage._default_res_model()
self.assertFalse(res_model)
self.assertEqual(res_model._name, 'ir.model')
def test_default_res_model_info_in_context(self):
'''It should return correct ir.model record if info in context'''
test_action = self.env['ir.actions.act_window'].create({
'name': 'Test Action',
'res_model': 'res.users',
})
test_stage = self.env['base.kanban.stage'].with_context(
params={'action': test_action.id},
)
self.assertEqual(
test_stage._default_res_model(),
self.env['ir.model'].search([('model', '=', 'res.users')])
)
def test_default_res_model_ignore_self(self):
'''It should not return ir.model record corresponding to stage model'''
test_action = self.env['ir.actions.act_window'].create({
'name': 'Test Action',
'res_model': 'base.kanban.stage',
})
test_stage = self.env['base.kanban.stage'].with_context(
params={'action': test_action.id},
)
self.assertFalse(test_stage._default_res_model())

64
base_kanban_stage/views/base_kanban_abstract.xml

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<record id="base_kanban_abstract_view_kanban" model="ir.ui.view">
<field name="name">Kanban Abstract - Base Kanban View</field>
<field name="model">base.kanban.abstract</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" class="o_kanban_small_column">
<field name="display_name"/>
<field name="kanban_color"/>
<field name="kanban_priority"/>
<field name="stage_id" options="{'group_by_tooltip': {'description': 'Stage Description:', 'legend_priority': 'Use of Stars:'}}"/>
<field name="kanban_sequence"/>
<field name="kanban_legend_blocked"/>
<field name="kanban_legend_normal"/>
<field name="kanban_legend_done"/>
<field name="kanban_user_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.kanban_color.raw_value)} o_kanban_record oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown" name="card_dropdown_menu">
<a class="dropdown-toggle btn" data-toggle="dropdown" href="#">
<span class="fa fa-bars fa-lg"/>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
<li t-if="widget.editable">
<a type="edit">Edit</a>
</li>
<li t-if="widget.deletable">
<a type="delete">Delete</a>
</li>
<li>
<ul class="oe_kanban_colorpicker" data-field="kanban_color"/>
</li>
</ul>
</div>
<div class="o_kanban_title" name="card_header">
<field name="kanban_status" widget="kanban_state_selection"/>
<field name="display_name"/>
</div>
<div class="oe_kanban_details" name="card_body">
<!-- Child modules should add model-specific data here -->
</div>
<div class="o_kanban_footer" name="card_footer">
<div class="oe_kanban_bottom_left">
<field name="kanban_priority" widget="priority"/>
</div>
<div class="oe_kanban_bottom_right" t-if="record.kanban_user_id">
<img t-att-src="kanban_image('res.users', 'image_small', record.kanban_user_id.raw_value)"
class="oe_kanban_avatar_smallbox pull-right"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>

72
base_kanban_stage/views/base_kanban_stage.xml

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<record id="base_kanban_stage_view_form" model="ir.ui.view">
<field name="name">Kanban Stage - Form View</field>
<field name="model">base.kanban.stage</field>
<field name="arch" type="xml">
<form string="Kanban Stage">
<header/>
<sheet>
<group string="Core Info" name="core_info">
<field name="name"/>
<field name="res_model"/>
<field name="sequence"/>
<field name="fold"/>
<field name="description"/>
</group>
<group string="Priority and Statuses" name="explanations">
<field name="legend_priority"/>
<field name="legend_blocked"/>
<field name="legend_done"/>
<field name="legend_normal"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="base_kanban_stage_view_tree" model="ir.ui.view">
<field name="name">Kanban Stages - Tree View</field>
<field name="model">base.kanban.stage</field>
<field name="arch" type="xml">
<tree string="Kanban Stages">
<field name="name"/>
<field name="res_model"/>
<field name="sequence"/>
</tree>
</field>
</record>
<record id="base_kanban_stage_view_search" model="ir.ui.view">
<field name="name">Kanban Stages - Search View</field>
<field name="model">base.kanban.stage</field>
<field name="arch" type="xml">
<search string="Kanban Stages">
<field name="name"/>
<field name="res_model"/>
<field name="sequence"/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="base_kanban_stage_action">
<field name="name">Kanban Stages</field>
<field name="res_model">base.kanban.stage</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="base_kanban_stage_root_menu" name="Kanban"
parent="base.menu_custom" sequence="50"/>
<menuitem id="base_kanban_stage_menu" name="Stages"
parent="base_kanban_stage_root_menu"
action="base_kanban_stage_action" />
</odoo>
Loading…
Cancel
Save