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.

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