Ajout d'un champ charge_reel dans chantier
[OpenERP/cmmi.git] / axes.py
1 #-*- coding: utf8 -*-
2 '''
3 '''
4
5 from openerp.osv import osv, fields
6 from datetime import date, timedelta, datetime
7
8 # ================================ MESURABLE ================================ #
9 class Mesurable(osv.Model):
10     _name = "cmmi.axes.mesurable"
11
12     _description = "Table de reference des mesusrables."
13
14     _states = [("cree", "Crée"), ("encours", "En cours"),
15                 ("termine", "Terminé"), ("abandonne", "Abandonné"),
16                 ("suspendu", "Suspendu"), ("generique", "Générique")]
17
18
19     def _nb_jours_init(self, cr, uid, ids, field, arg, context=None):
20         result = {}
21         for m in self.browse(cr, uid, ids, context=context):
22             if not m.date_init_deb or not m.date_init_fin:
23                 result[m.id] = 0
24                 continue
25             result[m.id] = Mesurable._nb_jours_ouvre_entre_2_dates(
26                         datetime.strptime(m.date_init_deb, "%Y-%m-%d").date(),
27                         datetime.strptime(m.date_init_fin, "%Y-%m-%d").date())
28         return result
29
30
31     def _nb_jours_plan(self, cr, uid, ids, field, arg, context=None):
32         result = {}
33         for m in self.browse(cr, uid, ids, context=context):
34             if not m.date_plan_deb or not m.date_plan_fin:
35                 result[m.id] = 0
36                 continue
37             result[m.id] = Mesurable._nb_jours_ouvre_entre_2_dates(
38                         datetime.strptime(m.date_plan_deb, "%Y-%m-%d").date(),
39                         datetime.strptime(m.date_plan_fin, "%Y-%m-%d").date())
40         return result
41
42
43     def _nb_jours_reel(self, cr, uid, ids, field, arg, context=None):
44         result = {}
45         for m in self.browse(cr, uid, ids, context=context):
46             if not m.date_reel_deb or not m.date_reel_fin:
47                 result[m.id] = 0
48                 continue
49             result[m.id] = Mesurable._nb_jours_ouvre_entre_2_dates(
50                         datetime.strptime(m.date_reel_deb, "%Y-%m-%d").date(),
51                         datetime.strptime(m.date_reel_fin, "%Y-%m-%d").date())
52         return result
53
54
55     _columns = {
56         "name": fields.char(string="Title", size=64, required=True),
57         "description": fields.text(string="Description"),
58         "commentaire": fields.text(string="Commentaire"),
59         "state": fields.selection(_states, string="State"),
60         "version": fields.char(string="Version", size=16),
61         "date_jalon": fields.date(string="Jalon"),
62         "date_init_deb": fields.date(string="Init début"),
63         "date_init_fin": fields.date(string="Init fin"),
64         "date_plan_deb": fields.date(string="Plan début"),
65         "date_plan_fin": fields.date(string="Plan fin"),
66         "date_reel_deb": fields.date(string="Réel début"),
67         "date_reel_fin": fields.date(string="Réel fin"),
68         "nb_jours_init": fields.function(_nb_jours_init,
69                                          type="integer",
70                                          string="Nombre de jours initials"),
71         "nb_jours_plan": fields.function(_nb_jours_plan,
72                                          type="integer",
73                                          string="Nombre de jours planifiés"),
74         "nb_jours_reel": fields.function(_nb_jours_reel,
75                                          type="integer",
76                                          string="Nombre de jours réels"),
77     }
78
79     _defaults = {
80         "state": "cree",
81     }
82
83     _sql_constraints = [
84         (
85             "date_init_deb_before_date_init_fin",
86             "CHECK(date_init_deb <= date_init_fin)",
87             "The date_init_deb should be previous date_init_fin",
88         ),
89         (
90             "date_plan_deb_before_date_plan_fin",
91             "CHECK(date_plan_deb <= date_plan_fin)",
92             "The date_plan_deb should be previous date_plan_fin",
93         ),
94         (
95             "date_reel_deb_before_date_reel_fin",
96             "CHECK(date_reel_deb <= date_reel_fin)",
97             "The date_reel_deb should be previous date_reel_fin",
98         ),
99     ]
100
101
102     def action_commencer(self, cr, uid, ids, context=None):
103         if type(ids) == list:
104             if len(ids) != 1:
105                 return
106             ids = ids[0]
107
108         palier = self.read(cr, uid, ids, ['date_plan_deb', 'date_plan_fin', 'state'], context)
109
110         if palier['state'] != 'cree':
111             return
112
113         self.write(
114             cr,
115             uid,
116             ids, {
117                 'date_init_deb' : palier['date_plan_deb'],
118                 'date_init_fin' : palier['date_plan_fin'],
119                 'state': 'encours'
120             },
121             context)
122         return self
123
124
125     def action_suspendre(self, cr, uid, ids, context=None):
126         if type(ids) == list:
127             if len(ids) != 1:
128                 return # TODO: message d'avertissement
129             ids = ids[0]
130
131         mesurable = self.read(cr, uid, ids, ['state'], context)
132         if mesurable['state'] != 'encours':
133             return
134         self.write(
135             cr,
136             uid,
137             ids,
138             {'state': 'suspendu'},
139             context,
140         )
141         return self
142
143     def action_terminer(self, cr, uid, ids, context=None):
144         if type(ids) == list:
145             if len(ids) != 1:
146                 return # TODO: message d'avertissement
147             ids = ids[0]
148
149         mesurable = self.read(cr, uid, ids, ['state'], context)
150         if mesurable['state'] != 'encours':
151             return
152         self.write(
153             cr,
154             uid,
155             ids,
156             {'state': 'termine'},
157             context,
158         )
159         return self
160
161     def action_abandonner(self, cr, uid, ids, context=None):
162         if type(ids) == list:
163             if len(ids) != 1:
164                 return # TODO: message d'avertissement
165             ids = ids[0]
166
167         mesurable = self.read(cr, uid, ids, ['state'], context)
168         if not ('encours', 'cree').__contains__(mesurable['state']):
169             return
170         self.write(
171             cr,
172             uid,
173             ids,
174             {'state': 'abandonne'},
175             context,
176         )
177         return self
178
179     def action_reprendre(self, cr, uid, ids, context=None):
180         if type(ids) == list:
181             if len(ids) != 1:
182                 return # TODO: message d'avertissement
183             ids = ids[0]
184
185         mesurable = self.read(cr, uid, ids, ['state'], context)
186         if mesurable['state'] != 'suspendu':
187             return
188         self.write(
189             cr,
190             uid,
191             ids,
192             {'state': 'encours'},
193             context,
194         )
195         return self
196
197 #------------ TRAVAIL CALCUL JOURS OUVRES ------------
198     @staticmethod
199     def _get_date_paques(annee):
200         """
201         Retourne la date du dimanque de pâques pour une année donnée
202             sous la forme d'un objet date.
203         """
204         a = annee % 19
205         b = annee // 100
206         c = annee % 100
207         d = (19 * a + b - b // 4 - ((b - (b + 8) // 25 + 1) // 3) + 15) % 30
208         e = (32 + 2 * (b % 4) + 2 * (c // 4) - d - (c % 4)) % 7
209         f = d + e - 7 * ((a + 11 * d + 22 * e) // 451) + 114
210         mois = f // 31
211         jours = f % 31 + 1
212         return date(annee, mois, jours)
213
214
215     @staticmethod
216     def _get_jours_feries(annee):
217         """
218         Retourne une liste contenant les jours fériés d'une année donnée.
219         """
220         date_paques = Mesurable._get_date_paques(annee)
221         return [
222             date_paques+timedelta(days=1), # Lundi de Pâques
223             date_paques+timedelta(days=39), # Jeudi de l'Ascension
224             date_paques+timedelta(days=50), # Lundi de Pentecôte
225             date(annee, 1, 1), # Jour de l'An
226             date(annee, 5, 1), # Fête du travail
227             date(annee, 5, 8), # Armistice 1945
228             date(annee, 7, 14), # Fête Nationale
229             date(annee, 8, 15), # Assomption
230             date(annee, 11, 1), # Toussaint
231             date(annee, 11, 11), # Armistice 1918
232             date(annee, 12, 25), # Noël
233         ]
234
235
236     @staticmethod
237     def _jour_ouvre(d):
238         """ Retourne vrai si la date est un jour ouvre, faux sinon."""
239         if (5,6).__contains__(d.weekday()) or Mesurable._get_jours_feries(d.year).__contains__(d):
240             return False
241         else:
242             return True
243
244
245     @staticmethod
246     def _nb_jours_ouvre_entre_2_dates(d1, d2):
247         """
248         Retourne le nombre de jours ouvres entre deux dates données.
249
250         >>> nb_jours_ouvre_entre_2_dates(date(2013, 5, 1), date(2013, 7, 31))
251         62
252         >>> nb_jours_ouvre_entre_2_dates(date(2013, 7, 31), date(2013, 5, 1))
253         62
254         """
255         if not d1 or not d2:
256             return 0
257         if d1>d2:
258             d1, d2 = d2, d1 #Switch les 2 dates pour que d1 soit la plus petite
259         tmp = d1
260         jour_ouvres = 0
261         while tmp <= d2:
262             if Mesurable._jour_ouvre(tmp): # Si tmp est un jour ouvre
263                 jour_ouvres += 1
264             tmp += timedelta(days=1)
265         return jour_ouvres
266
267
268
269 # ================================= PALIER ================================== #
270 class Palier(osv.Model):
271     _name = "cmmi.axes.palier"
272
273     _description = "Palier d'un projet."
274
275     _inherit = "cmmi.axes.mesurable"
276
277     _types_palier = [("normal", "Normal"), ("exceptionnel", "Exceptionnel"),
278                      ("correctif", "Correctif"), ("autre", "Autre")]
279
280
281     def _get_charge_init(self, cr, uid, ids, field, arg, context=None):
282         result = {}
283         for palier in self.browse(cr, uid, ids, context=context):
284             result[palier.id] = sum([e.charge_init for e in palier.evolutions])
285         return result
286
287
288     def _get_charge_plan(self, cr, uid, ids, field, arg, context=None):
289         result = {}
290         for palier in self.browse(cr, uid, ids, context=context):
291             result[palier.id] = sum([e.charge_plan for e in palier.evolutions])
292         return result
293
294
295     _columns = {
296         "type_palier": fields.selection(_types_palier, string="Type"),
297         "projet_id": fields.many2one("cmmi.projet",
298                                      string="Projet",
299                                      required=True),
300         "evolutions": fields.one2many("cmmi.evolution",
301                                       "palier_id",
302                                       string="Evolutions"),
303         "phases": fields.one2many("cmmi.axes.palier.phase",
304                                   "palier_id",
305                                   string="Phases"),
306         "charge_init": fields.function(_get_charge_init,
307                                        type="integer",
308                                        string="Charge initiale"),
309         "charge_plan": fields.function(_get_charge_plan,
310                                        type="integer",
311                                        string="Charge plannifiée"),
312     }
313
314     _defaults = {
315         "type_palier": "normal",
316     }
317
318
319     def create(self, cr, uid, vals, context=None):
320         palier_id = osv.Model.create(self, cr, uid, vals, context=context)
321
322         # Récupération des ids de toutes les phases
323         phase_model = self.pool.get("cmmi.projet.phase")
324         phases_ids = phase_model.search(cr, uid, [('selectionne', '=', True)])
325
326         palier_model = self.pool.get("cmmi.axes.palier")
327         palier = palier_model.read(cr, uid, palier_id, ['date_plan_deb', 'date_plan_fin'])
328
329         # Création des PalierPhase
330         palier_phase_model = self.pool.get("cmmi.axes.palier.phase")
331         for phase_id in phases_ids:
332             palier_phase_model.create(
333                 cr,
334                 uid,
335                 {
336                     'phase_id': phase_id,
337                     'palier_id': palier_id,
338                     'date_plan_deb': palier['date_plan_deb'],
339                     'date_plan_fin': palier['date_plan_fin'],
340                 }
341             )
342         return palier_id
343
344
345
346 # =============================== PALIER-PHASE ============================== #
347 class PalierPhase(osv.Model):
348     _name = "cmmi.axes.palier.phase"
349
350     _description = "Phase d'un palier"
351
352     _inherit = "cmmi.axes.mesurable"
353
354     def _get_name(self, cr, uid, ids, field_name=None, arg=None, context=None):
355         if isinstance(ids, (int, long)):
356             ids = [ids]
357         return dict([(i, r.phase_id.name) for i, r in
358                 zip(ids, self.browse(cr, uid, ids, context=context))])
359
360
361     def _get_charge_init(self, cr, uid, ids, field, arg, context=None):
362         result = {}
363         for pp in self.browse(cr, uid, ids, context=context):
364             result[pp.id] = sum([p.charge_init for p in pp.phases])
365         return result
366
367
368     def _get_charge_plan(self, cr, uid, ids, field, arg, context=None):
369         result = {}
370         for pp in self.browse(cr, uid, ids, context=context):
371             result[pp.id] = sum([p.charge_plan for p in pp.phases])
372         return result
373
374     def _get_charge_reel(self, cr, uid, ids, field, arg, context=None):
375         result = {}
376         for pp in self.browse(cr, uid, ids, context=context):
377             result[pp.id] = sum([c.quantite for c in pp.charges])
378         return result
379
380
381     _columns = {
382         "name": fields.function(_get_name,
383                                 type='char',
384                                 store=True,
385                                 string="Nom de la phase"),
386         "phase_id": fields.many2one("cmmi.projet.phase",
387                                     string="Phase du projet"),
388         "palier_id": fields.many2one("cmmi.axes.palier",
389                                      string="Palier"),
390         "charge_init": fields.function(_get_charge_init,
391                                        type="integer",
392                                        string="Charge initiale"),
393         "charge_plan": fields.function(_get_charge_plan,
394                                        type="integer",
395                                        string="Charge plannifiée"),
396         "charge_reel": fields.function(_get_charge_reel,
397                                        type="integer",
398                                        string="Charge plannifiée"),
399         # backrefs
400         "charges": fields.one2many("cmmi.evolution.charge",
401                                    "phase_id",
402                                    string="Charges"),
403         "phases": fields.one2many("cmmi.evolution.phase",
404                                   "phase_id",
405                                   string="Phases"),
406 #        "evolutions": fields.one2many("cmmi.evolution", #Supprimé !
407 #                                      "phase_id",
408 #                                      string="Evolutions"),
409     }
410
411     def create(self, cr, uid, vals, context=None):
412         # TODO: gérer la création d'une phase de palier.
413         # Vérifier les valeurs contenues dans vals et les modifier / rajouter si nécessaire selon les cas suivants
414
415         # Si description est vide, alors par défaut, recopie de la description du palier et de la phase (concaténés avec un retour à la ligne entre les deux).
416         # Si commentaire est vide, alors par défaut, recopie du commentaire du palier.
417         # Si version est vide, alors par dégaut, recopie de la version du palier.
418
419         return osv.Model.create(self, cr, uid, vals, context=context)
420
421
422 # ================================ CHANTIER ================================= #
423 class Chantier(osv.Model):
424     _name = "cmmi.axes.chantier"
425
426     _description = "Chantiers d'un projet."
427
428     _inherit = "cmmi.axes.mesurable"
429
430
431     def _get_charge_init(self, cr, uid, ids, field, arg, context=None):
432         result = {}
433         for chantier in self.browse(cr, uid, ids, context=context):
434             result[chantier.id] = sum([e.charge_init for e in chantier.evolutions])
435         return result
436
437
438     def _get_charge_plan(self, cr, uid, ids, field, arg, context=None):
439         result = {}
440         for chantier in self.browse(cr, uid, ids, context=context):
441             result[chantier.id] = sum([e.charge_plan for e in chantier.evolutions])
442         return result
443
444     def _get_charge_reel(self, cr, uid, ids, field, arg, context=None):
445         result = {}
446         for c in self.browse(cr, uid, ids, context=context):
447             result[c.id] = sum([e.charge_reel for e in c.evolutions])
448         return result
449
450
451     _columns = {
452         "projet_id": fields.many2one("cmmi.projet",
453                                      string="Projet",
454                                      required=True),
455         "module_ids": fields.many2many("cmmi.description.module",
456                                        "cmmi_module_chantier_rel",
457                                        "chantier_id",
458                                        "module_id",
459                                        "Modules"),
460         "evolutions": fields.one2many("cmmi.evolution",
461                                       "chantier_id",
462                                       string="Evolutions"),
463         "charge_init": fields.function(_get_charge_init,
464                                        type="integer",
465                                        string="Charge initiale"),
466         "charge_plan": fields.function(_get_charge_plan,
467                                        type="integer",
468                                        string="Charge plannifiée"),
469         "charge_reel": fields.function(_get_charge_reel,
470                                        type="integer",
471                                        string="Charge réelle"),
472     }