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.

661 lines
22 KiB

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