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.

582 lines
20 KiB

  1. # -*- coding: utf-8 -*-
  2. from datetime import date, datetime, timedelta
  3. from lxml import etree
  4. from openerp import _, api, exceptions, fields, models
  5. from openerp.exceptions import UserError, ValidationError
  6. class AttendanceSheetShift(models.AbstractModel):
  7. _name = "beesdoo.shift.sheet.shift"
  8. _description = "Copy of an actual shift into an attendance sheet"
  9. @api.model
  10. def default_task_type_id(self):
  11. parameters = self.env["ir.config_parameter"]
  12. id = (
  13. int(parameters.get_param("beesdoo_shift.default_task_type_id"))
  14. or 1
  15. )
  16. task_types = self.env["beesdoo.shift.type"]
  17. return task_types.browse(id)
  18. # Related actual shift
  19. task_id = fields.Many2one("beesdoo.shift.shift", string="Task")
  20. attendance_sheet_id = fields.Many2one(
  21. "beesdoo.shift.sheet",
  22. string="Attendance Sheet",
  23. required=True,
  24. ondelete="cascade",
  25. )
  26. state = fields.Selection(
  27. [("done", "Present"), ("absent", "Absent"),],
  28. string="Shift State",
  29. required=True,
  30. )
  31. worker_id = fields.Many2one(
  32. "res.partner",
  33. string="Worker",
  34. domain=[
  35. ("eater", "=", "worker_eater"),
  36. ("working_mode", "in", ("regular", "irregular")),
  37. ("state", "not in", ("unsubscribed", "resigning")),
  38. ],
  39. required=True,
  40. )
  41. task_type_id = fields.Many2one(
  42. "beesdoo.shift.type", string="Task Type", default=default_task_type_id
  43. )
  44. super_coop_id = fields.Many2one(
  45. "res.users",
  46. string="Super Cooperative",
  47. domain=[("partner_id.super", "=", True)],
  48. )
  49. working_mode = fields.Selection(
  50. related="worker_id.working_mode", string="Working Mode"
  51. )
  52. # The two exclusive booleans are gathered in a simple one
  53. is_compensation = fields.Boolean(
  54. string="Compensation shift ?", help="Only for regular workers"
  55. )
  56. class AttendanceSheetShiftExpected(models.Model):
  57. """
  58. Irregulars can only have two compensations
  59. """
  60. _name = "beesdoo.shift.sheet.expected"
  61. _description = "Expected Shift"
  62. _inherit = ["beesdoo.shift.sheet.shift"]
  63. compensation_no = fields.Selection(
  64. [("0", "0"), ("1", "1"), ("2", "2"),], string="Compensations",
  65. )
  66. replacement_worker_id = fields.Many2one(
  67. "res.partner",
  68. string="Replacement Worker",
  69. help="Replacement Worker (must be regular)",
  70. domain=[
  71. ("eater", "=", "worker_eater"),
  72. ("working_mode", "=", "regular"),
  73. ("state", "not in", ("unsubscribed", "resigning")),
  74. ],
  75. )
  76. @api.onchange("state")
  77. def on_change_state(self):
  78. if not self.state or self.state == "done":
  79. self.compensation_no = False
  80. if self.state == "absent":
  81. self.compensation_no = "2"
  82. @api.constrains("state", "compensation_no")
  83. def _constrain_compensation_no(self):
  84. if self.state == "absent":
  85. if not self.compensation_no:
  86. raise UserError(_("You must choose a compensation number."))
  87. class AttendanceSheetShiftAdded(models.Model):
  88. """
  89. Added shifts are necessarily 'Present'
  90. """
  91. _name = "beesdoo.shift.sheet.added"
  92. _description = "Added Shift"
  93. _inherit = ["beesdoo.shift.sheet.shift"]
  94. state = fields.Selection(default="done")
  95. @api.onchange("working_mode")
  96. def on_change_working_mode(self):
  97. self.state = "done"
  98. self.is_compensation = self.working_mode == "regular"
  99. class AttendanceSheet(models.Model):
  100. _name = "beesdoo.shift.sheet"
  101. _inherit = [
  102. "mail.thread",
  103. "ir.needaction_mixin",
  104. "barcodes.barcode_events_mixin",
  105. ]
  106. _description = "Attendance sheet"
  107. _order = "start_time"
  108. name = fields.Char(string="Name", compute="_compute_name")
  109. time_slot = fields.Char(
  110. string="Time Slot",
  111. compute="_compute_time_slot",
  112. store=True,
  113. readonly=True,
  114. )
  115. active = fields.Boolean(string="Active", default=1)
  116. state = fields.Selection(
  117. [("not_validated", "Not Validated"), ("validated", "Validated"),],
  118. string="State",
  119. readonly=True,
  120. index=True,
  121. default="not_validated",
  122. track_visibility="onchange",
  123. )
  124. start_time = fields.Datetime(
  125. string="Start Time", required=True, readonly=True
  126. )
  127. end_time = fields.Datetime(string="End Time", required=True, readonly=True)
  128. day = fields.Date(string="Day", compute="_compute_day", store=True)
  129. week = fields.Char(
  130. string="Week",
  131. help="Computed from planning names",
  132. compute="_compute_week",
  133. )
  134. expected_shift_ids = fields.One2many(
  135. "beesdoo.shift.sheet.expected",
  136. "attendance_sheet_id",
  137. string="Expected Shifts",
  138. )
  139. added_shift_ids = fields.One2many(
  140. "beesdoo.shift.sheet.added",
  141. "attendance_sheet_id",
  142. string="Added Shifts",
  143. )
  144. max_worker_no = fields.Integer(
  145. string="Maximum number of workers",
  146. default=0,
  147. readonly=True,
  148. help="Indicative maximum number of workers.",
  149. )
  150. annotation = fields.Text("Annotation", default="")
  151. is_annotated = fields.Boolean(
  152. compute="_compute_is_annotated",
  153. string="Annotation",
  154. readonly=True,
  155. store=True,
  156. )
  157. is_read = fields.Boolean(
  158. string="Mark as read",
  159. help="Has annotation been read by an administrator ?",
  160. default=False,
  161. track_visibility="onchange",
  162. )
  163. feedback = fields.Text("Feedback")
  164. worker_nb_feedback = fields.Selection(
  165. [
  166. ("not_enough", "Not enough"),
  167. ("enough", "Enough"),
  168. ("too_many", "Too many"),
  169. ],
  170. string="Number of workers",
  171. )
  172. validated_by = fields.Many2one(
  173. "res.partner",
  174. string="Validated by",
  175. domain=[
  176. ("eater", "=", "worker_eater"),
  177. ("super", "=", True),
  178. ("working_mode", "=", "regular"),
  179. ("state", "not in", ("unsubscribed", "resigning")),
  180. ],
  181. track_visibility="onchange",
  182. readonly=True,
  183. )
  184. _sql_constraints = [
  185. (
  186. "check_no_annotation_mark_read",
  187. "CHECK ((is_annotated=FALSE AND is_read=FALSE) OR is_annotated=TRUE)",
  188. _("Non-annotated sheets can't be marked as read."),
  189. )
  190. ]
  191. @api.depends("start_time", "end_time", "week")
  192. def _compute_name(self):
  193. for rec in self:
  194. start_time_dt = fields.Datetime.from_string(rec.start_time)
  195. start_time_dt = fields.Datetime.context_timestamp(
  196. rec, start_time_dt
  197. )
  198. name = "[%s] - " % fields.Date.to_string(start_time_dt)
  199. if rec.week:
  200. name += rec.week + " - "
  201. if rec.time_slot:
  202. name += rec.time_slot
  203. rec.name = name
  204. @api.depends("start_time", "end_time")
  205. def _compute_time_slot(self):
  206. for rec in self:
  207. start_time_dt = fields.Datetime.from_string(rec.start_time)
  208. start_time_dt = fields.Datetime.context_timestamp(
  209. rec, start_time_dt
  210. )
  211. end_time_dt = fields.Datetime.from_string(rec.end_time)
  212. end_time_dt = fields.Datetime.context_timestamp(rec, end_time_dt)
  213. rec.time_slot = (
  214. start_time_dt.strftime("%H:%M")
  215. + " - "
  216. + end_time_dt.strftime("%H:%M")
  217. )
  218. @api.depends("start_time")
  219. def _compute_day(self):
  220. for rec in self:
  221. rec.day = fields.Date.from_string(rec.start_time)
  222. @api.depends("expected_shift_ids")
  223. def _compute_week(self):
  224. """
  225. Compute Week Name from Planning Name of first expected shift
  226. """
  227. for rec in self:
  228. if rec.expected_shift_ids:
  229. rec.week = rec.expected_shift_ids[0].task_id.planning_id.name
  230. @api.depends("annotation")
  231. def _compute_is_annotated(self):
  232. for rec in self:
  233. rec.is_annotated = bool(rec.annotation.strip())
  234. @api.constrains("expected_shift_ids", "added_shift_ids")
  235. def _constrain_unique_worker(self):
  236. # Warning : map return generator in python3 (for Odoo 12)
  237. added_ids = map(lambda s: s.worker_id.id, self.added_shift_ids)
  238. expected_ids = map(lambda s: s.worker_id.id, self.expected_shift_ids)
  239. replacement_ids = map(
  240. lambda s: s.replacement_worker_id.id, self.expected_shift_ids
  241. )
  242. replacement_ids = filter(bool, replacement_ids)
  243. ids = added_ids + expected_ids + replacement_ids
  244. if (len(ids) - len(set(ids))) > 0:
  245. raise UserError(
  246. _(
  247. "You can't add the same worker more than once to an attendance sheet."
  248. )
  249. )
  250. @api.constrains(
  251. "expected_shift_ids",
  252. "added_shift_ids",
  253. "annotation",
  254. "feedback",
  255. "worker_nb_feedback",
  256. )
  257. def _lock_after_validation(self):
  258. if self.state == "validated":
  259. raise UserError(
  260. _("The sheet has already been validated and can't be edited.")
  261. )
  262. def on_barcode_scanned(self, barcode):
  263. if self.state == "validated":
  264. raise UserError(
  265. _("You cannot modify a validated attendance sheet.")
  266. )
  267. worker = self.env["res.partner"].search([("barcode", "=", barcode)])
  268. if not len(worker):
  269. raise UserError(_("Worker not found (invalid barcode or status)."))
  270. if len(worker) > 1:
  271. raise UserError(
  272. _("Multiple workers are corresponding this barcode.")
  273. )
  274. if worker.state in ("unsubscribed", "resigning"):
  275. raise UserError(_("Worker is %s.") % worker.state)
  276. if worker.working_mode not in ("regular", "irregular"):
  277. raise UserError(
  278. _("Worker is %s and should be regular or irregular.")
  279. % worker.working_mode
  280. )
  281. for id in self.expected_shift_ids.ids:
  282. shift = self.env["beesdoo.shift.sheet.expected"].browse(id)
  283. if (
  284. shift.worker_id == worker and not shift.replacement_worker_id
  285. ) or shift.replacement_worker_id == worker:
  286. shift.state = "done"
  287. return
  288. if shift.worker_id == worker and shift.replacement_worker_id:
  289. raise UserError(
  290. _("%s was expected as replaced.") % worker.name
  291. )
  292. is_compensation = worker.working_mode == "regular"
  293. added_ids = map(lambda s: s.worker_id.id, self.added_shift_ids)
  294. if worker.id in added_ids:
  295. return
  296. self.added_shift_ids |= self.added_shift_ids.new(
  297. {
  298. "task_type_id": self.added_shift_ids.default_task_type_id(),
  299. "state": "done",
  300. "attendance_sheet_id": self._origin.id,
  301. "worker_id": worker.id,
  302. "is_compensation": is_compensation,
  303. }
  304. )
  305. @api.model
  306. def create(self, vals):
  307. new_sheet = super(AttendanceSheet, self).create(vals)
  308. # Creation and addition of the expected shifts corresponding
  309. # to the time range
  310. tasks = self.env["beesdoo.shift.shift"]
  311. expected_shift = self.env["beesdoo.shift.sheet.expected"]
  312. s_time = fields.Datetime.from_string(new_sheet.start_time)
  313. e_time = fields.Datetime.from_string(new_sheet.end_time)
  314. delta = timedelta(minutes=1)
  315. tasks = tasks.search(
  316. [
  317. ("start_time", ">", fields.Datetime.to_string(s_time - delta)),
  318. ("start_time", "<", fields.Datetime.to_string(s_time + delta)),
  319. ("end_time", ">", fields.Datetime.to_string(e_time - delta)),
  320. ("end_time", "<", fields.Datetime.to_string(e_time + delta)),
  321. ]
  322. )
  323. for task in tasks:
  324. if task.worker_id and (task.state != "cancel"):
  325. new_expected_shift = expected_shift.create(
  326. {
  327. "attendance_sheet_id": new_sheet.id,
  328. "task_id": task.id,
  329. "worker_id": task.worker_id.id,
  330. "replacement_worker_id": task.replaced_id.id,
  331. "task_type_id": task.task_type_id.id,
  332. "state": "absent",
  333. "compensation_no": "2",
  334. "working_mode": task.working_mode,
  335. "is_compensation": task.is_compensation,
  336. }
  337. )
  338. # Maximum number of workers calculation (count empty shifts)
  339. new_sheet.max_worker_no = len(tasks)
  340. return new_sheet
  341. @api.multi
  342. def button_mark_as_read(self):
  343. if self.is_read:
  344. raise UserError(_("The sheet has already been marked as read."))
  345. self.is_read = True
  346. # Workaround to display notifications only
  347. # for unread and not validated sheets, via a check on domain.
  348. @api.model
  349. def _needaction_count(self, domain=None):
  350. if domain == [
  351. ("is_annotated", "=", True),
  352. ("is_read", "=", False),
  353. ] or domain == [("state", "=", "not_validated")]:
  354. return self.search_count(domain)
  355. return
  356. def validate(self, user):
  357. self.ensure_one()
  358. if self.state == "validated":
  359. raise UserError("The sheet has already been validated.")
  360. shift = self.env["beesdoo.shift.shift"]
  361. # Fields validation
  362. for added_shift in self.added_shift_ids:
  363. if not added_shift.worker_id:
  364. raise UserError(
  365. _("Worker must be set for shift %s") % added_shift.id
  366. )
  367. if added_shift.state != "done":
  368. raise UserError(
  369. _("Shift State is missing or wrong for %s")
  370. % added_shift.worker_id.name
  371. )
  372. if not added_shift.task_type_id:
  373. raise UserError(
  374. _("Task Type is missing for %s")
  375. % added_shift.worker_id.name
  376. )
  377. if not added_shift.working_mode:
  378. raise UserError(
  379. _("Working mode is missing for %s")
  380. % added_shift.worker_id.name
  381. )
  382. for expected_shift in self.expected_shift_ids:
  383. if not expected_shift.state:
  384. raise UserError(
  385. _("Shift State is missing for %s")
  386. % expected_shift.worker_id.name
  387. )
  388. if (
  389. expected_shift.state == "absent"
  390. and not expected_shift.compensation_no
  391. ):
  392. raise UserError(
  393. _("Compensation number is missing for %s")
  394. % expected_shift.worker_id.name
  395. )
  396. # Expected shifts status update
  397. for expected_shift in self.expected_shift_ids:
  398. actual_shift = expected_shift.task_id
  399. # Merge state with compensations number to fit Task model
  400. if (
  401. expected_shift.state == "absent"
  402. and expected_shift.compensation_no
  403. ):
  404. state_converted = "absent_%s" % expected_shift.compensation_no
  405. else:
  406. state_converted = expected_shift.state
  407. actual_shift.replaced_id = expected_shift.replacement_worker_id
  408. actual_shift.state = state_converted
  409. if expected_shift.state == "absent":
  410. mail_template = self.env.ref(
  411. "beesdoo_shift.email_template_non_attendance", False
  412. )
  413. mail_template.send_mail(expected_shift.task_id.id, True)
  414. # Added shifts status update
  415. for added_shift in self.added_shift_ids:
  416. is_regular_worker = added_shift.worker_id.working_mode == "regular"
  417. is_compensation = added_shift.is_compensation
  418. # Edit a non-assigned shift or create one if none
  419. non_assigned_shifts = shift.search(
  420. [
  421. ("worker_id", "=", False),
  422. ("start_time", "=", self.start_time),
  423. ("end_time", "=", self.end_time),
  424. ("task_type_id", "=", added_shift.task_type_id.id),
  425. ],
  426. limit=1,
  427. )
  428. if len(non_assigned_shifts):
  429. actual_shift = non_assigned_shifts[0]
  430. actual_shift.write(
  431. {
  432. "state": added_shift.state,
  433. "worker_id": added_shift.worker_id.id,
  434. "is_regular": not is_compensation
  435. and is_regular_worker,
  436. "is_compensation": is_compensation
  437. and is_regular_worker,
  438. }
  439. )
  440. else:
  441. actual_shift = self.env["beesdoo.shift.shift"].create(
  442. {
  443. "name": _("[Added Shift]"),
  444. "task_type_id": added_shift.task_type_id.id,
  445. "state": added_shift.state,
  446. "worker_id": added_shift.worker_id.id,
  447. "start_time": self.start_time,
  448. "end_time": self.end_time,
  449. "is_regular": not is_compensation
  450. and is_regular_worker,
  451. "is_compensation": is_compensation
  452. and is_regular_worker,
  453. }
  454. )
  455. added_shift.task_id = actual_shift.id
  456. self.validated_by = user
  457. self.state = "validated"
  458. return
  459. @api.multi
  460. def validate_via_wizard(self):
  461. self.ensure_one()
  462. if self.env.user.has_group("beesdoo_shift.group_cooperative_admin"):
  463. self.validate(self.env.user.partner_id)
  464. return
  465. return {
  466. "type": "ir.actions.act_window",
  467. "res_model": "beesdoo.shift.sheet.validate",
  468. "view_type": "form",
  469. "view_mode": "form",
  470. "target": "new",
  471. }
  472. @api.model
  473. def _generate_attendance_sheet(self):
  474. """
  475. Generate sheets with shifts in the time interval
  476. defined from corresponding CRON time interval.
  477. """
  478. time_ranges = set()
  479. tasks = self.env["beesdoo.shift.shift"]
  480. sheets = self.env["beesdoo.shift.sheet"]
  481. current_time = datetime.now()
  482. generation_interval_setting = int(
  483. self.env["ir.config_parameter"].get_param(
  484. "beesdoo_shift.attendance_sheet_generation_interval"
  485. )
  486. )
  487. allowed_time_range = timedelta(minutes=generation_interval_setting)
  488. tasks = tasks.search(
  489. [
  490. ("start_time", ">", str(current_time),),
  491. ("start_time", "<", str(current_time + allowed_time_range),),
  492. ]
  493. )
  494. for task in tasks:
  495. start_time = task.start_time
  496. end_time = task.end_time
  497. sheets = sheets.search(
  498. [("start_time", "=", start_time), ("end_time", "=", end_time),]
  499. )
  500. if not sheets:
  501. sheet = sheets.create(
  502. {"start_time": start_time, "end_time": end_time}
  503. )
  504. @api.model
  505. def _cron_non_validated_sheets(self):
  506. sheets = self.env["beesdoo.shift.sheet"]
  507. non_validated_sheets = sheets.search(
  508. [
  509. ("day", "=", date.today() - timedelta(days=1)),
  510. ("state", "=", "not_validated"),
  511. ]
  512. )
  513. if non_validated_sheets:
  514. mail_template = self.env.ref(
  515. "beesdoo_shift.email_template_non_validated_sheet", False
  516. )
  517. for rec in non_validated_sheets:
  518. mail_template.send_mail(rec.id, True)