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.

642 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. for task in tasks:
  371. if task.worker_id and (task.state != "cancel"):
  372. new_expected_shift = expected_shift.create(
  373. {
  374. "attendance_sheet_id": new_sheet.id,
  375. "task_id": task.id,
  376. "worker_id": task.worker_id.id,
  377. "replaced_id": task.replaced_id.id,
  378. "task_type_id": task.task_type_id.id,
  379. "state": "absent_2",
  380. "working_mode": task.working_mode,
  381. "is_compensation": task.is_compensation,
  382. }
  383. )
  384. # Maximum number of workers calculation (count empty shifts)
  385. new_sheet.max_worker_no = len(tasks)
  386. return new_sheet
  387. @api.multi
  388. def button_mark_as_read(self):
  389. if self.is_read:
  390. raise UserError(_("The sheet has already been marked as read."))
  391. self.is_read = True
  392. # Workaround to display notifications only
  393. # for unread and not validated sheets, via a check on domain.
  394. @api.model
  395. def _needaction_count(self, domain=None):
  396. if domain == [
  397. ("is_annotated", "=", True),
  398. ("is_read", "=", False),
  399. ] or domain == [("state", "=", "not_validated")]:
  400. return self.search_count(domain)
  401. return
  402. def _validate(self, user):
  403. self.ensure_one()
  404. if self.state == "validated":
  405. raise UserError("The sheet has already been validated.")
  406. # Expected shifts status update
  407. for expected_shift in self.expected_shift_ids:
  408. actual_shift = expected_shift.task_id
  409. actual_shift.replaced_id = expected_shift.replaced_id
  410. actual_shift.state = expected_shift.state
  411. if expected_shift.state != "done":
  412. mail_template = self.env.ref(
  413. "beesdoo_shift.email_template_non_attendance", False
  414. )
  415. mail_template.send_mail(expected_shift.task_id.id, True)
  416. # Added shifts status update
  417. for added_shift in self.added_shift_ids:
  418. is_regular_worker = added_shift.worker_id.working_mode == "regular"
  419. is_compensation = added_shift.is_compensation
  420. # Edit a non-assigned shift or create one if none
  421. # Fix issues with equality check on datetime
  422. # by searching on a small intervall instead
  423. delta = timedelta(minutes=1)
  424. non_assigned_shifts = self.env["beesdoo.shift.shift"].search(
  425. [
  426. ("worker_id", "=", False),
  427. ("start_time", ">", self.start_time - delta),
  428. ("start_time", "<", self.start_time + delta),
  429. ("end_time", ">", self.end_time - delta),
  430. ("end_time", "<", self.end_time + delta),
  431. ("task_type_id", "=", added_shift.task_type_id.id),
  432. ],
  433. limit=1,
  434. )
  435. if len(non_assigned_shifts):
  436. actual_shift = non_assigned_shifts[0]
  437. else:
  438. actual_shift = self.env["beesdoo.shift.shift"].create(
  439. {
  440. "name": _("%s (added)" % self.name),
  441. "task_type_id": added_shift.task_type_id.id,
  442. "start_time": self.start_time,
  443. "end_time": self.end_time,
  444. }
  445. )
  446. actual_shift.write(
  447. {
  448. "state": added_shift.state,
  449. "worker_id": added_shift.worker_id.id,
  450. "is_regular": not is_compensation and is_regular_worker,
  451. "is_compensation": is_compensation and is_regular_worker,
  452. }
  453. )
  454. added_shift.task_id = actual_shift.id
  455. self.validated_by = user
  456. self.state = "validated"
  457. return
  458. @api.multi
  459. def validate_with_checks(self):
  460. self.ensure_one()
  461. if self.state == "validated":
  462. raise UserError(_("The sheet has already been validated."))
  463. if self.start_time > datetime.now():
  464. raise UserError(
  465. _(
  466. "Attendance sheet can only be validated once the shifts have started."
  467. )
  468. )
  469. # Fields validation
  470. for added_shift in self.added_shift_ids:
  471. if not added_shift.worker_id:
  472. raise UserError(
  473. _("Worker name is missing for an added shift.")
  474. )
  475. if added_shift.state != "done":
  476. raise UserError(
  477. _("Shift State is missing or wrong for %s")
  478. % added_shift.worker_id.name
  479. )
  480. if not added_shift.task_type_id:
  481. raise UserError(
  482. _("Task Type is missing for %s")
  483. % added_shift.worker_id.name
  484. )
  485. if not added_shift.working_mode:
  486. raise UserError(
  487. _("Working mode is missing for %s")
  488. % added_shift.worker_id.name
  489. )
  490. if added_shift.working_mode not in ["regular", "irregular"]:
  491. raise UserError(
  492. _("Warning : Working mode for %s is %s")
  493. % (
  494. added_shift.worker_id.name,
  495. added_shift.worker_id.working_mode,
  496. )
  497. )
  498. for expected_shift in self.expected_shift_ids:
  499. if not expected_shift.state:
  500. raise UserError(
  501. _("Shift State is missing for %s")
  502. % expected_shift.worker_id.name
  503. )
  504. if (
  505. expected_shift.state == "absent"
  506. and not expected_shift.compensation_no
  507. ):
  508. raise UserError(
  509. _("Compensation number is missing for %s")
  510. % expected_shift.worker_id.name
  511. )
  512. # Open a validation wizard only if not admin
  513. if self.env.user.has_group("beesdoo_shift.group_shift_attendance_sheet_validation"):
  514. if not self.worker_nb_feedback:
  515. raise UserError(
  516. _("Please give your feedback about the number of workers.")
  517. )
  518. self._validate(self.env.user.partner_id)
  519. return
  520. return {
  521. "type": "ir.actions.act_window",
  522. "res_model": "beesdoo.shift.sheet.validate",
  523. "view_type": "form",
  524. "view_mode": "form",
  525. "target": "new",
  526. }
  527. @api.model
  528. def _generate_attendance_sheet(self):
  529. """
  530. Generate sheets with shifts in the time interval
  531. defined from corresponding CRON time interval.
  532. """
  533. time_ranges = set()
  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)