Merge lp:~dreis-pt/contract-management/7.0-project_sla-dr into lp:~contract-management-core-editors/contract-management/7.0

Proposed by Daniel Reis
Status: Merged
Approved by: Guewen Baconnier @ Camptocamp
Approved revision: 10
Merged at revision: 10
Proposed branch: lp:~dreis-pt/contract-management/7.0-project_sla-dr
Merge into: lp:~contract-management-core-editors/contract-management/7.0
Diff against target: 1521 lines (+1421/-0)
17 files modified
project_sla/__init__.py (+5/-0)
project_sla/__openerp__.py (+132/-0)
project_sla/analytic_account.py (+69/-0)
project_sla/analytic_account_view.xml (+24/-0)
project_sla/i18n/project_sla.pot (+296/-0)
project_sla/m2m.py (+75/-0)
project_sla/project_issue.py (+29/-0)
project_sla/project_issue_view.xml (+60/-0)
project_sla/project_sla.py (+86/-0)
project_sla/project_sla_control.py (+322/-0)
project_sla/project_sla_control_data.xml (+18/-0)
project_sla/project_sla_control_view.xml (+25/-0)
project_sla/project_sla_demo.xml (+138/-0)
project_sla/project_sla_view.xml (+48/-0)
project_sla/project_view.xml (+20/-0)
project_sla/security/ir.model.access.csv (+8/-0)
project_sla/test/project_sla.yml (+66/-0)
To merge this branch: bzr merge lp:~dreis-pt/contract-management/7.0-project_sla-dr
Reviewer Review Type Date Requested Status
Sandy Carter (http://www.savoirfairelinux.com) code review Approve
Joël Grand-Guillaume @ camptocamp code review, no tests Approve
Review via email: mp+199645@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi Daniel,

Thanks for submitting that here. A little tips: did you know you can click on "Resubmit Proposal" on the top right corner of an existing MP and change the source and destination branch ?

Using this, you conserve the historic of the discussion tha took place on the previous MP, even if source and destination branch are different. Very useful when you set a wrong destination branch or a wrong source branch or both !

Otherwise, LGTM, Thanks you.

review: Approve (code review, no tests)
Revision history for this message
Daniel Reis (dreis-pt) wrote :

Good to know that, thanks.

Revision history for this message
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote :
Download full text (3.4 KiB)

PEP8 issues:
project_sla/analytic_account.py:32:9: E123 closing bracket does not match indentation of opening bracket's line
project_sla/project_sla.py:41:40: E241 multiple spaces after ','
project_sla/project_sla.py:42:9: E123 closing bracket does not match indentation of opening bracket's line
project_sla/project_sla.py:45:9: E123 closing bracket does not match indentation of opening bracket's line
project_sla/project_sla.py:81:9: E123 closing bracket does not match indentation of opening bracket's line
project_sla.py:84:9: E123 closing bracket does not match indentation of opening bracket's line
project_sla/project_sla_control.py:83:9: E123 closing bracket does not match indentation of opening bracket's line
project_sla/project_sla_control.py:149:37: E241 multiple spaces after ','
project_sla/project_sla_control.py:298:9: E123 closing bracket does not match indentation of opening bracket's line

Code issues:
project_sla/m2m.py:58:12: redundant parenthesis, if you want to make it a tuple, it should be [(5, )]
project_sla/m2m.py:73:23: redundant parenthesis, if you want to make it a tuple, it should be [(5, )]
project_sla/analytic_account.py:34:5: context is None not handled (if context is None: context = dict())
project_sla/analytic_account.py:69:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla.py:47:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla.py:60:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla_control.py:85:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla_control.py:104:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla_control.py:127:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla_control.py:164:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla_control.py:300:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla_control.py:307:5: context is None not handled (if context is None: context = dict())
project_sla/project_sla_control.py:317:5: context is None not handled (if context is None: context = dict())

Minor code issues:
project_sla/project_sla_control.py:112:15: would have gone with: now = dt.now().strftime(DT_FMT)
project_sla/project_issue_view.xml:60:1: Commented out code
project_sla/project_sla_control.py:104:17: Xml Tag Has Empty Body
project_sla/project_sla_control.py:104:17: Xml Tag Has Empty Body
project_sla/analytic_account.py:23:1: _logger declared but never used
project_sla/project_sla.py:24:1: no _description
project_sla/project_sla.py:65:1: no _description
project_sla/project_sla_control.py:65:20: _description not translated
project_sla/project_sla_control.py:292:20: _description not translated

Spelling:
project_sla/project_view.xml:6:37: projec -> project
project_sla/project_issue_view.xml:32:56: slak -> sla
project_sla/__openerp__.py:41:43: easilly -> easily
project_sla/project_issue_view.xml:81:19: recomputations -> re-computations
project_sla/project_sla_control.p...

Read more...

review: Needs Fixing (code review, no test)
10. By Daniel Reis

Minor fixes

Revision history for this message
Daniel Reis (dreis-pt) wrote :

Thanks for the thorough review, Sandy.
I just pushed the fixes, and here go some comments on it:

> PEP8 issues:

You just made me realize my pep8 configs needed some tuning.
E123 is not strict PEP8 (see E123 on https://pep8.readthedocs.org/en/latest/intro.html), and I choose not to follow it: I feel that it makes you add unnecessary indentation or extra lines.
The E241 were fixed.

> Code issues:
> project_sla/m2m.py:58:12: redundant parenthesis, if you want to make it a
> tuple, it should be [(5, )]

You are right. I ended up using [(5, 0)] because I found code in the standard addons using it like that.

> context is None not handled (if context is None: context = dict())

If context is not manipulated and is only passed through, it's unnecessary to handle the context == None case. So, I didn't fix these.

> Minor code issues:
> project_sla/project_sla_control.py:112:15: would have gone with: now =
> dt.now().strftime(DT_FMT)
> project_sla/project_issue_view.xml:60:1: Commented out code
> project_sla/project_sla.py:24:1: no _description
> project_sla/project_sla.py:65:1: no _description
> project_sla/analytic_account.py:23:1: _logger declared but never used

Yes; fixed.

> project_sla/project_sla_control.py:104:17: Xml Tag Has Empty Body
> project_sla/project_sla_control.py:104:17: Xml Tag Has Empty Body
> project_sla/project_sla_control.py:65:20: _description not translated
> project_sla/project_sla_control.py:292:20: _description not translated

Hmm, didn't find XML tags on those .py files, and I found the translation strings in the .pot file. Can you check this again?

> Spelling:

Fixed.

> Out of curiosity, I noticed that you're using CamelCase for your class names,
> is there a reason for that?

I'm using it because [PEP8](http://www.python.org/dev/peps/pep-0008/#class-names) states that "class names should normally use the CapWords convention", and I don't any reason to not follow it.

Revision history for this message
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote :

> > project_sla/project_sla_control.py:104:17: Xml Tag Has Empty Body
> > project_sla/project_sla_control.py:104:17: Xml Tag Has Empty Body
> > project_sla/project_sla_control.py:65:20: _description not translated
> > project_sla/project_sla_control.py:292:20: _description not translated
>
> Hmm, didn't find XML tags on those .py files, and I found the translation
> strings in the .pot file. Can you check this again?
>
Sorry, copy Paste error.
project_sla/project_sla_demo.xml:104:17: Xml Tag Has Empty Body
project_sla/project_sla_demo.xml:127:17: Xml Tag Has Empty Body

As for translating _description, if it's not done here (_description = _('...')), then it won't be translated in python code (ex: mail_thread widget -- though that one has a soon-to-be-fixed bug too https://bugs.launchpad.net/openobject-addons/+bug/1262000).
Essentially if you invoke _(self.pool.get('project.sla')._description), it will not check the pot file because it doesn't include the following for 'SLA Definition':
#, python-format

> > Out of curiosity, I noticed that you're using CamelCase for your class
> names,
> > is there a reason for that?
>
> I'm using it because [PEP8](http://www.python.org/dev/peps/pep-0008/#class-
> names) states that "class names should normally use the CapWords convention",
> and I don't any reason to not follow it.

Glad to hear that. I think I'll start using that convention too.

Revision history for this message
Daniel Reis (dreis-pt) wrote :

> project_sla/project_sla_demo.xml:104:17: Xml Tag Has Empty Body
> project_sla/project_sla_demo.xml:127:17: Xml Tag Has Empty Body

I can see that it could be neater, but strictly speaking this neither a programming nor a style issue, so I think it isn't worth the trouble.

> As for translating _description, if it's not done here (_description =
> _('...')), then it won't be translated in python code (ex: mail_thread widget
> -- though that one has a soon-to-be-fixed bug too https://bugs.launchpad.net
> /openobject-addons/+bug/1262000).

