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.

658 lines
22 KiB

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