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.

691 lines
23 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. import logging
  2. from datetime import date, datetime, timedelta
  3. from odoo import _, api, fields, models
  4. from odoo.exceptions import UserError
  5. _logger = logging.getLogger(__name__)
  6. class AttendanceSheetShift(models.Model):
  7. """
  8. Partial copy of Task class to use in AttendanceSheet,
  9. actual Task is updated at validation.
  10. Should be Abstract and not used alone (common code for
  11. AttendanceSheetShiftAdded and AttendanceSheetShiftExpected),
  12. but create() method from res.partner raise error
  13. when class is Abstract.
  14. """
  15. _name = "beesdoo.shift.sheet.shift"
  16. _description = "Copy of an actual shift into an attendance sheet"
  17. _order = "task_type_id, worker_name"
  18. @api.model
  19. def pre_filled_task_type_id(self):
  20. parameters = self.env["ir.config_parameter"].sudo()
  21. tasktype_id = int(
  22. parameters.get_param(
  23. "beesdoo_shift_attendance.pre_filled_task_type_id", default=1
  24. )
  25. )
  26. task_types = self.env["beesdoo.shift.type"]
  27. return task_types.browse(tasktype_id)
  28. # Related actual shift
  29. task_id = fields.Many2one("beesdoo.shift.shift", string="Task")
  30. attendance_sheet_id = fields.Many2one(
  31. "beesdoo.shift.sheet",
  32. string="Attendance Sheet",
  33. required=True,
  34. ondelete="cascade",
  35. )
  36. state = fields.Selection(
  37. [
  38. ("done", "Present"),
  39. ("absent_0", "Absent - 0 Compensation"),
  40. ("absent_1", "Absent - 1 Compensation"),
  41. ("absent_2", "Absent - 2 Compensations"),
  42. ],
  43. string="Shift State",
  44. required=True,
  45. )
  46. worker_id = fields.Many2one(
  47. "res.partner",
  48. string="Worker",
  49. domain=[
  50. ("is_worker", "=", True),
  51. ("working_mode", "in", ("regular", "irregular")),
  52. ("state", "not in", ("unsubscribed", "resigning")),
  53. ],
  54. required=True,
  55. )
  56. worker_name = fields.Char(related="worker_id.name", store=True)
  57. task_type_id = fields.Many2one(
  58. "beesdoo.shift.type",
  59. string="Task Type",
  60. default=pre_filled_task_type_id,
  61. )
  62. working_mode = fields.Selection(
  63. related="worker_id.working_mode", string="Working Mode"
  64. )
  65. # The two exclusive booleans are gathered in a simple one
  66. is_compensation = fields.Boolean(
  67. string="Compensation shift ?", help="Only for regular workers"
  68. )
  69. class AttendanceSheetShiftExpected(models.Model):
  70. """
  71. Shifts already expected.
  72. """
  73. _name = "beesdoo.shift.sheet.expected"
  74. _description = "Expected Shift"
  75. _inherit = ["beesdoo.shift.sheet.shift"]
  76. super_coop_id = fields.Many2one(
  77. related="task_id.super_coop_id", store=True
  78. )
  79. replaced_id = fields.Many2one(
  80. "res.partner",
  81. string="Replacement Worker",
  82. help="Replacement Worker (must be regular)",
  83. domain=[
  84. ("eater", "=", "worker_eater"),
  85. ("working_mode", "=", "regular"),
  86. ("state", "not in", ("unsubscribed", "resigning")),
  87. ],
  88. )
  89. @api.onchange("replaced_id")
  90. def on_change_replacement_worker(self):
  91. if self.replaced_id:
  92. self.state = "done"
  93. class AttendanceSheetShiftAdded(models.Model):
  94. """
  95. Shifts added during time slot.
  96. """
  97. _name = "beesdoo.shift.sheet.added"
  98. _description = "Added Shift"
  99. _inherit = ["beesdoo.shift.sheet.shift"]
  100. state = fields.Selection(default="done")
  101. @api.onchange("working_mode")
  102. def on_change_working_mode(self):
  103. self.state = "done"
  104. self.is_compensation = self.working_mode == "regular"
  105. class AttendanceSheet(models.Model):
  106. _name = "beesdoo.shift.sheet"
  107. _inherit = ["mail.thread", "barcodes.barcode_events_mixin"]
  108. _description = "Attendance sheet"
  109. _order = "start_time"
  110. name = fields.Char(string="Name", compute="_compute_name")
  111. time_slot = fields.Char(
  112. string="Time Slot",
  113. compute="_compute_time_slot",
  114. store=True,
  115. readonly=True,
  116. )
  117. active = fields.Boolean(string="Active", default=1)
  118. state = fields.Selection(
  119. [("not_validated", "Not Validated"), ("validated", "Validated")],
  120. string="State",
  121. readonly=True,
  122. index=True,
  123. default="not_validated",
  124. track_visibility="onchange",
  125. )
  126. start_time = fields.Datetime(
  127. string="Start Time", required=True, readonly=True
  128. )
  129. end_time = fields.Datetime(string="End Time", required=True, readonly=True)
  130. day = fields.Date(string="Day", compute="_compute_day", store=True)
  131. day_abbrevation = fields.Char(
  132. string="Day Abbrevation", compute="_compute_day_abbrevation"
  133. )
  134. week = fields.Char(
  135. string="Week",
  136. help="Computed from planning name",
  137. compute="_compute_week",
  138. )
  139. expected_shift_ids = fields.One2many(
  140. "beesdoo.shift.sheet.expected",
  141. "attendance_sheet_id",
  142. string="Expected Shifts",
  143. )
  144. added_shift_ids = fields.One2many(
  145. "beesdoo.shift.sheet.added",
  146. "attendance_sheet_id",
  147. string="Added Shifts",
  148. )
  149. max_worker_no = fields.Integer(
  150. string="Maximum number of workers",
  151. default=0,
  152. readonly=True,
  153. help="Indicative maximum number of workers.",
  154. )
  155. attended_worker_no = fields.Integer(
  156. string="Number of workers present", default=0, readonly=True
  157. )
  158. notes = fields.Text(
  159. "Notes",
  160. default="",
  161. help="Notes about the attendance for the Members Office",
  162. )
  163. is_annotated = fields.Boolean(
  164. compute="_compute_is_annotated",
  165. string="Is annotated",
  166. readonly=True,
  167. store=True,
  168. )
  169. is_read = fields.Boolean(
  170. string="Mark as read",
  171. help="Has notes been read by an administrator ?",
  172. default=False,
  173. track_visibility="onchange",
  174. )
  175. feedback = fields.Text("Comments about the shift")
  176. worker_nb_feedback = fields.Selection(
  177. [
  178. ("not_enough", "Not enough workers"),
  179. ("enough", "Enough workers"),
  180. ("too_many", "Too many workers"),
  181. ("empty", "I was not there during the shift"),
  182. ],
  183. string="Was your team big enough ? *",
  184. )
  185. validated_by = fields.Many2one(
  186. "res.partner",
  187. string="Validated by",
  188. domain=[
  189. ("eater", "=", "worker_eater"),
  190. ("super", "=", True),
  191. ("working_mode", "=", "regular"),
  192. ("state", "not in", ("unsubscribed", "resigning")),
  193. ],
  194. track_visibility="onchange",
  195. readonly=True,
  196. )
  197. _sql_constraints = [
  198. (
  199. "check_not_annotated_mark_as_read",
  200. "CHECK"
  201. " ((is_annotated=FALSE AND is_read=FALSE) OR is_annotated=TRUE)",
  202. _("Non-annotated sheets can't be marked as read."),
  203. )
  204. ]
  205. @api.depends("start_time", "end_time")
  206. def _compute_time_slot(self):
  207. for rec in self:
  208. start_time = fields.Datetime.context_timestamp(rec, rec.start_time)
  209. end_time = fields.Datetime.context_timestamp(rec, rec.end_time)
  210. rec.time_slot = (
  211. start_time.strftime("%H:%M") + "-" + end_time.strftime("%H:%M")
  212. )
  213. @api.depends("start_time", "end_time", "week", "day_abbrevation")
  214. def _compute_name(self):
  215. for rec in self:
  216. start_time = fields.Datetime.context_timestamp(rec, rec.start_time)
  217. name = "[%s] " % fields.Date.to_string(start_time)
  218. if rec.week:
  219. name += rec.week + " "
  220. if rec.day_abbrevation:
  221. name += rec.day_abbrevation + " "
  222. if rec.time_slot:
  223. name += "(%s)" % rec.time_slot
  224. rec.name = name
  225. @api.depends("start_time")
  226. def _compute_day(self):
  227. for rec in self:
  228. rec.day = rec.start_time.date()
  229. @api.depends("expected_shift_ids")
  230. def _compute_day_abbrevation(self):
  231. """
  232. Compute Day Abbrevation from Planning Name
  233. of first expected shift with one.
  234. """
  235. for rec in self:
  236. for shift in rec.expected_shift_ids:
  237. if shift.task_id.task_template_id.day_nb_id.name:
  238. rec.day_abbrevation = (
  239. shift.task_id.task_template_id.day_nb_id.name
  240. )
  241. @api.depends("expected_shift_ids")
  242. def _compute_week(self):
  243. """
  244. Compute Week Name from Planning Name
  245. of first expected shift with one.
  246. """
  247. for rec in self:
  248. for shift in rec.expected_shift_ids:
  249. if shift.task_id.planning_id.name:
  250. rec.week = shift.task_id.planning_id.name
  251. @api.depends("notes")
  252. def _compute_is_annotated(self):
  253. for rec in self:
  254. if rec.notes:
  255. rec.is_annotated = bool(rec.notes.strip())
  256. @api.constrains("expected_shift_ids", "added_shift_ids")
  257. def _constrain_unique_worker(self):
  258. # Warning : map return generator in python3 (for Odoo 12)
  259. added_ids = [s.worker_id.id for s in self.added_shift_ids]
  260. expected_ids = [s.worker_id.id for s in self.expected_shift_ids]
  261. replacement_ids = [
  262. s.replaced_id.id
  263. for s in self.expected_shift_ids
  264. if s.replaced_id.id
  265. ]
  266. ids = added_ids + expected_ids + replacement_ids
  267. if (len(ids) - len(set(ids))) > 0:
  268. raise UserError(
  269. _(
  270. "You can't add the same worker more than once to an "
  271. "attendance sheet. "
  272. )
  273. )
  274. @api.constrains(
  275. "expected_shift_ids",
  276. "added_shift_ids",
  277. "notes",
  278. "feedback",
  279. "worker_nb_feedback",
  280. )
  281. def _lock_after_validation(self):
  282. if self.state == "validated":
  283. raise UserError(
  284. _("The sheet has already been validated and can't be edited.")
  285. )
  286. def on_barcode_scanned(self, barcode):
  287. if self.env.user.has_group(
  288. "beesdoo_shift_attendance.group_shift_attendance"
  289. ):
  290. raise UserError(
  291. _(
  292. "You must be logged as 'Attendance Sheet Generic Access' "
  293. " if you want to scan cards."
  294. )
  295. )
  296. if self.state == "validated":
  297. raise UserError(
  298. _("A validated attendance sheet can't be modified")
  299. )
  300. worker = self.env["res.partner"].search([("barcode", "=", barcode)])
  301. if not len(worker):
  302. raise UserError(
  303. _(
  304. "Worker not found (invalid barcode or status). \n"
  305. "Barcode : %s"
  306. )
  307. % barcode
  308. )
  309. if len(worker) > 1:
  310. raise UserError(
  311. _(
  312. "Multiple workers are corresponding this barcode. \n"
  313. "Barcode : %s"
  314. )
  315. % barcode
  316. )
  317. if worker.state == "unsubscribed":
  318. shift_counter = (
  319. worker.cooperative_status_ids.sc
  320. + worker.cooperative_status_ids.sr
  321. )
  322. raise UserError(
  323. _(
  324. "Beware, your account is frozen because your shift counter "
  325. "is at %s. Please contact Members Office to unfreeze it. "
  326. "If you want to attend this shift, your supercoop "
  327. "can write your name in the notes field during validation."
  328. )
  329. % shift_counter
  330. )
  331. if worker.state == "resigning":
  332. raise UserError(
  333. _(
  334. "Beware, you are recorded as resigning. "
  335. "Please contact member's office if this is incorrect. "
  336. "Thank you. "
  337. )
  338. )
  339. if worker.working_mode not in ("regular", "irregular"):
  340. raise UserError(
  341. _(
  342. "%s's working mode is %s and should be regular or "
  343. "irregular. "
  344. )
  345. % (worker.name, worker.working_mode)
  346. )
  347. # Expected shifts status update
  348. for id_ in self.expected_shift_ids.ids:
  349. shift = self.env["beesdoo.shift.sheet.expected"].browse(id_)
  350. if (
  351. shift.worker_id == worker and not shift.replaced_id
  352. ) or shift.replaced_id == worker:
  353. shift.state = "done"
  354. return
  355. if shift.worker_id == worker and shift.replaced_id:
  356. raise UserError(
  357. _("%s is registered as replaced.") % worker.name
  358. )
  359. is_compensation = worker.working_mode == "regular"
  360. added_ids = [s.worker_id.id for s in self.added_shift_ids]
  361. if worker.id not in added_ids:
  362. # Added shift creation
  363. self.added_shift_ids |= self.added_shift_ids.new(
  364. {
  365. "task_type_id": (
  366. self.added_shift_ids.pre_filled_task_type_id()
  367. ),
  368. "state": "done",
  369. "attendance_sheet_id": self._origin.id,
  370. "worker_id": worker.id,
  371. "is_compensation": is_compensation,
  372. }
  373. )
  374. @api.model
  375. def create(self, vals):
  376. new_sheet = super(AttendanceSheet, self).create(vals)
  377. # Creation and addition of the expected shifts corresponding
  378. # to the time range
  379. tasks = self.env["beesdoo.shift.shift"]
  380. expected_shift = self.env["beesdoo.shift.sheet.expected"]
  381. # Fix issues with equality check on datetime
  382. # by searching on a small intervall instead
  383. delta = timedelta(minutes=1)
  384. tasks = tasks.search(
  385. [
  386. ("start_time", ">", new_sheet.start_time - delta),
  387. ("start_time", "<", new_sheet.start_time + delta),
  388. ("end_time", ">", new_sheet.end_time - delta),
  389. ("end_time", "<", new_sheet.end_time + delta),
  390. ]
  391. )
  392. workers = []
  393. for task in tasks:
  394. # Only one shift is added if multiple similar exist
  395. if (
  396. task.worker_id
  397. and task.worker_id not in workers
  398. and (task.state != "cancel")
  399. ):
  400. expected_shift.create(
  401. {
  402. "attendance_sheet_id": new_sheet.id,
  403. "task_id": task.id,
  404. "worker_id": task.worker_id.id,
  405. "replaced_id": task.replaced_id.id,
  406. "task_type_id": task.task_type_id.id,
  407. "state": "absent_2",
  408. "working_mode": task.working_mode,
  409. "is_compensation": task.is_compensation,
  410. }
  411. )
  412. workers.append(task.worker_id)
  413. # Maximum number of workers calculation (count empty shifts)
  414. new_sheet.max_worker_no = len(tasks)
  415. return new_sheet
  416. @api.multi
  417. def button_mark_as_read(self):
  418. if self.is_read:
  419. raise UserError(_("The sheet has already been marked as read."))
  420. self.is_read = True
  421. def _validate(self, user):
  422. self.ensure_one()
  423. if self.state == "validated":
  424. raise UserError(_("The sheet has already been validated."))
  425. # Expected shifts status update
  426. for expected_shift in self.expected_shift_ids:
  427. actual_shift = expected_shift.task_id
  428. if not actual_shift:
  429. _logger.warning(
  430. "The shift linked to the expected shift with id %s "
  431. "for the partner id %s does not exist anymore."
  432. "The expected shift is ignored during the validation "
  433. "process of the attendance sheet."
  434. % (expected_shift.id, expected_shift.worker_id.id)
  435. )
  436. self._message_log(
  437. body=_(
  438. "The shift linked to the expected shift of %s "
  439. "does exist any more."
  440. "This expected shift is ignored in the "
  441. "validation process."
  442. % expected_shift.worker_id.name
  443. )
  444. )
  445. continue
  446. actual_shift.replaced_id = expected_shift.replaced_id
  447. actual_shift.state = expected_shift.state
  448. if expected_shift.state == "done":
  449. self.attended_worker_no += 1
  450. if expected_shift.state != "done":
  451. mail_template = self.env.ref(
  452. "beesdoo_shift_attendance.email_template_non_attendance",
  453. False,
  454. )
  455. mail_template.send_mail(expected_shift.task_id.id, True)
  456. # Added shifts status update
  457. for added_shift in self.added_shift_ids:
  458. is_regular_worker = added_shift.worker_id.working_mode == "regular"
  459. is_compensation = added_shift.is_compensation
  460. # Edit a non-assigned shift or create one if none
  461. # Fix issues with equality check on datetime
  462. # by searching on a small intervall instead
  463. delta = timedelta(minutes=1)
  464. non_assigned_shifts = self.env["beesdoo.shift.shift"].search(
  465. [
  466. ("worker_id", "=", False),
  467. ("start_time", ">", self.start_time - delta),
  468. ("start_time", "<", self.start_time + delta),
  469. ("end_time", ">", self.end_time - delta),
  470. ("end_time", "<", self.end_time + delta),
  471. ("task_type_id", "=", added_shift.task_type_id.id),
  472. ],
  473. limit=1,
  474. )
  475. if len(non_assigned_shifts):
  476. actual_shift = non_assigned_shifts[0]
  477. else:
  478. actual_shift = self.env["beesdoo.shift.shift"].create(
  479. {
  480. "name": _("%s (added)" % self.name),
  481. "task_type_id": added_shift.task_type_id.id,
  482. "start_time": self.start_time,
  483. "end_time": self.end_time,
  484. }
  485. )
  486. actual_shift.write(
  487. {
  488. "state": added_shift.state,
  489. "worker_id": added_shift.worker_id.id,
  490. "is_regular": not is_compensation and is_regular_worker,
  491. "is_compensation": is_compensation and is_regular_worker,
  492. }
  493. )
  494. added_shift.task_id = actual_shift.id
  495. if actual_shift.state == "done":
  496. self.attended_worker_no += 1
  497. self.validated_by = user
  498. self.state = "validated"
  499. return
  500. @api.multi
  501. def validate_with_checks(self):
  502. self.ensure_one()
  503. if self.state == "validated":
  504. raise UserError(_("The sheet has already been validated."))
  505. if self.start_time > datetime.now():
  506. raise UserError(
  507. _(
  508. "Attendance sheet can only be validated once the shifts "
  509. "have started. "
  510. )
  511. )
  512. # Fields validation
  513. for added_shift in self.added_shift_ids:
  514. if not added_shift.worker_id:
  515. raise UserError(
  516. _("Worker name is missing for an added shift.")
  517. )
  518. if added_shift.state != "done":
  519. raise UserError(
  520. _("Shift State is missing or wrong for %s")
  521. % added_shift.worker_id.name
  522. )
  523. if not added_shift.task_type_id:
  524. raise UserError(
  525. _("Task Type is missing for %s")
  526. % added_shift.worker_id.name
  527. )
  528. if not added_shift.working_mode:
  529. raise UserError(
  530. _("Working mode is missing for %s")
  531. % added_shift.worker_id.name
  532. )
  533. if added_shift.working_mode not in ["regular", "irregular"]:
  534. raise UserError(
  535. _("Warning : Working mode for %s is %s")
  536. % (
  537. added_shift.worker_id.name,
  538. added_shift.worker_id.working_mode,
  539. )
  540. )
  541. for expected_shift in self.expected_shift_ids:
  542. if not expected_shift.state:
  543. raise UserError(
  544. _("Shift State is missing for %s")
  545. % expected_shift.worker_id.name
  546. )
  547. if (
  548. expected_shift.state == "absent"
  549. and not expected_shift.compensation_no
  550. ):
  551. raise UserError(
  552. _("Compensation number is missing for %s")
  553. % expected_shift.worker_id.name
  554. )
  555. # Open a validation wizard only if not admin
  556. if self.env.user.has_group(
  557. "beesdoo_shift_attendance.group_shift_attendance_sheet_validation"
  558. ):
  559. if not self.worker_nb_feedback:
  560. raise UserError(
  561. _("Please give your feedback about the number of workers.")
  562. )
  563. self._validate(self.env.user.partner_id)
  564. return
  565. return {
  566. "type": "ir.actions.act_window",
  567. "res_model": "beesdoo.shift.sheet.validate",
  568. "view_type": "form",
  569. "view_mode": "form",
  570. "target": "new",
  571. }
  572. @api.model
  573. def _generate_attendance_sheet(self):
  574. """
  575. Generate sheets with shifts in the time interval
  576. defined from corresponding CRON time interval.
  577. """
  578. tasks = self.env["beesdoo.shift.shift"]
  579. sheets = self.env["beesdoo.shift.sheet"]
  580. current_time = datetime.now()
  581. generation_interval_setting = int(
  582. self.env["ir.config_parameter"]
  583. .sudo()
  584. .get_param(
  585. "beesdoo_shift_attendance.attendance_sheet_generation_interval"
  586. )
  587. )
  588. allowed_time_range = timedelta(minutes=generation_interval_setting)
  589. tasks = tasks.search(
  590. [
  591. ("start_time", ">", str(current_time)),
  592. ("start_time", "<", str(current_time + allowed_time_range)),
  593. ]
  594. )
  595. for task in tasks:
  596. start_time = task.start_time
  597. end_time = task.end_time
  598. sheets = sheets.search(
  599. [("start_time", "=", start_time), ("end_time", "=", end_time)]
  600. )
  601. if not sheets:
  602. sheets.create({"start_time": start_time, "end_time": end_time})
  603. @api.model
  604. def _cron_non_validated_sheets(self):
  605. sheets = self.env["beesdoo.shift.sheet"]
  606. non_validated_sheets = sheets.search(
  607. [
  608. ("day", "=", date.today() - timedelta(days=1)),
  609. ("state", "=", "not_validated"),
  610. ]
  611. )
  612. if non_validated_sheets:
  613. mail_template = self.env.ref(
  614. "beesdoo_shift_attendance.email_template_non_validated_sheet",
  615. False,
  616. )
  617. for rec in non_validated_sheets:
  618. mail_template.send_mail(rec.id, True)