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.

487 lines
22 KiB

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