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.

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