Browse Source

New project_sla module

pull/2/head
Daniel Reis 11 years ago
parent
commit
f52e44fdb4
  1. 5
      project_sla/__init__.py
  2. 132
      project_sla/__openerp__.py
  3. 71
      project_sla/analytic_account.py
  4. 24
      project_sla/analytic_account_view.xml
  5. 296
      project_sla/i18n/project_sla.pot
  6. 75
      project_sla/m2m.py
  7. 29
      project_sla/project_issue.py
  8. 85
      project_sla/project_issue_view.xml
  9. 84
      project_sla/project_sla.py
  10. 322
      project_sla/project_sla_control.py
  11. 18
      project_sla/project_sla_control_data.xml
  12. 25
      project_sla/project_sla_control_view.xml
  13. 138
      project_sla/project_sla_demo.xml
  14. 48
      project_sla/project_sla_view.xml
  15. 20
      project_sla/project_view.xml
  16. 8
      project_sla/security/ir.model.access.csv
  17. BIN
      project_sla/static/src/img/icon.png
  18. 66
      project_sla/test/project_sla.yml

5
project_sla/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
import project_sla
import analytic_account
import project_sla_control
import project_issue

132
project_sla/__openerp__.py

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013 Daniel Reis
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Service Level Agreements',
'summary': 'Define SLAs for your Contracts',
'version': '1.0',
"category": "Project Management",
'description': """\
Contract SLAs
===============
SLAs are assigned to Contracts, on the Analytic Account form, SLA Definition
separator. This is also where new SLA Definitions are created.
One Contract can have several SLA Definitions attached, allowing for
"composite SLAs". For example, a contract could have a Response Time SLA (time
to start resolution) and a Resolution Time SLA (time to close request).
SLA Controlled Documents
========================
Only Project Issue documents are made SLA controllable.
However, a framework is made available to easilly build extensions to make
other documents models SLA controlled.
SLA controlled documents have attached information on the list of SLA rules
they should meet (more than one in the case for composite SLAs) and a summary
SLA status:
* "watching" the service level (it has SLA requirements to meet)
* under "warning" (limit dates are close, special attention is needed)
* "failed" (one on the SLA limits has not been met)
* "achieved" (all SLA limits have been met)
Transient states, such as "watching" and "warning", are regularly updated by
a hourly scheduled job, that reevaluates the warning and limit dates against
the current time and changes the state when find dates that have been exceeded.
To decide what SLA Definitions apply for a specific document, first a lookup
is made for a ``analytic_account_id`` field. If not found, then it will
look up for the ``project_id`` and it's corresponding ``analytic_account_id``.
Specifically, the Service Desk module introduces a Analytic Account field for
Project Issues. This makes it possible for a Service Team (a "Project") to
have a generic SLA, but at the same time allow for some Contracts to have
specific SLAs (such as the case for "premium" service conditions).
SLA Definitions and Rules
=========================
New SLA Definitions are created from the Analytic Account form, SLA Definition
field.
Each definition can have one or more Rules.
The particular rule to use is decided by conditions, so that you can set
different service levels based on request attributes, such as Priority or
Category.
Each rule condition is evaluated in "sequence" order, and the first onea to met
is the one to be used.
In the simplest case, a single rule with no condition is just what is needed.
Each rule sets a number of hours until the "limit date", and the number of
hours until a "warning date". The former will be used to decide if the SLA
was achieved, and the later can be used for automatic alarms or escalation
procedures.
Time will be counted from creation date, until the "Control Date" specified for
the SLA Definition. That would usually be the "Close" (time until resolution)
or the "Open" (time until response) dates.
The working calendar set in the related Project definitions will be used (see
the "Other Info" tab). If none is defined, a builtin "all days, 8-12 13-17"
default calendar is used.
A timezone and leave calendars will also used, based on either the assigned
user (document's `user_id`) or on the current user.
Setup checklist
===============
The basic steps to configure SLAs for a Project are:
* Set Project's Working Calendar, at Project definitions, "Other Info" tab
* Go to the Project's Analytic Account form; create and set SLA Definitions
* Use the "Reapply SLAs" button on the Analytic Account form
* See Project Issue's calculated SLAs in the new "Service Levels" tab
Credits and Contributors
========================
* Daniel Reis (https://launchpad.net/~dreis-pt)
* David Vignoni, author of the icon from the KDE 3.x Nuvola icon theme
""",
'author': 'Daniel Reis',
'website': '',
'depends': [
'project_issue',
],
'data': [
'project_sla_view.xml',
'project_sla_control_view.xml',
'project_sla_control_data.xml',
'analytic_account_view.xml',
'project_view.xml',
'project_issue_view.xml',
'security/ir.model.access.csv',
],
'demo': ['project_sla_demo.xml'],
'test': ['test/project_sla.yml'],
'installable': True,
}

