Merge lp:~dreis-pt/contract-management/7.0-project_sla-dr into lp:~contract-management-core-editors/contract-management/7.0
- 7.0-project_sla-dr
- Merge into 7.0
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 |
Related bugs: |
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 |
Commit message
Description of the change
New module for SLA control.
Replaces the former MP: https:/
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote : | # |
Daniel Reis (dreis-pt) wrote : | # |
Good to know that, thanks.
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote : | # |
PEP8 issues:
project_
project_
project_
project_
project_
project_
project_
project_
project_
Code issues:
project_
project_
project_
project_
project_
project_
project_
project_
project_
project_
project_
project_
project_
Minor code issues:
project_
project_
project_
project_
project_
project_
project_
project_
project_
Spelling:
project_
project_
project_
project_
project_
- 10. By Daniel Reis
-
Minor fixes
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:/
The E241 were fixed.
> Code issues:
> project_
> 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_
> dt.now(
> project_
> project_
> project_
> project_
Yes; fixed.
> project_
> project_
> project_
> project_
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://
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote : | # |
> > project_
> > project_
> > project_
> > project_
>
> 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_
project_
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:/
Essentially if you invoke _(self.
#, 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://
> 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.
Daniel Reis (dreis-pt) wrote : | # |
> project_
> project_
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:/
> /openobject-
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.
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote : | # |
LGTM
Preview Diff
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' |
556 | 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-26 15:27:30 +0000 differ |
557 | === added file 'project_sla/images/20_sla_definition.png' |
558 | 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-26 15:27:30 +0000 differ |
559 | === added file 'project_sla/images/30_sla_controlled.png' |
560 | 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-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 <= '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 <= '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' |
1450 | 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-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"' |
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.