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.

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