71
project_sla/analytic_account.py

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013 Daniel Reis
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields, orm
import logging
_logger = logging.getLogger(__name__)
class AnalyticAccount(orm.Model):
""" Add SLA to Analytic Accounts """
_inherit = 'account.analytic.account'
_columns = {
'sla_ids': fields.many2many(
'project.sla', string='Service Level Agreement'),
}
def _reapply_sla(self, cr, uid, ids, recalc_closed=False, context=None):
"""
Force SLA recalculation on open documents that already are subject to
this SLA Definition.
To use after changing a Contract SLA or it's Definitions.
The ``recalc_closed`` flag allows to also recompute closed documents.
"""
ctrl_obj = self.pool.get('project.sla.control')
proj_obj = self.pool.get('project.project')
exclude_states = ['cancelled'] + (not recalc_closed and ['done'] or [])
for contract in self.browse(cr, uid, ids, context=context):
# for each contract, and for each model under SLA control ...
for m_name in set([sla.control_model for sla in contract.sla_ids]):
model = self.pool.get(m_name)
doc_ids = []
if 'analytic_account_id' in model._columns:
doc_ids += model.search(
cr, uid,
[('analytic_account_id', '=', contract.id),
('state', 'not in', exclude_states)],
context=context)
if 'project_id' in model._columns:
proj_ids = proj_obj.search(
cr, uid, [('analytic_account_id', '=', contract.id)],
context=context)
doc_ids += model.search(
cr, uid,
[('project_id', 'in', proj_ids),
('state', 'not in', exclude_states)],
context=context)
if doc_ids:
docs = model.browse(cr, uid, doc_ids, context=context)
ctrl_obj.store_sla_control(cr, uid, docs, context=context)
return True
def reapply_sla(self, cr, uid, ids, context=None):
""" Reapply SLAs button action """
return self._reapply_sla(cr, uid, ids, context=context)

24
project_sla/analytic_account_view.xml

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_account_analytic_account_form_sla" model="ir.ui.view">
<field name="name">view_account_analytic_account_form_sla</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field name="arch" type="xml">
<page name="contract_page" position="after">
<page name="sla_page" string="Service Level Agreement">
<field name="sla_ids" nolabel="1"/>
<button name="reapply_sla" string="Reapply" type="object"
help="Reapply the SLAs to all Contract's documents."
groups="project.group_project_manager" />
</page>
</page>
</field>
</record>
</data>
</openerp>

296
project_sla/i18n/project_sla.pot

