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.

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