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.

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