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.

605 lines
21 KiB

  1. # -*- coding: utf-8 -*-
  2. from lxml import etree
  3. from openerp import models, exceptions, fields, api, _
  4. from openerp.exceptions import UserError, ValidationError
  5. from datetime import date, datetime, timedelta
  6. from lxml import etree
  7. class AttendanceSheetShift(models.AbstractModel):
  8. _name = "beesdoo.shift.sheet.shift"
  9. _description = "Copy of an actual shift into an attendance sheet"
  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, not required because doesn't exist for added shift before validation
  20. # To update after validation
  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. stage = fields.Selection(
  29. [
  30. ("present", "Present"),
  31. ("absent_0", "Absent / 0 Compensation"),
  32. ("absent_1", "Absent / 1 Compensation"),
  33. ("absent_2", "Absent / 2 Compensations"),
  34. ("cancelled", "Cancelled"),
  35. ],
  36. string="Shift Stage",
  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. )
  47. task_type_id = fields.Many2one(
  48. "beesdoo.shift.type", string="Task Type", default=default_task_type_id
  49. )
  50. super_coop_id = fields.Many2one(
  51. "res.users",
  52. string="Super Cooperative",
  53. domain=[("partner_id.super", "=", True)],
  54. )
  55. working_mode = fields.Selection(
  56. related="worker_id.working_mode", string="Working Mode", store=True
  57. )
  58. def get_actual_stage(self):
  59. """
  60. Mapping function returning the actual id
  61. of corresponding beesdoo.shift.stage,
  62. because we prefer users to select number of compensations
  63. on the sheet rather than the exact stage name.
  64. """
  65. if not self.working_mode or not self.stage:
  66. raise UserError(
  67. _("Impossible to map task status, all values are not set.")
  68. )
  69. if self.working_mode == "regular":
  70. if self.stage == "present":
  71. return "done"
  72. if self.stage == "absent_0":
  73. return "excused_necessity"
  74. if self.stage == "absent_1":
  75. return "excused"
  76. if self.stage == "absent_2":
  77. return "absent"
  78. if self.stage == "cancelled":
  79. return "cancel"
  80. if self.working_mode == "irregular":
  81. if self.stage == "present":
  82. return "done"
  83. if self.stage == "cancelled":
  84. return "cancel"
  85. return "absent"
  86. class AttendanceSheetShiftExpected(models.Model):
  87. _name = "beesdoo.shift.sheet.expected"
  88. _description = "Expected Shift"
  89. _inherit = ["beesdoo.shift.sheet.shift"]
  90. replacement_worker_id = fields.Many2one(
  91. "res.partner",
  92. string="Replacement Worker",
  93. domain=[
  94. ("eater", "=", "worker_eater"),
  95. ("working_mode", "=", "regular"),
  96. ("state", "not in", ("unsubscribed", "resigning")),
  97. ],
  98. )
  99. class AttendanceSheetShiftAdded(models.Model):
  100. """The added shifts stage must be Present
  101. (add an SQL constraint ?)
  102. """
  103. _name = "beesdoo.shift.sheet.added"
  104. _description = "Added Shift"
  105. _inherit = ["beesdoo.shift.sheet.shift"]
  106. # Change the previously determined two booleans for a more comprehensive field
  107. regular_task_type = fields.Selection(
  108. [("normal", "Normal"), ("compensation", "Compensation")],
  109. string="Task Mode (if regular)",
  110. help="Shift type for regular workers. ",
  111. )
  112. stage = fields.Selection(default="present")
  113. # WARNING: check the code, readonly fields modified by onchange are not inserted on write
  114. @api.onchange("working_mode")
  115. def on_change_working_mode(self):
  116. self.stage = "present"
  117. if self.working_mode == "regular":
  118. self.regular_task_type = "compensation"
  119. if self.working_mode == "irregular":
  120. self.regular_task_type = False
  121. class AttendanceSheet(models.Model):
  122. _name = "beesdoo.shift.sheet"
  123. _inherit = [
  124. "mail.thread",
  125. "ir.needaction_mixin",
  126. "barcodes.barcode_events_mixin",
  127. ]
  128. _description = "Attendance sheets with all the shifts in one time range."
  129. _order = "start_time"
  130. name = fields.Char(string="Name", compute="_compute_name")
  131. time_slot = fields.Char(
  132. string="Time Slot",
  133. compute="_compute_time_slot",
  134. store=True,
  135. readonly=True,
  136. )
  137. active = fields.Boolean(string="Active", default=1)
  138. state = fields.Selection(
  139. [("not_validated", "Not Validated"), ("validated", "Validated"),],
  140. string="Status",
  141. readonly=True,
  142. index=True,
  143. default="not_validated",
  144. track_visibility="onchange",
  145. )
  146. start_time = fields.Datetime(
  147. string="Start Time", required=True, readonly=True
  148. )
  149. end_time = fields.Datetime(string="End Time", required=True, readonly=True)
  150. day = fields.Date(string="Day", compute="_compute_day", store=True)
  151. default_super_coop_id = fields.Many2one(
  152. "res.users",
  153. string="Default Super Cooperative",
  154. help="Super Cooperative for default Task Type",
  155. domain=[("partner_id.super", "=", True)],
  156. compute="_compute_default_super_coop_id",
  157. store=True,
  158. )
  159. expected_shift_ids = fields.One2many(
  160. "beesdoo.shift.sheet.expected",
  161. "attendance_sheet_id",
  162. string="Expected Shifts",
  163. )
  164. added_shift_ids = fields.One2many(
  165. "beesdoo.shift.sheet.added",
  166. "attendance_sheet_id",
  167. string="Added Shifts",
  168. )
  169. max_worker_no = fields.Integer(
  170. string="Maximum number of workers",
  171. default=0,
  172. readonly=True,
  173. help="Indicative maximum number of workers for the shifts.",
  174. )
  175. annotation = fields.Text("Annotation", default="")
  176. is_annotated = fields.Boolean(
  177. compute="_compute_is_annotated",
  178. string="Annotation",
  179. readonly=True,
  180. store=True,
  181. )
  182. is_read = fields.Boolean(
  183. string="Mark as read",
  184. help="Has annotation been read by an administrator ?",
  185. default=False,
  186. track_visibility="onchange",
  187. )
  188. feedback = fields.Text("Feedback")
  189. worker_nb_feedback = fields.Selection(
  190. [
  191. ("not_enough", "Not enough"),
  192. ("enough", "Enough"),
  193. ("too_many", "Too many"),
  194. ],
  195. string="Number of workers.",
  196. )
  197. attended_worker_nb = fields.Integer(
  198. string="Number of attended workers",
  199. default=0,
  200. help="Number of workers who attended the session.",
  201. readonly=True,
  202. )
  203. validated_by = fields.Many2one(
  204. "res.partner",
  205. string="Validated by",
  206. domain=[
  207. ("eater", "=", "worker_eater"),
  208. ("working_mode", "=", "regular"),
  209. ("state", "not in", ("unsubscribed", "resigning")),
  210. ],
  211. track_visibility="onchange",
  212. readonly=True,
  213. )
  214. _sql_constraints = [
  215. (
  216. "check_no_annotation_mark_read",
  217. "CHECK ((is_annotated=FALSE AND is_read=FALSE) OR is_annotated=TRUE)",
  218. _("Non-annotated sheets can't be marked as read."),
  219. )
  220. ]
  221. @api.constrains(
  222. "expected_shift_ids",
  223. "added_shift_ids",
  224. "annotation",
  225. "feedback",
  226. "worker_nb_feedback",
  227. )
  228. def _lock_after_validation(self):
  229. if self.state == "validated":
  230. raise UserError(
  231. _("The sheet has already been validated and can't be edited.")
  232. )
  233. @api.multi
  234. def button_mark_as_read(self):
  235. if self.is_read:
  236. raise UserError(_("The sheet has already been marked as read."))
  237. self.is_read = True
  238. @api.constrains("expected_shift_ids", "added_shift_ids")
  239. def _constrain_unique_worker(self):
  240. # Warning : map return generator in python3 (for Odoo 12)
  241. added_ids = map(lambda s: s.worker_id.id, self.added_shift_ids)
  242. expected_ids = map(lambda s: s.worker_id.id, self.expected_shift_ids)
  243. replacement_ids = map(
  244. lambda s: s.replacement_worker_id.id, self.expected_shift_ids
  245. )
  246. replacement_ids = filter(bool, replacement_ids)
  247. ids = added_ids + expected_ids + replacement_ids
  248. if (len(ids) - len(set(ids))) > 0:
  249. raise UserError(
  250. _(
  251. "You can't add the same worker more than once to an attendance sheet."
  252. )
  253. )
  254. @api.depends("start_time", "end_time")
  255. def _compute_time_slot(self):
  256. for rec in self:
  257. start_time_dt = fields.Datetime.from_string(rec.start_time)
  258. start_time_dt = fields.Datetime.context_timestamp(
  259. rec, start_time_dt
  260. )
  261. end_time_dt = fields.Datetime.from_string(rec.end_time)
  262. end_time_dt = fields.Datetime.context_timestamp(rec, end_time_dt)
  263. rec.time_slot = (
  264. start_time_dt.strftime("%H:%M")
  265. + " - "
  266. + end_time_dt.strftime("%H:%M")
  267. )
  268. @api.depends("start_time", "end_time")
  269. def _compute_name(self):
  270. for rec in self:
  271. start_time_dt = fields.Datetime.from_string(rec.start_time)
  272. start_time_dt = fields.Datetime.context_timestamp(
  273. rec, start_time_dt
  274. )
  275. if rec.time_slot:
  276. rec.name = (
  277. fields.Date.to_string(start_time_dt) + " " + rec.time_slot
  278. )
  279. @api.depends("start_time")
  280. def _compute_day(self):
  281. for rec in self:
  282. rec.day = fields.Date.from_string(rec.start_time)
  283. @api.depends("expected_shift_ids")
  284. def _compute_default_super_coop_id(self):
  285. """
  286. Look for the super cooperator of a shift
  287. with default Task Type
  288. """
  289. for rec in self:
  290. default_task_type = rec.env[
  291. "beesdoo.shift.sheet.expected"
  292. ].default_task_type_id()
  293. shift = rec.expected_shift_ids.search(
  294. [
  295. ("task_type_id", "=", default_task_type.id),
  296. ("super_coop_id", "!=", False),
  297. ],
  298. limit=1,
  299. )
  300. rec.default_super_coop_id = shift.super_coop_id
  301. # Is this method necessary ?
  302. @api.depends("annotation")
  303. def _compute_is_annotated(self):
  304. for rec in self:
  305. if rec.annotation:
  306. rec.is_annotated = len(rec.annotation) != 0
  307. @api.model
  308. def create(self, vals):
  309. new_sheet = super(AttendanceSheet, self).create(vals)
  310. # Creation and addition of the expected shifts corresponding
  311. # to the time range
  312. tasks = self.env["beesdoo.shift.shift"]
  313. tasks = tasks.search(
  314. [
  315. ("start_time", "=", new_sheet.start_time),
  316. ("end_time", "=", new_sheet.end_time),
  317. ]
  318. )
  319. expected_shift = self.env["beesdoo.shift.sheet.expected"]
  320. task_templates = set()
  321. for task in tasks:
  322. if task.working_mode == "irregular":
  323. stage = "absent_1"
  324. else:
  325. stage = "absent_2"
  326. if task.worker_id:
  327. new_expected_shift = expected_shift.create(
  328. {
  329. "attendance_sheet_id": new_sheet.id,
  330. "task_id": task.id,
  331. "worker_id": task.worker_id.id,
  332. "replacement_worker_id": task.replaced_id.id,
  333. "task_type_id": task.task_type_id.id,
  334. "super_coop_id": task.super_coop_id.id,
  335. "stage": stage,
  336. "working_mode": task.working_mode,
  337. }
  338. )
  339. task_templates.add(task.task_template_id)
  340. # Maximum number of workers calculation
  341. new_sheet.max_worker_no = sum(r.worker_nb for r in task_templates)
  342. return new_sheet
  343. # Workaround to display notifications only for unread and not validated
  344. # sheets, via a check on domain.
  345. @api.model
  346. def _needaction_count(self, domain=None):
  347. if domain == [
  348. ("is_annotated", "=", True),
  349. ("is_read", "=", False),
  350. ] or domain == [("state", "=", "not_validated")]:
  351. return self.search_count(domain)
  352. return
  353. def validate(self):
  354. self.ensure_one()
  355. if self.state == "validated":
  356. raise UserError("The sheet has already been validated.")
  357. shift = self.env["beesdoo.shift.shift"]
  358. stage = self.env["beesdoo.shift.stage"]
  359. # Fields validation
  360. for added_shift in self.added_shift_ids:
  361. if not added_shift.worker_id:
  362. raise UserError(
  363. _("Worker must be set for shift %s") % added_shift.id
  364. )
  365. if not added_shift.stage:
  366. raise UserError(
  367. _("Shift Stage is missing for %s")
  368. % added_shift.worker_id.name
  369. )
  370. if not added_shift.task_type_id:
  371. raise UserError(
  372. _("Task Type is missing for %s")
  373. % added_shift.worker_id.name
  374. )
  375. if not added_shift.working_mode:
  376. raise UserError(
  377. _("Working mode is missing for %s")
  378. % added_shift.worker_id.name
  379. )
  380. if (
  381. added_shift.worker_id.working_mode == "regular"
  382. and not added_shift.regular_task_type
  383. ):
  384. raise UserError(
  385. _("Regular Task Type is missing for %s")
  386. % added_shift.worker_id.name
  387. )
  388. for expected_shift in self.expected_shift_ids:
  389. if not expected_shift.stage:
  390. raise UserError(
  391. _("Shift Stage is missing for %s")
  392. % expected_shift.worker_id.name
  393. )
  394. # Expected shifts status update
  395. for expected_shift in self.expected_shift_ids:
  396. actual_shift = expected_shift.task_id
  397. # We get stage record corresponding to mapped stage id
  398. actual_stage = self.env.ref(
  399. "beesdoo_shift.%s" % expected_shift.get_actual_stage()
  400. )
  401. # If the actual stage has been deleted, the sheet is still validated.
  402. # Raising an exception would stop this but would prevent validation.
  403. # How can we show a message without stopping validation ?
  404. if actual_stage:
  405. actual_shift.stage_id = actual_stage
  406. actual_shift.replaced_id = expected_shift.replacement_worker_id
  407. # Added shifts status update
  408. for added_shift in self.added_shift_ids:
  409. actual_stage = self.env.ref(
  410. "beesdoo_shift.%s" % added_shift.get_actual_stage()
  411. )
  412. is_regular_worker = added_shift.worker_id.working_mode == "regular"
  413. is_regular_shift = added_shift.regular_task_type == "normal"
  414. # Add an annotation if a regular worker is doing its regular shift
  415. if is_regular_shift and is_regular_worker:
  416. self.annotation += (
  417. _(
  418. "\n\nWarning : %s attended its shift as a normal one but was not expected."
  419. " Something may be wrong in his/her personnal informations.\n"
  420. )
  421. % added_shift.worker_id.name
  422. )
  423. # Edit a non-assigned shift or create one if none
  424. non_assigned_shifts = shift.search(
  425. [
  426. ("worker_id", "=", False),
  427. ("start_time", "=", self.start_time),
  428. ("end_time", "=", self.end_time),
  429. ("task_type_id", "=", added_shift.task_type_id.id),
  430. ],
  431. limit=1,
  432. )
  433. if len(non_assigned_shifts):
  434. actual_shift = non_assigned_shifts[0]
  435. actual_shift.write(
  436. {
  437. "stage_id": actual_stage.id,
  438. "worker_id": added_shift.worker_id.id,
  439. "stage_id": actual_stage.id,
  440. "is_regular": is_regular_shift and is_regular_worker,
  441. "is_compensation": not is_regular_shift
  442. and is_regular_worker,
  443. }
  444. )
  445. else:
  446. actual_shift = self.env["beesdoo.shift.shift"].create(
  447. {
  448. "name": _("[Added Shift] %s") % self.start_time,
  449. "task_type_id": added_shift.task_type_id.id,
  450. "worker_id": added_shift.worker_id.id,
  451. "start_time": self.start_time,
  452. "end_time": self.end_time,
  453. "stage_id": actual_stage.id,
  454. "is_regular": is_regular_shift and is_regular_worker,
  455. "is_compensation": not is_regular_shift
  456. and is_regular_worker,
  457. }
  458. )
  459. added_shift.task_id = actual_shift.id
  460. self.state = "validated"
  461. return
  462. # @api.multi is needed to call the wizard, but doesn't match @api.one
  463. # from the validate() method
  464. @api.multi
  465. def validate_via_wizard(self):
  466. if self.env.user.has_group("beesdoo_shift.group_cooperative_admin"):
  467. self.validated_by = self.env.user.partner_id
  468. self.validate()
  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 20 minutes before current time.
  481. Corresponding CRON intervall time must be the same.
  482. Check if any task exists in the time intervall.
  483. If no sheet is already created, it is created.
  484. """
  485. time_ranges = set()
  486. tasks = self.env["beesdoo.shift.shift"]
  487. sheets = self.env["beesdoo.shift.sheet"]
  488. current_time = datetime.now()
  489. allowed_time_range = timedelta(minutes=20)
  490. tasks = tasks.search(
  491. [
  492. ("start_time", ">", str(current_time),),
  493. ("start_time", "<", str(current_time + allowed_time_range),),
  494. ]
  495. )
  496. for task in tasks:
  497. start_time = task.start_time
  498. end_time = task.end_time
  499. sheets = sheets.search(
  500. [("start_time", "=", start_time), ("end_time", "=", end_time),]
  501. )
  502. if not sheets:
  503. sheet = sheets.create(
  504. {"start_time": start_time, "end_time": end_time}
  505. )
  506. def on_barcode_scanned(self, barcode):
  507. if self.state == "validated":
  508. raise UserError(
  509. _("You cannot modify a validated attendance sheet.")
  510. )
  511. worker = self.env["res.partner"].search([("barcode", "=", barcode)])
  512. if not len(worker):
  513. raise UserError(_("Worker not found (invalid barcode or status)."))
  514. if len(worker) > 1:
  515. raise UserError(
  516. _("Multiple workers are corresponding this barcode.")
  517. )
  518. if worker.state in ("unsubscribed", "resigning"):
  519. raise UserError(_("Worker is %s.") % worker.state)
  520. if worker.working_mode not in ("regular", "irregular"):
  521. raise UserError(
  522. _("Worker is %s and should be regular or irregular.")
  523. % worker.working_mode
  524. )
  525. for id in self.expected_shift_ids.ids:
  526. shift = self.env["beesdoo.shift.sheet.expected"].browse(id)
  527. if (
  528. shift.worker_id == worker
  529. or shift.replacement_worker_id == worker
  530. ):
  531. shift.stage = "present"
  532. return
  533. if worker.working_mode == "regular":
  534. regular_task_type = "normal"
  535. else:
  536. regular_task_type = False
  537. added_ids = map(lambda s: s.worker_id.id, self.added_shift_ids)
  538. if worker.id in added_ids:
  539. raise UserError(_("Worker is already present."))
  540. self.added_shift_ids |= self.added_shift_ids.new(
  541. {
  542. "task_type_id": self.added_shift_ids.default_task_type_id(),
  543. "stage": "present",
  544. "attendance_sheet_id": self._origin.id,
  545. "worker_id": worker.id,
  546. "regular_task_type": regular_task_type,
  547. }
  548. )