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.

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