@ -0,0 +1,296 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * project_sla
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-12-19 10:28+0000\n"
"PO-Revision-Date: 2013-12-19 10:28+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: project_sla
#: model:project.sla,name:project_sla.sla_response
msgid "Standard Response Time"
msgstr ""
#. module: project_sla
#: help:project.sla,control_field_id:0
msgid "Date field used to check if the SLA was achieved."
msgstr ""
#. module: project_sla
#: model:ir.model,name:project_sla.model_project_issue
msgid "Project Issue"
msgstr ""
#. module: project_sla
#: model:ir.model,name:project_sla.model_project_sla_control
msgid "SLA Control Registry"
msgstr ""
#. module: project_sla
#: view:project.sla:0
msgid "Reapply SLA on Contracts"
msgstr ""
#. module: project_sla
#: view:project.project:0
msgid "Administration"
msgstr ""
#. module: project_sla
#: view:project.issue:0
msgid "Priority"
msgstr ""
#. module: project_sla
#: selection:project.issue,sla_state:0
#: selection:project.sla.control,sla_state:0
#: selection:project.sla.controlled,sla_state:0
msgid "Failed"
msgstr ""
#. module: project_sla
#: field:project.sla.control,sla_warn_date:0
msgid "Warning Date"
msgstr ""
#. module: project_sla
#: field:project.sla.line,warn_qty:0
msgid "Hours to Warn"
msgstr ""
#. module: project_sla
#: view:project.sla.control:0
msgid "Service Level"
msgstr ""
#. module: project_sla
#: selection:project.issue,sla_state:0
#: selection:project.sla.control,sla_state:0
#: selection:project.sla.controlled,sla_state:0
msgid "Watching"
msgstr ""
#. module: project_sla
#: view:project.sla:0
#: field:project.sla,analytic_ids:0
msgid "Contracts"
msgstr ""
#. module: project_sla
#: field:project.sla,name:0
#: field:project.sla.line,name:0
msgid "Title"
msgstr ""
#. module: project_sla
#: field:project.sla,active:0
msgid "Active"
msgstr ""
#. module: project_sla
#: field:project.issue,sla_control_ids:0
#: field:project.sla.controlled,sla_control_ids:0
msgid "SLA Control"
msgstr ""
#. module: project_sla
#: field:project.sla.control,sla_achieved:0
msgid "Achieved?"
msgstr ""
#. module: project_sla
#: view:project.issue:0
#: field:project.issue,sla_state:0
#: field:project.sla.control,sla_state:0
#: field:project.sla.controlled,sla_state:0
msgid "SLA Status"
msgstr ""
#. module: project_sla
#: field:project.sla.line,condition:0
msgid "Condition"
msgstr ""
#. module: project_sla
#: field:project.sla,control_model:0
msgid "For documents"
msgstr ""
#. module: project_sla
#: view:project.sla:0
#: field:project.sla.line,sla_id:0
msgid "SLA Definition"
msgstr ""
#. module: project_sla
#: model:project.sla.line,name:project_sla.sla_response_rule2
msgid "Response in two business days"
msgstr ""
#. module: project_sla
#: selection:project.issue,sla_state:0
#: selection:project.sla.control,sla_state:0
#: selection:project.sla.controlled,sla_state:0
msgid "Will Fail"
msgstr ""
#. module: project_sla
#: field:project.sla.control,sla_line_id:0
msgid "Service Agreement"
msgstr ""
#. module: project_sla
#: field:project.sla.control,doc_id:0
msgid "Document ID"
msgstr ""
#. module: project_sla
#: field:project.sla.control,locked:0
msgid "Recalculation disabled"
msgstr ""
#. module: project_sla
#: field:project.sla.control,sla_limit_date:0
msgid "Limit Date"
msgstr ""
#. module: project_sla
#: help:project.sla.line,condition:0
msgid "Apply only if this expression is evaluated to True. The document fields can be accessed using either o, obj or object. Example: obj.priority <= '2'"
msgstr ""
#. module: project_sla
#: model:project.sla.line,name:project_sla.sla_resolution_rule1
msgid "Resolution in two business days"
msgstr ""
#. module: project_sla
#: view:account.analytic.account:0
msgid "Reapply"
msgstr ""
#. module: project_sla
#: field:project.sla.line,limit_qty:0
msgid "Hours to Limit"
msgstr ""
#. module: project_sla
#: field:project.sla,sla_line_ids:0
#: view:project.sla.line:0
msgid "Definitions"
msgstr ""
#. module: project_sla
#: model:project.sla.line,name:project_sla.sla_resolution_rule2
msgid "Resolution in three business days"
msgstr ""
#. module: project_sla
#: field:project.sla.control,sla_close_date:0
msgid "Close Date"
msgstr ""
#. module: project_sla
#: model:ir.model,name:project_sla.model_project_sla
msgid "project.sla"
msgstr ""
#. module: project_sla
#: field:project.sla,control_field_id:0
msgid "Control Date"
msgstr ""
#. module: project_sla
#: model:project.sla.line,name:project_sla.sla_response_rule1
msgid "Response in one business day"
msgstr ""
#. module: project_sla
#: selection:project.issue,sla_state:0
#: selection:project.sla.control,sla_state:0
#: selection:project.sla.controlled,sla_state:0
msgid "Achieved"
msgstr ""
#. module: project_sla
#: view:account.analytic.account:0
#: field:account.analytic.account,sla_ids:0
msgid "Service Level Agreement"
msgstr ""
#. module: project_sla
#: model:project.sla,name:project_sla.sla_resolution
msgid "Standard Resolution Time"
msgstr ""
#. module: project_sla
#: field:project.sla.line,sequence:0
msgid "Sequence"
msgstr ""
#. module: project_sla
#: view:project.issue:0
msgid "Service Levels"
msgstr ""
#. module: project_sla
#: model:ir.model,name:project_sla.model_account_analytic_account
msgid "Analytic Account"
msgstr ""
#. module: project_sla
#: view:project.sla:0
msgid "Rules"
msgstr ""
#. module: project_sla
#: help:project.sla.control,locked:0
msgid "Safeguard manual changes from future automatic recomputations."
msgstr ""
#. module: project_sla
#: selection:project.issue,sla_state:0
#: selection:project.sla.control,sla_state:0
#: selection:project.sla.controlled,sla_state:0
msgid "Warning"
msgstr ""
#. module: project_sla
#: view:project.issue:0
msgid "Extra Info"
msgstr ""
#. module: project_sla
#: field:project.sla.control,doc_model:0
msgid "Document Model"
msgstr ""
#. module: project_sla
#: model:ir.model,name:project_sla.model_project_sla_line
msgid "project.sla.line"
msgstr ""
#. module: project_sla
#: view:account.analytic.account:0
msgid "Reapply the SLAs to all Contract's documents."
msgstr ""
#. module: project_sla
#: field:project.sla.control,sla_start_date:0
msgid "Start Date"
msgstr ""
#. module: project_sla
#: model:ir.model,name:project_sla.model_project_sla_controlled
msgid "SLA Controlled Document"
msgstr ""

75
project_sla/m2m.py

@ -0,0 +1,75 @@
"""
Wrapper for OpenERP's cryptic write conventions for x2many fields.
Example usage:
import m2m
browse_rec.write({'many_ids: m2m.clear())
browse_rec.write({'many_ids: m2m.link(99))
browse_rec.write({'many_ids: m2m.add({'name': 'Monty'}))
browse_rec.write({'many_ids: m2m.replace([98, 99]))
Since returned values are lists, the can be joined using the plus operator:
browse_rec.write({'many_ids: m2m.clear() + m2m.link(99))
(Source: https://github.com/dreispt/openerp-write2many)
"""
def create(values):
""" Create a referenced record """
assert isinstance(values, dict)
return [(0, 0, values)]
def add(values):
""" Intuitive alias for create() """
return create(values)
def write(id, values):
""" Write on referenced record """
assert isinstance(id, int)
assert isinstance(values, dict)
return [(1, id, values)]
def remove(id):
""" Unlink and delete referenced record """
assert isinstance(id, int)
return [(2, id)]
def unlink(id):
""" Unlink but do not delete the referenced record """
assert isinstance(id, int)
return [(3, id)]
def link(id):
""" Link but do not delete the referenced record """
assert isinstance(id, int)
return [(4, id)]
def clear():
""" Unlink all referenced records (doesn't delete them) """
return [(5)]
def replace(ids):
""" Unlink all current records and replace them with a new list """
assert isinstance(ids, list)
return [(6, 0, ids)]
if __name__ == "__main__":
# Tests:
assert create({'name': 'Monty'}) == [(0, 0, {'name': 'Monty'})]
assert write(99, {'name': 'Monty'}) == [(1, 99, {'name': 'Monty'})]
assert remove(99) == [(2, 99)]
assert unlink(99) == [(3, 99)]
assert clear() == [(5)]
assert replace([97, 98, 99]) == [(6, 0, [97, 98, 99])]
print("Done!")