I can't find a similar example in the official addons; it seems to me that it's a temporary workaround for a bug. I would prefer to not use it unless proven to be absolutely necessary.

Revision history for this message
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote :

LGTM

review: Approve (code review)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'project_sla'
2=== added file 'project_sla/__init__.py'
3--- project_sla/__init__.py 1970-01-01 00:00:00 +0000
4+++ project_sla/__init__.py 2013-12-26 15:27:30 +0000
5@@ -0,0 +1,5 @@
6+# -*- coding: utf-8 -*-
7+import project_sla
8+import analytic_account
9+import project_sla_control
10+import project_issue
11
12=== added file 'project_sla/__openerp__.py'
13--- project_sla/__openerp__.py 1970-01-01 00:00:00 +0000
14+++ project_sla/__openerp__.py 2013-12-26 15:27:30 +0000
15@@ -0,0 +1,132 @@
16+# -*- coding: utf-8 -*-
17+##############################################################################
18+#
19+# Copyright (C) 2013 Daniel Reis
20+#
21+# This program is free software: you can redistribute it and/or modify
22+# it under the terms of the GNU Affero General Public License as
23+# published by the Free Software Foundation, either version 3 of the
24+# License, or (at your option) any later version.
25+#
26+# This program is distributed in the hope that it will be useful,
27+# but WITHOUT ANY WARRANTY; without even the implied warranty of
28+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29+# GNU Affero General Public License for more details.
30+#
31+# You should have received a copy of the GNU Affero General Public License
32+# along with this program. If not, see <http://www.gnu.org/licenses/>.
33+#
34+##############################################################################
35+{
36+ 'name': 'Service Level Agreements',
37+ 'summary': 'Define SLAs for your Contracts',
38+ 'version': '1.0',
39+ "category": "Project Management",
40+ 'description': """\
41+Contract SLAs
42+===============
43+
44+SLAs are assigned to Contracts, on the Analytic Account form, SLA Definition
45+separator. This is also where new SLA Definitions are created.
46+
47+One Contract can have several SLA Definitions attached, allowing for
48+"composite SLAs". For example, a contract could have a Response Time SLA (time
49+to start resolution) and a Resolution Time SLA (time to close request).
50+
51+
52+SLA Controlled Documents
53+========================
54+
55+Only Project Issue documents are made SLA controllable.
56+However, a framework is made available to easily build extensions to make
57+other documents models SLA controlled.
58+
59+SLA controlled documents have attached information on the list of SLA rules
60+they should meet (more than one in the case for composite SLAs) and a summary
61+SLA status:
62+
63+ * "watching" the service level (it has SLA requirements to meet)
64+ * under "warning" (limit dates are close, special attention is needed)
65+ * "failed" (one on the SLA limits has not been met)
66+ * "achieved" (all SLA limits have been met)
67+
68+Transient states, such as "watching" and "warning", are regularly updated by
69+a hourly scheduled job, that reevaluates the warning and limit dates against
70+the current time and changes the state when find dates that have been exceeded.
71+
72+To decide what SLA Definitions apply for a specific document, first a lookup
73+is made for a ``analytic_account_id`` field. If not found, then it will
74+look up for the ``project_id`` and it's corresponding ``analytic_account_id``.
75+
76+Specifically, the Service Desk module introduces a Analytic Account field for
77+Project Issues. This makes it possible for a Service Team (a "Project") to
78+have a generic SLA, but at the same time allow for some Contracts to have
79+specific SLAs (such as the case for "premium" service conditions).
80+
81+
82+SLA Definitions and Rules
83+=========================
84+
85+New SLA Definitions are created from the Analytic Account form, SLA Definition
86+field.
87+
88+Each definition can have one or more Rules.
89+The particular rule to use is decided by conditions, so that you can set
90+different service levels based on request attributes, such as Priority or
91+Category.
92+Each rule condition is evaluated in "sequence" order, and the first onea to met
93+is the one to be used.
94+In the simplest case, a single rule with no condition is just what is needed.
95+
96+Each rule sets a number of hours until the "limit date", and the number of
97+hours until a "warning date". The former will be used to decide if the SLA
98+was achieved, and the later can be used for automatic alarms or escalation
99+procedures.
100+
101+Time will be counted from creation date, until the "Control Date" specified for
102+the SLA Definition. That would usually be the "Close" (time until resolution)
103+or the "Open" (time until response) dates.
104+
105+The working calendar set in the related Project definitions will be used (see
106+the "Other Info" tab). If none is defined, a builtin "all days, 8-12 13-17"
107+default calendar is used.
108+
109+A timezone and leave calendars will also used, based on either the assigned
110+user (document's `user_id`) or on the current user.
111+
112+
113+Setup checklist
114+===============
115+
116+The basic steps to configure SLAs for a Project are:
117+
118+ * Set Project's Working Calendar, at Project definitions, "Other Info" tab
119+ * Go to the Project's Analytic Account form; create and set SLA Definitions
120+ * Use the "Reapply SLAs" button on the Analytic Account form
121+ * See Project Issue's calculated SLAs in the new "Service Levels" tab
122+
123+
124+Credits and Contributors
125+========================
126+
127+ * Daniel Reis (https://launchpad.net/~dreis-pt)
128+ * David Vignoni, author of the icon from the KDE 3.x Nuvola icon theme
129+""",
130+ 'author': 'Daniel Reis',
131+ 'website': '',
132+ 'depends': [
133+ 'project_issue',
134+ ],
135+ 'data': [
136+ 'project_sla_view.xml',
137+ 'project_sla_control_view.xml',
138+ 'project_sla_control_data.xml',
139+ 'analytic_account_view.xml',
140+ 'project_view.xml',
141+ 'project_issue_view.xml',
142+ 'security/ir.model.access.csv',
143+ ],
144+ 'demo': ['project_sla_demo.xml'],
145+ 'test': ['test/project_sla.yml'],
146+ 'installable': True,
147+}
148
149=== added file 'project_sla/analytic_account.py'
150--- project_sla/analytic_account.py 1970-01-01 00:00:00 +0000
151+++ project_sla/analytic_account.py 2013-12-26 15:27:30 +0000
152@@ -0,0 +1,69 @@
153+# -*- coding: utf-8 -*-
154+##############################################################################
155+#
156+# Copyright (C) 2013 Daniel Reis
157+#
158+# This program is free software: you can redistribute it and/or modify
159+# it under the terms of the GNU Affero General Public License as
160+# published by the Free Software Foundation, either version 3 of the
161+# License, or (at your option) any later version.
162+#
163+# This program is distributed in the hope that it will be useful,
164+# but WITHOUT ANY WARRANTY; without even the implied warranty of
165+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
166+# GNU Affero General Public License for more details.
167+#
168+# You should have received a copy of the GNU Affero General Public License
169+# along with this program. If not, see <http://www.gnu.org/licenses/>.
170+#
171+##############################################################################
172+
173+from openerp.osv import fields, orm
174+
175+
176+class AnalyticAccount(orm.Model):
177+ """ Add SLA to Analytic Accounts """
178+ _inherit = 'account.analytic.account'
179+ _columns = {
180+ 'sla_ids': fields.many2many(
181+ 'project.sla', string='Service Level Agreement'),
182+ }
183+
184+ def _reapply_sla(self, cr, uid, ids, recalc_closed=False, context=None):
185+ """
186+ Force SLA recalculation on open documents that already are subject to
187+ this SLA Definition.
188+ To use after changing a Contract SLA or it's Definitions.
189+ The ``recalc_closed`` flag allows to also recompute closed documents.
190+ """
191+ ctrl_obj = self.pool.get('project.sla.control')
192+ proj_obj = self.pool.get('project.project')
193+ exclude_states = ['cancelled'] + (not recalc_closed and ['done'] or [])
194+ for contract in self.browse(cr, uid, ids, context=context):
195+ # for each contract, and for each model under SLA control ...
196+ for m_name in set([sla.control_model for sla in contract.sla_ids]):
197+ model = self.pool.get(m_name)
198+ doc_ids = []
199+ if 'analytic_account_id' in model._columns:
200+ doc_ids += model.search(
201+ cr, uid,
202+ [('analytic_account_id', '=', contract.id),
203+ ('state', 'not in', exclude_states)],
204+ context=context)
205+ if 'project_id' in model._columns:
206+ proj_ids = proj_obj.search(
207+ cr, uid, [('analytic_account_id', '=', contract.id)],
208+ context=context)
209+ doc_ids += model.search(
210+ cr, uid,
211+ [('project_id', 'in', proj_ids),
212+ ('state', 'not in', exclude_states)],
213+ context=context)
214+ if doc_ids:
215+ docs = model.browse(cr, uid, doc_ids, context=context)
216+ ctrl_obj.store_sla_control(cr, uid, docs, context=context)
217+ return True
218+
219+ def reapply_sla(self, cr, uid, ids, context=None):
220+ """ Reapply SLAs button action """
221+ return self._reapply_sla(cr, uid, ids, context=context)
222
223=== added file 'project_sla/analytic_account_view.xml'
224--- project_sla/analytic_account_view.xml 1970-01-01 00:00:00 +0000
225+++ project_sla/analytic_account_view.xml 2013-12-26 15:27:30 +0000
226@@ -0,0 +1,24 @@
227+<?xml version="1.0" encoding="utf-8"?>
228+<openerp>
229+ <data>
230+
231+ <record id="view_account_analytic_account_form_sla" model="ir.ui.view">
232+ <field name="name">view_account_analytic_account_form_sla</field>
233+ <field name="model">account.analytic.account</field>
234+ <field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
235+ <field name="arch" type="xml">
236+
237+ <page name="contract_page" position="after">
238+ <page name="sla_page" string="Service Level Agreement">
239+ <field name="sla_ids" nolabel="1"/>
240+ <button name="reapply_sla" string="Reapply" type="object"
241+ help="Reapply the SLAs to all Contract's documents."
242+ groups="project.group_project_manager" />
243+ </page>
244+ </page>
245+
246+ </field>
247+ </record>
248+
249+ </data>
250+</openerp>
251
252=== added directory 'project_sla/i18n'
253=== added file 'project_sla/i18n/project_sla.pot'
254--- project_sla/i18n/project_sla.pot 1970-01-01 00:00:00 +0000
255+++ project_sla/i18n/project_sla.pot 2013-12-26 15:27:30 +0000
256@@ -0,0 +1,296 @@
257+# Translation of OpenERP Server.
258+# This file contains the translation of the following modules:
259+# * project_sla
260+#
261+msgid ""
262+msgstr ""
263+"Project-Id-Version: OpenERP Server 7.0\n"
264+"Report-Msgid-Bugs-To: \n"
265+"POT-Creation-Date: 2013-12-19 10:28+0000\n"
266+"PO-Revision-Date: 2013-12-19 10:28+0000\n"
267+"Last-Translator: <>\n"
268+"Language-Team: \n"
269+"MIME-Version: 1.0\n"
270+"Content-Type: text/plain; charset=UTF-8\n"
271+"Content-Transfer-Encoding: \n"
272+"Plural-Forms: \n"
273+
274+#. module: project_sla
275+#: model:project.sla,name:project_sla.sla_response
276+msgid "Standard Response Time"
277+msgstr ""
278+
279+#. module: project_sla
280+#: help:project.sla,control_field_id:0
281+msgid "Date field used to check if the SLA was achieved."
282+msgstr ""
283+
284+#. module: project_sla
285+#: model:ir.model,name:project_sla.model_project_issue
286+msgid "Project Issue"
287+msgstr ""
288+
289+#. module: project_sla
290+#: model:ir.model,name:project_sla.model_project_sla_control
291+msgid "SLA Control Registry"
292+msgstr ""
293+
294+#. module: project_sla
295+#: view:project.sla:0
296+msgid "Reapply SLA on Contracts"
297+msgstr ""
298+
299+#. module: project_sla
300+#: view:project.project:0
301+msgid "Administration"
302+msgstr ""
303+
304+#. module: project_sla
305+#: view:project.issue:0
306+msgid "Priority"
307+msgstr ""
308+
309+#. module: project_sla
310+#: selection:project.issue,sla_state:0
311+#: selection:project.sla.control,sla_state:0
312+#: selection:project.sla.controlled,sla_state:0
313+msgid "Failed"
314+msgstr ""
315+
316+#. module: project_sla
317+#: field:project.sla.control,sla_warn_date:0
318+msgid "Warning Date"
319+msgstr ""
320+
321+#. module: project_sla
322+#: field:project.sla.line,warn_qty:0
323+msgid "Hours to Warn"
324+msgstr ""
325+
326+#. module: project_sla
327+#: view:project.sla.control:0
328+msgid "Service Level"
329+msgstr ""
330+
331+#. module: project_sla
332+#: selection:project.issue,sla_state:0
333+#: selection:project.sla.control,sla_state:0
334+#: selection:project.sla.controlled,sla_state:0
335+msgid "Watching"
336+msgstr ""
337+
338+#. module: project_sla
339+#: view:project.sla:0
340+#: field:project.sla,analytic_ids:0
341+msgid "Contracts"
342+msgstr ""
343+
344+#. module: project_sla
345+#: field:project.sla,name:0
346+#: field:project.sla.line,name:0
347+msgid "Title"
348+msgstr ""
349+
350+#. module: project_sla
351+#: field:project.sla,active:0
352+msgid "Active"
353+msgstr ""
354+
355+#. module: project_sla
356+#: field:project.issue,sla_control_ids:0
357+#: field:project.sla.controlled,sla_control_ids:0
358+msgid "SLA Control"
359+msgstr ""
360+
361+#. module: project_sla
362+#: field:project.sla.control,sla_achieved:0
363+msgid "Achieved?"
364+msgstr ""
365+
366+#. module: project_sla
367+#: view:project.issue:0
368+#: field:project.issue,sla_state:0
369+#: field:project.sla.control,sla_state:0
370+#: field:project.sla.controlled,sla_state:0
371+msgid "SLA Status"
372+msgstr ""
373+
374+#. module: project_sla
375+#: field:project.sla.line,condition:0
376+msgid "Condition"
377+msgstr ""
378+
379+#. module: project_sla
380+#: field:project.sla,control_model:0
381+msgid "For documents"
382+msgstr ""
383+
384+#. module: project_sla
385+#: view:project.sla:0
386+#: field:project.sla.line,sla_id:0
387+msgid "SLA Definition"
388+msgstr ""
389+
390+#. module: project_sla
391+#: model:project.sla.line,name:project_sla.sla_response_rule2
392+msgid "Response in two business days"
393+msgstr ""
394+
395+#. module: project_sla
396+#: selection:project.issue,sla_state:0
397+#: selection:project.sla.control,sla_state:0
398+#: selection:project.sla.controlled,sla_state:0
399+msgid "Will Fail"
400+msgstr ""
401+
402+#. module: project_sla
403+#: field:project.sla.control,sla_line_id:0
404+msgid "Service Agreement"
405+msgstr ""
406+
407+#. module: project_sla
408+#: field:project.sla.control,doc_id:0
409+msgid "Document ID"
410+msgstr ""
411+
412+#. module: project_sla
413+#: field:project.sla.control,locked:0
414+msgid "Recalculation disabled"
415+msgstr ""
416+
417+#. module: project_sla
418+#: field:project.sla.control,sla_limit_date:0
419+msgid "Limit Date"
420+msgstr ""
421+
422+#. module: project_sla
423+#: help:project.sla.line,condition:0
424+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'"
425+msgstr ""
426+
427+#. module: project_sla
428+#: model:project.sla.line,name:project_sla.sla_resolution_rule1
429+msgid "Resolution in two business days"
430+msgstr ""
431+
432+#. module: project_sla
433+#: view:account.analytic.account:0
434+msgid "Reapply"
435+msgstr ""
436+
437+#. module: project_sla
438+#: field:project.sla.line,limit_qty:0
439+msgid "Hours to Limit"
440+msgstr ""
441+
442+#. module: project_sla
443+#: field:project.sla,sla_line_ids:0
444+#: view:project.sla.line:0
445+msgid "Definitions"
446+msgstr ""
447+
448+#. module: project_sla
449+#: model:project.sla.line,name:project_sla.sla_resolution_rule2
450+msgid "Resolution in three business days"
451+msgstr ""
452+
453+#. module: project_sla
454+#: field:project.sla.control,sla_close_date:0
455+msgid "Close Date"
456+msgstr ""
457+
458+#. module: project_sla
459+#: model:ir.model,name:project_sla.model_project_sla
460+msgid "project.sla"
461+msgstr ""
462+
463+#. module: project_sla
464+#: field:project.sla,control_field_id:0
465+msgid "Control Date"
466+msgstr ""
467+
468+#. module: project_sla
469+#: model:project.sla.line,name:project_sla.sla_response_rule1
470+msgid "Response in one business day"
471+msgstr ""
472+
473+#. module: project_sla
474+#: selection:project.issue,sla_state:0
475+#: selection:project.sla.control,sla_state:0
476+#: selection:project.sla.controlled,sla_state:0
477+msgid "Achieved"
478+msgstr ""
479+
480+#. module: project_sla
481+#: view:account.analytic.account:0
482+#: field:account.analytic.account,sla_ids:0
483+msgid "Service Level Agreement"
484+msgstr ""
485+
486+#. module: project_sla
487+#: model:project.sla,name:project_sla.sla_resolution
488+msgid "Standard Resolution Time"
489+msgstr ""
490+
491+#. module: project_sla
492+#: field:project.sla.line,sequence:0
493+msgid "Sequence"
494+msgstr ""
495+
496+#. module: project_sla
497+#: view:project.issue:0
498+msgid "Service Levels"
499+msgstr ""
500+
501+#. module: project_sla
502+#: model:ir.model,name:project_sla.model_account_analytic_account
503+msgid "Analytic Account"
504+msgstr ""
505+
506+#. module: project_sla
507+#: view:project.sla:0
508+msgid "Rules"
509+msgstr ""
510+
511+#. module: project_sla
512+#: help:project.sla.control,locked:0
513+msgid "Safeguard manual changes from future automatic recomputations."
514+msgstr ""
515+
516+#. module: project_sla
517+#: selection:project.issue,sla_state:0
518+#: selection:project.sla.control,sla_state:0
519+#: selection:project.sla.controlled,sla_state:0
520+msgid "Warning"
521+msgstr ""
522+
523+#. module: project_sla
524+#: view:project.issue:0
525+msgid "Extra Info"
526+msgstr ""
527+
528+#. module: project_sla
529+#: field:project.sla.control,doc_model:0
530+msgid "Document Model"
531+msgstr ""
532+
533+#. module: project_sla
534+#: model:ir.model,name:project_sla.model_project_sla_line
535+msgid "project.sla.line"
536+msgstr ""
537+
538+#. module: project_sla
539+#: view:account.analytic.account:0
540+msgid "Reapply the SLAs to all Contract's documents."
541+msgstr ""
542+
543+#. module: project_sla
544+#: field:project.sla.control,sla_start_date:0
545+msgid "Start Date"
546+msgstr ""
547+
548+#. module: project_sla
549+#: model:ir.model,name:project_sla.model_project_sla_controlled
550+msgid "SLA Controlled Document"
551+msgstr ""
552+
553
554=== added directory 'project_sla/images'
555=== added file 'project_sla/images/10_sla_contract.png'
556Binary 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-26 15:27:30 +0000 differ
557=== added file 'project_sla/images/20_sla_definition.png'
558Binary 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-26 15:27:30 +0000 differ
559=== added file 'project_sla/images/30_sla_controlled.png'
560Binary 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-26 15:27:30 +0000 differ
561=== added file 'project_sla/m2m.py'
562--- project_sla/m2m.py 1970-01-01 00:00:00 +0000
563+++ project_sla/m2m.py 2013-12-26 15:27:30 +0000
564@@ -0,0 +1,75 @@
565+"""
566+Wrapper for OpenERP's cryptic write conventions for x2many fields.
567+
568+Example usage:
569+
570+ import m2m
571+ browse_rec.write({'many_ids: m2m.clear())
572+ browse_rec.write({'many_ids: m2m.link(99))
573+ browse_rec.write({'many_ids: m2m.add({'name': 'Monty'}))
574+ browse_rec.write({'many_ids: m2m.replace([98, 99]))
575+
576+Since returned values are lists, the can be joined using the plus operator:
577+
578+ browse_rec.write({'many_ids: m2m.clear() + m2m.link(99))
579+
580+(Source: https://github.com/dreispt/openerp-write2many)
581+"""
582+
583+
584+def create(values):
585+ """ Create a referenced record """
586+ assert isinstance(values, dict)
587+ return [(0, 0, values)]
588+
589+
590+def add(values):
591+ """ Intuitive alias for create() """
592+ return create(values)
593+
594+
595+def write(id, values):
596+ """ Write on referenced record """
597+ assert isinstance(id, int)
598+ assert isinstance(values, dict)
599+ return [(1, id, values)]
600+
601+
602+def remove(id):
603+ """ Unlink and delete referenced record """
604+ assert isinstance(id, int)
605+ return [(2, id)]
606+
607+
608+def unlink(id):
609+ """ Unlink but do not delete the referenced record """
610+ assert isinstance(id, int)
611+ return [(3, id)]
612+
613+
614+def link(id):
615+ """ Link but do not delete the referenced record """
616+ assert isinstance(id, int)
617+ return [(4, id)]
618+
619+
620+def clear():
621+ """ Unlink all referenced records (doesn't delete them) """
622+ return [(5, 0)]
623+
624+
625+def replace(ids):
626+ """ Unlink all current records and replace them with a new list """
627+ assert isinstance(ids, list)
628+ return [(6, 0, ids)]
629+
630+
631+if __name__ == "__main__":
632+ # Tests:
633+ assert create({'name': 'Monty'}) == [(0, 0, {'name': 'Monty'})]
634+ assert write(99, {'name': 'Monty'}) == [(1, 99, {'name': 'Monty'})]
635+ assert remove(99) == [(2, 99)]
636+ assert unlink(99) == [(3, 99)]
637+ assert clear() == [(5, 0)]
638+ assert replace([97, 98, 99]) == [(6, 0, [97, 98, 99])]
639+ print("Done!")
640
641=== added file 'project_sla/project_issue.py'
642--- project_sla/project_issue.py 1970-01-01 00:00:00 +0000
643+++ project_sla/project_issue.py 2013-12-26 15:27:30 +0000
644@@ -0,0 +1,29 @@
645+# -*- coding: utf-8 -*-
646+##############################################################################
647+#
648+# Copyright (C) 2013 Daniel Reis
649+#
650+# This program is free software: you can redistribute it and/or modify
651+# it under the terms of the GNU Affero General Public License as
652+# published by the Free Software Foundation, either version 3 of the
653+# License, or (at your option) any later version.
654+#
655+# This program is distributed in the hope that it will be useful,
656+# but WITHOUT ANY WARRANTY; without even the implied warranty of
657+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
658+# GNU Affero General Public License for more details.
659+#
660+# You should have received a copy of the GNU Affero General Public License
661+# along with this program. If not, see <http://www.gnu.org/licenses/>.
662+#
663+##############################################################################
664+
665+from openerp.osv import orm
666+
667+
668+class ProjectIssue(orm.Model):
669+ """
670+ Extend Project Issues to be SLA Controlled
671+ """
672+ _name = 'project.issue'
673+ _inherit = ['project.issue', 'project.sla.controlled']
674
675=== added file 'project_sla/project_issue_view.xml'
676--- project_sla/project_issue_view.xml 1970-01-01 00:00:00 +0000
677+++ project_sla/project_issue_view.xml 2013-12-26 15:27:30 +0000
678@@ -0,0 +1,60 @@
679+<?xml version="1.0" encoding="utf-8"?>
680+<openerp>
681+ <data>
682+
683+ <!-- Project Issue Form -->
684+ <record id="project_issue_form_view_sla" model="ir.ui.view">
685+ <field name="name">project_issue_form_view_sla</field>
686+ <field name="model">project.issue</field>
687+ <field name="inherit_id" ref="project_issue.project_issue_form_view"/>
688+ <field name="arch" type="xml">
689+
690+ <page string="Extra Info" position="after">
691+ <page name="sla_page" string="Service Levels"
692+ attrs="{'invisible': [('sla_state', '=', False)]}">
693+ <group>
694+ <group>
695+ <field name="sla_state" />
696+ </group>
697+ <group>
698+ <field name="write_date" />
699+ </group>
700+ </group>
701+ <field name="sla_control_ids"/>
702+ </page>
703+ </page>
704+
705+ </field>
706+ </record>
707+
708+ <!-- Project Issue List -->
709+ <record model="ir.ui.view" id="project_issue_tree_view_sla">
710+ <field name="name">project_issue_tree_view_sla</field>
711+ <field name="model">project.issue</field>
712+ <field name="inherit_id" ref="project_issue.project_issue_tree_view"/>
713+ <field name="arch" type="xml">
714+
715+ <field name="project_id" position="after">
716+ <field name="sla_state"/>
717+ </field>
718+
719+ </field>
720+ </record>
721+
722+
723+ <!-- Project Issue Filter -->
724+ <record id="view_project_issue_filter_sdesk" model="ir.ui.view">
725+ <field name="name">view_project_issue_filter_sdesk</field>
726+ <field name="model">project.issue</field>
727+ <field name="inherit_id" ref="project_issue.view_project_issue_filter"/>
728+ <field name="arch" type="xml">
729+
730+ <filter string="Priority" position="after">
731+ <filter string="SLA Status" context="{'group_by':'sla_state'}" />
732+ </filter>
733+
734+ </field>
735+ </record>
736+
737+ </data>
738+</openerp>
739
740=== added file 'project_sla/project_sla.py'
741--- project_sla/project_sla.py 1970-01-01 00:00:00 +0000
742+++ project_sla/project_sla.py 2013-12-26 15:27:30 +0000
743@@ -0,0 +1,86 @@
744+# -*- coding: utf-8 -*-
745+##############################################################################
746+#
747+# Copyright (C) 2013 Daniel Reis
748+#
749+# This program is free software: you can redistribute it and/or modify
750+# it under the terms of the GNU Affero General Public License as
751+# published by the Free Software Foundation, either version 3 of the
752+# License, or (at your option) any later version.
753+#
754+# This program is distributed in the hope that it will be useful,
755+# but WITHOUT ANY WARRANTY; without even the implied warranty of
756+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
757+# GNU Affero General Public License for more details.
758+#
759+# You should have received a copy of the GNU Affero General Public License
760+# along with this program. If not, see <http://www.gnu.org/licenses/>.
761+#
762+##############################################################################
763+
764+from openerp.osv import fields, orm
765+
766+
767+class SLADefinition(orm.Model):
768+ """
769+ SLA Definition
770+ """
771+ _name = 'project.sla'
772+ _description = 'SLA Definition'
773+ _columns = {
774+ 'name': fields.char('Title', size=64, required=True, translate=True),
775+ 'active': fields.boolean('Active'),
776+ 'control_model': fields.char('For documents', size=128, required=True),
777+ 'control_field_id': fields.many2one(
778+ 'ir.model.fields', 'Control Date', required=True,
779+ domain="[('model_id.model', '=', control_model),"
780+ " ('ttype', 'in', ['date', 'datetime'])]",
781+ help="Date field used to check if the SLA was achieved."),
782+ 'sla_line_ids': fields.one2many(
783+ 'project.sla.line', 'sla_id', 'Definitions'),
784+ 'analytic_ids': fields.many2many(
785+ 'account.analytic.account', string='Contracts'),
786+ }
787+ _defaults = {
788+ 'active': True,
789+ }
790+
791+ def _reapply_slas(self, cr, uid, ids, recalc_closed=False, context=None):
792+ """
793+ Force SLA recalculation on all _open_ Contracts for the selected SLAs.
794+ To use upon SLA Definition modifications.
795+ """
796+ contract_obj = self.pool.get('account.analytic.account')
797+ for sla in self.browse(cr, uid, ids, context=context):
798+ contr_ids = [x.id for x in sla.analytic_ids if x.state == 'open']
799+ contract_obj._reapply_sla(
800+ cr, uid, contr_ids, recalc_closed=recalc_closed,
801+ context=context)
802+ return True
803+
804+ def reapply_slas(self, cr, uid, ids, context=None):
805+ """ Reapply SLAs button action """
806+ return self._reapply_slas(cr, uid, ids, context=context)
807+
808+
809+class SLARules(orm.Model):
810+ """
811+ SLA Definition Rule Lines
812+ """
813+ _name = 'project.sla.line'
814+ _definition = 'SLA Definition Rule Lines'
815+ _order = 'sla_id,sequence'
816+ _columns = {
817+ 'sla_id': fields.many2one('project.sla', 'SLA Definition'),
818+ 'sequence': fields.integer('Sequence'),
819+ 'name': fields.char('Title', size=64, required=True, translate=True),
820+ 'condition': fields.char(
821+ 'Condition', size=256, help="Apply only if this expression is "
822+ "evaluated to True. The document fields can be accessed using "
823+ "either o, obj or object. Example: obj.priority <= '2'"),
824+ 'limit_qty': fields.integer('Hours to Limit'),
825+ 'warn_qty': fields.integer('Hours to Warn'),
826+ }
827+ _defaults = {
828+ 'sequence': 10,
829+ }
830
831=== added file 'project_sla/project_sla_control.py'
832--- project_sla/project_sla_control.py 1970-01-01 00:00:00 +0000
833+++ project_sla/project_sla_control.py 2013-12-26 15:27:30 +0000
834@@ -0,0 +1,322 @@
835+# -*- coding: utf-8 -*-
836+##############################################################################
837+#
838+# Copyright (C) 2013 Daniel Reis
839+#
840+# This program is free software: you can redistribute it and/or modify
841+# it under the terms of the GNU Affero General Public License as
842+# published by the Free Software Foundation, either version 3 of the
843+# License, or (at your option) any later version.
844+#
845+# This program is distributed in the hope that it will be useful,
846+# but WITHOUT ANY WARRANTY; without even the implied warranty of
847+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
848+# GNU Affero General Public License for more details.
849+#
850+# You should have received a copy of the GNU Affero General Public License
851+# along with this program. If not, see <http://www.gnu.org/licenses/>.
852+#
853+##############################################################################
854+
855+from openerp.osv import fields, orm
856+from openerp.tools.safe_eval import safe_eval
857+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DT_FMT
858+from openerp import SUPERUSER_ID
859+from datetime import timedelta
860+from datetime import datetime as dt
861+import m2m
862+
863+import logging
864+_logger = logging.getLogger(__name__)
865+
866+
867+SLA_STATES = [('5', 'Failed'), ('4', 'Will Fail'), ('3', 'Warning'),
868+ ('2', 'Watching'), ('1', 'Achieved')]
869+
870+
871+def safe_getattr(obj, dotattr, default=False):
872+ """
873+ Follow an object attribute dot-notation chain to find the leaf value.
874+ If any attribute doesn't exist or has no value, just return False.
875+ Checks hasattr ahead, to avoid ORM Browse log warnings.
876+ """
877+ attrs = dotattr.split('.')
878+ while attrs:
879+ attr = attrs.pop(0)
880+ if attr in obj._model._columns:
881+ try:
882+ obj = getattr(obj, attr)
883+ except AttributeError:
884+ return default
885+ if not obj:
886+ return default
887+ else:
888+ return default
889+ return obj
890+
891+
892+class SLAControl(orm.Model):
893+ """
894+ SLA Control Registry
895+ Each controlled document (Issue, Claim, ...) will have a record here.
896+ This model concentrates all the logic for Service Level calculation.
897+ """
898+ _name = 'project.sla.control'
899+ _description = 'SLA Control Registry'
900+
901+ _columns = {
902+ 'doc_id': fields.integer('Document ID', readonly=True),
903+ 'doc_model': fields.char('Document Model', size=128, readonly=True),
904+ 'sla_line_id': fields.many2one(
905+ 'project.sla.line', 'Service Agreement'),
906+ 'sla_warn_date': fields.datetime('Warning Date'),
907+ 'sla_limit_date': fields.datetime('Limit Date'),
908+ 'sla_start_date': fields.datetime('Start Date'),
909+ 'sla_close_date': fields.datetime('Close Date'),
910+ 'sla_achieved': fields.integer('Achieved?'),
911+ 'sla_state': fields.selection(SLA_STATES, string="SLA Status"),
912+ 'locked': fields.boolean(
913+ 'Recalculation disabled',
914+ help="Safeguard manual changes from future automatic "
915+ "recomputations."),
916+ # Future: perfect SLA manual handling
917+ }
918+
919+ def write(self, cr, uid, ids, vals, context=None):
920+ """
921+ Update the related Document's SLA State when any of the SLA Control
922+ lines changes state
923+ """
924+ res = super(SLAControl, self).write(
925+ cr, uid, ids, vals, context=context)
926+ new_state = vals.get('sla_state')
927+ if new_state:
928+ # just update sla_state without recomputing the whole thing
929+ context = context or {}
930+ context['__sla_stored__'] = 1
931+ for sla in self.browse(cr, uid, ids, context=context):
932+ doc = self.pool.get(sla.doc_model).browse(
933+ cr, uid, sla.doc_id, context=context)
934+ if doc.sla_state < new_state:
935+ doc.write({'sla_state': new_state})
936+ return res
937+
938+ def update_sla_states(self, cr, uid, context=None):
939+ """
940+ Updates SLA States, given the current datetime:
941+ Only works on "open" sla states (watching, warning and will fail):
942+ - exceeded limit date are set to "will fail"
943+ - exceeded warning dates are set to "warning"
944+ To be used by a scheduled job.
945+ """
946+ now = dt.now().strftime(DT_FMT)
947+ # SLAs to mark as "will fail"
948+ control_ids = self.search(
949+ cr, uid,
950+ [('sla_state', 'in', ['2', '3']), ('sla_limit_date', '<', now)],
951+ context=context)
952+ self.write(cr, uid, control_ids, {'sla_state': '4'}, context=context)
953+ # SLAs to mark as "warning"
954+ control_ids = self.search(
955+ cr, uid,
956+ [('sla_state', 'in', ['2']), ('sla_warn_date', '<', now)],
957+ context=context)
958+ self.write(cr, uid, control_ids, {'sla_state': '3'}, context=context)
959+ return True
960+
961+ def _compute_sla_date(self, cr, uid, working_hours, res_uid,
962+ start_date, hours, context=None):
963+ """
964+ Return a limit datetime by adding hours to a start_date, honoring
965+ a working_time calendar and a resource's (res_uid) timezone and
966+ availability (leaves)
967+
968+ Currently implemented using a binary search using
969+ _interval_hours_get() from resource.calendar. This is
970+ resource.calendar agnostic, but could be more efficient if
971+ implemented based on it's logic.
972+
973+ Known issue: the end date can be a non-working time; it would be
974+ best for it to be the latest working time possible. Example:
975+ if working time is 08:00 - 16:00 and start_date is 19:00, the +8h
976+ end date will be 19:00 of the next day, and it should rather be
977+ 16:00 of the next day.
978+ """
979+ assert isinstance(start_date, dt)
980+ assert isinstance(hours, int) and hours >= 0
981+
982+ cal_obj = self.pool.get('resource.calendar')
983+ target, step = hours * 3600, 16 * 3600
984+ lo, hi = start_date, start_date
985+ while target > 0 and step > 60:
986+ hi = lo + timedelta(seconds=step)
987+ check = int(3600 * cal_obj._interval_hours_get(
988+ cr, uid, working_hours, lo, hi,
989+ timezone_from_uid=res_uid, exclude_leaves=False,
990+ context=context))
991+ if check <= target:
992+ target -= check
993+ lo = hi
994+ else:
995+ step = int(step / 4.0)
996+ return hi
997+
998+ def _get_computed_slas(self, cr, uid, doc, context=None):
999+ """
1000+ Returns a dict with the computed data for SLAs, given a browse record
1001+ for the target document.
1002+
1003+ * The SLA used is either from a related analytic_account_id or
1004+ project_id, whatever is found first.
1005+ * The work calendar is taken from the Project's definitions ("Other
1006+ Info" tab -> Working Time).
1007+ * The timezone used for the working time calculations are from the
1008+ document's responsible User (user_id) or from the current User (uid).
1009+
1010+ For the SLA Achieved calculation:
1011+
1012+ * Creation date is used to start counting time
1013+ * Control date, used to calculate SLA achievement, is defined in the
1014+ SLA Definition rules.
1015+ """
1016+ def datetime2str(dt_value, fmt): # tolerant datetime to string
1017+ return dt_value and dt.strftime(dt_value, fmt) or None
1018+
1019+ res = []
1020+ sla_ids = (safe_getattr(doc, 'analytic_account_id.sla_ids') or
1021+ safe_getattr(doc, 'project_id.analytic_account_id.sla_ids'))
1022+ if not sla_ids:
1023+ return res
1024+
1025+ for sla in sla_ids:
1026+ if sla.control_model != doc._table_name:
1027+ continue # SLA not for this model; skip
1028+
1029+ for l in sla.sla_line_ids:
1030+ eval_context = {'o': doc, 'obj': doc, 'object': doc}
1031+ if not l.condition or safe_eval(l.condition, eval_context):
1032+ start_date = dt.strptime(doc.create_date, DT_FMT)
1033+ res_uid = doc.user_id.id or uid
1034+ cal = safe_getattr(
1035+ doc, 'project_id.resource_calendar_id.id')
1036+ warn_date = self._compute_sla_date(
1037+ cr, uid, cal, res_uid, start_date, l.warn_qty,
1038+ context=context)
1039+ lim_date = self._compute_sla_date(
1040+ cr, uid, cal, res_uid, warn_date,
1041+ l.limit_qty - l.warn_qty,
1042+ context=context)
1043+ # evaluate sla state
1044+ control_val = getattr(doc, sla.control_field_id.name)
1045+ if control_val:
1046+ control_date = dt.strptime(control_val, DT_FMT)
1047+ if control_date > lim_date:
1048+ sla_val, sla_state = 0, '5' # failed
1049+ else:
1050+ sla_val, sla_state = 1, '1' # achieved
1051+ else:
1052+ control_date = None
1053+ now = dt.now()
1054+ if now > lim_date:
1055+ sla_val, sla_state = 0, '4' # will fail
1056+ elif now > warn_date:
1057+ sla_val, sla_state = 0, '3' # warning
1058+ else:
1059+ sla_val, sla_state = 0, '2' # watching
1060+
1061+ res.append(
1062+ {'sla_line_id': l.id,
1063+ 'sla_achieved': sla_val,
1064+ 'sla_state': sla_state,
1065+ 'sla_warn_date': datetime2str(warn_date, DT_FMT),
1066+ 'sla_limit_date': datetime2str(lim_date, DT_FMT),
1067+ 'sla_start_date': datetime2str(start_date, DT_FMT),
1068+ 'sla_close_date': datetime2str(control_date, DT_FMT),
1069+ 'doc_id': doc.id,
1070+ 'doc_model': sla.control_model})
1071+ break
1072+
1073+ if sla_ids and not res:
1074+ _logger.warning("No valid SLA rule found for %d, SLA Ids %s"
1075+ % (doc.id, repr([x.id for x in sla_ids])))
1076+ return res
1077+
1078+ def store_sla_control(self, cr, uid, docs, context=None):
1079+ """
1080+ Used by controlled documents to ask for SLA calculation and storage.
1081+ ``docs`` is a Browse object
1082+ """
1083+ # context flag to avoid infinite loops on further writes
1084+ context = context or {}
1085+ if '__sla_stored__' in context:
1086+ return False
1087+ else:
1088+ context['__sla_stored__'] = 1
1089+
1090+ res = []
1091+ for ix, doc in enumerate(docs):
1092+ if ix and ix % 50 == 0:
1093+ _logger.info('...%d SLAs recomputed for %s' % (ix, doc._name))
1094+ control = {x.sla_line_id.id: x
1095+ for x in doc.sla_control_ids}
1096+ sla_recs = self._get_computed_slas(cr, uid, doc, context=context)
1097+ # calc sla control lines
1098+ if sla_recs:
1099+ slas = []
1100+ for sla_rec in sla_recs:
1101+ sla_line_id = sla_rec.get('sla_line_id')
1102+ if sla_line_id in control:
1103+ control_rec = control.get(sla_line_id)
1104+ if not control_rec.locked:
1105+ slas += m2m.write(control_rec.id, sla_rec)
1106+ else:
1107+ slas += m2m.add(sla_rec)
1108+ else:
1109+ slas = m2m.clear()
1110+ # calc sla control summary
1111+ vals = {'sla_state': None, 'sla_control_ids': slas}
1112+ if sla_recs and doc.sla_control_ids:
1113+ vals['sla_state'] = max(
1114+ x.sla_state for x in doc.sla_control_ids)
1115+ # store sla
1116+ doc._model.write( # regular users can't write on SLA Control
1117+ cr, SUPERUSER_ID, [doc.id], vals, context=context)
1118+ return res
1119+
1120+
1121+class SLAControlled(orm.AbstractModel):
1122+ """
1123+ SLA Controlled documents: AbstractModel to apply SLA control on Models
1124+ """
1125+ _name = 'project.sla.controlled'
1126+ _description = 'SLA Controlled Document'
1127+ _columns = {
1128+ 'sla_control_ids': fields.many2many(
1129+ 'project.sla.control', string="SLA Control", ondelete='cascade'),
1130+ 'sla_state': fields.selection(
1131+ SLA_STATES, string="SLA Status", readonly=True),
1132+ }
1133+
1134+ def create(self, cr, uid, vals, context=None):
1135+ res = super(SLAControlled, self).create(cr, uid, vals, context=context)
1136+ docs = self.browse(cr, uid, [res], context=context)
1137+ self.pool.get('project.sla.control').store_sla_control(
1138+ cr, uid, docs, context=context)
1139+ return res
1140+
1141+ def write(self, cr, uid, ids, vals, context=None):
1142+ res = super(SLAControlled, self).write(
1143+ cr, uid, ids, vals, context=context)
1144+ docs = [x for x in self.browse(cr, uid, ids, context=context)
1145+ if (x.state != 'cancelled') and
1146+ (x.state != 'done' or x.sla_state not in ['1', '5'])]
1147+ self.pool.get('project.sla.control').store_sla_control(
1148+ cr, uid, docs, context=context)
1149+ return res
1150+
1151+ def unlink(self, cr, uid, ids, context=None):
1152+ # Unlink and delete all related Control records
1153+ for doc in self.browse(cr, uid, ids, context=context):
1154+ vals = [m2m.remove(x.id)[0] for x in doc.sla_control_ids]
1155+ doc.write({'sla_control_ids': vals})
1156+ return super(SLAControlled, self).unlink(cr, uid, ids, context=context)
1157
1158=== added file 'project_sla/project_sla_control_data.xml'
1159--- project_sla/project_sla_control_data.xml 1970-01-01 00:00:00 +0000
1160+++ project_sla/project_sla_control_data.xml 2013-12-26 15:27:30 +0000
1161@@ -0,0 +1,18 @@
1162+<?xml version="1.0" encoding="utf-8"?>
1163+<openerp>
1164+ <data noupdate="1">
1165+
1166+ <record id="ir_cron_sla_action" model="ir.cron">
1167+ <field name="name">Update SLA States</field>
1168+ <field name="priority" eval="100"/>
1169+ <field name="interval_number">1</field>
1170+ <field name="interval_type">hours</field>
1171+ <field name="numbercall">-1</field>
1172+ <field name="doall" eval="False"/>
1173+ <field name="model">project.sla.control</field>
1174+ <field name="function">update_sla_states</field>
1175+ <field name="args">()</field>
1176+ </record>
1177+
1178+ </data>
1179+</openerp>
1180
1181=== added file 'project_sla/project_sla_control_view.xml'
1182--- project_sla/project_sla_control_view.xml 1970-01-01 00:00:00 +0000
1183+++ project_sla/project_sla_control_view.xml 2013-12-26 15:27:30 +0000
1184@@ -0,0 +1,25 @@
1185+<?xml version="1.0" encoding="utf-8"?>
1186+<openerp>
1187+ <data>
1188+
1189+ <!-- List view used when the sla_control_ids field
1190+ is added to controlled document's form -->
1191+ <record id="view_sla_control_tree" model="ir.ui.view">
1192+ <field name="name">view_sla_control_tree</field>
1193+ <field name="model">project.sla.control</field>
1194+ <field name="arch" type="xml">
1195+
1196+ <tree string="Service Level">
1197+ <field name="sla_line_id"/>
1198+ <field name="sla_state"/>
1199+ <field name="sla_start_date"/>
1200+ <field name="sla_warn_date"/>
1201+ <field name="sla_limit_date"/>
1202+ <field name="sla_close_date"/>
1203+ </tree>
1204+
1205+ </field>
1206+ </record>
1207+
1208+ </data>
1209+</openerp>
1210
1211=== added file 'project_sla/project_sla_demo.xml'
1212--- project_sla/project_sla_demo.xml 1970-01-01 00:00:00 +0000
1213+++ project_sla/project_sla_demo.xml 2013-12-26 15:27:30 +0000
1214@@ -0,0 +1,138 @@
1215+<?xml version="1.0" encoding="utf-8"?>
1216+<openerp>
1217+ <data>
1218+
1219+ <!-- Working Time calendar -->
1220+ <record id="worktime_9_18" model="resource.calendar">
1221+ <field name="name">Working Days 09-13 14-18</field>
1222+ </record>
1223+ <record id="worktime 9_18_0M" model="resource.calendar.attendance">
1224+ <field name="dayofweek">0</field>
1225+ <field name="name">Monday Morning</field>
1226+ <field name="hour_from">9</field>
1227+ <field name="hour_to">13</field>
1228+ <field name="calendar_id" ref="worktime_9_18" />
1229+ </record>
1230+ <record id="worktime 9_18_0A" model="resource.calendar.attendance">
1231+ <field name="dayofweek">0</field>
1232+ <field name="name">Monday Afternoon</field>
1233+ <field name="hour_from">14</field>
1234+ <field name="hour_to">18</field>
1235+ <field name="calendar_id" ref="worktime_9_18" />
1236+ </record>
1237+ <record id="worktime 9_18_1M" model="resource.calendar.attendance">
1238+ <field name="dayofweek">1</field>
1239+ <field name="name">Tuesday Morning</field>
1240+ <field name="hour_from">9</field>
1241+ <field name="hour_to">13</field>
1242+ <field name="calendar_id" ref="worktime_9_18" />
1243+ </record>
1244+ <record id="worktime 9_18_1A" model="resource.calendar.attendance">
1245+ <field name="dayofweek">1</field>
1246+ <field name="name">Tuesday Afternoon</field>
1247+ <field name="hour_from">14</field>
1248+ <field name="hour_to">18</field>
1249+ <field name="calendar_id" ref="worktime_9_18" />
1250+ </record>
1251+ <record id="worktime 9_18_2M" model="resource.calendar.attendance">
1252+ <field name="dayofweek">2</field>
1253+ <field name="name">Wednesday Morning</field>
1254+ <field name="hour_from">9</field>
1255+ <field name="hour_to">13</field>
1256+ <field name="calendar_id" ref="worktime_9_18" />
1257+ </record>
1258+ <record id="worktime 9_18_2A" model="resource.calendar.attendance">
1259+ <field name="dayofweek">2</field>
1260+ <field name="name">Wednesday Afternoon</field>
1261+ <field name="hour_from">14</field>
1262+ <field name="hour_to">18</field>
1263+ <field name="calendar_id" ref="worktime_9_18" />
1264+ </record>
1265+ <record id="worktime 9_18_3M" model="resource.calendar.attendance">
1266+ <field name="dayofweek">3</field>
1267+ <field name="name">Thursday Morning</field>
1268+ <field name="hour_from">9</field>
1269+ <field name="hour_to">13</field>
1270+ <field name="calendar_id" ref="worktime_9_18" />
1271+ </record>
1272+ <record id="worktime 9_18_3A" model="resource.calendar.attendance">
1273+ <field name="dayofweek">3</field>
1274+ <field name="name">Thursday Afternoon</field>
1275+ <field name="hour_from">14</field>
1276+ <field name="hour_to">18</field>
1277+ <field name="calendar_id" ref="worktime_9_18" />
1278+ </record>
1279+ <record id="worktime 9_18_4M" model="resource.calendar.attendance">
1280+ <field name="dayofweek">4</field>
1281+ <field name="name">Friday Morning</field>
1282+ <field name="hour_from">9</field>
1283+ <field name="hour_to">13</field>
1284+ <field name="calendar_id" ref="worktime_9_18" />
1285+ </record>
1286+ <record id="worktime 9_18_4A" model="resource.calendar.attendance">
1287+ <field name="dayofweek">4</field>
1288+ <field name="name">Friday Afternoon</field>
1289+ <field name="hour_from">14</field>
1290+ <field name="hour_to">18</field>
1291+ <field name="calendar_id" ref="worktime_9_18" />
1292+ </record>
1293+
1294+ <!-- Set Project Calendar -->
1295+ <record id="project.project_project_1" model="project.project">
1296+ <field name="resource_calendar_id" ref="worktime_9_18" />
1297+ </record>
1298+
1299+ <!-- SLA Definition and Rules -->
1300+ <record id="sla_resolution" model="project.sla">
1301+ <field name="name">Standard Resolution Time</field>
1302+ <field name="control_model">project.issue</field>
1303+ <field name="control_field_id"
1304+ ref="project_issue.field_project_issue_date_closed"/>
1305+ </record>
1306+ <record id="sla_resolution_rule1" model="project.sla.line">
1307+ <field name="sla_id" ref="sla_resolution"/>
1308+ <field name="sequence">10</field>
1309+ <field name="name">Resolution in two business days</field>
1310+ <field name="condition">obj.priority &lt;= '2'</field>
1311+ <field name="limit_qty">16</field>
1312+ <field name="warn_qty">8</field>
1313+ </record>
1314+ <record id="sla_resolution_rule2" model="project.sla.line">
1315+ <field name="sla_id" ref="sla_resolution"/>
1316+ <field name="sequence">20</field>
1317+ <field name="name">Resolution in three business days</field>
1318+ <field name="condition"></field>
1319+ <field name="limit_qty">24</field>
1320+ <field name="warn_qty">16</field>
1321+ </record>
1322+
1323+ <record id="sla_response" model="project.sla">
1324+ <field name="name">Standard Response Time</field>
1325+ <field name="control_model">project.issue</field>
1326+ <field name="control_field_id"
1327+ ref="project_issue.field_project_issue_date_open"/>
1328+ </record>
1329+ <record id="sla_response_rule1" model="project.sla.line">
1330+ <field name="sla_id" ref="sla_response"/>
1331+ <field name="sequence">10</field>
1332+ <field name="name">Response in one business day</field>
1333+ <field name="condition">obj.priority &lt;= '2'</field>
1334+ <field name="limit_qty">8</field>
1335+ <field name="warn_qty">4</field>
1336+ </record>
1337+ <record id="sla_response_rule2" model="project.sla.line">
1338+ <field name="sla_id" ref="sla_response"/>
1339+ <field name="sequence">20</field>
1340+ <field name="name">Response in two business days</field>
1341+ <field name="condition"></field>
1342+ <field name="limit_qty">16</field>
1343+ <field name="warn_qty">8</field>
1344+ </record>
1345+
1346+ <!-- Set Contract Resolution SLA Definition -->
1347+ <record id="project.project_project_1_account_analytic_account" model="account.analytic.account">
1348+ <field name="sla_ids" eval="[(6, 0, [ref('sla_resolution')])]" />
1349+ </record>
1350+
1351+ </data>
1352+</openerp>
1353
1354=== added file 'project_sla/project_sla_view.xml'
1355--- project_sla/project_sla_view.xml 1970-01-01 00:00:00 +0000
1356+++ project_sla/project_sla_view.xml 2013-12-26 15:27:30 +0000
1357@@ -0,0 +1,48 @@
1358+<?xml version="1.0" encoding="utf-8"?>
1359+<openerp>
1360+ <data>
1361+
1362+ <record id="view_sla_lines_tree" model="ir.ui.view">
1363+ <field name="name">view_sla_lines_tree</field>
1364+ <field name="model">project.sla.line</field>
1365+ <field name="arch" type="xml">
1366+
1367+ <tree string="Definitions">
1368+ <field name="sequence"/>
1369+ <field name="name"/>
1370+ <field name="condition"/>
1371+ <field name="limit_qty"/>
1372+ <field name="warn_qty"/>
1373+ </tree>
1374+
1375+ </field>
1376+ </record>
1377+
1378+ <record id="view_sla_form" model="ir.ui.view">
1379+ <field name="name">view_sla_form</field>
1380+ <field name="model">project.sla</field>
1381+ <field name="arch" type="xml">
1382+
1383+ <form string="SLA Definition">
1384+ <field name="name"/>
1385+ <field name="active"/>
1386+ <field name="control_model"/>
1387+ <field name="control_field_id"/>
1388+ <notebook colspan="4">
1389+ <page string="Rules" name="rules_page">
1390+ <field name="sla_line_ids" nolabel="1"/>
1391+ </page>
1392+ <page string="Contracts" name="contracts_page">
1393+ <field name="analytic_ids" nolabel="1" />
1394+ </page>
1395+ </notebook>
1396+ <button name="reapply_slas" colspan="2"
1397+ string="Reapply SLA on Contracts"
1398+ type="object" />
1399+ </form>
1400+
1401+ </field>
1402+ </record>
1403+
1404+ </data>
1405+</openerp>
1406
1407=== added file 'project_sla/project_view.xml'
1408--- project_sla/project_view.xml 1970-01-01 00:00:00 +0000
1409+++ project_sla/project_view.xml 2013-12-26 15:27:30 +0000
1410@@ -0,0 +1,20 @@
1411+<?xml version="1.0" encoding="utf-8"?>
1412+<openerp>
1413+ <data>
1414+
1415+ <record id="edit_project_sla" model="ir.ui.view">
1416+ <field name="name">edit_project_sla</field>
1417+ <field name="model">project.project</field>
1418+ <field name="inherit_id" ref="project.edit_project"/>
1419+ <field name="arch" type="xml">
1420+
1421+ <!-- make resource calendar always visible -->
1422+ <group string="Administration" position="attributes">
1423+ <attribute name="groups"/>
1424+ </group>
1425+
1426+ </field>
1427+ </record>
1428+
1429+ </data>
1430+</openerp>
1431
1432=== added directory 'project_sla/security'
1433=== added file 'project_sla/security/ir.model.access.csv'
1434--- project_sla/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
1435+++ project_sla/security/ir.model.access.csv 2013-12-26 15:27:30 +0000
1436@@ -0,0 +1,8 @@
1437+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1438+access_sla_manager,access_sla_manager,model_project_sla,project.group_project_manager,1,1,1,1
1439+access_sla_user,access_sla_user,model_project_sla,base.group_user,1,0,0,0
1440+access_sla_lines_manager,access_sla_lines_manager,model_project_sla_line,project.group_project_manager,1,1,1,1
1441+access_sla_lines_user,access_sla_lines_user,model_project_sla_line,base.group_user,1,0,0,0
1442+access_sla_control_manager,access_sla_control_manager,model_project_sla_control,project.group_project_manager,1,1,0,0
1443+access_sla_control_user,access_sla_control_user,model_project_sla_control,base.group_user,1,0,0,0
1444+access_sla_controlled_manager,access_sla_controlled_manager,model_project_sla_controlled,project.group_project_manager,1,1,1,1
1445
1446=== added directory 'project_sla/static'
1447=== added directory 'project_sla/static/src'
1448=== added directory 'project_sla/static/src/img'
1449=== added file 'project_sla/static/src/img/icon.png'
1450Binary 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-26 15:27:30 +0000 differ
1451=== added directory 'project_sla/test'
1452=== added file 'project_sla/test/project_sla.yml'
1453--- project_sla/test/project_sla.yml 1970-01-01 00:00:00 +0000
1454+++ project_sla/test/project_sla.yml 2013-12-26 15:27:30 +0000
1455@@ -0,0 +1,66 @@
1456+-
1457+ Cleanup previous test run
1458+-
1459+ !python {model: project.issue}: |
1460+ res = self.search(cr, uid, [('name', '=', 'My monitor is flickering')])
1461+ self.unlink(cr, uid, res)
1462+-
1463+ Create a new Issue
1464+-
1465+ !record {model: project.issue, id: issue1, view: False}:
1466+ name: "My monitor is flickering"
1467+ project_id: project.project_project_1
1468+ priority: "3"
1469+ user_id: base.user_root
1470+ partner_id: base.res_partner_2
1471+ email_from: agr@agrolait.com
1472+ categ_ids:
1473+ - project_issue.project_issue_category_01
1474+-
1475+ Close the Issue
1476+-
1477+ !python {model: project.issue}: |
1478+ self.case_close(cr, uid, [ref("issue1")])
1479+-
1480+ Force the Issue's Create Date and Close Date
1481+ Created friday before opening hour, closed on next monday near closing hour
1482+-
1483+ !python {model: project.issue}: |
1484+ import time
1485+ self.write(cr, uid, [ref("issue1"),], {
1486+ 'create_date': time.strftime('2013-11-22 06:15:00'),
1487+ 'date_closed': time.strftime('2013-11-25 16:45:00'),
1488+ })
1489+-
1490+ There should be Service Level info generated on the Issue
1491+-
1492+ !assert {model: project.issue, id: issue1, string: Issue should have calculated service levels}:
1493+ - len(sla_control_ids) == 2
1494+-
1495+ Assign an additional "Response SLA" to the Contract
1496+-
1497+ !python {model: account.analytic.account}: |
1498+ self.write(cr, uid, [ref('project.project_project_1_account_analytic_account')],
1499+ {'sla_ids': [(4, ref('sla_response'))]})
1500+-
1501+ Button to Reapply the SLA Definition
1502+-
1503+ !python {model: project.sla}: |
1504+ self._reapply_slas(cr, uid, [ref('sla_resolution')], recalc_closed=True)
1505+-
1506+ There should be two Service Level lines generated on the Issue
1507+-
1508+ !assert {model: project.issue, id: issue1, string: Issue should have two calculated service levels}:
1509+ - len(sla_control_ids) == 2
1510+-
1511+ The Issue's Resolution SLA should be "3 business days"
1512+-
1513+ !python {model: project.issue}: |
1514+ issue = self.browse(cr, uid, ref('issue1'))
1515+ for x in issue.sla_control_ids:
1516+ print x.sla_line_id.name
1517+ if x.sla_line_id.id == ref("sla_resolution_rule2"):
1518+ assert x.sla_achieved == 1, "Issue resolution SLA should be achieved"
1519+ break
1520+ else:
1521+ assert False, 'Issue Resolution SLA should be "3 business days"'

Subscribers

People subscribed via source and target branches