You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

495 lines
22 KiB

7 years ago
7 years ago
7 years ago
7 years ago
  1. # -*- coding: utf-8 -*-
  2. from openerp import models, fields, api, _
  3. from openerp.exceptions import ValidationError
  4. from datetime import timedelta, datetime
  5. import logging
  6. from openerp.osv.fields import related
  7. _logger = logging.getLogger(__name__)
  8. PERIOD = 28 # TODO: use system parameter
  9. def add_days_delta(date_from, days_delta):
  10. if not date_from:
  11. return date_from
  12. next_date = fields.Date.from_string(date_from) + timedelta(days=days_delta)
  13. return fields.Date.to_string(next_date)
  14. class ExemptReason(models.Model):
  15. _name = 'cooperative.exempt.reason'
  16. name = fields.Char(required=True)
  17. class HistoryStatus(models.Model):
  18. _name = 'cooperative.status.history'
  19. _order= 'create_date desc'
  20. status_id = fields.Many2one('cooperative.status')
  21. cooperator_id = fields.Many2one('res.partner')
  22. change = fields.Char()
  23. type = fields.Selection([('status', 'Status Change'), ('counter', 'Counter Change')])
  24. user_id = fields.Many2one('res.users', string="User")
  25. class CooperativeStatus(models.Model):
  26. _name = 'cooperative.status'
  27. _rec_name = 'cooperator_id'
  28. _order = 'cooperator_id'
  29. today = fields.Date(help="Field that allow to compute field and store them even if they are based on the current date", default=fields.Date.today)
  30. cooperator_id = fields.Many2one('res.partner')
  31. active = fields.Boolean(related="cooperator_id.active", store=True, index=True)
  32. info_session = fields.Boolean('Information Session ?')
  33. info_session_date = fields.Datetime('Information Session Date')
  34. super = fields.Boolean("Super Cooperative")
  35. sr = fields.Integer("Compteur shift regulier", default=0)
  36. sc = fields.Integer("Compteur shift de compensation", default=0)
  37. time_extension = fields.Integer("Extension Days NB", default=0, help="Addtional days to the automatic extension, 5 mean that you have a total of 15 extension days of default one is set to 10")
  38. holiday_start_time = fields.Date("Holidays Start Day")
  39. holiday_end_time = fields.Date("Holidays End Day")
  40. alert_start_time = fields.Date("Alert Start Day")
  41. extension_start_time = fields.Date("Extension Start Day")
  42. #Champ compute
  43. working_mode = fields.Selection(
  44. [
  45. ('regular', 'Regular worker'),
  46. ('irregular', 'Irregular worker'),
  47. ('exempt', 'Exempted'),
  48. ],
  49. string="Working mode"
  50. )
  51. exempt_reason_id = fields.Many2one('cooperative.exempt.reason', 'Exempt Reason')
  52. status = fields.Selection([('ok', 'Up to Date'),
  53. ('holiday', 'Holidays'),
  54. ('alert', 'Alerte'),
  55. ('extension', 'Extension'),
  56. ('suspended', 'Suspended'),
  57. ('exempted', 'Exempted'),
  58. ('unsubscribed', 'Unsubscribed'),
  59. ('resigning', 'Resigning')],
  60. compute="_compute_status", string="Cooperative Status", store=True)
  61. can_shop = fields.Boolean(compute='_compute_status', store=True)
  62. history_ids = fields.One2many('cooperative.status.history', 'status_id', readonly=True)
  63. unsubscribed = fields.Boolean(default=False, help="Manually unsubscribed")
  64. resigning = fields.Boolean(default=False, help="Want to leave the beescoop")
  65. #Specific to irregular
  66. irregular_start_date = fields.Date() #TODO migration script
  67. irregular_absence_date = fields.Date()
  68. irregular_absence_counter = fields.Integer() #TODO unsubscribe when reach -2
  69. future_alert_date = fields.Date(compute='_compute_future_alert_date')
  70. next_countdown_date = fields.Date(compute='_compute_next_countdown_date')
  71. temporary_exempt_reason_id = fields.Many2one('cooperative.exempt.reason', 'Exempt Reason')
  72. temporary_exempt_start_date = fields.Date()
  73. temporary_exempt_end_date = fields.Date()
  74. @api.depends('today', 'sr', 'sc', 'holiday_end_time',
  75. 'holiday_start_time', 'time_extension',
  76. 'alert_start_time', 'extension_start_time',
  77. 'unsubscribed', 'irregular_absence_date',
  78. 'irregular_absence_counter', 'temporary_exempt_start_date',
  79. 'temporary_exempt_end_date', 'resigning', 'cooperator_id.subscribed_shift_ids')
  80. def _compute_status(self):
  81. alert_delay = int(self.env['ir.config_parameter'].get_param('alert_delay', 28))
  82. grace_delay = int(self.env['ir.config_parameter'].get_param('default_grace_delay', 10))
  83. update = int(self.env['ir.config_parameter'].get_param('always_update', False))
  84. for rec in self:
  85. if update or not rec.today:
  86. rec.status = 'ok'
  87. rec.can_shop = True
  88. continue
  89. if rec.resigning:
  90. rec.status = 'resigning'
  91. rec.can_shop = False
  92. continue
  93. if rec.working_mode == 'regular':
  94. rec._set_regular_status(grace_delay, alert_delay)
  95. elif rec.working_mode == 'irregular':
  96. rec._set_irregular_status(grace_delay, alert_delay)
  97. elif rec.working_mode == 'exempt':
  98. rec.status = 'ok'
  99. rec.can_shop = True
  100. @api.depends('today', 'irregular_start_date', 'sr', 'holiday_start_time',
  101. 'holiday_end_time', 'temporary_exempt_start_date',
  102. 'temporary_exempt_end_date')
  103. def _compute_future_alert_date(self):
  104. """Compute date before which the worker is up to date"""
  105. for rec in self:
  106. # Only for irregular worker
  107. if rec.working_mode != 'irregular' and not rec.irregular_start_date:
  108. rec.future_alert_date = False
  109. # Alert start time already set
  110. elif rec.alert_start_time:
  111. rec.future_alert_date = False
  112. # Holidays are not set properly
  113. elif bool(rec.holiday_start_time) != bool(rec.holiday_end_time):
  114. rec.future_alert_date = False
  115. # Exemption have not a start and end time
  116. elif (bool(rec.temporary_exempt_start_date)
  117. != bool(rec.temporary_exempt_end_date)):
  118. rec.future_alert_date = False
  119. else:
  120. date = rec.today
  121. counter = rec.sr
  122. # Simulate the countdown
  123. while counter > 0:
  124. date = self._next_countdown_date(
  125. rec.irregular_start_date, date
  126. )
  127. # Check holidays
  128. if (rec.holiday_start_time and rec.holiday_end_time
  129. and date >= rec.holiday_start_time
  130. and date <= rec.holiday_end_time):
  131. continue
  132. # Check temporary exemption
  133. elif (rec.temporary_exempt_start_date
  134. and rec.temporary_exempt_end_date
  135. and date >= rec.temporary_exempt_start_date
  136. and date <= rec.temporary_exempt_end_date):
  137. continue
  138. else:
  139. counter -= 1
  140. date = add_days_delta(date, 1)
  141. rec.future_alert_date = self._next_countdown_date(
  142. rec.irregular_start_date, date
  143. )
  144. @api.depends('today', 'irregular_start_date', 'holiday_start_time',
  145. 'holiday_end_time', 'temporary_exempt_start_date',
  146. 'temporary_exempt_end_date')
  147. def _compute_next_countdown_date(self):
  148. """
  149. Compute the following countdown date. This date is the date when
  150. the worker will see his counter changed du to the cron. This
  151. date is like the birthday date of the worker that occurred each
  152. PERIOD.
  153. """
  154. for rec in self:
  155. # Only for irregular worker
  156. if rec.working_mode != 'irregular' and not rec.irregular_start_date:
  157. rec.next_countdown_date = False
  158. # Holidays are not set properly
  159. elif bool(rec.holiday_start_time) != bool(rec.holiday_end_time):
  160. rec.next_countdown_date = False
  161. # Exemption have not a start and end time
  162. elif (bool(rec.temporary_exempt_start_date)
  163. != bool(rec.temporary_exempt_end_date)):
  164. rec.next_countdown_date = False
  165. else:
  166. date = rec.today
  167. next_countdown_date = False
  168. while not next_countdown_date:
  169. date = self._next_countdown_date(rec.irregular_start_date, date)
  170. # Check holidays
  171. if (rec.holiday_start_time and rec.holiday_end_time
  172. and date >= rec.holiday_start_time
  173. and date <= rec.holiday_end_time):
  174. date = add_days_delta(date, 1)
  175. continue
  176. # Check temporary exemption
  177. elif (rec.temporary_exempt_start_date
  178. and rec.temporary_exempt_end_date
  179. and date >= rec.temporary_exempt_start_date
  180. and date <= rec.temporary_exempt_end_date):
  181. date = add_days_delta(date, 1)
  182. continue
  183. else:
  184. next_countdown_date = date
  185. rec.next_countdown_date = next_countdown_date
  186. def _next_countdown_date(self, irregular_start_date, today=False):
  187. """
  188. Return the next countdown date given irregular_start_date and
  189. today dates.
  190. This does not take holiday and other status into account.
  191. """
  192. today = today or fields.Date.today()
  193. today_dt = fields.Date.from_string(today)
  194. irregular_start_dt = fields.Date.from_string(irregular_start_date)
  195. delta = (today_dt - irregular_start_dt).days
  196. if not delta % PERIOD:
  197. return today
  198. return add_days_delta(today, PERIOD - (delta % PERIOD))
  199. def _set_regular_status(self, grace_delay, alert_delay):
  200. self.ensure_one()
  201. counter_unsubscribe = int(self.env['ir.config_parameter'].get_param('regular_counter_to_unsubscribe', -4))
  202. ok = self.sr >= 0 and self.sc >= 0
  203. grace_delay = grace_delay + self.time_extension
  204. if (self.sr + self.sc) <= counter_unsubscribe or self.unsubscribed:
  205. self.status = 'unsubscribed'
  206. self.can_shop = False
  207. elif self.today >= self.temporary_exempt_start_date and self.today <= self.temporary_exempt_end_date:
  208. self.status = 'exempted'
  209. self.can_shop = True
  210. #Transition to alert sr < 0 or stay in alert sr < 0 or sc < 0 and thus alert time is defined
  211. elif not ok and self.alert_start_time and self.extension_start_time and self.today <= add_days_delta(self.extension_start_time, grace_delay):
  212. self.status = 'extension'
  213. self.can_shop = True
  214. elif not ok and self.alert_start_time and self.extension_start_time and self.today > add_days_delta(self.extension_start_time, grace_delay):
  215. self.status = 'suspended'
  216. self.can_shop = False
  217. elif not ok and self.alert_start_time and self.today > add_days_delta(self.alert_start_time, alert_delay):
  218. self.status = 'suspended'
  219. self.can_shop = False
  220. elif (self.sr < 0) or (not ok and self.alert_start_time):
  221. self.status = 'alert'
  222. self.can_shop = True
  223. #Check for holidays; Can be in holidays even in alert or other mode ?
  224. elif self.today >= self.holiday_start_time and self.today <= self.holiday_end_time:
  225. self.status = 'holiday'
  226. self.can_shop = False
  227. elif ok or (not self.alert_start_time and self.sr >= 0):
  228. self.status = 'ok'
  229. self.can_shop = True
  230. def _set_irregular_status(self, grace_delay, alert_delay):
  231. counter_unsubscribe = int(self.env['ir.config_parameter'].get_param('irregular_counter_to_unsubscribe', -3))
  232. self.ensure_one()
  233. ok = self.sr >= 0
  234. grace_delay = grace_delay + self.time_extension
  235. if self.sr <= counter_unsubscribe or self.unsubscribed:
  236. self.status = 'unsubscribed'
  237. self.can_shop = False
  238. elif self.today >= self.temporary_exempt_start_date and self.today <= self.temporary_exempt_end_date:
  239. self.status = 'exempted'
  240. self.can_shop = True
  241. #Transition to alert sr < 0 or stay in alert sr < 0 or sc < 0 and thus alert time is defined
  242. elif not ok and self.alert_start_time and self.extension_start_time and self.today <= add_days_delta(self.extension_start_time, grace_delay):
  243. self.status = 'extension'
  244. self.can_shop = True
  245. elif not ok and self.alert_start_time and self.extension_start_time and self.today > add_days_delta(self.extension_start_time, grace_delay):
  246. self.status = 'suspended'
  247. self.can_shop = False
  248. elif not ok and self.alert_start_time and self.today > add_days_delta(self.alert_start_time, alert_delay):
  249. self.status = 'suspended'
  250. self.can_shop = False
  251. elif (self.sr < 0) or (not ok and self.alert_start_time):
  252. self.status = 'alert'
  253. self.can_shop = True
  254. #Check for holidays; Can be in holidays even in alert or other mode ?
  255. elif self.today >= self.holiday_start_time and self.today <= self.holiday_end_time:
  256. self.status = 'holiday'
  257. self.can_shop = False
  258. elif ok or (not self.alert_start_time and self.sr >= 0):
  259. self.status = 'ok'
  260. self.can_shop = True
  261. @api.multi
  262. def write(self, vals):
  263. """
  264. Overwrite write to historize the change
  265. """
  266. for field in ['sr', 'sc', 'time_extension', 'extension_start_time', 'alert_start_time', 'unsubscribed']:
  267. if not field in vals:
  268. continue
  269. for rec in self:
  270. data = {
  271. 'status_id': rec.id,
  272. 'cooperator_id': rec.cooperator_id.id,
  273. 'type': 'counter',
  274. 'user_id': self.env.context.get('real_uid', self.env.uid),
  275. }
  276. if vals.get(field, rec[field]) != rec[field]:
  277. data['change'] = '%s: %s -> %s' % (field.upper(), rec[field], vals.get(field))
  278. self.env['cooperative.status.history'].sudo().create(data)
  279. return super(CooperativeStatus, self).write(vals)
  280. def _state_change(self, new_state):
  281. self.ensure_one()
  282. if new_state == 'alert':
  283. self.write({'alert_start_time': self.today, 'extension_start_time': False, 'time_extension': 0})
  284. if new_state == 'ok':
  285. data = {'extension_start_time': False, 'time_extension': 0}
  286. data['alert_start_time'] = False
  287. self.write(data)
  288. if new_state == 'unsubscribed' or new_state == 'resigning':
  289. # Remove worker from task_templates
  290. self.cooperator_id.sudo().write(
  291. {'subscribed_shift_ids': [(5, 0, 0)]})
  292. # Remove worker from supercoop in task_templates
  293. task_tpls = self.env['beesdoo.shift.template'].search(
  294. [('super_coop_id', 'in', self.cooperator_id.user_ids.ids)]
  295. )
  296. task_tpls.write({'super_coop_id': False})
  297. # Remove worker for future tasks (remove also supercoop)
  298. self.env['beesdoo.shift.shift'].sudo().unsubscribe_from_today(
  299. [self.cooperator_id.id], now=fields.Datetime.now()
  300. )
  301. def _change_counter(self, data):
  302. self.sc += data.get('sc', 0)
  303. self.sr += data.get('sr', 0)
  304. self.irregular_absence_counter += data.get('irregular_absence_counter', 0)
  305. self.irregular_absence_date = data.get('irregular_absence_date', False)
  306. @api.multi
  307. def _write(self, vals):
  308. """
  309. Overwrite write to historize the change of status
  310. and make action on status change
  311. """
  312. if 'status' in vals:
  313. self._cr.execute('select id, status, sr, sc from "%s" where id in %%s' % self._table, (self._ids,))
  314. result = self._cr.dictfetchall()
  315. old_status_per_id = {r['id'] : r for r in result}
  316. for rec in self:
  317. if old_status_per_id[rec.id]['status'] != vals['status']:
  318. data = {
  319. 'status_id': rec.id,
  320. 'cooperator_id': rec.cooperator_id.id,
  321. 'type': 'status',
  322. 'change': "STATUS: %s -> %s" % (old_status_per_id[rec.id]['status'], vals['status']),
  323. 'user_id': self.env.context.get('real_uid', self.env.uid),
  324. }
  325. self.env['cooperative.status.history'].sudo().create(data)
  326. rec._state_change(vals['status'])
  327. return super(CooperativeStatus, self)._write(vals)
  328. _sql_constraints = [
  329. ('cooperator_uniq', 'unique (cooperator_id)', _('You can only set one cooperator status per cooperator')),
  330. ]
  331. @api.model
  332. def _set_today(self):
  333. """
  334. Method call by the cron to update store value base on the date
  335. """
  336. self.search([]).write({'today': fields.Date.today()})
  337. @api.multi
  338. def clear_history(self):
  339. self.ensure_one()
  340. self.history_ids.unlink()
  341. @api.model
  342. def _cron_compute_counter_irregular(self, today=False):
  343. today = today or fields.Date.today()
  344. journal = self.env['beesdoo.shift.journal'].search([('date', '=', today)])
  345. if not journal:
  346. journal = self.env['beesdoo.shift.journal'].create({'date': today})
  347. domain = ['&',
  348. '&',
  349. '&', ('status', '!=', 'unsubscribed'),
  350. ('working_mode', '=', 'irregular'),
  351. ('irregular_start_date', '!=', False),
  352. '|',
  353. '|', ('holiday_start_time', '=', False), ('holiday_end_time', '=', False),
  354. '|', ('holiday_start_time', '>', today), ('holiday_end_time', '<', today),
  355. ]
  356. irregular = self.search(domain)
  357. today_date = fields.Date.from_string(today)
  358. for status in irregular:
  359. if status.status == 'exempted':
  360. continue
  361. delta = (today_date - fields.Date.from_string(status.irregular_start_date)).days
  362. if delta and delta % PERIOD == 0 and status not in journal.line_ids:
  363. if status.sr > 0:
  364. status.sr -= 1
  365. elif status.alert_start_time:
  366. status.sr -= 1
  367. else:
  368. status.sr -= 2
  369. journal.line_ids |= status
  370. class ShiftCronJournal(models.Model):
  371. _name = 'beesdoo.shift.journal'
  372. _order = 'date desc'
  373. _rec_name = 'date'
  374. date = fields.Date()
  375. line_ids = fields.Many2many('cooperative.status')
  376. _sql_constraints = [
  377. ('one_entry_per_day', 'unique (date)', _('You can only create one journal per day')),
  378. ]
  379. @api.multi
  380. def run(self):
  381. self.ensure_one()
  382. if not self.user_has_groups('beesdoo_shift.group_cooperative_admin'):
  383. raise ValidationError(_("You don't have the access to perform this action"))
  384. self.sudo().env['cooperative.status']._cron_compute_counter_irregular(today=self.date)
  385. class ResPartner(models.Model):
  386. _inherit = 'res.partner'
  387. cooperative_status_ids = fields.One2many('cooperative.status', 'cooperator_id', readonly=True)
  388. super = fields.Boolean(related='cooperative_status_ids.super', string="Super Cooperative", readonly=True, store=True)
  389. info_session = fields.Boolean(related='cooperative_status_ids.info_session', string='Information Session ?', readonly=True, store=True)
  390. info_session_date = fields.Datetime(related='cooperative_status_ids.info_session_date', string='Information Session Date', readonly=True, store=True)
  391. working_mode = fields.Selection(related='cooperative_status_ids.working_mode', readonly=True, store=True)
  392. exempt_reason_id = fields.Many2one(related='cooperative_status_ids.exempt_reason_id', readonly=True, store=True)
  393. state = fields.Selection(related='cooperative_status_ids.status', readonly=True, store=True)
  394. extension_start_time = fields.Date(related='cooperative_status_ids.extension_start_time', string="Extension Start Day", readonly=True, store=True)
  395. subscribed_shift_ids = fields.Many2many('beesdoo.shift.template')
  396. @api.multi
  397. def coop_subscribe(self):
  398. return {
  399. 'name': _('Subscribe Cooperator'),
  400. 'type': 'ir.actions.act_window',
  401. 'view_type': 'form',
  402. 'view_mode': 'form',
  403. 'res_model': 'beesdoo.shift.subscribe',
  404. 'target': 'new',
  405. }
  406. @api.multi
  407. def coop_unsubscribe(self):
  408. res = self.coop_subscribe()
  409. res['context'] = {'default_unsubscribed': True}
  410. return res
  411. @api.multi
  412. def manual_extension(self):
  413. return {
  414. 'name': _('Manual Extension'),
  415. 'type': 'ir.actions.act_window',
  416. 'view_type': 'form',
  417. 'view_mode': 'form',
  418. 'res_model': 'beesdoo.shift.extension',
  419. 'target': 'new',
  420. }
  421. @api.multi
  422. def auto_extension(self):
  423. res = self.manual_extension()
  424. res['context'] = {'default_auto': True}
  425. res['name'] = _('Trigger Grace Delay')
  426. return res
  427. @api.multi
  428. def register_holiday(self):
  429. return {
  430. 'name': _('Register Holiday'),
  431. 'type': 'ir.actions.act_window',
  432. 'view_type': 'form',
  433. 'view_mode': 'form',
  434. 'res_model': 'beesdoo.shift.holiday',
  435. 'target': 'new',
  436. }
  437. @api.multi
  438. def temporary_exempt(self):
  439. return {
  440. 'name': _('Temporary Exemption'),
  441. 'type': 'ir.actions.act_window',
  442. 'view_type': 'form',
  443. 'view_mode': 'form',
  444. 'res_model': 'beesdoo.shift.temporary_exemption',
  445. 'target': 'new',
  446. }
  447. #TODO access right + vue on res.partner
  448. #TODO can_shop : Status can_shop ou extempted ou part C