Daniel Reis
11 years ago
committed by
Guewen Baconnier
21 changed files with 1421 additions and 0 deletions
-
5project_sla/__init__.py
-
132project_sla/__openerp__.py
-
69project_sla/analytic_account.py
-
24project_sla/analytic_account_view.xml
-
296project_sla/i18n/project_sla.pot
-
BINproject_sla/images/10_sla_contract.png
-
BINproject_sla/images/20_sla_definition.png
-
BINproject_sla/images/30_sla_controlled.png
-
75project_sla/m2m.py
-
29project_sla/project_issue.py
-
60project_sla/project_issue_view.xml
-
86project_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 easily 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,69 @@ |
|||||
|
# -*- 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 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 "" |
||||
|
|
After Width: 773 | Height: 464 | Size: 18 KiB |
After Width: 915 | Height: 437 | Size: 19 KiB |
After Width: 873 | Height: 572 | Size: 26 KiB |
@ -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, 0)] |
||||
|
|
||||
|
|
||||
|
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, 0)] |
||||
|
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,60 @@ |
|||||
|
<?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_sla</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> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,86 @@ |
|||||
|
# -*- 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' |
||||
|
_description = 'SLA Definition' |
||||
|
_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' |
||||
|
_definition = 'SLA Definition Rule Lines' |
||||
|
_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.now().strftime(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 found 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_project_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