Daniel Reis
11 years ago
18 changed files with 1446 additions and 0 deletions
-
5project_sla/__init__.py
-
132project_sla/__openerp__.py
-
71project_sla/analytic_account.py
-
24project_sla/analytic_account_view.xml
-
296project_sla/i18n/project_sla.pot
-
75project_sla/m2m.py
-
29project_sla/project_issue.py
-
85project_sla/project_issue_view.xml
-
84project_sla/project_sla.py
-
322project_sla/project_sla_control.py
-
18project_sla/project_sla_control_data.xml
-
25project_sla/project_sla_control_view.xml
-
138project_sla/project_sla_demo.xml
-
48project_sla/project_sla_view.xml
-
20project_sla/project_view.xml
-
8project_sla/security/ir.model.access.csv
-
BINproject_sla/static/src/img/icon.png
-
66project_sla/test/project_sla.yml
@ -0,0 +1,5 @@ |
|||
# -*- coding: utf-8 -*- |
|||
import project_sla |
|||
import analytic_account |
|||
import project_sla_control |
|||
import project_issue |
@ -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, |
|||
} |
@ -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) |
@ -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> |
@ -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 "" |
|||
|
@ -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!") |
@ -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'] |
@ -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> |
@ -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, |
|||
} |
@ -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) |
@ -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> |
@ -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> |
@ -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 <= '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 <= '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> |
@ -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> |
@ -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> |
@ -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 |
After Width: 128 | Height: 128 | Size: 9.3 KiB |
@ -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"' |
Write
Preview
Loading…
Cancel
Save
Reference in new issue