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.

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