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.

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