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.

655 lines
23 KiB

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