openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #02355
[Merge] lp:~dreis-pt/contract-management/7.0-project_sla-dr into lp:contract-management
Daniel Reis has proposed merging lp:~dreis-pt/contract-management/7.0-project_sla-dr into lp:contract-management.
Requested reviews:
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c)
For more details, see:
https://code.launchpad.net/~dreis-pt/contract-management/7.0-project_sla-dr/+merge/199645
New module for SLA control.
Replaces the former MP: https://code.launchpad.net/~dreis-pt/project-service/7.0-project_sla-dr/+merge/196960
--
https://code.launchpad.net/~dreis-pt/contract-management/7.0-project_sla-dr/+merge/199645
Your team OpenERP Community Reviewer/Maintainer is subscribed to branch lp:contract-management.
=== added directory 'project_sla'
=== added file 'project_sla/__init__.py'
--- project_sla/__init__.py 1970-01-01 00:00:00 +0000
+++ project_sla/__init__.py 2013-12-19 11:31:45 +0000
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+import project_sla
+import analytic_account
+import project_sla_control
+import project_issue
=== added file 'project_sla/__openerp__.py'
--- project_sla/__openerp__.py 1970-01-01 00:00:00 +0000
+++ project_sla/__openerp__.py 2013-12-19 11:31:45 +0000
@@ -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,
+}
=== added file 'project_sla/analytic_account.py'
--- project_sla/analytic_account.py 1970-01-01 00:00:00 +0000
+++ project_sla/analytic_account.py 2013-12-19 11:31:45 +0000
@@ -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)
=== added file 'project_sla/analytic_account_view.xml'
--- project_sla/analytic_account_view.xml 1970-01-01 00:00:00 +0000
+++ project_sla/analytic_account_view.xml 2013-12-19 11:31:45 +0000
@@ -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>
=== added directory 'project_sla/i18n'
=== added file 'project_sla/i18n/project_sla.pot'
--- project_sla/i18n/project_sla.pot 1970-01-01 00:00:00 +0000
+++ project_sla/i18n/project_sla.pot 2013-12-19 11:31:45 +0000
@@ -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 ""
+
=== added directory 'project_sla/images'
=== added file 'project_sla/images/10_sla_contract.png'
Binary files project_sla/images/10_sla_contract.png 1970-01-01 00:00:00 +0000 and project_sla/images/10_sla_contract.png 2013-12-19 11:31:45 +0000 differ
=== added file 'project_sla/images/20_sla_definition.png'
Binary files project_sla/images/20_sla_definition.png 1970-01-01 00:00:00 +0000 and project_sla/images/20_sla_definition.png 2013-12-19 11:31:45 +0000 differ
=== added file 'project_sla/images/30_sla_controlled.png'
Binary files project_sla/images/30_sla_controlled.png 1970-01-01 00:00:00 +0000 and project_sla/images/30_sla_controlled.png 2013-12-19 11:31:45 +0000 differ
=== added file 'project_sla/m2m.py'
--- project_sla/m2m.py 1970-01-01 00:00:00 +0000
+++ project_sla/m2m.py 2013-12-19 11:31:45 +0000
@@ -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!")
=== added file 'project_sla/project_issue.py'
--- project_sla/project_issue.py 1970-01-01 00:00:00 +0000
+++ project_sla/project_issue.py 2013-12-19 11:31:45 +0000
@@ -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']
=== added file 'project_sla/project_issue_view.xml'
--- project_sla/project_issue_view.xml 1970-01-01 00:00:00 +0000
+++ project_sla/project_issue_view.xml 2013-12-19 11:31:45 +0000
@@ -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>
=== added file 'project_sla/project_sla.py'
--- project_sla/project_sla.py 1970-01-01 00:00:00 +0000
+++ project_sla/project_sla.py 2013-12-19 11:31:45 +0000
@@ -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,
+ }
=== added file 'project_sla/project_sla_control.py'
--- project_sla/project_sla_control.py 1970-01-01 00:00:00 +0000
+++ project_sla/project_sla_control.py 2013-12-19 11:31:45 +0000
@@ -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)
=== added file 'project_sla/project_sla_control_data.xml'
--- project_sla/project_sla_control_data.xml 1970-01-01 00:00:00 +0000
+++ project_sla/project_sla_control_data.xml 2013-12-19 11:31:45 +0000
@@ -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>
=== added file 'project_sla/project_sla_control_view.xml'
--- project_sla/project_sla_control_view.xml 1970-01-01 00:00:00 +0000
+++ project_sla/project_sla_control_view.xml 2013-12-19 11:31:45 +0000
@@ -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>
=== added file 'project_sla/project_sla_demo.xml'
--- project_sla/project_sla_demo.xml 1970-01-01 00:00:00 +0000
+++ project_sla/project_sla_demo.xml 2013-12-19 11:31:45 +0000
@@ -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>
=== added file 'project_sla/project_sla_view.xml'
--- project_sla/project_sla_view.xml 1970-01-01 00:00:00 +0000
+++ project_sla/project_sla_view.xml 2013-12-19 11:31:45 +0000
@@ -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>
=== added file 'project_sla/project_view.xml'
--- project_sla/project_view.xml 1970-01-01 00:00:00 +0000
+++ project_sla/project_view.xml 2013-12-19 11:31:45 +0000
@@ -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>
=== added directory 'project_sla/security'
=== added file 'project_sla/security/ir.model.access.csv'
--- project_sla/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
+++ project_sla/security/ir.model.access.csv 2013-12-19 11:31:45 +0000
@@ -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
=== added directory 'project_sla/static'
=== added directory 'project_sla/static/src'
=== added directory 'project_sla/static/src/img'
=== added file 'project_sla/static/src/img/icon.png'
Binary files project_sla/static/src/img/icon.png 1970-01-01 00:00:00 +0000 and project_sla/static/src/img/icon.png 2013-12-19 11:31:45 +0000 differ
=== added directory 'project_sla/test'
=== added file 'project_sla/test/project_sla.yml'
--- project_sla/test/project_sla.yml 1970-01-01 00:00:00 +0000
+++ project_sla/test/project_sla.yml 2013-12-19 11:31:45 +0000
@@ -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@xxxxxxxxxxxx
+ 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"'
References