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.

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