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.

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