29
project_sla/project_issue.py

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013 Daniel Reis
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm
class ProjectIssue(orm.Model):
"""
Extend Project Issues to be SLA Controlled
"""
_name = 'project.issue'
_inherit = ['project.issue', 'project.sla.controlled']

85
project_sla/project_issue_view.xml

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Project Issue Form -->
<record id="project_issue_form_view_sla" model="ir.ui.view">
<field name="name">project_issue_form_view_sla</field>
<field name="model">project.issue</field>
<field name="inherit_id" ref="project_issue.project_issue_form_view"/>
<field name="arch" type="xml">
<page string="Extra Info" position="after">
<page name="sla_page" string="Service Levels"
attrs="{'invisible': [('sla_state', '=', False)]}">
<group>
<group>
<field name="sla_state" />
</group>
<group>
<field name="write_date" />
</group>
</group>
<field name="sla_control_ids"/>
</page>
</page>
</field>
</record>
<!-- Project Issue List -->
<record model="ir.ui.view" id="project_issue_tree_view_sla">
<field name="name">project_issue_tree_view_slak</field>
<field name="model">project.issue</field>
<field name="inherit_id" ref="project_issue.project_issue_tree_view"/>
<field name="arch" type="xml">
<field name="project_id" position="after">
<field name="sla_state"/>
</field>
</field>
</record>
<!-- Project Issue Filter -->
<record id="view_project_issue_filter_sdesk" model="ir.ui.view">
<field name="name">view_project_issue_filter_sdesk</field>
<field name="model">project.issue</field>
<field name="inherit_id" ref="project_issue.view_project_issue_filter"/>
<field name="arch" type="xml">
<filter string="Priority" position="after">
<filter string="SLA Status" context="{'group_by':'sla_state'}" />
</filter>
</field>
</record>
<!--
<record id="project_issue_kanban_view_sla" model="ir.ui.view">
<field name="name">project_issue_kanban_view_sla</field>
<field name="model">project.issue</field>
<field name="inherit_id" ref="project_issue.project_issue_kanban_view" />
<field name="arch" type="xml">
<field name="kanban_state" position="after">
<field name="sla_state" />
</field>
<div class="oe_kanban_footer_left" position="inside">
<div class="oe_kanban_highlight" name="sla_icon_placeholder">
<span t-if="record.sla_state.raw_value === '3'"
class="oe_e oe_kanban_text_red">c</span>
<span t-if="record.sla_state.raw_value === '4'"
class="oe_e oe_kanban_text_red">[</span>
<span t-if="record.sla_state.raw_value === '1'"
class="oe_e oe_kanban_text_green">W</span>
</div>
</div>
</field>
</record>
-->
</data>
</openerp>

84
project_sla/project_sla.py

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013 Daniel Reis
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields, orm
class SLADefinition(orm.Model):
"""
SLA Definition
"""
_name = 'project.sla'
_columns = {
'name': fields.char('Title', size=64, required=True, translate=True),
'active': fields.boolean('Active'),
'control_model': fields.char('For documents', size=128, required=True),
'control_field_id': fields.many2one(
'ir.model.fields', 'Control Date', required=True,
domain="[('model_id.model', '=', control_model),"
" ('ttype', 'in', ['date', 'datetime'])]",
help="Date field used to check if the SLA was achieved."),
'sla_line_ids': fields.one2many(
'project.sla.line', 'sla_id', 'Definitions'),
'analytic_ids': fields.many2many(
'account.analytic.account', string='Contracts'),
}
_defaults = {
'active': True,
}
def _reapply_slas(self, cr, uid, ids, recalc_closed=False, context=None):
"""
Force SLA recalculation on all _open_ Contracts for the selected SLAs.
To use upon SLA Definition modifications.
"""
contract_obj = self.pool.get('account.analytic.account')
for sla in self.browse(cr, uid, ids, context=context):
contr_ids = [x.id for x in sla.analytic_ids if x.state == 'open']
contract_obj._reapply_sla(
cr, uid, contr_ids, recalc_closed=recalc_closed,
context=context)
return True
def reapply_slas(self, cr, uid, ids, context=None):
""" Reapply SLAs button action """
return self._reapply_slas(cr, uid, ids, context=context)
class SLARules(orm.Model):
"""
SLA Definition Rule Lines
"""
_name = 'project.sla.line'
_order = 'sla_id,sequence'
_columns = {
'sla_id': fields.many2one('project.sla', 'SLA Definition'),
'sequence': fields.integer('Sequence'),
'name': fields.char('Title', size=64, required=True, translate=True),
'condition': fields.char(
'Condition', size=256, help="Apply only if this expression is "
"evaluated to True. The document fields can be accessed using "
"either o, obj or object. Example: obj.priority <= '2'"),
'limit_qty': fields.integer('Hours to Limit'),
'warn_qty': fields.integer('Hours to Warn'),
}
_defaults = {
'sequence': 10,
}

322
project_sla/project_sla_control.py

@ -0,0 +1,322 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013 Daniel Reis
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields, orm
from openerp.tools.safe_eval import safe_eval
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DT_FMT
from openerp import SUPERUSER_ID
from datetime import timedelta
from datetime import datetime as dt
import m2m
import logging
_logger = logging.getLogger(__name__)
SLA_STATES = [('5', 'Failed'), ('4', 'Will Fail'), ('3', 'Warning'),
('2', 'Watching'), ('1', 'Achieved')]
def safe_getattr(obj, dotattr, default=False):
"""
Follow an object attribute dot-notation chain to find the leaf value.
If any attribute doesn't exist or has no value, just return False.
Checks hasattr ahead, to avoid ORM Browse log warnings.
"""
attrs = dotattr.split('.')
while attrs:
attr = attrs.pop(0)
if attr in obj._model._columns:
try:
obj = getattr(obj, attr)
except AttributeError:
return default
if not obj:
return default
else:
return default
return obj
class SLAControl(orm.Model):
"""
SLA Control Registry
Each controlled document (Issue, Claim, ...) will have a record here.
This model concentrates all the logic for Service Level calculation.
"""
_name = 'project.sla.control'
_description = 'SLA Control Registry'
_columns = {
'doc_id': fields.integer('Document ID', readonly=True),
'doc_model': fields.char('Document Model', size=128, readonly=True),
'sla_line_id': fields.many2one(
'project.sla.line', 'Service Agreement'),
'sla_warn_date': fields.datetime('Warning Date'),
'sla_limit_date': fields.datetime('Limit Date'),
'sla_start_date': fields.datetime('Start Date'),
'sla_close_date': fields.datetime('Close Date'),
'sla_achieved': fields.integer('Achieved?'),
'sla_state': fields.selection(SLA_STATES, string="SLA Status"),
'locked': fields.boolean(
'Recalculation disabled',
help="Safeguard manual changes from future automatic "
"recomputations."),
# Future: perfect SLA manual handling
}
def write(self, cr, uid, ids, vals, context=None):
"""
Update the related Document's SLA State when any of the SLA Control
lines changes state
"""
res = super(SLAControl, self).write(
cr, uid, ids, vals, context=context)
new_state = vals.get('sla_state')
if new_state:
# just update sla_state without recomputing the whole thing
context = context or {}
context['__sla_stored__'] = 1
for sla in self.browse(cr, uid, ids, context=context):
doc = self.pool.get(sla.doc_model).browse(
cr, uid, sla.doc_id, context=context)
if doc.sla_state < new_state:
doc.write({'sla_state': new_state})
return res
def update_sla_states(self, cr, uid, context=None):
"""
Updates SLA States, given the current datetime:
Only works on "open" sla states (watching, warning and will fail):
- exceeded limit date are set to "will fail"
- exceeded warning dates are set to "warning"
To be used by a scheduled job.
"""
now = dt.strftime(dt.now(), DT_FMT)
# SLAs to mark as "will fail"
control_ids = self.search(
cr, uid,
[('sla_state', 'in', ['2', '3']), ('sla_limit_date', '<', now)],
context=context)
self.write(cr, uid, control_ids, {'sla_state': '4'}, context=context)
# SLAs to mark as "warning"
control_ids = self.search(
cr, uid,
[('sla_state', 'in', ['2']), ('sla_warn_date', '<', now)],
context=context)
self.write(cr, uid, control_ids, {'sla_state': '3'}, context=context)
return True
def _compute_sla_date(self, cr, uid, working_hours, res_uid,
start_date, hours, context=None):
"""
Return a limit datetime by adding hours to a start_date, honoring
a working_time calendar and a resource's (res_uid) timezone and
availability (leaves)
Currently implemented using a binary search using
_interval_hours_get() from resource.calendar. This is
resource.calendar agnostic, but could be more efficient if
implemented based on it's logic.
Known issue: the end date can be a non-working time; it would be
best for it to be the latest working time possible. Example:
if working time is 08:00 - 16:00 and start_date is 19:00, the +8h
end date will be 19:00 of the next day, and it should rather be
16:00 of the next day.
"""
assert isinstance(start_date, dt)
assert isinstance(hours, int) and hours >= 0
cal_obj = self.pool.get('resource.calendar')
target, step = hours * 3600, 16 * 3600
lo, hi = start_date, start_date
while target > 0 and step > 60:
hi = lo + timedelta(seconds=step)
check = int(3600 * cal_obj._interval_hours_get(
cr, uid, working_hours, lo, hi,
timezone_from_uid=res_uid, exclude_leaves=False,
context=context))
if check <= target:
target -= check
lo = hi
else:
step = int(step / 4.0)
return hi
def _get_computed_slas(self, cr, uid, doc, context=None):
"""
Returns a dict with the computed data for SLAs, given a browse record
for the target document.
* The SLA used is either from a related analytic_account_id or
project_id, whatever is found first.
* The work calendar is taken from the Project's definitions ("Other
Info" tab -> Working Time).
* The timezone used for the working time calculations are from the
document's responsible User (user_id) or from the current User (uid).
For the SLA Achieved calculation:
* Creation date is used to start counting time
* Control date, used to calculate SLA achievement, is defined in the
SLA Definition rules.
"""
def datetime2str(dt_value, fmt): # tolerant datetime to string
return dt_value and dt.strftime(dt_value, fmt) or None
res = []
sla_ids = (safe_getattr(doc, 'analytic_account_id.sla_ids') or
safe_getattr(doc, 'project_id.analytic_account_id.sla_ids'))
if not sla_ids:
return res
for sla in sla_ids:
if sla.control_model != doc._table_name:
continue # SLA not for this model; skip
for l in sla.sla_line_ids:
eval_context = {'o': doc, 'obj': doc, 'object': doc}
if not l.condition or safe_eval(l.condition, eval_context):
start_date = dt.strptime(doc.create_date, DT_FMT)
res_uid = doc.user_id.id or uid
cal = safe_getattr(
doc, 'project_id.resource_calendar_id.id')
warn_date = self._compute_sla_date(
cr, uid, cal, res_uid, start_date, l.warn_qty,
context=context)
lim_date = self._compute_sla_date(
cr, uid, cal, res_uid, warn_date,
l.limit_qty - l.warn_qty,
context=context)
# evaluate sla state
control_val = getattr(doc, sla.control_field_id.name)
if control_val:
control_date = dt.strptime(control_val, DT_FMT)
if control_date > lim_date:
sla_val, sla_state = 0, '5' # failed
else:
sla_val, sla_state = 1, '1' # achieved
else:
control_date = None
now = dt.now()
if now > lim_date:
sla_val, sla_state = 0, '4' # will fail
elif now > warn_date:
sla_val, sla_state = 0, '3' # warning
else:
sla_val, sla_state = 0, '2' # watching
res.append(
{'sla_line_id': l.id,
'sla_achieved': sla_val,
'sla_state': sla_state,
'sla_warn_date': datetime2str(warn_date, DT_FMT),
'sla_limit_date': datetime2str(lim_date, DT_FMT),
'sla_start_date': datetime2str(start_date, DT_FMT),
'sla_close_date': datetime2str(control_date, DT_FMT),
'doc_id': doc.id,
'doc_model': sla.control_model})
break
if sla_ids and not res:
_logger.warning("No valid SLA rule foun for %d, SLA Ids %s"
% (doc.id, repr([x.id for x in sla_ids])))
return res
def store_sla_control(self, cr, uid, docs, context=None):
"""
Used by controlled documents to ask for SLA calculation and storage.
``docs`` is a Browse object
"""
# context flag to avoid infinite loops on further writes
context = context or {}
if '__sla_stored__' in context:
return False
else:
context['__sla_stored__'] = 1
res = []
for ix, doc in enumerate(docs):
if ix and ix % 50 == 0:
_logger.info('...%d SLAs recomputed for %s' % (ix, doc._name))
control = {x.sla_line_id.id: x
for x in doc.sla_control_ids}
sla_recs = self._get_computed_slas(cr, uid, doc, context=context)
# calc sla control lines
if sla_recs:
slas = []
for sla_rec in sla_recs:
sla_line_id = sla_rec.get('sla_line_id')
if sla_line_id in control:
control_rec = control.get(sla_line_id)
if not control_rec.locked:
slas += m2m.write(control_rec.id, sla_rec)
else:
slas += m2m.add(sla_rec)
else:
slas = m2m.clear()
# calc sla control summary
vals = {'sla_state': None, 'sla_control_ids': slas}
if sla_recs and doc.sla_control_ids:
vals['sla_state'] = max(
x.sla_state for x in doc.sla_control_ids)
# store sla
doc._model.write( # regular users can't write on SLA Control
cr, SUPERUSER_ID, [doc.id], vals, context=context)
return res
class SLAControlled(orm.AbstractModel):
"""
SLA Controlled documents: AbstractModel to apply SLA control on Models
"""
_name = 'project.sla.controlled'
_description = 'SLA Controlled Document'
_columns = {
'sla_control_ids': fields.many2many(
'project.sla.control', string="SLA Control", ondelete='cascade'),
'sla_state': fields.selection(
SLA_STATES, string="SLA Status", readonly=True),
}
def create(self, cr, uid, vals, context=None):
res = super(SLAControlled, self).create(cr, uid, vals, context=context)
docs = self.browse(cr, uid, [res], context=context)
self.pool.get('project.sla.control').store_sla_control(
cr, uid, docs, context=context)
return res
def write(self, cr, uid, ids, vals, context=None):
res = super(SLAControlled, self).write(
cr, uid, ids, vals, context=context)
docs = [x for x in self.browse(cr, uid, ids, context=context)
if (x.state != 'cancelled') and
(x.state != 'done' or x.sla_state not in ['1', '5'])]
self.pool.get('project.sla.control').store_sla_control(
cr, uid, docs, context=context)
return res
def unlink(self, cr, uid, ids, context=None):
# Unlink and delete all related Control records
for doc in self.browse(cr, uid, ids, context=context):
vals = [m2m.remove(x.id)[0] for x in doc.sla_control_ids]
doc.write({'sla_control_ids': vals})
return super(SLAControlled, self).unlink(cr, uid, ids, context=context)

18
project_sla/project_sla_control_data.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="ir_cron_sla_action" model="ir.cron">
<field name="name">Update SLA States</field>
<field name="priority" eval="100"/>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model">project.sla.control</field>
<field name="function">update_sla_states</field>
<field name="args">()</field>
</record>
</data>
</openerp>

25
project_sla/project_sla_control_view.xml

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- List view used when the sla_control_ids field
is added to controlled document's form -->
<record id="view_sla_control_tree" model="ir.ui.view">
<field name="name">view_sla_control_tree</field>
<field name="model">project.sla.control</field>
<field name="arch" type="xml">
<tree string="Service Level">
<field name="sla_line_id"/>
<field name="sla_state"/>
<field name="sla_start_date"/>
<field name="sla_warn_date"/>
<field name="sla_limit_date"/>
<field name="sla_close_date"/>
</tree>
</field>
</record>
</data>
</openerp>

138
project_sla/project_sla_demo.xml

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Working Time calendar -->
<record id="worktime_9_18" model="resource.calendar">
<field name="name">Working Days 09-13 14-18</field>
</record>
<record id="worktime 9_18_0M" model="resource.calendar.attendance">
<field name="dayofweek">0</field>
<field name="name">Monday Morning</field>
<field name="hour_from">9</field>
<field name="hour_to">13</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_0A" model="resource.calendar.attendance">
<field name="dayofweek">0</field>
<field name="name">Monday Afternoon</field>
<field name="hour_from">14</field>
<field name="hour_to">18</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_1M" model="resource.calendar.attendance">
<field name="dayofweek">1</field>
<field name="name">Tuesday Morning</field>
<field name="hour_from">9</field>
<field name="hour_to">13</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_1A" model="resource.calendar.attendance">
<field name="dayofweek">1</field>
<field name="name">Tuesday Afternoon</field>
<field name="hour_from">14</field>
<field name="hour_to">18</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_2M" model="resource.calendar.attendance">
<field name="dayofweek">2</field>
<field name="name">Wednesday Morning</field>
<field name="hour_from">9</field>
<field name="hour_to">13</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_2A" model="resource.calendar.attendance">
<field name="dayofweek">2</field>
<field name="name">Wednesday Afternoon</field>
<field name="hour_from">14</field>
<field name="hour_to">18</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_3M" model="resource.calendar.attendance">
<field name="dayofweek">3</field>
<field name="name">Thursday Morning</field>
<field name="hour_from">9</field>
<field name="hour_to">13</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_3A" model="resource.calendar.attendance">
<field name="dayofweek">3</field>
<field name="name">Thursday Afternoon</field>
<field name="hour_from">14</field>
<field name="hour_to">18</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_4M" model="resource.calendar.attendance">
<field name="dayofweek">4</field>
<field name="name">Friday Morning</field>
<field name="hour_from">9</field>
<field name="hour_to">13</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<record id="worktime 9_18_4A" model="resource.calendar.attendance">
<field name="dayofweek">4</field>
<field name="name">Friday Afternoon</field>
<field name="hour_from">14</field>
<field name="hour_to">18</field>
<field name="calendar_id" ref="worktime_9_18" />
</record>
<!-- Set Project Calendar -->
<record id="project.project_project_1" model="project.project">
<field name="resource_calendar_id" ref="worktime_9_18" />
</record>
<!-- SLA Definition and Rules -->
<record id="sla_resolution" model="project.sla">
<field name="name">Standard Resolution Time</field>
<field name="control_model">project.issue</field>
<field name="control_field_id"
ref="project_issue.field_project_issue_date_closed"/>
</record>
<record id="sla_resolution_rule1" model="project.sla.line">
<field name="sla_id" ref="sla_resolution"/>
<field name="sequence">10</field>
<field name="name">Resolution in two business days</field>
<field name="condition">obj.priority &lt;= '2'</field>
<field name="limit_qty">16</field>
<field name="warn_qty">8</field>
</record>
<record id="sla_resolution_rule2" model="project.sla.line">
<field name="sla_id" ref="sla_resolution"/>
<field name="sequence">20</field>
<field name="name">Resolution in three business days</field>
<field name="condition"></field>
<field name="limit_qty">24</field>
<field name="warn_qty">16</field>
</record>
<record id="sla_response" model="project.sla">
<field name="name">Standard Response Time</field>
<field name="control_model">project.issue</field>
<field name="control_field_id"
ref="project_issue.field_project_issue_date_open"/>
</record>
<record id="sla_response_rule1" model="project.sla.line">
<field name="sla_id" ref="sla_response"/>
<field name="sequence">10</field>
<field name="name">Response in one business day</field>
<field name="condition">obj.priority &lt;= '2'</field>
<field name="limit_qty">8</field>
<field name="warn_qty">4</field>
</record>
<record id="sla_response_rule2" model="project.sla.line">
<field name="sla_id" ref="sla_response"/>
<field name="sequence">20</field>
<field name="name">Response in two business days</field>
<field name="condition"></field>
<field name="limit_qty">16</field>
<field name="warn_qty">8</field>
</record>
<!-- Set Contract Resolution SLA Definition -->
<record id="project.project_project_1_account_analytic_account" model="account.analytic.account">
<field name="sla_ids" eval="[(6, 0, [ref('sla_resolution')])]" />
</record>
</data>
</openerp>

48
project_sla/project_sla_view.xml

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_sla_lines_tree" model="ir.ui.view">
<field name="name">view_sla_lines_tree</field>
<field name="model">project.sla.line</field>
<field name="arch" type="xml">
<tree string="Definitions">
<field name="sequence"/>
<field name="name"/>
<field name="condition"/>
<field name="limit_qty"/>
<field name="warn_qty"/>
</tree>
</field>
</record>
<record id="view_sla_form" model="ir.ui.view">
<field name="name">view_sla_form</field>
<field name="model">project.sla</field>
<field name="arch" type="xml">
<form string="SLA Definition">
<field name="name"/>
<field name="active"/>
<field name="control_model"/>
<field name="control_field_id"/>
<notebook colspan="4">
<page string="Rules" name="rules_page">
<field name="sla_line_ids" nolabel="1"/>
</page>
<page string="Contracts" name="contracts_page">
<field name="analytic_ids" nolabel="1" />
</page>
</notebook>
<button name="reapply_slas" colspan="2"
string="Reapply SLA on Contracts"
type="object" />
</form>
</field>
</record>
</data>
</openerp>

20
project_sla/project_view.xml

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="edit_project_sla" model="ir.ui.view">
<field name="name">edit_projec_sla</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<!-- make resource calendar always visible -->
<group string="Administration" position="attributes">
<attribute name="groups"/>
</group>
</field>
</record>
</data>
</openerp>

8
project_sla/security/ir.model.access.csv

@ -0,0 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sla_manager,access_sla_manager,model_project_sla,project.group_project_manager,1,1,1,1
access_sla_user,access_sla_user,model_project_sla,base.group_user,1,0,0,0
access_sla_lines_manager,access_sla_lines_manager,model_project_sla_line,project.group_project_manager,1,1,1,1
access_sla_lines_user,access_sla_lines_user,model_project_sla_line,base.group_user,1,0,0,0
access_sla_control_manager,access_sla_control_manager,model_project_sla_control,project.group_project_manager,1,1,0,0
access_sla_control_user,access_sla_control_user,model_project_sla_control,base.group_user,1,0,0,0
access_sla_controlled_manager,access_sla_controlled_manager,model_project_sla_controlled,project.group_project_manager,1,1,1,1

BIN
project_sla/static/src/img/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.3 KiB

66
project_sla/test/project_sla.yml

@ -0,0 +1,66 @@
-
Cleanup previous test run
-
!python {model: project.issue}: |
res = self.search(cr, uid, [('name', '=', 'My monitor is flickering')])
self.unlink(cr, uid, res)
-
Create a new Issue
-
!record {model: project.issue, id: issue1, view: False}:
name: "My monitor is flickering"
project_id: project.project_project_1
priority: "3"
user_id: base.user_root
partner_id: base.res_partner_2
email_from: agr@agrolait.com
categ_ids:
- project_issue.project_issue_category_01
-
Close the Issue
-
!python {model: project.issue}: |
self.case_close(cr, uid, [ref("issue1")])
-
Force the Issue's Create Date and Close Date
Created friday before opening hour, closed on next monday near closing hour
-
!python {model: project.issue}: |
import time
self.write(cr, uid, [ref("issue1"),], {
'create_date': time.strftime('2013-11-22 06:15:00'),
'date_closed': time.strftime('2013-11-25 16:45:00'),
})
-
There should be Service Level info generated on the Issue
-
!assert {model: project.issue, id: issue1, string: Issue should have calculated service levels}:
- len(sla_control_ids) == 2
-
Assign an additional "Response SLA" to the Contract
-
!python {model: account.analytic.account}: |
self.write(cr, uid, [ref('project.project_project_1_account_analytic_account')],
{'sla_ids': [(4, ref('sla_response'))]})
-
Button to Reapply the SLA Definition
-
!python {model: project.sla}: |
self._reapply_slas(cr, uid, [ref('sla_resolution')], recalc_closed=True)
-
There should be two Service Level lines generated on the Issue
-
!assert {model: project.issue, id: issue1, string: Issue should have two calculated service levels}:
- len(sla_control_ids) == 2
-
The Issue's Resolution SLA should be "3 business days"
-
!python {model: project.issue}: |
issue = self.browse(cr, uid, ref('issue1'))
for x in issue.sla_control_ids:
print x.sla_line_id.name
if x.sla_line_id.id == ref("sla_resolution_rule2"):
assert x.sla_achieved == 1, "Issue resolution SLA should be achieved"
break
else:
assert False, 'Issue Resolution SLA should be "3 business days"'
Loading…
Cancel
Save