← Back to team overview

openerp-community-reviewer team mailing list archive

[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 &lt;= '2'</field>
+                <field name="limit_qty">16</field>
+                <field name="warn_qty">8</field>
+            </record>
+            <record id="sla_resolution_rule2" model="project.sla.line">
+                <field name="sla_id" ref="sla_resolution"/>
+                <field name="sequence">20</field>
+                <field name="name">Resolution in three business days</field>
+                <field name="condition"></field>
+                <field name="limit_qty">24</field>
+                <field name="warn_qty">16</field>
+            </record>
+
+        <record id="sla_response" model="project.sla">
+            <field name="name">Standard Response Time</field>
+            <field name="control_model">project.issue</field>
+            <field name="control_field_id"
+                   ref="project_issue.field_project_issue_date_open"/>
+        </record>
+            <record id="sla_response_rule1" model="project.sla.line">
+                <field name="sla_id" ref="sla_response"/>
+                <field name="sequence">10</field>
+                <field name="name">Response in one business day</field>
+                <field name="condition">obj.priority &lt;= '2'</field>
+                <field name="limit_qty">8</field>
+                <field name="warn_qty">4</field>
+            </record>
+            <record id="sla_response_rule2" model="project.sla.line">
+                <field name="sla_id" ref="sla_response"/>
+                <field name="sequence">20</field>
+                <field name="name">Response in two business days</field>
+                <field name="condition"></field>
+                <field name="limit_qty">16</field>
+                <field name="warn_qty">8</field>
+            </record>
+
+        <!-- Set Contract Resolution SLA Definition -->
+        <record id="project.project_project_1_account_analytic_account" model="account.analytic.account">
+            <field name="sla_ids" eval="[(6, 0, [ref('sla_resolution')])]" />
+        </record>
+
+    </data>
+</openerp>

=== 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