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.

462 lines
16 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 datetime
  6. from lxml import etree
  7. class AttendanceSheetShift(models.Model):
  8. _name = "beesdoo.shift.sheet.shift"
  9. _description = "Copy of an actual shift into an attendance sheet"
  10. # Related actual shift, not required because doesn't exist for added shift before validation
  11. # To update after validation
  12. task_id = fields.Many2one("beesdoo.shift.shift", string="Task")
  13. attendance_sheet_id = fields.Many2one(
  14. "beesdoo.shift.sheet",
  15. string="Attendance Sheet",
  16. required=True,
  17. ondelete="cascade",
  18. )
  19. stage = fields.Selection(
  20. [
  21. ("present", "Present"),
  22. ("absent", "Absent"),
  23. ("cancelled", "Cancelled"),
  24. ],
  25. string="Shift Stage",
  26. copy=False,
  27. )
  28. worker_id = fields.Many2one(
  29. "res.partner",
  30. string="Worker",
  31. domain=[
  32. ("eater", "=", "worker_eater"),
  33. ("working_mode", "in", ("regular", "irregular")),
  34. ("state", "not in", ("unsubscribed", "resigning")),
  35. ],
  36. required=True,
  37. )
  38. task_type_id = fields.Many2one("beesdoo.shift.type", string="Task Type")
  39. working_mode = fields.Selection(
  40. related="worker_id.working_mode", string="Working Mode", store=True
  41. )
  42. def get_actual_stage(self):
  43. """
  44. Mapping function returning the actual id
  45. of corresponding beesdoo.shift.stage
  46. This behavior should be temporary
  47. (increases lack of understanding).
  48. """
  49. if not self.working_mode or not self.stage:
  50. raise UserError(
  51. "Impossible to map task status, all values are not set."
  52. )
  53. if self.working_mode == "regular":
  54. if self.stage == "present":
  55. return "done"
  56. if self.stage == "absent" and self.compensation_nb:
  57. if self.compensation_nb == "0":
  58. return "excused_necessity"
  59. if self.compensation_nb == "1":
  60. return "excused"
  61. if self.compensation_nb == "2":
  62. return "absent"
  63. if self.stage == "cancelled":
  64. return "cancel"
  65. if self.working_mode == "irregular":
  66. if self.stage == "present":
  67. return "done"
  68. if self.stage == "cancelled":
  69. return "cancel"
  70. return "absent"
  71. class AttendanceSheetShiftExpected(models.Model):
  72. _name = "beesdoo.shift.sheet.expected"
  73. _description = "Expected Shift"
  74. _inherit = ["beesdoo.shift.sheet.shift"]
  75. compensation_nb = fields.Selection(
  76. [("0", "0"), ("1", "1"), ("2", "2")],
  77. string="Compensations (if absent)",
  78. )
  79. replacement_worker_id = fields.Many2one(
  80. "res.partner",
  81. string="Replacement Worker",
  82. domain=[
  83. ("eater", "=", "worker_eater"),
  84. ("working_mode", "=", "regular"),
  85. ("state", "not in", ("unsubscribed", "resigning")),
  86. ],
  87. )
  88. # The webclient has display issues with this method.
  89. @api.onchange("stage")
  90. def on_change_stage(self):
  91. if self.working_mode == "irregular":
  92. if self.stage == "present" or "cancelled":
  93. self.compensation_nb = False
  94. if self.stage == "absent":
  95. self.compensation_nb = "1"
  96. if self.working_mode == "regular":
  97. if self.stage == "present" or "cancelled":
  98. self.compensation_nb = False
  99. if self.stage == "absent":
  100. self.compensation_nb = "2"
  101. class AttendanceSheetShiftAdded(models.Model):
  102. """The added shifts stage must be Present
  103. (add an SQL constraint ?)
  104. """
  105. _name = "beesdoo.shift.sheet.added"
  106. _description = "Added Shift"
  107. _inherit = ["beesdoo.shift.sheet.shift"]
  108. # Change the previously determined two booleans for a more comprehensive field
  109. regular_task_type = fields.Selection(
  110. [("normal", "Normal"), ("compensation", "Compensation")],
  111. string="Task Mode (if regular)",
  112. help="Shift type for regular workers. ",
  113. )
  114. @api.model
  115. def create(self, vals):
  116. vals["stage"] = "present"
  117. return super(AttendanceSheetShiftAdded, self).create(vals)
  118. @api.onchange("working_mode")
  119. def on_change_working_mode(self):
  120. self.stage = "present"
  121. if self.working_mode == "regular":
  122. self.regular_task_type = "compensation"
  123. if self.working_mode == "irregular":
  124. self.regular_task_type = False
  125. class AttendanceSheet(models.Model):
  126. _name = "beesdoo.shift.sheet"
  127. _inherit = ["mail.thread", "ir.needaction_mixin"]
  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", store=True)
  131. state = fields.Selection(
  132. [
  133. ("not_validated", "Not Validated"),
  134. ("validated", "Validated"),
  135. ("cancelled", "Cancelled"),
  136. ],
  137. string="Status",
  138. readonly=True,
  139. index=True,
  140. copy=False,
  141. default="not_validated",
  142. track_visibility="onchange",
  143. )
  144. start_time = fields.Datetime(
  145. string="Start Time", required=True, readonly=True
  146. )
  147. end_time = fields.Datetime(string="End Time", required=True, readonly=True)
  148. expected_shift_ids = fields.One2many(
  149. "beesdoo.shift.sheet.expected",
  150. "attendance_sheet_id",
  151. string="Expected Shifts",
  152. )
  153. added_shift_ids = fields.One2many(
  154. "beesdoo.shift.sheet.added",
  155. "attendance_sheet_id",
  156. string="Added Shifts",
  157. )
  158. max_worker_nb = fields.Integer(
  159. string="Maximum number of workers",
  160. default=0,
  161. readonly=True,
  162. help="Indicative maximum number of workers for the shifts.",
  163. )
  164. expected_worker_nb = fields.Integer(
  165. string="Number of expected workers", readonly=True, default=0
  166. )
  167. added_worker_nb = fields.Integer(
  168. compute="_compute_added_shift_nb",
  169. string="Number of added workers",
  170. readonly=True,
  171. default=0,
  172. )
  173. annotation = fields.Text(
  174. "Attendance Sheet annotation", default="", track_visibility="onchange"
  175. )
  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(
  189. "Attendance Sheet feedback", track_visibility="onchange"
  190. )
  191. worker_nb_feedback = fields.Selection(
  192. [
  193. ("not_enough", "Not enough"),
  194. ("enough", "Enough"),
  195. ("too much", "Too much"),
  196. ],
  197. string="Feedback regarding the number of workers.",
  198. track_visibility="onchange",
  199. )
  200. attended_worker_nb = fields.Integer(
  201. string="Number of attended workers",
  202. default=0,
  203. help="Number of workers who attended the session.",
  204. )
  205. validated_by = fields.Many2one(
  206. "res.partner",
  207. string="Validated by",
  208. domain=[
  209. ("eater", "=", "worker_eater"),
  210. ("super", "=", True),
  211. ("working_mode", "=", "regular"),
  212. ("state", "not in", ("unsubscribed", "resigning")),
  213. ],
  214. track_visibility="onchange",
  215. )
  216. _sql_constraints = [
  217. (
  218. "check_no_annotation_mark_read",
  219. "CHECK ((is_annotated=FALSE AND is_read=FALSE) OR is_annotated=TRUE)",
  220. "Non-annotated sheets can't be marked as read. ",
  221. )
  222. ]
  223. @api.constrains("expected_shift_ids", "added_shift_ids")
  224. def _constrain_unique_worker(self):
  225. added_workers = set(self.added_shift_ids.mapped("worker_id").ids)
  226. expected_workers = self.expected_shift_ids.mapped("worker_id").ids
  227. replacement_workers = self.expected_shift_ids.mapped(
  228. "replacement_worker_id"
  229. ).ids
  230. if len(
  231. added_workers.intersection(replacement_workers + expected_workers)
  232. ):
  233. raise UserError("You can't add an already expected worker.")
  234. @api.depends("added_shift_ids")
  235. def _compute_added_shift_nb(self):
  236. self.added_worker_nb = len(self.added_shift_ids)
  237. return
  238. # Compute name (not hardcorded to prevent incoherence with timezone)
  239. # Actually not working, should depends on timezone as well
  240. @api.depends("start_time", "end_time")
  241. def _compute_name(self):
  242. start_time_dt = fields.Datetime.from_string(self.start_time)
  243. start_time_dt = fields.Datetime.context_timestamp(self, start_time_dt)
  244. end_time_dt = fields.Datetime.from_string(self.end_time)
  245. end_time_dt = fields.Datetime.context_timestamp(self, end_time_dt)
  246. self.name = (
  247. start_time_dt.strftime("%Y-%m-%d")
  248. + " "
  249. + start_time_dt.strftime("%H:%M")
  250. + " - "
  251. + end_time_dt.strftime("%H:%M")
  252. )
  253. return
  254. # Is this method necessary ?
  255. @api.depends("annotation")
  256. def _compute_is_annotated(self):
  257. if self.annotation:
  258. self.is_annotated = len(self.annotation) != 0
  259. return
  260. @api.model
  261. def create(self, vals):
  262. new_sheet = super(AttendanceSheet, self).create(vals)
  263. # Creation and addition of the expected shifts corresponding
  264. # to the time range
  265. tasks = self.env["beesdoo.shift.shift"]
  266. tasks = tasks.search(
  267. [
  268. ("start_time", "=", new_sheet.start_time),
  269. ("end_time", "=", new_sheet.end_time),
  270. ]
  271. )
  272. expected_shift = self.env["beesdoo.shift.sheet.expected"]
  273. task_templates = set()
  274. for task in tasks:
  275. if task.working_mode == "irregular":
  276. compensation_nb = "1"
  277. else:
  278. compensation_nb = "2"
  279. new_expected_shift = expected_shift.create(
  280. {
  281. "attendance_sheet_id": new_sheet.id,
  282. "task_id": task.id,
  283. "worker_id": task.worker_id.id,
  284. "replacement_worker_id": task.replaced_id.id,
  285. "task_type_id": task.task_type_id.id,
  286. "stage": "absent",
  287. "compensation_nb": compensation_nb,
  288. "working_mode": task.working_mode,
  289. }
  290. )
  291. task_templates.add(task.task_template_id)
  292. new_sheet.expected_worker_nb += 1
  293. # Maximum number of workers calculation
  294. for task_template in task_templates:
  295. new_sheet.max_worker_nb += task_template.worker_nb
  296. return new_sheet
  297. # Workaround to display notifications only for unread and not validated
  298. # sheets, via a check on domain.
  299. @api.model
  300. def _needaction_count(self, domain=None):
  301. if domain == [
  302. ("is_annotated", "=", True),
  303. ("is_read", "=", False),
  304. ] or domain == [("state", "=", "not_validated")]:
  305. return self.search_count(domain)
  306. return
  307. @api.multi
  308. def write(self, vals):
  309. if self.state == "validated" and not self.env.user.has_group(
  310. "beesdoo_shift.group_cooperative_admin"
  311. ):
  312. raise UserError(
  313. "The sheet has already been validated and can't be edited."
  314. )
  315. return super(AttendanceSheet, self).write(vals)
  316. @api.one
  317. def validate(self):
  318. self.ensure_one()
  319. if self.state == "validated":
  320. raise UserError("The sheet has already been validated.")
  321. shift = self.env["beesdoo.shift.shift"]
  322. stage = self.env["beesdoo.shift.stage"]
  323. # Fields validation
  324. for added_shift in self.added_shift_ids:
  325. if (
  326. not added_shift.stage
  327. or not added_shift.worker_id
  328. or not added_shift.task_type_id
  329. or not added_shift.working_mode
  330. or (
  331. added_shift.worker_id.working_mode == "regular"
  332. and not added_shift.regular_task_type
  333. )
  334. ):
  335. raise UserError("All fields must be set before validation.")
  336. # Expected shifts status update
  337. for expected_shift in self.expected_shift_ids:
  338. actual_shift = expected_shift.task_id
  339. actual_stage = stage.search(
  340. [("code", "=", expected_shift.get_actual_stage())]
  341. )
  342. # If the actual stage has been deleted, the sheet is still validated.
  343. # Raising an exception would stop this.
  344. # How can we show a message without stopping validation ?
  345. if actual_stage:
  346. actual_shift.stage_id = actual_stage
  347. actual_shift.replaced_id = expected_shift.replacement_worker_id
  348. # Added shifts status update
  349. for added_shift in self.added_shift_ids:
  350. actual_stage = stage.search(
  351. [("code", "=", added_shift.get_actual_stage())]
  352. )
  353. # WARNING: mapping the selection field to the booleans used in Task
  354. is_regular_worker = added_shift.worker_id.working_mode == "regular"
  355. is_regular_shift = added_shift.regular_task_type == "normal"
  356. # Add an annotation if a regular worker is doing its regular shift
  357. if is_regular_shift and is_regular_worker:
  358. self.annotation += (
  359. "\n\nWarning : %s attended its shift as a normal one but was not expected."
  360. " Something may be wrong in his/her personnal informations.\n"
  361. % added_shift.worker_id.name
  362. )
  363. # Edit a non-assigned shift or create one if none
  364. non_assigned_shifts = shift.search(
  365. [
  366. ("worker_id", "=", False),
  367. ("start_time", "=", self.start_time),
  368. ("end_time", "=", self.end_time),
  369. ("task_type_id", "=", added_shift.task_type_id.id)
  370. ]
  371. )
  372. if len(non_assigned_shifts):
  373. actual_shift = non_assigned_shifts[0]
  374. actual_shift.write(
  375. {
  376. "stage_id": actual_stage.id,
  377. "worker_id": added_shift.worker_id.id,
  378. "stage_id": actual_stage.id,
  379. "is_regular": is_regular_shift and is_regular_worker,
  380. "is_compensation": not is_regular_shift
  381. and is_regular_worker,
  382. }
  383. )
  384. else:
  385. actual_shift = self.env["beesdoo.shift.shift"].create(
  386. {
  387. "name": "Added shift TEST %s" % self.start_time,
  388. "task_type_id": added_shift.task_type_id.id,
  389. "worker_id": added_shift.worker_id.id,
  390. "start_time": self.start_time,
  391. "end_time": self.end_time,
  392. "stage_id": actual_stage.id,
  393. "is_regular": is_regular_shift and is_regular_worker,
  394. "is_compensation": not is_regular_shift
  395. and is_regular_worker,
  396. }
  397. )
  398. added_shift.task_id = actual_shift.id
  399. self.state = "validated"
  400. return
  401. # @api.multi is needed to call the wizard, but doesn't match @api.one
  402. # from the validate() method
  403. @api.multi
  404. def validate_via_wizard(self):
  405. if self.env.user.has_group("beesdoo_shift.group_cooperative_admin"):
  406. self.validated_by = self.env.user.partner_id
  407. self.validate()
  408. return
  409. return {
  410. "type": "ir.actions.act_window",
  411. "res_model": "beesdoo.shift.sheet.validate",
  412. "view_type": "form",
  413. "view_mode": "form",
  414. "target": "new",
  415. }