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.

452 lines
16 KiB

  1. # -*- coding: utf-8 -*-
  2. from openerp import models, exceptions, fields, api
  3. from openerp.exceptions import UserError, ValidationError
  4. from datetime import datetime
  5. from lxml import etree
  6. class AttendanceSheetShift(models.Model):
  7. _name = "beesdoo.shift.sheet.shift"
  8. _description = "Copy of an actual shift into an attendance sheet"
  9. # Related actual shift, not required because doesn't exist for added shift before validation
  10. # To update after validation
  11. task_id = fields.Many2one("beesdoo.shift.shift", string="Task")
  12. attendance_sheet_id = fields.Many2one(
  13. "beesdoo.shift.sheet",
  14. string="Attendance Sheet",
  15. required=True,
  16. ondelete="cascade",
  17. )
  18. stage = fields.Selection(
  19. [
  20. ("present", "Present"),
  21. ("absent", "Absent"),
  22. ("cancelled", "Cancelled"),
  23. ],
  24. string="Shift Stage",
  25. copy=False,
  26. )
  27. worker_id = fields.Many2one(
  28. "res.partner",
  29. string="Worker",
  30. domain=[
  31. ("eater", "=", "worker_eater"),
  32. ("working_mode", "in", ("regular", "irregular")),
  33. ("state", "not in", ("unsubscribed", "resigning")),
  34. ],
  35. required=True,
  36. )
  37. task_type_id = fields.Many2one("beesdoo.shift.type", string="Task Type")
  38. working_mode = fields.Selection(
  39. related="worker_id.working_mode", string="Working Mode", store=True
  40. )
  41. def get_actual_stage(self):
  42. """
  43. Mapping function returning the actual id
  44. of corresponding beesdoo.shift.stage
  45. This behavior should be temporary
  46. (increases lack of understanding).
  47. """
  48. if not self.working_mode or not self.stage:
  49. raise UserError(
  50. "Impossible to map task status, all values are not set."
  51. )
  52. if self.working_mode == "regular":
  53. if self.stage == "present":
  54. return "done"
  55. if self.stage == "absent" and self.compensation_nb:
  56. if self.compensation_nb == "0":
  57. return "excused_necessity"
  58. if self.compensation_nb == "1":
  59. return "excused"
  60. if self.compensation_nb == "2":
  61. return "absent"
  62. if self.stage == "cancelled":
  63. return "cancel"
  64. if self.working_mode == "irregular":
  65. if self.stage == "present":
  66. return "done"
  67. if self.stage == "cancelled":
  68. return "cancel"
  69. return "absent"
  70. class AttendanceSheetShiftExpected(models.Model):
  71. _name = "beesdoo.shift.sheet.expected"
  72. _description = "Expected Shift"
  73. _inherit = ["beesdoo.shift.sheet.shift"]
  74. compensation_nb = fields.Selection(
  75. [("0", "0"), ("1", "1"), ("2", "2")],
  76. string="Compensations (if absent)",
  77. )
  78. replacement_worker_id = fields.Many2one(
  79. "res.partner",
  80. string="Replacement Worker",
  81. domain=[
  82. ("eater", "=", "worker_eater"),
  83. ("working_mode", "=", "regular"),
  84. ("state", "not in", ("unsubscribed", "resigning")),
  85. ],
  86. )
  87. # The webclient has display issues with this method.
  88. @api.onchange("stage")
  89. def on_change_stage(self):
  90. if self.working_mode == "irregular":
  91. if self.stage == "present" or "cancelled":
  92. self.compensation_nb = False
  93. if self.stage == "absent":
  94. self.compensation_nb = "1"
  95. if self.working_mode == "regular":
  96. if self.stage == "present" or "cancelled":
  97. self.compensation_nb = False
  98. if self.stage == "absent":
  99. self.compensation_nb = "2"
  100. class AttendanceSheetShiftAdded(models.Model):
  101. """The added shifts stage must be Present
  102. (add an SQL constraint ?)
  103. """
  104. _name = "beesdoo.shift.sheet.added"
  105. _description = "Added Shift"
  106. _inherit = ["beesdoo.shift.sheet.shift"]
  107. # Change the previously determined two booleans for a more comprehensive field
  108. regular_task_type = fields.Selection(
  109. [("normal", "Normal"), ("compensation", "Compensation")],
  110. string="Task Mode (if regular)",
  111. help="Shift type for regular workers. ",
  112. )
  113. @api.model
  114. def create(self, vals):
  115. vals["stage"] = "present"
  116. return super(AttendanceSheetShiftAdded, self).create(vals)
  117. @api.onchange("working_mode")
  118. def on_change_working_mode(self):
  119. self.stage = "present"
  120. if self.working_mode == "regular":
  121. self.regular_task_type = "compensation"
  122. if self.working_mode == "irregular":
  123. self.regular_task_type = False
  124. class AttendanceSheet(models.Model):
  125. _name = "beesdoo.shift.sheet"
  126. _inherit = ["mail.thread"]
  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. @api.depends("start_time", "end_time")
  240. def _compute_name(self):
  241. start_time_dt = fields.Datetime.from_string(self.start_time)
  242. start_time_dt = fields.Datetime.context_timestamp(self, start_time_dt)
  243. end_time_dt = fields.Datetime.from_string(self.end_time)
  244. end_time_dt = fields.Datetime.context_timestamp(self, end_time_dt)
  245. self.name = (
  246. start_time_dt.strftime("%d/%m/%y")
  247. + " | "
  248. + start_time_dt.strftime("%H:%M")
  249. + "-"
  250. + end_time_dt.strftime("%H:%M")
  251. )
  252. return
  253. # Is this method necessary ?
  254. @api.depends("annotation")
  255. def _compute_is_annotated(self):
  256. if self.annotation:
  257. self.is_annotated = len(self.annotation) != 0
  258. return
  259. @api.model
  260. def create(self, vals):
  261. new_sheet = super(AttendanceSheet, self).create(vals)
  262. # Creation and addition of the expected shifts corresponding
  263. # to the time range
  264. tasks = self.env["beesdoo.shift.shift"]
  265. tasks = tasks.search(
  266. [
  267. ("start_time", "=", new_sheet.start_time),
  268. ("end_time", "=", new_sheet.end_time),
  269. ]
  270. )
  271. expected_shift = self.env["beesdoo.shift.sheet.expected"]
  272. task_templates = set()
  273. for task in tasks:
  274. if task.working_mode == "irregular":
  275. compensation_nb = "1"
  276. else:
  277. compensation_nb = "2"
  278. new_expected_shift = expected_shift.create(
  279. {
  280. "attendance_sheet_id": new_sheet.id,
  281. "task_id": task.id,
  282. "worker_id": task.worker_id.id,
  283. "replacement_worker_id": task.replaced_id.id,
  284. "task_type_id": task.task_type_id.id,
  285. "stage": "absent",
  286. "compensation_nb": compensation_nb,
  287. "working_mode": task.working_mode,
  288. }
  289. )
  290. task_templates.add(task.task_template_id)
  291. new_sheet.expected_worker_nb += 1
  292. # Maximum number of workers calculation
  293. for task_template in task_templates:
  294. new_sheet.max_worker_nb += task_template.worker_nb
  295. return new_sheet
  296. # @api.model
  297. # def _needaction_domain_get(self):
  298. # return [('state','=','not_validated')]
  299. @api.multi
  300. def write(self, vals):
  301. if self.state == "validated" and not self.env.user.has_group(
  302. "beesdoo_shift.group_cooperative_admin"
  303. ):
  304. raise UserError(
  305. "The sheet has already been validated and can't be edited."
  306. )
  307. return super(AttendanceSheet, self).write(vals)
  308. @api.one
  309. def validate(self):
  310. self.ensure_one()
  311. if self.state == "validated":
  312. raise UserError("The sheet has already been validated.")
  313. shift = self.env["beesdoo.shift.shift"]
  314. stage = self.env["beesdoo.shift.stage"]
  315. # Fields validation
  316. for added_shift in self.added_shift_ids:
  317. if (
  318. not added_shift.stage
  319. or not added_shift.worker_id
  320. or not added_shift.task_type_id
  321. or not added_shift.working_mode
  322. or (
  323. added_shift.worker_id.working_mode == "regular"
  324. and not added_shift.regular_task_type
  325. )
  326. ):
  327. raise UserError("All fields must be set before validation.")
  328. # Expected shifts status update
  329. for expected_shift in self.expected_shift_ids:
  330. actual_shift = expected_shift.task_id
  331. actual_stage = stage.search(
  332. [("code", "=", expected_shift.get_actual_stage())]
  333. )
  334. # If the actual stage has been deleted, the sheet is still validated.
  335. # Raising an exception would stop this.
  336. # How can we show a message without stopping validation ?
  337. if actual_stage:
  338. actual_shift.stage_id = actual_stage
  339. actual_shift.replacement_worker_id = (
  340. expected_shift.replacement_worker_id
  341. )
  342. # Added shifts status update
  343. for added_shift in self.added_shift_ids:
  344. actual_stage = stage.search(
  345. [("code", "=", added_shift.get_actual_stage())]
  346. )
  347. # WARNING: mapping the selection field to the booleans used in Task
  348. is_regular_worker = added_shift.worker_id.working_mode == "regular"
  349. is_regular_shift = added_shift.regular_task_type == "normal"
  350. # Add an annotation if a regular worker is doing its regular shift
  351. if is_regular_shift and is_regular_worker:
  352. self.annotation += (
  353. "\n\nWarning : %s attended its shift as a normal one but was not expected."
  354. " Something may be wrong in his/her personnal informations.\n"
  355. % added_shift.worker_id.name
  356. )
  357. # Edit a non-assigned shift or create one if none
  358. non_assigned_shifts = shift.search(
  359. [
  360. ("worker_id", "=", False),
  361. ("start_time", "=", self.start_time),
  362. ("end_time", "=", self.end_time),
  363. ("task_type_id", "=", added_shift.task_type_id.id)
  364. ]
  365. )
  366. if len(non_assigned_shifts):
  367. actual_shift = non_assigned_shifts[0]
  368. actual_shift.write(
  369. {
  370. "stage_id": actual_stage.id,
  371. "worker_id": added_shift.worker_id.id,
  372. "stage_id": actual_stage.id,
  373. "is_regular": is_regular_shift and is_regular_worker,
  374. "is_compensation": not is_regular_shift
  375. and is_regular_worker,
  376. }
  377. )
  378. else:
  379. actual_shift = self.env["beesdoo.shift.shift"].create(
  380. {
  381. "name": "Added shift TEST %s" % self.start_time,
  382. "task_type_id": added_shift.task_type_id.id,
  383. "worker_id": added_shift.worker_id.id,
  384. "start_time": self.start_time,
  385. "end_time": self.end_time,
  386. "stage_id": actual_stage.id,
  387. "is_regular": is_regular_shift and is_regular_worker,
  388. "is_compensation": not is_regular_shift
  389. and is_regular_worker,
  390. }
  391. )
  392. added_shift.task_id = actual_shift.id
  393. self.state = "validated"
  394. return
  395. # @api.multi is needed to call the wizard, but doesn't match @api.one
  396. # from the validate() method
  397. @api.multi
  398. def validate_via_wizard(self):
  399. if self.env.user.has_group("beesdoo_shift.group_cooperative_admin"):
  400. self.validated_by = self.env.user.partner_id
  401. self.validate()
  402. return
  403. return {
  404. "type": "ir.actions.act_window",
  405. "res_model": "beesdoo.shift.sheet.validate",
  406. "view_type": "form",
  407. "view_mode": "form",
  408. "target": "new",
  409. }