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.

635 lines
22 KiB

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