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.

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