Browse Source

Merge branch '12.0-mig-beesdoo_shift' of github.com:beescoop/obeesdoo into 12.0-target

pull/161/head
Manuel Claeys Bouuaert 4 years ago
parent
commit
990e331079
  1. 1
      beesdoo_base/__manifest__.py
  2. 48
      beesdoo_base/demo/cooperators.xml
  3. 37
      beesdoo_base/demo/users.xml
  4. 2
      beesdoo_base/views/partner.xml
  5. 5
      beesdoo_shift/__init__.py
  6. 16
      beesdoo_shift/__manifest__.py
  7. 41
      beesdoo_shift/data/cron.xml
  8. 58
      beesdoo_shift/data/mail_template.xml
  9. 44
      beesdoo_shift/data/stage.xml
  10. 84
      beesdoo_shift/demo/cooperators.xml
  11. 107
      beesdoo_shift/demo/templates.xml
  12. 53
      beesdoo_shift/demo/workers.xml
  13. 2654
      beesdoo_shift/i18n/fr_BE.po
  14. 2633
      beesdoo_shift/i18n/nl_BE.po
  15. 6
      beesdoo_shift/models/__init__.py
  16. 413
      beesdoo_shift/models/cooperative_status.py
  17. 53
      beesdoo_shift/models/planning.py
  18. 248
      beesdoo_shift/models/task.py
  19. 23
      beesdoo_shift/security/group.xml
  20. 35
      beesdoo_shift/security/ir.model.access.csv
  21. 62
      beesdoo_shift/views/cooperative_status.xml
  22. 4
      beesdoo_shift/views/planning.xml
  23. 123
      beesdoo_shift/views/task.xml
  24. 90
      beesdoo_shift/views/task_template.xml
  25. 14
      beesdoo_shift/wizard/__init__.py
  26. 3
      beesdoo_shift/wizard/assign_super_coop.py
  27. 3
      beesdoo_shift/wizard/batch_template.py
  28. 13
      beesdoo_shift/wizard/extension.py
  29. 5
      beesdoo_shift/wizard/holiday.py
  30. 5
      beesdoo_shift/wizard/instanciate_planning.py
  31. 11
      beesdoo_shift/wizard/subscribe.py
  32. 2
      beesdoo_shift/wizard/subscribe.xml
  33. 5
      beesdoo_shift/wizard/temporary_exemption.py
  34. 2
      beesdoo_shift_attendance/__init__.py
  35. 44
      beesdoo_shift_attendance/__manifest__.py
  36. 29
      beesdoo_shift_attendance/data/cron.xml
  37. 118
      beesdoo_shift_attendance/data/mail_template.xml
  38. 14
      beesdoo_shift_attendance/data/system_parameter.xml
  39. 40
      beesdoo_shift_attendance/demo/users.xml
  40. 2
      beesdoo_shift_attendance/models/__init__.py
  41. 656
      beesdoo_shift_attendance/models/attendance_sheet.py
  42. 63
      beesdoo_shift_attendance/models/res_config_settings.py
  43. 14
      beesdoo_shift_attendance/security/group.xml
  44. 12
      beesdoo_shift_attendance/security/ir.model.access.csv
  45. 1
      beesdoo_shift_attendance/tests/__init__.py
  46. 391
      beesdoo_shift_attendance/tests/test_beesdoo_shift.py
  47. 263
      beesdoo_shift_attendance/views/attendance_sheet.xml
  48. 141
      beesdoo_shift_attendance/views/res_config_settings_view.xml
  49. 2
      beesdoo_shift_attendance/wizard/__init__.py
  50. 57
      beesdoo_shift_attendance/wizard/generate_missing_attendance_sheets.py
  51. 35
      beesdoo_shift_attendance/wizard/generate_missing_attendance_sheets.xml
  52. 138
      beesdoo_shift_attendance/wizard/validate_attendance_sheet.py
  53. 48
      beesdoo_shift_attendance/wizard/validate_attendance_sheet.xml
  54. 26
      beesdoo_website_shift/__manifest__.py
  55. 32
      beesdoo_website_shift/__openerp__.py
  56. 90
      beesdoo_website_shift/controllers/main.py
  57. 38
      beesdoo_website_shift/data/res_config_data.xml
  58. 2
      beesdoo_website_shift/i18n/fr_BE.po
  59. 11
      beesdoo_website_shift/migrations/9.0.2.1.1/post-migrate.py
  60. 1
      beesdoo_website_shift/models/__init__.py
  61. 127
      beesdoo_website_shift/models/res_config.py
  62. 47
      beesdoo_website_shift/models/website.py
  63. 1417
      beesdoo_website_shift/views/my_shift_website_templates.xml
  64. 184
      beesdoo_website_shift/views/res_config_views.xml
  65. 335
      beesdoo_website_shift/views/shift_website_templates.xml
  66. 0
      beesdoo_worker_status/__init__.py
  67. 27
      beesdoo_worker_status/__manifest__.py
  68. 1
      beesdoo_worker_status/models/__init__.py
  69. 280
      beesdoo_worker_status/models/cooperative_status.py
  70. 81
      beesdoo_worker_status/models/task.py
  71. 1
      beesdoo_worker_status/tests/__init__.py
  72. 247
      beesdoo_worker_status/tests/test_beesdoo_shift.py
  73. 15
      beesdoo_worker_status/views/res_partner.xml
  74. 1
      macavrac_base/__init__.py
  75. 23
      macavrac_base/__manifest__.py
  76. 1
      macavrac_base/models/__init__.py
  77. 49
      macavrac_base/models/res_partner.py
  78. 35
      macavrac_base/views/res_partner.xml

1
beesdoo_base/__manifest__.py

@ -35,6 +35,5 @@
'installable': True,
'demo': [
'demo/cooperators.xml',
'demo/users.xml',
]
}

48
beesdoo_base/demo/cooperators.xml

@ -83,4 +83,52 @@
<field name="zip">1060</field>
<field name="country_id" ref="base.be"/>
</record>
<record id="member_card_1_demo" model="member.card">
<field name="barcode">421457731741</field>
<field name="comment">Demo data</field>
<field name="create_date" eval="datetime.now() - timedelta(days=15)" />
<field name="partner_id" ref="res_partner_cooperator_1_demo" />
<field name="valid" eval="True" />
</record>
<record id="member_card_2_demo" model="member.card">
<field name="barcode">429919251493</field>
<field name="comment">Demo data</field>
<field name="create_date" eval="datetime.now() - timedelta(days=10)" />
<field name="partner_id" ref="res_partner_cooperator_2_demo" />
<field name="valid" eval="True" />
</record>
<record id="member_card_3_demo" model="member.card">
<field name="barcode">421457731742</field>
<field name="comment">Demo data</field>
<field name="create_date" eval="datetime.now() - timedelta(days=15)" />
<field name="partner_id" ref="res_partner_cooperator_3_demo" />
<field name="valid" eval="True" />
</record>
<record id="member_card_4_demo" model="member.card">
<field name="barcode">421457731743</field>
<field name="comment">Demo data</field>
<field name="create_date" eval="datetime.now() - timedelta(days=15)" />
<field name="partner_id" ref="res_partner_cooperator_4_demo" />
<field name="valid" eval="True" />
</record>
<record id="member_card_5_demo" model="member.card">
<field name="barcode">421457731744</field>
<field name="comment">Demo data</field>
<field name="create_date" eval="datetime.now() - timedelta(days=15)" />
<field name="partner_id" ref="res_partner_cooperator_5_demo" />
<field name="valid" eval="True" />
</record>
<record id="member_card_6_demo" model="member.card">
<field name="barcode">421457731745</field>
<field name="comment">Demo data</field>
<field name="create_date" eval="datetime.now() - timedelta(days=15)" />
<field name="partner_id" ref="res_partner_cooperator_6_demo" />
<field name="valid" eval="True" />
</record>
</odoo>

37
beesdoo_base/demo/users.xml

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Coop IT Easy
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<!-- Generic and permanent account -->
<record id="beesdoo_shift_partner_1_demo" model="res.partner">
<field name="firstname">Generic Account</field>
<field name="lastname">Demo</field>
<field name="email">generic@demo.net</field>
</record>
<record id="beesdoo_shift_partner_2_demo" model="res.partner">
<field name="firstname">Permanent Member</field>
<field name="lastname">Demo</field>
<field name="is_company" eval="False"/>
<field name="email">permanent@demo.net</field>
<field name="city">Ixelles</field>
<field name="zip">1050</field>
<field name="country_id" ref="base.be"/>
</record>
<record id="beesdoo_shift_user_1_demo" model="res.users">
<field name="partner_id" ref="beesdoo_shift_partner_1_demo"/>
<field name="login">generic</field>
<field name="password">demo</field>
</record>
<record id="beesdoo_shift_user_2_demo" model="res.users">
<field name="partner_id" ref="beesdoo_shift_partner_2_demo"/>
<field name="login">permanent</field>
<field name="password">demo</field>
</record>
</odoo>

2
beesdoo_base/views/partner.xml

@ -20,7 +20,7 @@
<field name="inherit_id" ref="point_of_sale.view_partner_property_form" />
<field name="arch" type="xml">
<field name="type" position="before">
<field name="cooperator_type" />
<field name="cooperator_type"/>
</field>
<field name="website" position="after">
<field name="eater"

5
beesdoo_shift/__init__.py

@ -1,3 +1,2 @@
# -*- coding: utf-8 -*-
import models
import wizard
from . import models
from . import wizard

16
beesdoo_shift/__openerp__.py → beesdoo_shift/__manifest__.py

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
{
'name': "Beescoop Shift Management",
@ -9,18 +8,20 @@
""",
'author': "Thibault Francois",
'author': "THibault Francois, Elouan Le Bars, Coop It Easy",
'website': "https://github.com/beescoop/Obeesdoo",
'category': 'Cooperative management',
'version': '9.0.1.2.0',
'version': '12.0.1.0.0',
'depends': ['beesdoo_base'],
'depends': [
'mail',
],
'data': [
"data/stage.xml",
"data/system_parameter.xml",
"data/cron.xml",
"data/mail_template.xml",
"security/group.xml",
"security/ir.model.access.csv",
"views/task_template.xml",
@ -36,4 +37,9 @@
"wizard/holiday.xml",
"wizard/temporary_exemption.xml",
],
'demo': [
"demo/cooperators.xml",
"demo/workers.xml",
"demo/templates.xml",
]
}

41
beesdoo_shift/data/cron.xml

@ -2,38 +2,53 @@
<data noupdate="1">
<record id="ir_cron_update_today" model="ir.cron">
<field name="name">Update Cooperatoor status base on the date</field>
<field name="model_id" ref="model_cooperative_status" />
<field name="state">code</field>
<field name="code">model._set_today()</field>
<field name="interval_number">24</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="model">cooperative.status</field>
<field name="function">_set_today</field>
<field name="args">()</field>
</record>
<record id="ir_cron_generate_next_planning" model="ir.cron">
<field name="name">Generate Next Planning</field>
<field name="model_id" ref="model_beesdoo_shift_planning" />
<field name="state">code</field>
<field name="code">model._generate_next_planning()</field>
<field name="interval_number">1</field>
<field name="interval_type">weeks</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="model">beesdoo.shift.planning</field>
<field name="function">_generate_next_planning</field>
<field name="args">()</field>
<field name="active" eval="False" />
</record>
<record id="ir_cron_compute_shift_counter" model="ir.cron">
<field name="name">Compute Shift Counter</field>
<field name="model_id" ref="model_cooperative_status" />
<field name="state">code</field>
<field name="code">model._cron_compute_counter_irregular()</field>
<field name="interval_number">4</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True" />
<field name="model">cooperative.status</field>
<field name="function">_cron_compute_counter_irregular</field>
<field name="args">()</field>
<field name="active" eval="False" />
<field name="state">code</field>
</record>
<record id="ir_cron_send_weekly_emails" model="ir.cron">
<field name="name">Send weekly shift summary</field>
<field name="model_id" ref="model_beesdoo_shift_shift" />
<field name="state">code</field>
<field name="code">model._cron_send_weekly_emails()</field>
<field name="interval_number">7</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True" />
<field name="nextcall"
eval="datetime.now() + timedelta((6 - datetime.now().weekday()) % 7)"
/>
<field name="active" eval="False" />
</record>
</data>
</odoo>
</odoo>

58
beesdoo_shift/data/mail_template.xml

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Mail template are declared in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<record id="email_template_shift_summary" model="mail.template">
<field name="name">Shift Summary</field>
<field name="subject">Your next shift (${format_tz(object.start_time,object.worker_id.tz or 'Europe/Brussels','%d.%m.%Y - %H:%M')})</field>
<field name="email_from">${object.worker_id.company_id.email}</field>
<field name="partner_to">${object.replaced_id.id or object.worker_id.id|safe}</field>
<field name="model_id" ref="model_beesdoo_shift_shift"/>
<field name="auto_delete" eval="True"/>
<field name="lang">${object.worker_id.lang}</field>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
<p>Hello ${object.worker_id.name},</p>
<p>You are awaited the ${format_tz(object.start_time,object.worker_id.tz or 'Europe/Brussels','%d.%m.%Y')}
for the shift starting at ${format_tz(object.start_time,object.worker_id.tz or 'Europe/Brussels','%H:%M')}.
<br/><br/>Please contact us at ${object.worker_id.company_id.email} if you have any trouble attending the shift.
</p>
<br/>
<p>Sustainably yours,</p>
<p>${object.worker_id.company_id.name}.</p>
% if object.worker_id.company_id.street:
${object.worker_id.company_id.street}
% endif
% if object.worker_id.company_id.street2:
${object.worker_id.company_id.street2}<br/>
% endif
% if object.worker_id.company_id.city or object.worker_id.company_id.zip:
${object.worker_id.company_id.zip} ${object.worker_id.company_id.city}<br/>
% endif
% if object.worker_id.company_id.country_id:
${object.worker_id.company_id.state_id and ('%s, ' % object.worker_id.company_id.state_id.name) or ''} ${object.worker_id.company_id.country_id.name or ''}<br/>
% endif
% if object.worker_id.company_id.phone:
Phone:&nbsp; ${object.worker_id.company_id.phone}
% endif
% if object.worker_id.company_id.website:
<div>
Web :&nbsp;<a href="${object.worker_id.company_id.website}">${object.worker_id.company_id.website}</a>
</div>
%endif
% if object.worker_id.company_id.logo_url:
<div>
<img src=${object.worker_id.company_id.logo_url}>
</div>
%endif
</div>
]]></field>
</record>
</data>
</odoo>

44
beesdoo_shift/data/stage.xml

@ -1,44 +0,0 @@
<odoo>
<record model="beesdoo.shift.stage" id="draft">
<field name="name">Unconfirmed</field>
<field name="sequence">1</field>
<field name="color">0</field>
<field name="code">draft</field>
</record>
<record model="beesdoo.shift.stage" id="open">
<field name="name">Confirmed</field>
<field name="sequence">2</field>
<field name="color">5</field>
<field name="code">open</field>
</record>
<record model="beesdoo.shift.stage" id="done">
<field name="name">Attended</field>
<field name="sequence">3</field>
<field name="color">1</field>
<field name="code">done</field>
</record>
<record model="beesdoo.shift.stage" id="absent">
<field name="name">Absent</field>
<field name="sequence">5</field>
<field name="color">2</field>
<field name="code">absent</field>
</record>
<record model="beesdoo.shift.stage" id="excused">
<field name="name">Excused</field>
<field name="sequence">6</field>
<field name="color">4</field>
<field name="code">excused</field>
</record>
<record model="beesdoo.shift.stage" id="excused_necessity">
<field name="name">Excused - Absolute Necessity</field>
<field name="sequence">7</field>
<field name="color">4</field>
<field name="code">excused_necessity</field>
</record>
<record model="beesdoo.shift.stage" id="cancel">
<field name="name">Cancelled</field>
<field name="sequence">8</field>
<field name="color">8</field>
<field name="code">cancel</field>
</record>
</odoo>

84
beesdoo_shift/demo/cooperators.xml

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Coop IT Easy
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="res_partner_cooperator_1_demo" model="res.partner">
<field name="name">Fernand Peso</field>
<field name="customer" eval="True"/>
<field name="is_company" eval="False"/>
<field name="email">fernand_peso@demo.net</field>
<field name="street">Avenue des Bas-de-Callanques, 15</field>
<field name="city">Etterbeek</field>
<field name="zip">1040</field>
<field name="country_id" ref="base.be"/>
<field name="working_mode">regular</field>
<field name="is_worker" eval="True" />
</record>
<record id="res_partner_cooperator_2_demo" model="res.partner">
<field name="name">Dupont Dupont</field>
<field name="customer" eval="True"/>
<field name="is_company" eval="False"/>
<field name="email">d_dupont@demo.net</field>
<field name="street">Rue des sables, 20</field>
<field name="city">Bruxelles</field>
<field name="zip">10000</field>
<field name="country_id" ref="base.be"/>
<field name="working_mode">irregular</field>
<field name="is_worker" eval="True" />
</record>
<record id="res_partner_cooperator_3_demo" model="res.partner">
<field name="name">Ronan Le Gall</field>
<field name="customer" eval="True"/>
<field name="is_company" eval="False"/>
<field name="email">ronan_gall@demo.net</field>
<field name="street">Rue des pecheurs, 23</field>
<field name="city">Landudec</field>
<field name="zip">29710</field>
<field name="country_id" ref="base.fr"/>
<field name="working_mode">regular</field>
<field name="is_worker" eval="True" />
</record>
<record id="res_partner_cooperator_4_demo" model="res.partner">
<field name="name">Elouan Bees</field>
<field name="customer" eval="True"/>
<field name="is_company" eval="False"/>
<field name="email">elouan_bees@demo.net</field>
<field name="street">Rue Wéry, 15</field>
<field name="city">Ixelles</field>
<field name="zip">1050</field>
<field name="country_id" ref="base.be"/>
<field name="working_mode">irregular</field>
<field name="is_worker" eval="True" />
</record>
<record id="res_partner_cooperator_5_demo" model="res.partner">
<field name="name">Anne de Marchalo</field>
<field name="customer" eval="True"/>
<field name="is_company" eval="False"/>
<field name="email">anne_marchalo@demo.net</field>
<field name="street">Rue du Wels, 6</field>
<field name="city">Nantes</field>
<field name="zip">44000</field>
<field name="country_id" ref="base.fr"/>
<field name="working_mode">regular</field>
<field name="is_worker" eval="True" />
</record>
<record id="res_partner_cooperator_6_demo" model="res.partner">
<field name="name">Jean Beaumont</field>
<field name="customer" eval="True"/>
<field name="is_company" eval="False"/>
<field name="email">jean_beaumont@demo.net</field>
<field name="street">Rue de la Jungle, 8</field>
<field name="city">St-Gilles</field>
<field name="zip">1060</field>
<field name="country_id" ref="base.be"/>
<field name="working_mode">regular</field>
<field name="is_worker" eval="True" />
</record>
</odoo>

107
beesdoo_shift/demo/templates.xml

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Coop IT Easy
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="beesdoo_shift_task_type_1_demo" model="beesdoo.shift.type">
<field name="name">Inventaire</field>
<field name="description">Inventaire frais et vrac.</field>
</record>
<record id="beesdoo_shift_task_type_2_demo" model="beesdoo.shift.type">
<field name="name">Bureau des Membres</field>
<field name="description">Gestion admin (encodage liste présences, etc.)</field>
</record>
<record id="beesdoo_shift_task_type_3_demo" model="beesdoo.shift.type">
<field name="name">Magasin</field>
<field name="description">Gestion du magasin.</field>
</record>
<record id="beesdoo_shift_task_type_4_demo" model="beesdoo.shift.type">
<field name="name">Découpe fromage</field>
<field name="description">Caisse, remplissage des rayons, accueil.</field>
</record>
<record id="beesdoo_shift_daynumber_1_demo" model="beesdoo.shift.daynumber">
<field name="name">Lundi</field>
<field name="number">1</field>
</record>
<record id="beesdoo_shift_daynumber_2_demo" model="beesdoo.shift.daynumber">
<field name="name">Mardi</field>
<field name="number">2</field>
</record>
<record id="beesdoo_shift_daynumber_3_demo" model="beesdoo.shift.daynumber">
<field name="name">Mercredi</field>
<field name="number">3</field>
</record>
<record id="beesdoo_shift_daynumber_4_demo" model="beesdoo.shift.daynumber">
<field name="name">Jeudi</field>
<field name="number">4</field>
</record>
<record id="beesdoo_shift_daynumber_5_demo" model="beesdoo.shift.daynumber">
<field name="name">Vendredi</field>
<field name="number">5</field>
</record>
<record id="beesdoo_shift_daynumber_6_demo" model="beesdoo.shift.daynumber">
<field name="name">Samedi</field>
<field name="number">6</field>
</record>
<record id="beesdoo_shift_daynumber_7_demo" model="beesdoo.shift.daynumber">
<field name="name">Dimanche</field>
<field name="number">7</field>
</record>
<record id="beesdoo_shift_planning_1_demo" model="beesdoo.shift.planning">
<field name="sequence">1</field>
<field name="name">Semaine A</field>
</record>
<record id="beesdoo_shift_task_template_1_demo" model="beesdoo.shift.template">
<field name="name">A_LUN_7:00-9:30</field>
<field name="planning_id" ref="beesdoo_shift_planning_1_demo" />
<field name="day_nb_id" ref="beesdoo_shift_daynumber_1_demo" />
<field name="task_type_id" ref="beesdoo_shift_task_type_3_demo" />
<field name="start_time">7</field>
<field name="end_time">9.5</field>
<field name="duration">2.5</field>
<field name="worker_nb">12</field>
<!-- WARNING: issue, worker not registered in demo -->
<field name="worker_ids" eval="[(6, 0, [ref('beesdoo_shift.res_partner_cooperator_1_demo')])]"/>
</record>
<record id="beesdoo_shift_task_template_2_demo" model="beesdoo.shift.template">
<field name="name">A_MAR_12:00-14:30</field>
<field name="planning_id" ref="beesdoo_shift_planning_1_demo" />
<field name="day_nb_id" ref="beesdoo_shift_daynumber_2_demo" />
<field name="task_type_id" ref="beesdoo_shift_task_type_2_demo" />
<field name="start_time">12</field>
<field name="end_time">14.5</field>
<field name="duration">2.5</field>
<field name="worker_nb">9</field>
<!-- WARNING: issue, worker not registered in demo -->
<field name="worker_ids" eval="[(6, 0, [ref('beesdoo_shift.res_partner_cooperator_3_demo')])]"/>
</record>
<record id="beesdoo_shift_task_template_3_demo" model="beesdoo.shift.template">
<field name="name">A_VEN_7:00-9:30</field>
<field name="planning_id" ref="beesdoo_shift_planning_1_demo" />
<field name="day_nb_id" ref="beesdoo_shift_daynumber_5_demo" />
<field name="task_type_id" ref="beesdoo_shift_task_type_1_demo" />
<field name="start_time">7</field>
<field name="end_time">9.5</field>
<field name="duration">2.5</field>
<field name="worker_nb">7</field>
<!-- WARNING: issue, worker not registered in demo -->
<field name="worker_ids" eval="[(6, 0, [ref('beesdoo_shift.res_partner_cooperator_6_demo')])]"/>
</record>
</odoo>

53
beesdoo_shift/demo/workers.xml

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Coop IT Easy
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="beesdoo_shift_cooperative_status_1_demo" model="cooperative.status">
<field name="cooperator_id" ref="res_partner_cooperator_1_demo" />
<field name="info_session" eval="True" />
<field name="info_session_date" eval="datetime.now() - timedelta(days=58)" />
<field name="super" eval="True" />
<field name="sr">2</field>
<field name="working_mode">regular</field>
</record>
<record id="beesdoo_shift_cooperative_status_2_demo" model="cooperative.status">
<field name="cooperator_id" ref="res_partner_cooperator_2_demo" />
<field name="info_session" eval="False" />
<field name="sr">2</field>
<field name="working_mode">irregular</field>
<field name="irregular_start_date" eval="datetime.now() - timedelta(days=3)" />
</record>
<record id="beesdoo_shift_cooperative_status_3_demo" model="cooperative.status">
<field name="cooperator_id" ref="res_partner_cooperator_3_demo" />
<field name="info_session" eval="True" />
<field name="info_session_date" eval="datetime.now() - timedelta(days=98)" />
<field name="sc">2</field>
<field name="working_mode">regular</field>
</record>
<record id="beesdoo_shift_cooperative_status_4_demo" model="cooperative.status">
<field name="cooperator_id" ref="res_partner_cooperator_4_demo" />
<field name="sr">2</field>
<field name="working_mode">irregular</field>
<field name="irregular_start_date" eval="datetime.now() - timedelta(days=6)" />
</record>
<record id="beesdoo_shift_cooperative_status_5_demo" model="cooperative.status">
<field name="cooperator_id" ref="res_partner_cooperator_5_demo" />
<field name="sr">2</field>
<field name="working_mode">regular</field>
</record>
<record id="beesdoo_shift_cooperative_status_6_demo" model="cooperative.status">
<field name="cooperator_id" ref="res_partner_cooperator_6_demo" />
<field name="info_session" eval="True" />
<field name="info_session_date" eval="datetime.now() - timedelta(days=36)" />
<field name="sc">2</field>
<field name="working_mode">regular</field>
</record>
</odoo>

2654
beesdoo_shift/i18n/fr_BE.po
File diff suppressed because it is too large
View File

2633
beesdoo_shift/i18n/nl_BE.po
File diff suppressed because it is too large
View File

6
beesdoo_shift/models/__init__.py

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
import planning
import task
import cooperative_status
from . import task
from . import planning
from . import cooperative_status

413
beesdoo_shift/models/cooperative_status.py

@ -1,19 +1,16 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from openerp.exceptions import ValidationError
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError, UserError
from datetime import timedelta, datetime
import logging
from openerp.osv.fields import related
_logger = logging.getLogger(__name__)
PERIOD = 28 # TODO: use system parameter
def add_days_delta(date_from, days_delta):
if not date_from:
return date_from
next_date = fields.Date.from_string(date_from) + timedelta(days=days_delta)
return fields.Date.to_string(next_date)
next_date = date_from + timedelta(days=days_delta)
return next_date
class ExemptReason(models.Model):
_name = 'cooperative.exempt.reason'
@ -35,22 +32,33 @@ class CooperativeStatus(models.Model):
_name = 'cooperative.status'
_rec_name = 'cooperator_id'
_order = 'cooperator_id'
_period = 28
def _get_status(self):
return [
('ok', 'Up to Date'),
('holiday', 'Holidays'),
('alert', 'Alerte'),
('extension', 'Extension'),
('suspended', 'Suspended'),
('exempted', 'Exempted'),
('unsubscribed', 'Unsubscribed'),
('resigning', 'Resigning')
]
today = fields.Date(help="Field that allow to compute field and store them even if they are based on the current date", default=fields.Date.today)
cooperator_id = fields.Many2one('res.partner')
active = fields.Boolean(related="cooperator_id.active", store=True, index=True)
info_session = fields.Boolean('Information Session ?')
info_session_date = fields.Datetime('Information Session Date')
info_session_date = fields.Date('Information Session Date')
super = fields.Boolean("Super Cooperative")
sr = fields.Integer("Compteur shift regulier", default=0)
sc = fields.Integer("Compteur shift de compensation", default=0)
sr = fields.Integer("Regular shifts counter", default=0)
sc = fields.Integer("Compensation shifts counter", default=0)
time_extension = fields.Integer("Extension Days NB", default=0, help="Addtional days to the automatic extension, 5 mean that you have a total of 15 extension days of default one is set to 10")
holiday_start_time = fields.Date("Holidays Start Day")
holiday_end_time = fields.Date("Holidays End Day")
alert_start_time = fields.Date("Alert Start Day")
extension_start_time = fields.Date("Extension Start Day")
#Champ compute
working_mode = fields.Selection(
[
('regular', 'Regular worker'),
@ -60,16 +68,9 @@ class CooperativeStatus(models.Model):
string="Working mode"
)
exempt_reason_id = fields.Many2one('cooperative.exempt.reason', 'Exempt Reason')
status = fields.Selection([('ok', 'Up to Date'),
('holiday', 'Holidays'),
('alert', 'Alerte'),
('extension', 'Extension'),
('suspended', 'Suspended'),
('exempted', 'Exempted'),
('unsubscribed', 'Unsubscribed'),
('resigning', 'Resigning')],
status = fields.Selection(selection=_get_status,
compute="_compute_status", string="Cooperative Status", store=True)
can_shop = fields.Boolean(compute='_compute_status', store=True)
can_shop = fields.Boolean(compute='_compute_can_shop', store=True)
history_ids = fields.One2many('cooperative.status.history', 'status_id', readonly=True)
unsubscribed = fields.Boolean(default=False, help="Manually unsubscribed")
resigning = fields.Boolean(default=False, help="Want to leave the beescoop")
@ -85,6 +86,10 @@ class CooperativeStatus(models.Model):
temporary_exempt_start_date = fields.Date()
temporary_exempt_end_date = fields.Date()
@api.depends('status')
def _compute_can_shop(self):
for rec in self:
rec.can_shop = rec.status in self._can_shop_status()
@api.depends('today', 'sr', 'sc', 'holiday_end_time',
'holiday_start_time', 'time_extension',
@ -93,190 +98,30 @@ class CooperativeStatus(models.Model):
'irregular_absence_counter', 'temporary_exempt_start_date',
'temporary_exempt_end_date', 'resigning', 'cooperator_id.subscribed_shift_ids')
def _compute_status(self):
alert_delay = int(self.env['ir.config_parameter'].get_param('alert_delay', 28))
grace_delay = int(self.env['ir.config_parameter'].get_param('default_grace_delay', 10))
update = int(self.env['ir.config_parameter'].get_param('always_update', False))
update = int(self.env['ir.config_parameter'].sudo().get_param('always_update', False))
for rec in self:
if update or not rec.today:
rec.status = 'ok'
rec.can_shop = True
continue
if rec.resigning:
rec.status = 'resigning'
rec.can_shop = False
continue
if rec.working_mode == 'regular':
rec._set_regular_status(grace_delay, alert_delay)
rec.status = rec._get_regular_status()
elif rec.working_mode == 'irregular':
rec._set_irregular_status(grace_delay, alert_delay)
rec.status = rec._get_irregular_status()
elif rec.working_mode == 'exempt':
rec.status = 'ok'
rec.can_shop = True
@api.depends('today', 'irregular_start_date', 'sr', 'holiday_start_time',
'holiday_end_time', 'temporary_exempt_start_date',
'temporary_exempt_end_date')
def _compute_future_alert_date(self):
"""Compute date before which the worker is up to date"""
for rec in self:
# Only for irregular worker
if rec.working_mode != 'irregular' and not rec.irregular_start_date:
rec.future_alert_date = False
# Alert start time already set
elif rec.alert_start_time:
rec.future_alert_date = False
# Holidays are not set properly
elif bool(rec.holiday_start_time) != bool(rec.holiday_end_time):
rec.future_alert_date = False
# Exemption have not a start and end time
elif (bool(rec.temporary_exempt_start_date)
!= bool(rec.temporary_exempt_end_date)):
rec.future_alert_date = False
else:
date = rec.today
counter = rec.sr
# Simulate the countdown
while counter >= 0:
date = add_days_delta(date, 1)
date = self._next_countdown_date(rec.irregular_start_date,
date)
# Check holidays
if (rec.holiday_start_time and rec.holiday_end_time
and date >= rec.holiday_start_time
and date <= rec.holiday_end_time):
continue
# Check temporary exemption
elif (rec.temporary_exempt_start_date
and rec.temporary_exempt_end_date
and date >= rec.temporary_exempt_start_date
and date <= rec.temporary_exempt_end_date):
continue
else:
counter -= 1
rec.future_alert_date = date
@api.depends('today', 'irregular_start_date', 'holiday_start_time',
'holiday_end_time', 'temporary_exempt_start_date',
'temporary_exempt_end_date')
def _compute_next_countdown_date(self):
"""
Compute the following countdown date. This date is the date when
the worker will see his counter changed du to the cron. This
date is like the birthday date of the worker that occurred each
PERIOD.
"""
for rec in self:
# Only for irregular worker
if rec.working_mode != 'irregular' and not rec.irregular_start_date:
rec.next_countdown_date = False
# Holidays are not set properly
elif bool(rec.holiday_start_time) != bool(rec.holiday_end_time):
rec.next_countdown_date = False
# Exemption have not a start and end time
elif (bool(rec.temporary_exempt_start_date)
!= bool(rec.temporary_exempt_end_date)):
rec.next_countdown_date = False
else:
date = rec.today
next_countdown_date = False
while not next_countdown_date:
date = add_days_delta(date, 1)
date = self._next_countdown_date(rec.irregular_start_date, date)
# Check holidays
if (rec.holiday_start_time and rec.holiday_end_time
and date >= rec.holiday_start_time
and date <= rec.holiday_end_time):
continue
# Check temporary exemption
elif (rec.temporary_exempt_start_date
and rec.temporary_exempt_end_date
and date >= rec.temporary_exempt_start_date
and date <= rec.temporary_exempt_end_date):
continue
else:
next_countdown_date = date
rec.next_countdown_date = next_countdown_date
def _next_countdown_date(self, irregular_start_date, today=False):
"""
Return the next countdown date given irregular_start_date and
today dates.
This does not take holiday and other status into account.
"""
today = today or fields.Date.today()
today_dt = fields.Date.from_string(today)
irregular_start_dt = fields.Date.from_string(irregular_start_date)
delta = (today_dt - irregular_start_dt).days
return add_days_delta(today, PERIOD - (delta % PERIOD))
_sql_constraints = [
('cooperator_uniq', 'unique (cooperator_id)', _('You can only set one cooperator status per cooperator')),
]
def _set_regular_status(self, grace_delay, alert_delay):
self.ensure_one()
counter_unsubscribe = int(self.env['ir.config_parameter'].get_param('regular_counter_to_unsubscribe', -4))
ok = self.sr >= 0 and self.sc >= 0
grace_delay = grace_delay + self.time_extension
if (self.sr + self.sc) <= counter_unsubscribe or self.unsubscribed:
self.status = 'unsubscribed'
self.can_shop = False
elif self.today >= self.temporary_exempt_start_date and self.today <= self.temporary_exempt_end_date:
self.status = 'exempted'
self.can_shop = True
#Transition to alert sr < 0 or stay in alert sr < 0 or sc < 0 and thus alert time is defined
elif not ok and self.alert_start_time and self.extension_start_time and self.today <= add_days_delta(self.extension_start_time, grace_delay):
self.status = 'extension'
self.can_shop = True
elif not ok and self.alert_start_time and self.extension_start_time and self.today > add_days_delta(self.extension_start_time, grace_delay):
self.status = 'suspended'
self.can_shop = False
elif not ok and self.alert_start_time and self.today > add_days_delta(self.alert_start_time, alert_delay):
self.status = 'suspended'
self.can_shop = False
elif (self.sr < 0) or (not ok and self.alert_start_time):
self.status = 'alert'
self.can_shop = True
#Check for holidays; Can be in holidays even in alert or other mode ?
elif self.today >= self.holiday_start_time and self.today <= self.holiday_end_time:
self.status = 'holiday'
self.can_shop = False
elif ok or (not self.alert_start_time and self.sr >= 0):
self.status = 'ok'
self.can_shop = True
def _set_irregular_status(self, grace_delay, alert_delay):
counter_unsubscribe = int(self.env['ir.config_parameter'].get_param('irregular_counter_to_unsubscribe', -3))
self.ensure_one()
ok = self.sr >= 0
grace_delay = grace_delay + self.time_extension
if self.sr <= counter_unsubscribe or self.unsubscribed:
self.status = 'unsubscribed'
self.can_shop = False
elif self.today >= self.temporary_exempt_start_date and self.today <= self.temporary_exempt_end_date:
self.status = 'exempted'
self.can_shop = True
#Transition to alert sr < 0 or stay in alert sr < 0 or sc < 0 and thus alert time is defined
elif not ok and self.alert_start_time and self.extension_start_time and self.today <= add_days_delta(self.extension_start_time, grace_delay):
self.status = 'extension'
self.can_shop = True
elif not ok and self.alert_start_time and self.extension_start_time and self.today > add_days_delta(self.extension_start_time, grace_delay):
self.status = 'suspended'
self.can_shop = False
elif not ok and self.alert_start_time and self.today > add_days_delta(self.alert_start_time, alert_delay):
self.status = 'suspended'
self.can_shop = False
elif (self.sr < 0) or (not ok and self.alert_start_time):
self.status = 'alert'
self.can_shop = True
#Check for holidays; Can be in holidays even in alert or other mode ?
elif self.today >= self.holiday_start_time and self.today <= self.holiday_end_time:
self.status = 'holiday'
self.can_shop = False
elif ok or (not self.alert_start_time and self.sr >= 0):
self.status = 'ok'
self.can_shop = True
@api.constrains("working_mode", "irregular_start_date")
def _constrains_irregular_start_date(self):
if self.working_mode == "irregular" and not self.irregular_start_date:
raise UserError(_("Irregular workers must have an irregular start date."))
@api.multi
def write(self, vals):
@ -298,34 +143,6 @@ class CooperativeStatus(models.Model):
self.env['cooperative.status.history'].sudo().create(data)
return super(CooperativeStatus, self).write(vals)
def _state_change(self, new_state):
self.ensure_one()
if new_state == 'alert':
self.write({'alert_start_time': self.today, 'extension_start_time': False, 'time_extension': 0})
if new_state == 'ok':
data = {'extension_start_time': False, 'time_extension': 0}
data['alert_start_time'] = False
self.write(data)
if new_state == 'unsubscribed' or new_state == 'resigning':
# Remove worker from task_templates
self.cooperator_id.sudo().write(
{'subscribed_shift_ids': [(5, 0, 0)]})
# Remove worker from supercoop in task_templates
task_tpls = self.env['beesdoo.shift.template'].search(
[('super_coop_id', 'in', self.cooperator_id.user_ids.ids)]
)
task_tpls.write({'super_coop_id': False})
# Remove worker for future task (remove also supercoop)
# TODO: Add one day otherwise unsubscribed from the shift you were absent
self.env['beesdoo.shift.shift'].sudo().unsubscribe_from_today(
[self.cooperator_id.id], today=fields.Date.today())
def _change_counter(self, data):
self.sc += data.get('sc', 0)
self.sr += data.get('sr', 0)
self.irregular_absence_counter += data.get('irregular_absence_counter', 0)
self.irregular_absence_date = data.get('irregular_absence_date', False)
@api.multi
def _write(self, vals):
"""
@ -349,9 +166,12 @@ class CooperativeStatus(models.Model):
rec._state_change(vals['status'])
return super(CooperativeStatus, self)._write(vals)
_sql_constraints = [
('cooperator_uniq', 'unique (cooperator_id)', _('You can only set one cooperator status per cooperator')),
]
def get_status_value(self):
"""
Workararound to get translated selection value instead of key in mail template.
"""
state_list = self.env["cooperative.status"]._fields['status']._description_selection(self.env)
return dict(state_list)[self.status]
@api.model
def _set_today(self):
@ -360,68 +180,152 @@ class CooperativeStatus(models.Model):
"""
self.search([]).write({'today': fields.Date.today()})
@api.multi
def clear_history(self):
self.ensure_one()
self.history_ids.unlink()
@api.model
def _cron_compute_counter_irregular(self, today=False):
"""
Journal ensure that a irregular worker will be only check
once per day
"""
today = today or fields.Date.today()
journal = self.env['beesdoo.shift.journal'].search([('date', '=', today)])
if not journal:
journal = self.env['beesdoo.shift.journal'].create({'date': today})
domain = ['&',
'&',
'&', ('status', '!=', 'unsubscribed'),
('working_mode', '=', 'irregular'),
('irregular_start_date', '!=', False),
'|',
'|', ('holiday_start_time', '=', False), ('holiday_end_time', '=', False),
'|', ('holiday_start_time', '>', today), ('holiday_end_time', '<', today),
]
domain = self._get_irregular_worker_domain(today=today)
irregular = self.search(domain)
today_date = fields.Date.from_string(today)
for status in irregular:
if status.status == 'exempted':
continue
delta = (today_date - fields.Date.from_string(status.irregular_start_date)).days
if delta and delta % PERIOD == 0 and status not in journal.line_ids:
if status.sr > 0:
status.sr -= 1
elif status.alert_start_time:
status.sr -= 1
else:
status.sr -= 2
delta = (today - status.irregular_start_date).days
if delta and delta % self._period == 0 and status not in journal.line_ids:
status._change_irregular_counter()
journal.line_ids |= status
@api.multi
def clear_history(self):
self.ensure_one()
self.history_ids.unlink()
########################################################
# Method to override #
# To define the behavior of the status #
# #
# By default: everyone is always up to date #
########################################################
##############################
# Computed field section #
##############################
@api.depends('today')
def _compute_future_alert_date(self):
"""
Compute date until the worker is up to date
for irregular worker
"""
for rec in self:
rec.future_alert_date = False
@api.depends('today')
def _compute_next_countdown_date(self):
"""
Compute the following countdown date. This date is the date when
the worker will see his counter changed due to the cron. This
date is like the birthday date of the worker that occurred each
_period.
"""
for rec in self:
rec.next_countdown_date = False
def _can_shop_status(self):
"""
return the list of status that give access
to active cooperator privilege
"""
return ['ok', 'alert', 'extension', 'exempted']
#####################################
# Status Change implementation #
#####################################
def _get_regular_status(self):
"""
Return the value of the status
for the regular worker
"""
return 'ok'
def _get_irregular_status(self):
"""
Return the value of the status
for the irregular worker
"""
return 'ok'
def _state_change(self, new_state):
"""
Hook to watch change in the state
"""
pass
def _change_counter(self, data):
"""
Call when a shift state is changed
use data generated by _get_counter_date_state_change
"""
pass
###############################################
###### Irregular Cron implementation ##########
###############################################
def _get_irregular_worker_domain(self):
"""
return the domain the give the list
of valid irregular worker that should
get their counter changed by the cron
"""
return [(0, '=', 1)]
def _change_irregular_counter(self):
"""
Define how the counter will change
for the irregular worker
where today - start_date is a multiple of the period
by default 28 days
"""
pass
class ShiftCronJournal(models.Model):
_name = 'beesdoo.shift.journal'
_order = 'date desc'
_rec_name = 'date'
date = fields.Date()
line_ids = fields.Many2many('cooperative.status')
_sql_constraints = [
('one_entry_per_day', 'unique (date)', _('You can only create one journal per day')),
]
@api.multi
def run(self):
self.ensure_one()
if not self.user_has_groups('beesdoo_shift.group_cooperative_admin'):
raise ValidationError(_("You don't have the access to perform this action"))
self.sudo().env['cooperative.status']._cron_compute_counter_irregular(today=self.date)
class ResPartner(models.Model):
"""
One2many relationship with CooperativeStatus should
be replaced by inheritance.
"""
_inherit = 'res.partner'
worker_store = fields.Boolean(default=False)
is_worker = fields.Boolean(related="worker_store", string="Worker", readonly=False)
cooperative_status_ids = fields.One2many('cooperative.status', 'cooperator_id', readonly=True)
super = fields.Boolean(related='cooperative_status_ids.super', string="Super Cooperative", readonly=True, store=True)
info_session = fields.Boolean(related='cooperative_status_ids.info_session', string='Information Session ?', readonly=True, store=True)
info_session_date = fields.Datetime(related='cooperative_status_ids.info_session_date', string='Information Session Date', readonly=True, store=True)
info_session_date = fields.Date(related='cooperative_status_ids.info_session_date', string='Information Session Date', readonly=True, store=True)
working_mode = fields.Selection(related='cooperative_status_ids.working_mode', readonly=True, store=True)
exempt_reason_id = fields.Many2one(related='cooperative_status_ids.exempt_reason_id', readonly=True, store=True)
state = fields.Selection(related='cooperative_status_ids.status', readonly=True, store=True)
@ -486,4 +390,3 @@ class ResPartner(models.Model):
}
#TODO access right + vue on res.partner
#TODO can_shop : Status can_shop ou extempted ou part C

53
beesdoo_shift/models/planning.py

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from openerp.exceptions import UserError
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from pytz import timezone, UTC
import math
from datetime import datetime, timedelta
from datetime import datetime, time, timedelta
def float_to_time(f):
@ -18,7 +17,7 @@ def floatime_to_hour_minute(f):
def get_first_day_of_week():
today = datetime.now()
return (datetime.now() - timedelta(days=today.weekday())).strftime("%Y-%m-%d")
return (datetime.now() - timedelta(days=today.weekday())).date()
class TaskType(models.Model):
_name = 'beesdoo.shift.type'
@ -56,13 +55,15 @@ class Planning(models.Model):
def _get_next_planning_date(self, date):
self.ensure_one()
nb_of_day = max(self.task_template_ids.mapped('day_nb_id.number'))
return fields.Date.to_string(fields.Date.from_string(date) + timedelta(days=nb_of_day))
return date + timedelta(days=nb_of_day)
@api.model
def _generate_next_planning(self):
config = self.env['ir.config_parameter']
config = self.env['ir.config_parameter'].sudo()
last_seq = int(config.get_param('last_planning_seq', 0))
date = config.get_param('next_planning_date', 0)
date = fields.Date.from_string(
config.get_param('next_planning_date', 0)
)
planning = self._get_next_planning(last_seq)
planning = planning.with_context(visualize_date=date)
@ -81,13 +82,14 @@ class TaskTemplate(models.Model):
planning_id = fields.Many2one('beesdoo.shift.planning', required=True)
day_nb_id = fields.Many2one('beesdoo.shift.daynumber', string='Day', required=True)
task_type_id = fields.Many2one('beesdoo.shift.type', string="Type")
# attendance_sheet_id = fields.Many2one('beesdoo.shift.sheet', string="Attendance Sheet") FIXME removed because beesdoo.shift.sheet is from another module.
start_time = fields.Float(required=True)
end_time = fields.Float(required=True)
super_coop_id = fields.Many2one('res.users', string="Super Cooperative", domain=[('partner_id.super', '=', True)])
duration = fields.Float(help="Duration in Hour")
worker_nb = fields.Integer(string="Number of worker", help="Max number of worker for this task", default=1)
worker_ids = fields.Many2many('res.partner', string="Recurrent worker assigned", domain=[('eater', '=', 'worker_eater'), ('working_mode', '=', 'regular')])
worker_ids = fields.Many2many('res.partner', string="Recurrent worker assigned", domain=[('is_worker', '=', True)])
remaining_worker = fields.Integer(compute="_get_remaining", store=True, string="Remaining Place")
active = fields.Boolean(default=True)
#For Kanban View Only
@ -98,19 +100,19 @@ class TaskTemplate(models.Model):
end_date = fields.Datetime(compute="_get_fake_date", search="_dummy_search")
def _get_utc_date(self, day, hour, minute):
#Don't catch error since the error should be raise on the log as an error
#because generate time with UTC timezone is worse than not generate them
"""Combine day number, hours and minutes to save
corresponding UTC datetime in database.
"""
context_tz = timezone(self._context.get('tz') or self.env.user.tz)
day_time = day.replace(hour=hour, minute=minute)
day_local_time = context_tz.localize(day_time)
day_local_time = datetime.combine(day, time(hour=hour, minute=minute))
day_local_time = context_tz.localize(day_local_time)
day_utc_time = day_local_time.astimezone(UTC)
return day_utc_time
# Return naïve datetime so as to be saved in database
return day_utc_time.replace(tzinfo=None)
@api.depends('start_time', 'end_time')
def _get_fake_date(self):
today = self._context.get('visualize_date', get_first_day_of_week())
today = datetime.strptime(today, '%Y-%m-%d')
for rec in self:
# Find the day of this task template 'rec'.
day = today + timedelta(days=rec.day_nb_id.number - 1)
@ -138,7 +140,7 @@ class TaskTemplate(models.Model):
def _nb_worker_max(self):
for rec in self:
if len(rec.worker_ids) > rec.worker_nb:
raise UserError(_('you cannot assign more worker then the number maximal define on the template'))
raise UserError(_('You cannot assign more workers than the maximal number defined on template.'))
@api.onchange('start_time', 'end_time')
@ -154,19 +156,26 @@ class TaskTemplate(models.Model):
def _generate_task_day(self):
tasks = self.env['beesdoo.shift.shift']
for rec in self:
for i in xrange(0, rec.worker_nb):
for i in range(0, rec.worker_nb):
worker_id = rec.worker_ids[i] if len(rec.worker_ids) > i else False
#remove worker in holiday and temporary exempted
if worker_id and worker_id.cooperative_status_ids:
status = worker_id.cooperative_status_ids[0]
if status.holiday_start_time and status.holiday_end_time and \
status.holiday_start_time <= rec.start_date[:10] and status.holiday_end_time >= rec.end_date[:10]:
status.holiday_start_time <= rec.start_date.date() and status.holiday_end_time >= rec.end_date.date():
worker_id = False
if status.temporary_exempt_start_date and status.temporary_exempt_end_date and \
status.temporary_exempt_start_date <= rec.start_date[:10] and status.temporary_exempt_end_date >= rec.end_date[:10]:
status.temporary_exempt_start_date <= rec.start_date.date() and status.temporary_exempt_end_date >= rec.end_date.date():
worker_id = False
tasks |= tasks.create({
'name' : "%s %s (%s - %s) [%s]" % (rec.name, rec.day_nb_id.name, float_to_time(rec.start_time), float_to_time(rec.end_time), i),
'name' : "[%s] %s %s (%s - %s) [%s]" % (
rec.start_date.date(),
rec.planning_id.name,
rec.day_nb_id.name,
float_to_time(rec.start_time),
float_to_time(rec.end_time),
i,
),
'task_template_id' : rec.id,
'task_type_id' : rec.task_type_id.id,
'super_coop_id': rec.super_coop_id.id,
@ -174,7 +183,7 @@ class TaskTemplate(models.Model):
'is_regular': True if worker_id else False,
'start_time' : rec.start_date,
'end_time' : rec.end_date,
'stage_id': self.env.ref('beesdoo_shift.open').id,
'state': 'open',
})
return tasks

248
beesdoo_shift/models/task.py

@ -1,20 +1,9 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from openerp.exceptions import UserError, ValidationError
import json
from datetime import datetime, time, timedelta
class TaskStage(models.Model):
_name = 'beesdoo.shift.stage'
_order = 'sequence asc'
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
name = fields.Char()
sequence = fields.Integer()
color = fields.Integer()
code = fields.Char(readonly=True)
@api.multi
def unlink(self):
raise UserError(_("You Cannot delete Task Stage"))
class Task(models.Model):
@ -24,29 +13,73 @@ class Task(models.Model):
_order = "start_time asc"
##################################
# Method to override #
# to have different state #
# on the shift #
##################################
def _get_selection_status(self):
return [
("open","Confirmed"),
("done","Attended"),
("absent","Absent"),
("excused","Excused"),
("cancel","Cancelled")
]
def _get_color_mapping(self, state):
return {
"draft": 0,
"open": 1,
"done": 5,
"absent": 2,
"excused": 3,
"cancel": 9,
}[state]
def _get_final_state(self):
return ["done", "absent", "excused"]
name = fields.Char(track_visibility='always')
task_template_id = fields.Many2one('beesdoo.shift.template')
planning_id = fields.Many2one(related='task_template_id.planning_id', store=True)
task_type_id = fields.Many2one('beesdoo.shift.type', string="Task Type")
worker_id = fields.Many2one('res.partner', track_visibility='onchange',
domain=[
('eater', '=', 'worker_eater'),
('is_worker', '=', True),
('working_mode', 'in', ('regular', 'irregular')),
('state', 'not in', ('unsubscribed', 'resigning')),
])
start_time = fields.Datetime(track_visibility='always', index=True)
end_time = fields.Datetime(track_visibility='always')
stage_id = fields.Many2one('beesdoo.shift.stage', required=True, track_visibility='onchange', default=lambda self: self.env.ref('beesdoo_shift.open'))
start_time = fields.Datetime(track_visibility='always', index=True, required=True)
end_time = fields.Datetime(track_visibility='always', required=True)
state = fields.Selection(selection=_get_selection_status,
default="open",
required=True,
track_visibility='onchange',
group_expand='_expand_states'
)
color = fields.Integer(compute="_compute_color")
super_coop_id = fields.Many2one('res.users', string="Super Cooperative", domain=[('partner_id.super', '=', True)], track_visibility='onchange')
color = fields.Integer(related="stage_id.color", readonly=True)
# TODO: Maybe is_regular and is_compensation must be merged in a
# selection field as they are mutually exclusive.
is_regular = fields.Boolean(default=False, string="Regular shift")
is_compensation = fields.Boolean(default=False, string="Compensation shift")
replaced_id = fields.Many2one('res.partner', track_visibility='onchange', domain=[('eater', '=', 'worker_eater')])
replaced_id = fields.Many2one('res.partner', track_visibility='onchange',
domain=[
('eater', '=', 'worker_eater'),
('working_mode', '=', 'regular'),
('state', 'not in', ('unsubscribed', 'resigning')),
])
revert_info = fields.Text(copy=False)
working_mode = fields.Selection(related='worker_id.working_mode')
def _expand_states(self, states, domain, order):
return [key for key, val in self._fields['state'].selection]
@api.depends("state")
def _compute_color(self):
for rec in self:
rec.color = self._get_color_mapping(rec.state)
def _compensation_validation(self, task):
"""
Raise a validation error if the fields is_regular and
@ -59,6 +92,15 @@ class Task(models.Model):
"Compensation Shift."
)
@api.constrains("state")
def _lock_future_task(self):
if datetime.now() < self.start_time:
if self.state in self._get_final_state():
raise UserError(_(
"Shift state of a future shift "
"can't be set to 'present' or 'absent'."
))
@api.constrains('is_regular', 'is_compensation')
def _check_compensation(self):
for task in self:
@ -82,6 +124,9 @@ class Task(models.Model):
'is_regular': False,
'is_compensation': False,
})
if task.worker_id:
if task.worker_id == task.replaced_id:
raise UserError("A worker cannot replace himself.")
def message_auto_subscribe(self, updated_fields, values=None):
self._add_follower(values)
@ -92,31 +137,34 @@ class Task(models.Model):
worker = self.env['res.partner'].browse(vals['worker_id'])
self.message_subscribe(partner_ids=worker.ids)
#TODO button to replaced someone
@api.model
def _read_group_stage_id(self, ids, domain, read_group_order=None, access_rights_uid=None):
res = self.env['beesdoo.shift.stage'].search([]).name_get()
fold = dict.fromkeys([r[0] for r in res], False)
return res, fold
def unsubscribe_from_today(self, worker_ids, today=None, end_date=None, now=None):
"""
Unsubscribe workers from *worker_ids* from all shift that start *today* and later.
If *end_date* is given, unsubscribe workers from shift between *today* and *end_date*.
If *now* is given workers are unsubscribed from all shifts starting *now* and later.
If *now* is given, *end_date* is not taken into account.
_group_by_full = {
'stage_id': _read_group_stage_id,
}
:type today: date
:type end_date: date
:type now: datetime
"""
if now:
if not isinstance(now, datetime):
raise UserError (_("'Now' must be a datetime."))
date_domain = [('start_time', '>', now)]
else:
today = today or fields.Date.today()
today = datetime.combine(today, time())
date_domain = [('start_time', '>', today)]
if end_date:
end_date = datetime.combine(end_date,time(hour=23, minute=59, second=59))
date_domain.append(('end_time', '<=', end_date))
#TODO button to replaced someone
@api.model
def unsubscribe_from_today(self, worker_ids, today=None, end_date=None):
today = today or fields.Date.today()
today += ' 00:00:00'
if end_date:
end_date += ' 23:59:59'
# date_domain = [('worker_id', 'in', worker_ids), ('start_time', '>=', today)]
date_domain = [('start_time', '>=', today)]
if end_date:
date_domain.append(('end_time', '<=', end_date))
to_unsubscribe = self.search([('worker_id', 'in', worker_ids)] + date_domain)
to_unsubscribe.write({'worker_id': False})
# What about replacement ?
# Remove worker, replaced_id and regular
to_unsubscribe_replace = self.search([('replaced_id', 'in', worker_ids)] + date_domain)
to_unsubscribe_replace.write({'worker_id': False, 'replaced_id': False})
@ -133,11 +181,11 @@ class Task(models.Model):
@api.multi
def write(self, vals):
"""
Overwrite write to track stage change
Overwrite write to track state change
If worker is changer:
Revert for the current worker
Change the worker info
Compute stage change for the new worker
Compute state change for the new worker
"""
if 'worker_id' in vals:
for rec in self:
@ -152,11 +200,11 @@ class Task(models.Model):
'is_compensation': vals.get('is_compensation',
rec.is_compensation),
})
rec._update_stage(rec.stage_id.id)
if 'stage_id' in vals:
rec._update_state(rec.state)
if 'state' in vals:
for rec in self:
if vals['stage_id'] != rec.stage_id.id:
rec._update_stage(vals['stage_id'])
if vals['state'] != rec.state:
rec._update_state(vals['state'])
return super(Task, self).write(vals)
def _set_revert_info(self, data, status):
@ -176,60 +224,64 @@ class Task(models.Model):
self.env['cooperative.status'].browse(data['status_id']).sudo()._change_counter(data['data'])
self.revert_info = False
def _update_stage(self, new_stage):
def _update_state(self, new_state):
self.ensure_one()
self._revert()
update = int(self.env['ir.config_parameter'].get_param('always_update', False))
new_stage = self.env['beesdoo.shift.stage'].browse(new_stage)
data = {}
DONE = self.env.ref('beesdoo_shift.done')
ABSENT = self.env.ref('beesdoo_shift.absent')
EXCUSED = self.env.ref('beesdoo_shift.excused')
NECESSITY = self.env.ref('beesdoo_shift.excused_necessity')
if not (self.worker_id or self.replaced_id) and new_stage in (DONE, ABSENT, EXCUSED, NECESSITY):
raise UserError(_("You cannot change to the status %s if the is no worker defined on the shift") % new_stage.name)
if update or not (self.worker_id or self.replaced_id):
if not (self.worker_id or self.replaced_id) and new_state in self._get_final_state():
raise UserError(_("You cannot change to the status %s if no worker is defined for the shift") % new_state)
if not (self.worker_id.working_mode in ['regular', 'irregular']):
raise UserError(_("Working mode is not properly defined. Please check if the worker is subscribed"))
always_update = int(self.env['ir.config_parameter'].sudo().get_param('always_update', False))
if always_update or not (self.worker_id or self.replaced_id):
return
if self.worker_id.working_mode == 'regular':
if not self.replaced_id: #No replacement case
status = self.worker_id.cooperative_status_ids[0]
else:
status = self.replaced_id.cooperative_status_ids[0]
if new_stage == DONE and not self.is_regular:
if status.sr < 0:
data['sr'] = 1
elif status.sc < 0:
data['sc'] = 1
else:
data['sr'] = 1
if new_stage == ABSENT and not self.replaced_id:
data['sr'] = - 1
if status.sr <= 0:
data['sc'] = -1
if new_stage == ABSENT and self.replaced_id:
data['sr'] = -1
if new_stage == EXCUSED:
data['sr'] = -1
elif self.worker_id.working_mode == 'irregular':
status = self.worker_id.cooperative_status_ids[0]
if new_stage == DONE or new_stage == NECESSITY:
data['sr'] = 1
data['irregular_absence_date'] = False
data['irregular_absence_counter'] = 1 if status.irregular_absence_counter < 0 else 0
if new_stage == ABSENT or new_stage == EXCUSED:
data['sr'] = -1
data['irregular_absence_date'] = self.start_time[:10]
data['irregular_absence_counter'] = -1
else:
raise UserError(_("The worker has not a proper working mode define, please check the worker is subscribed"))
data = self._get_counter_date_state_change(new_state)
status.sudo()._change_counter(data)
self._set_revert_info(data, status)
@api.model
def _cron_send_weekly_emails(self):
"""
Send a summary email for all workers
if they have a shift planned during the week.
"""
tasks = self.env["beesdoo.shift.shift"]
shift_summary_mail_template = self.env.ref(
"beesdoo_shift.email_template_shift_summary", False
)
start_time = datetime.now() + timedelta(days=1)
end_time = datetime.now() + timedelta(days=7)
confirmed_tasks = tasks.search(
[
("start_time", ">", start_time),
("start_time", "<", end_time),
("worker_id", "!=", False),
("state", "=", "open"),
]
)
for rec in confirmed_tasks:
shift_summary_mail_template.send_mail(rec.id, True)
########################################################
# Method to override #
# To define the behavior of the status #
# #
# By default: everyone is always up to date #
########################################################
def _get_counter_date_state_change(self, new_state):
"""
Return the data to change counter or other things
that change on the cooperator status
see _change_counter
We have eheck the worker is legitimate
"""
return {}

23
beesdoo_shift/security/group.xml

@ -1,23 +1,28 @@
<odoo>
<data noupdate="0">
<record id="group_shift_attendance" model="res.groups">
<field name="name">Shift Attendance</field>
<field name="name">Shift and Worker Read Access</field>
<field name="category_id" ref="base.module_category_cooperative_management"/>
</record>
<record id="group_shift_management" model="res.groups">
<field name="name">Shift Management</field>
<field name="name">Shifts and Attendance Sheets Management</field>
<field name="category_id" ref="base.module_category_cooperative_management"/>
<field name="implied_ids" eval="[(4, ref('group_shift_attendance'))]"/>
</record>
<record id="group_planning_management" model="res.groups">
<field name="name">Planning Management</field>
<field name="category_id" ref="base.module_category_cooperative_management"/>
<field name="implied_ids" eval="[(4, ref('group_shift_management'))]"/>
<field name="category_id"
ref="base.module_category_cooperative_management"/>
<field name="implied_ids"
eval="[(4, ref('group_shift_management'))]"/>
</record>
<record id="group_cooperative_admin" model="res.groups">
<field name="name">Cooperative Admin</field>
<field name="category_id" ref="base.module_category_cooperative_management"/>
<field name="implied_ids" eval="[(4, ref('group_planning_management'))]"/>
<field name="category_id"
ref="base.module_category_cooperative_management"/>
<field name="implied_ids"
eval="[(4, ref('group_planning_management'))]" />
<field name="users"
eval="[(4, ref('base.user_root')),
(4, ref('base.user_admin'))]"/>
</record>
</data>
</odoo>
</odoo>

35
beesdoo_shift/security/ir.model.access.csv

@ -1,20 +1,17 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_coopplanning_task_stage,Attendance Read Stage,model_beesdoo_shift_stage,group_shift_attendance,1,0,0,0
access_coopplanning_task_type,Attendance Read Type,model_beesdoo_shift_type,group_shift_attendance,1,0,0,0
access_coopplanning_daynumber,Attendance Read Daynumber,model_beesdoo_shift_daynumber,group_shift_attendance,1,0,0,0
access_coopplanning_planning,Attendance Read Planning,model_beesdoo_shift_planning,group_shift_attendance,1,0,0,0
access_coopplanning_task_template,Attendance Read Template,model_beesdoo_shift_template,group_shift_attendance,1,0,0,0
access_coopplanning_task,Attendance Edit Shift,model_beesdoo_shift_shift,group_shift_attendance,1,1,0,0
access_coopplanning_task_full,Shift Management all Shift,model_beesdoo_shift_shift,group_shift_management,1,1,1,1
access_coop_status,Coop Status Read for all,model_cooperative_status,,1,0,0,0
access_coop_status_all,Coop Status Admin,model_cooperative_status,group_cooperative_admin,1,1,1,1
all_config_coopplanning_task_stage,Attendance Read Stage,model_beesdoo_shift_stage,group_planning_management,1,1,1,1
all_config_coopplanning_task_type,Attendance Read Type,model_beesdoo_shift_type,group_planning_management,1,1,1,1
all_config_coopplanning_daynumber,Attendance Read Daynumber,model_beesdoo_shift_daynumber,group_planning_management,1,1,1,1
all_config_coopplanning_planning,Attendance Read Planning,model_beesdoo_shift_planning,group_planning_management,1,1,1,1
all_config_coopplanning_task_template,Attendance Read Template,model_beesdoo_shift_template,group_planning_management,1,1,1,1
all_config_coopplanning_task,Attendance Edit Shift,model_beesdoo_shift_shift,group_planning_management,1,1,1,1
exempt_reason_read_all,Exempt Reason Read all ,beesdoo_shift.model_cooperative_exempt_reason,,1,0,0,0
exempt_reason,Exempt Reason Admin,beesdoo_shift.model_cooperative_exempt_reason,beesdoo_shift.group_cooperative_admin,1,1,1,1
history_read_all,History Read All,beesdoo_shift.model_cooperative_status_history,,1,0,0,0
access_beesdoo_shift_journal,access_beesdoo_shift_journal,model_beesdoo_shift_journal,beesdoo_shift.group_cooperative_admin,1,0,1,1
read_beesdoo_shift_planning,read_beesdoo_shift_planning,model_beesdoo_shift_planning,,1,0,0,0
access_beesdoo_shift_template,access_beesdoo_shift_template,model_beesdoo_shift_template,group_shift_attendance,1,0,0,0
write_beesdoo_shift_shift,write_beesdoo_shift_shift,model_beesdoo_shift_shift,group_shift_attendance,1,1,0,0
manage_beesdoo_shift_shift,manage_beesdoo_shift_shift,model_beesdoo_shift_shift,group_shift_attendance,1,1,1,1
manage_beesdoo_shift_type,manage_beesdoo_shift_type,model_beesdoo_shift_type,group_planning_management,1,1,1,1
manage_beesdoo_shift_daynumber,manage_beesdoo_shift_daynumber,model_beesdoo_shift_daynumber,group_planning_management,1,1,1,1
manage_beesdoo_shift_planning,manage_beesdoo_shift_planning,model_beesdoo_shift_planning,group_planning_management,1,1,1,1
manage_beesdoo_shift_template,manage_beesdoo_shift_template,model_beesdoo_shift_template,group_planning_management,1,1,1,1
manage_cooperative_status,manage_cooperative_status,model_cooperative_status,group_cooperative_admin,1,1,1,1
manage_cooperative_exempt_reason,manage_cooperative_exempt_reason,model_cooperative_exempt_reason,group_cooperative_admin,1,1,1,1
read_beesdoo_shift_journal,read_beesdoo_shift_journal,model_beesdoo_shift_journal,group_cooperative_admin,1,0,1,1
access_cooperative_status,access_cooperative_status,model_cooperative_status,,1,0,0,0
read_cooperative_exempt_reason,read_cooperative_exempt_reason,model_cooperative_exempt_reason,,1,0,0,0
read_cooperative_status_history,read_cooperative_status_history,model_cooperative_status_history,,1,0,0,0
access_beesdoo_shift_type,access_beesdoo_shift_type,model_beesdoo_shift_type,group_shift_attendance,1,0,0,0
access_attendance_beesdoo_shift_daynumber,access_beesdoo_shift_daynumber,model_beesdoo_shift_daynumber,group_shift_attendance,1,0,0,0

62
beesdoo_shift/views/cooperative_status.xml

@ -2,57 +2,65 @@
<record model="ir.ui.view" id="super_coop_partner_inherited_view_form">
<field name="name">Partner Super Coop</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="beesdoo_base.beesdoo_partner_form_view" />
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority">50</field>
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<header>
<button name="coop_subscribe" string="Subscribe to shift" class="oe_highlight"
type="object" groups="beesdoo_shift.group_shift_management"
attrs="{'invisible': [('cooperator_type', '!=', 'share_a')]}"/>
attrs="{'invisible': [('is_worker', '=', False)]}"/>
<button name="auto_extension" string="Auto Extension" class="oe_highlight"
type="object" groups="beesdoo_shift.group_shift_attendance"
attrs="{'invisible': ['|',
('cooperator_type', '!=', 'share_a'),
('is_worker', '=', False),
'|',
('state', '!=', 'suspended'),
('extension_start_time', '!=', False)]}" />
<button name="manual_extension" string="Manual Extension" class="oe_highlight"
type="object" groups="beesdoo_shift.group_shift_management"
attrs="{'invisible': ['|',
('cooperator_type', '!=', 'share_a'),
('is_worker', '=', False),
'|',
('state', '!=', 'suspended'),
('extension_start_time', '=', False)]}" />
<button name="coop_unsubscribe" string="Unsubscribe" class="oe_highlight"
type="object" groups="beesdoo_shift.group_shift_management"
attrs="{'invisible': [('cooperator_type', '!=', 'share_a')]}"/>
attrs="{'invisible': [('is_worker', '=', False)]}"/>
<button name="register_holiday" string="Register Holidays" class="oe_highlight"
type="object" groups="beesdoo_shift.group_shift_management"
attrs="{'invisible': ['|', ('cooperator_type', '!=', 'share_a'), ('state', '!=', 'ok')]}"/>
attrs="{'invisible': ['|', ('is_worker', '=', False), ('state', '!=', 'ok')]}"/>
<button name="temporary_exempt" string="Temporary Exemption"
type="object" groups="beesdoo_shift.group_shift_management"
attrs="{'invisible': ['|', ('cooperator_type', '!=', 'share_a'), ('state', '=', 'unsubscribed')]}"/>
<field name="state" widget="statusbar" attrs="{'invisible': [('cooperator_type', '!=', 'share_a')]}" />
attrs="{'invisible': ['|', ('is_worker', '=', False), ('state', '=', 'unsubscribed')]}"/>
<field name="state" widget="statusbar" attrs="{'invisible': [('is_worker', '=', False)]}" />
</header>
</xpath>
<page name="work" position="inside">
<group>
<group>
<field name="info_session" />
<field name="info_session_date"
attrs="{'invisible': ['|', ('info_session', '=', False)]}" />
<field name="extension_start_time" attrs="{'invisible': [('extension_start_time', '=', False)]}" />
</group>
<xpath expr="//field[@name='type']" position="before">
<field name="is_worker"/>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Worker information"
attrs="{'invisible': [('is_worker', '=', False)]}"
name="work">
<group name="info_session"/>
<group>
<field name="working_mode" />
<field name="exempt_reason_id" attrs="{'invisible':[('working_mode', '!=', 'exempt')]}"/>
<field name="super" />
<group>
<field name="info_session" />
<field name="info_session_date"
attrs="{'invisible': ['|', ('info_session', '=', False)]}" />
<field name="extension_start_time" attrs="{'invisible': [('extension_start_time', '=', False)]}" />
</group>
<group>
<field name="working_mode" />
<field name="exempt_reason_id" attrs="{'invisible':[('working_mode', '!=', 'exempt')]}"/>
<field name="super" />
</group>
</group>
</group>
<separator string="Subscribed Shift" />
<field name="subscribed_shift_ids" />
</page>
<separator string="Subscribed Shift" />
<field name="subscribed_shift_ids" />
</page>
</xpath>
</field>
</record>
@ -149,7 +157,7 @@
<field name="date" />
</group>
</group>
<separator string="Affected cooperator" />
<separator string="Assigned cooperator" />
<field name="line_ids" readonly="1">
<tree>
<field name="cooperator_id" />
@ -175,11 +183,11 @@
<field name="name">Worker</field>
<field name="res_model">res.partner</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('cooperator_type', '=', 'share_a')]</field>
<field name="domain">[('is_worker', '=', True)]</field>
</record>
<menuitem name="Worker" id="menu_worker_top" parent="menu_root"
sequence="1" />
groups="beesdoo_shift.group_shift_attendance" sequence="1" />
<menuitem name="Worker" id="menu_worker" parent="menu_worker_top"
action="action_worker" />
@ -192,5 +200,3 @@
<menuitem name="Counter Update Journal" id="menu_journal" parent="menu_status_top"
action="action_journal" groups="beesdoo_shift.group_cooperative_admin" />
</odoo>

4
beesdoo_shift/views/planning.xml

@ -31,8 +31,6 @@
<button class="oe_stat_button" type="action"
name="%(action_shift_template)d" icon="fa-book"
string="Shifts Template">
<!-- <field string="Ventes" name="sale_order_count"
widget="statinfo" modifiers="{'readonly': true}"/> -->
</button>
</div>
<div class="oe_title">
@ -69,6 +67,6 @@
</record>
<menuitem name="Planning Week" id="menu_planning" parent="menu_template_top"
sequence="10" action="action_planning" />
sequence="20" action="action_planning" />
</odoo>

123
beesdoo_shift/views/task.xml

@ -12,7 +12,7 @@
<field name="worker_id" />
<field name="replaced_id" />
<field name="end_time" />
<field name="stage_id" />
<field name="state" />
</tree>
</field>
</record>
@ -28,38 +28,46 @@
<field name="super_coop_id" />
<field name="worker_id" />
<filter string="My Team Shift"
domain="[('super_coop_id', '=', uid)]" />
name="my_team_shift"
domain="[('super_coop_id', '=', uid)]" />
<filter string="My Shift"
domain="[('worker_id.user_ids', 'in', uid)]" />
name="my_shift"
domain="[('worker_id.user_ids', 'in', uid)]" />
<separator />
<filter string="Assigned" name="assigned"
domain="[('worker_id', '!=', False)]" />
<filter string="Unassigned" name="unassigned"
domain="[('worker_id', '=', False)]" />
<filter string="Assigned"
name="assigned"
domain="[('worker_id', '!=', False)]" />
<filter string="Unassigned"
name="unassigned"
domain="[('worker_id', '=', False)]" />
<separator />
<filter string="4 next days" name="nextweek"
domain="[('end_time','&gt;', context_today().strftime('%%Y-%%m-%%d 00:00:00')), ('start_time','&lt;', (context_today() + datetime.timedelta(days=4)).strftime('%%Y-%%m-%%d 23:59:59'))]" />
domain="[('end_time','&gt;', context_today().strftime('%Y-%m-%d 00:00:00')), ('start_time','&lt;', (context_today() + datetime.timedelta(days=4)).strftime('%Y-%m-%d 23:59:59'))]" />
<filter string="J-5" name="jminus5"
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=5)).strftime('%%Y-%%m-%%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=5)).strftime('%%Y-%%m-%%d 23:59:59'))]" />
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=5)).strftime('%Y-%m-%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=5)).strftime('%Y-%m-%d 23:59:59'))]" />
<filter string="J-4" name="jminus4"
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=4)).strftime('%%Y-%%m-%%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=4)).strftime('%%Y-%%m-%%d 23:59:59'))]" />
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=4)).strftime('%Y-%m-%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=4)).strftime('%Y-%m-%d 23:59:59'))]" />
<filter string="J-3" name="jminus3"
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=3)).strftime('%%Y-%%m-%%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=3)).strftime('%%Y-%%m-%%d 23:59:59'))]" />
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=3)).strftime('%Y-%m-%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=3)).strftime('%Y-%m-%d 23:59:59'))]" />
<filter string="J-2" name="jminus2"
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=2)).strftime('%%Y-%%m-%%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=2)).strftime('%%Y-%%m-%%d 23:59:59'))]" />
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=2)).strftime('%Y-%m-%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=2)).strftime('%Y-%m-%d 23:59:59'))]" />
<filter string="J-1" name="jminus1"
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d 23:59:59'))]" />
domain="[('end_time','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')), ('start_time','&lt;', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d 23:59:59'))]" />
<filter string="Today" name="today"
domain="[('end_time','&gt;', context_today().strftime('%%Y-%%m-%%d 00:00:00')), ('start_time','&lt;', context_today().strftime('%%Y-%%m-%%d 23:59:59'))]" />
domain="[('end_time','&gt;', context_today().strftime('%Y-%m-%d 00:00:00')), ('start_time','&lt;', context_today().strftime('%Y-%m-%d 23:59:59'))]" />
<group expand="1" string="Group By">
<filter string="Shift Template"
context="{'group_by' : 'task_template_id'}" />
<filter string="Type" name="gb_type"
context="{'group_by' : 'task_type_id'}" />
<filter string="Status" name="gb_status"
context="{'group_by' : 'stage_id'}" />
<filter string="Day" name="gb_day"
context="{'group_by' : 'start_time:day'}" />
name="template"
context="{'group_by' : 'task_template_id'}" />
<filter string="Type"
name="gb_type"
context="{'group_by' : 'task_type_id'}" />
<filter string="Status"
name="gb_status"
context="{'group_by' : 'state'}" />
<filter string="Day"
name="gb_day"
context="{'group_by' : 'start_time:day'}" />
</group>
</search>
</field>
@ -84,8 +92,7 @@
<field name="arch" type="xml">
<form>
<header>
<field name="stage_id" widget="statusbar"
clickable="1" />
<field name="state" widget="statusbar" clickable="1" />
</header>
<sheet>
<div class="oe_title">
@ -103,13 +110,11 @@
options="{'no_create': True, 'no_open': True}"
domain="[('working_mode', '=', 'regular')]"
attrs="{'invisible': [('working_mode', '!=', 'regular')]}" />
<group>
<field name="is_regular"
attrs="{'invisible': [('working_mode', '!=', 'regular')]}"/>
<field name="is_compensation"
attrs="{'invisible': [('working_mode', '!=', 'regular')]}"/>
<field name="working_mode" invisible="1"/>
</group>
<field name="is_regular"
attrs="{'invisible': [('working_mode', '!=', 'regular')]}"/>
<field name="is_compensation"
attrs="{'invisible': [('working_mode', '!=', 'regular')]}"/>
<field name="working_mode" invisible="1"/>
</group>
<group>
<field name="start_time" />
@ -135,28 +140,22 @@
<field name="color" />
<field name="task_type_id" />
<field name="name" />
<field name="stage_id" />
<field name="state" />
<field name="worker_id" />
<field name="replaced_id" />
<field name="super_coop_id" />
<field name="is_regular" />
<templates>
<t t-name="kanban-box">
<div
t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown"
groups="base.group_user">
<a class="dropdown-toggle btn"
data-toggle="dropdown" href="#">
<span class="fa fa-bars fa-lg" />
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown" name="card_dropdown_menu" groups="base.group_user">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<ul class="dropdown-menu" role="menu"
aria-labelledby="dLabel">
<t t-if="widget.editable">
<li>
<a type="edit">Edit Shift</a>
</li>
</t>
</ul>
<div class="dropdown-menu" role="menu">
<t t-if="widget.editable"><a role="menuitem" type="edit" class="dropdown-item">Edit Shift</a></t>
<t t-if="widget.deletable"><a role="menuitem" type="delete" class="dropdown-item">Delete</a></t>
</div>
</div>
<div class="oe_kanban_content">
<strong>
@ -170,7 +169,7 @@
</div>
<div>
Status:
<field name="stage_id" />
<field name="state" />
</div>
<div t-if="record.task_type_id.raw_value">
Type:
@ -180,6 +179,10 @@
Worker:
<field name="worker_id" />
</div>
<div t-if="record.replaced_id.raw_value">
Replacement worker:
<field name="replaced_id" />
</div>
<div t-if="record.super_coop_id.raw_value">
Super Coop:
<field name="super_coop_id" />
@ -198,32 +201,18 @@
<!-- Actions -->
<record model="ir.actions.act_window" id="action_task">
<field name="name">Task Action</field>
<field name="name">Shifts</field>
<field name="res_model">beesdoo.shift.shift</field>
<field name="view_mode">kanban,calendar,tree,form,pivot</field>
<field name="context">{'search_default_today': 1,
'search_default_gb_type':1}</field>
</record>
<record model="ir.actions.act_window" id="action_task_attendance">
<field name="name">Shift Attendance</field>
<field name="res_model">beesdoo.shift.shift</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_assigned': 1,
'search_default_today': 1,
'search_default_gb_status':1}</field>
</record>
<!-- Top menu item -->
<menuitem name="Planning" id="menu_task_top" parent="menu_root"
sequence="1" />
sequence="1" groups="beesdoo_shift.group_shift_attendance" />
<!-- actions -->
<menuitem name="Shift" id="menu_task" parent="menu_task_top"
action="action_task" groups="beesdoo_shift.group_shift_management" />
<menuitem name="Shift Attendance" id="menu_task_attendance"
parent="menu_task_top" action="action_task_attendance" />
<menuitem name="Shifts" id="menu_task" parent="menu_task_top"
action="action_task" groups="beesdoo_shift.group_shift_attendance" />
</odoo>

90
beesdoo_shift/views/task_template.xml

@ -29,9 +29,9 @@
<field name="super_coop_id" />
<field name="day_nb_id" />
<field name="worker_ids" />
<filter string="My Team Shift" domain="[('super_coop_id', '=', uid)]" />
<filter string="Planning" context="{'group_by':'planning_id'}" />
<filter string="Week Day" context="{'group_by':'day_nb_id'}" />
<filter string="My Team Shift" name="my_team_shift" domain="[('super_coop_id', '=', uid)]" />
<filter string="Planning" name="planning" context="{'group_by':'planning_id'}" />
<filter string="Week Day" name="week_day" context="{'group_by':'day_nb_id'}" />
<filter string="Place Available" name="available"
domain="[('remaining_worker', '>', 0)]" />
</search>
@ -101,35 +101,17 @@
<field name="super_coop_id" />
<templates>
<t t-name="kanban-box">
<div
t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<t t-if="widget.editable">
<div class="o_dropdown_kanban dropdown"
groups="base.group_user">
<a class="dropdown-toggle btn"
data-toggle="dropdown" href="#">
<span class="fa fa-bars fa-lg" />
<div class="o_dropdown_kanban dropdown" name="card_dropdown_menu">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<ul class="dropdown-menu" role="menu"
aria-labelledby="dLabel">
<t t-if="widget.editable">
<li>
<a type="edit">Edit Shift
Template
</a>
</li>
</t>
<t t-if="widget.deletable">
<li>
<a type="delete">Delete</a>
</li>
</t>
<li>
<ul class="oe_kanban_colorpicker"
data-field="color" />
</li>
</ul>
<div class="dropdown-menu" role="menu">
<t t-if="widget.editable"><a role="menuitem" type="edit" class="dropdown-item">Edit Shift Template</a></t>
<t t-if="widget.deletable"><a role="menuitem" type="delete" class="dropdown-item">Delete</a></t>
<ul role="menu" class="oe_kanban_colorpicker" data-field="color" />
</div>
</div>
</t>
<div class="oe_kanban_content">
@ -180,33 +162,9 @@
<menuitem name="Shift Management" id="menu_root"
groups="beesdoo_shift.group_shift_attendance" />
<menuitem name="Templates" id="menu_template_top" parent="menu_root"
groups="beesdoo_shift.group_shift_management" />
groups="beesdoo_shift.group_shift_management"
sequence="20" />
<!-- Configuration -->
<record model="ir.ui.view" id="daynumber_view_tree">
<field name="name">Day Number List</field>
<field name="model">beesdoo.shift.daynumber</field>
<field name="arch" type="xml">
<tree editable="top">
<field name="name" />
<field name="number" />
<field name="active" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="type_view_tree">
<field name="name">Shift Type List</field>
<field name="model">beesdoo.shift.type</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="description" />
<field name="active" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_generate_shift_template_wizard">
<field name="name">Generate Shift Template</field>
@ -240,24 +198,4 @@
</form>
</field>
</record>
<menuitem name="Configuration" id="menu_configuration_top"
parent="menu_root" groups="beesdoo_shift.group_planning_management" />
<record model="ir.actions.act_window" id="action_day_number">
<field name="name">Day Number</field>
<field name="res_model">beesdoo.shift.daynumber</field>
<field name="view_mode">tree</field>
</record>
<menuitem name="Shift Day" id="menu_configuration_day"
parent="menu_configuration_top" action="action_day_number" />
<record model="ir.actions.act_window" id="action_type">
<field name="name">Shift Type</field>
<field name="res_model">beesdoo.shift.type</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Shift Type" id="menu_configuration_type"
parent="menu_configuration_top" action="action_type" />
</odoo>

14
beesdoo_shift/wizard/__init__.py

@ -1,7 +1,7 @@
import instanciate_planning
import batch_template
import assign_super_coop
import subscribe
import extension
import holiday
import temporary_exemption
from . import instanciate_planning
from . import batch_template
from . import assign_super_coop
from . import subscribe
from . import extension
from . import holiday
from . import temporary_exemption

3
beesdoo_shift/wizard/assign_super_coop.py

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from odoo import models, fields, api, _
class AssignSuperCoop(models.TransientModel):

3
beesdoo_shift/wizard/batch_template.py

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
'''
Created on 2 janv. 2017
@author: Thibault Francois
'''
from openerp import models, fields, api, _
from odoo import models, fields, api, _
class GenerateShiftTemplate(models.TransientModel):
_name = 'beesddoo.shift.generate_shift_template'

13
beesdoo_shift/wizard/extension.py

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from openerp.exceptions import UserError
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class Subscribe(models.TransientModel):
_name = 'beesdoo.shift.extension'
_inherit = 'beesdoo.shift.action_mixin'
def _get_default_extension_delay(self):
return int(self.env['ir.config_parameter'].get_param('default_extension_delay', 28))
return int(self.env['ir.config_parameter'].sudo().get_param('default_extension_delay', 28))
extension_start_date = fields.Date(string="Start date for the extension", default=fields.Date.today, readonly=True)
@ -23,13 +22,11 @@ class Subscribe(models.TransientModel):
@api.multi
def extension(self):
self = self._check() #maybe a different group
grace_delay = int(self.env['ir.config_parameter'].get_param('default_grace_delay', 10))
grace_delay = int(self.env['ir.config_parameter'].sudo().get_param('default_grace_delay', 10))
status_id = self.env['cooperative.status'].search([('cooperator_id', '=', self.cooperator_id.id)])
if not status_id.extension_start_time:
raise UserError(_('You should not make a manual extension when the grace delay has not been triggered yet'))
extension_date = fields.Date.from_string(status_id.extension_start_time)
today = fields.Date.from_string(status_id.today)
today_delay = (today - extension_date).days - grace_delay
today_delay = (status_id.today - status_id.extension_start_time).days - grace_delay
if today_delay < 0:
raise UserError(_('You should not start a manual extension during the grace delay'))
status_id.sudo().write({'time_extension': self.extension_days + today_delay})

5
beesdoo_shift/wizard/holiday.py

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from openerp.exceptions import ValidationError
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class Subscribe(models.TransientModel):
_name = 'beesdoo.shift.holiday'

5
beesdoo_shift/wizard/instanciate_planning.py

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from odoo import models, fields, api, _
class InstanciatePlanning(models.TransientModel):
@ -8,7 +7,7 @@ class InstanciatePlanning(models.TransientModel):
def _get_planning(self):
return self._context.get('active_id')
date_start = fields.Date("First Day of planning", required=True)
date_start = fields.Date("First Day of planning (should be monday)", required=True)
planning_id = fields.Many2one('beesdoo.shift.planning', readonly=True, default=_get_planning)
@api.multi

11
beesdoo_shift/wizard/subscribe.py

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from openerp.exceptions import UserError
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class StatusActionMixin(models.AbstractModel):
_name = "beesdoo.shift.action_mixin"
@ -75,11 +74,11 @@ class Subscribe(models.TransientModel):
nb_shifts = fields.Integer(string='Number of shifts', default=_get_nb_shifts)
reset_counter = fields.Boolean(default=_get_reset_counter_default)
reset_compensation_counter = fields.Boolean(default=False)
unsubscribed = fields.Boolean(default=False, string="Are you sure to unsubscribe this cooperator")
unsubscribed = fields.Boolean(default=False, string="Are you sure to remove this cooperator from his subscribed shift ?")
irregular_start_date = fields.Date(string="Start Date", default=fields.Date.today)
resigning = fields.Boolean(default=False, help="Want to leave the beescoop")
@api.multi
def unsubscribe(self):
@ -109,7 +108,7 @@ class Subscribe(models.TransientModel):
if self.working_mode != 'regular':
#Remove existing shift then subscribe to the new shift
self.cooperator_id.sudo().write({'subscribed_shift_ids': [(5,)]})
data = {
'info_session': self.info_session,

2
beesdoo_shift/wizard/subscribe.xml

@ -1,6 +1,6 @@
<odoo>
<record model="ir.ui.view" id="subscribe_coop_wizard_view_form">
<field name="name">Subscribre Cooperator</field>
<field name="name">Subscribe Cooperator</field>
<field name="model">beesdoo.shift.subscribe</field>
<field name="arch" type="xml">
<form>

5
beesdoo_shift/wizard/temporary_exemption.py

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from openerp import models, fields, api, _
from openerp.exceptions import ValidationError
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class TemporaryExemption(models.TransientModel):
_name = 'beesdoo.shift.temporary_exemption'

2
beesdoo_shift_attendance/__init__.py

@ -0,0 +1,2 @@
from . import models
from . import wizard

44
beesdoo_shift_attendance/__manifest__.py

@ -0,0 +1,44 @@
# This module is, for now, specific to the BEES coop.
# Therefore, this module depends on `beesdo_worker_status`.
# If someone needs this module but has another worker_status rules
# this module can be splitted into a generic part, and a specific part
# that implement the worker_status rules.
{
'name': "Beescoop Shift Attendance Sheet",
'summary': """
Volonteer Timetable Management
Attendance Sheet for BEES coop""",
'description': """
""",
'author': "Elouan Le Bars, Coop It Easy",
'website': "https://github.com/beescoop/Obeesdoo",
'category': 'Cooperative management',
'version': '12.0.1.0.0',
'depends': [
'beesdoo_shift',
'beesdoo_worker_status',
'mail',
'barcodes',
],
'data': [
"data/system_parameter.xml",
"data/cron.xml",
"data/mail_template.xml",
"security/group.xml",
"security/ir.model.access.csv",
"views/res_config_settings_view.xml",
"wizard/validate_attendance_sheet.xml",
"wizard/generate_missing_attendance_sheets.xml",
"views/attendance_sheet.xml",
],
'demo': [
"demo/users.xml",
]
}

29
beesdoo_shift_attendance/data/cron.xml

@ -0,0 +1,29 @@
<odoo>
<data noupdate="1">
<record id="ir_cron_generate_attendance_sheet" model="ir.cron">
<field name="name">Generate Attendance Sheets</field>
<field name="model_id" ref="model_beesdoo_shift_sheet" />
<field name="state">code</field>
<field name="code">model._generate_attendance_sheet()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">4</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="active" eval="False" />
</record>
<record id="ir_cron_check_non_validated_sheet" model="ir.cron">
<field name="name">Check for non-validated sheets</field>
<field name="model_id" ref="model_beesdoo_shift_sheet" />
<field name="state">code</field>
<field name="code">model._cron_non_validated_sheets()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="nextcall" eval="datetime.now().replace(hour=00, minute=00, second=10)" />
<field name="active" eval="False" />
</record>
</data>
</odoo>

118
beesdoo_shift_attendance/data/mail_template.xml

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Mail template are declared in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<record id="email_template_non_attendance" model="mail.template">
<field name="name">Shift Non-attendance</field>
<field name="subject">Non-attendance to your last shift.</field>
<field name="partner_to">${object.replaced_id.id or object.worker_id.id|safe}</field>
<field name="model_id" ref="beesdoo_shift.model_beesdoo_shift_shift"/>
<field name="auto_delete" eval="True"/>
<field name="lang">${object.worker_id.lang}</field>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
% if object.replaced_id:
<p>Hello ${object.replaced_id.name},
<br><br>You have been recorded as non-attended during your last shift (${format_tz(object.start_time,object.replaced_id.tz or 'Europe/Brussels','%d.%m.%Y - %H:%M')}),
and you were supposed to replace ${object.worker_id.name}.
% endif
% if not object.replaced_id:
</p><p>Hello ${object.worker_id.name},</p>
<p>You have been recorded as non-attended during your last shift (${format_tz(object.start_time,object.worker_id.tz or 'Europe/Brussels','%d.%m.%Y - %H:%M')}).
% endif
% if object.worker_id.working_mode == 'regular':
% if object.state == 'absent_0':
<br><br>Super-cooperator assigned you 0 compensation, so you won't have any additionnal shift to do before your next regular shift.
% endif
% if object.state == 'absent_1':
<br><br>Super-cooperator assigned you 1 compensation, so you have to attend one additionnal shift before your next regular shift.
% endif
% if object.state == 'absent_2':
<br><br>Super-cooperator assigned you 2 compensations, so you have to attend two additionnal shifts before your next regular shift.
% endif
% if object.replaced_id:
You were supposed to replace ${object.worker_id.name}.
You have to do ${(object.replaced_id.cooperative_status_ids.sr + object.replaced_id.cooperative_status_ids.sc) * -1 } shifts before your next regular shift.<br>
% else:
You have to do ${(object.worker_id.cooperative_status_ids.sr + object.worker_id.cooperative_status_ids.sc) * -1 } shifts before your next regular shift.<br>
% endif
% endif
% if object.worker_id.working_mode == 'irregular':
Your shift counter is at ${object.worker_id.cooperative_status_ids.sr}.
% if object.worker_id.cooperative_status_ids.future_alert_date:
It should be superior or equal to 1 before the
${object.worker_id.cooperative_status_ids.future_alert_date}.
% endif
<br>
% endif
% if object.replaced_id:
Your current status is "${object.replaced_id.cooperative_status_ids.get_status_value()}".
% else:
<br><br>Your current status is "${object.worker_id.cooperative_status_ids.get_status_value()}".
% endif
<br>If you have any question regarding this non-attendance, just answer this e-mail.
</p>
<br>
<p>Cooperatively yours,<br>
The Members' office volunteers</p>
<p>${object.worker_id.company_id.name}.</p>
% if object.worker_id.company_id.street:
${object.worker_id.company_id.street}
% endif
% if object.worker_id.company_id.street2:
${object.worker_id.company_id.street2}<br>
% endif
% if object.worker_id.company_id.city or object.worker_id.company_id.zip:
${object.worker_id.company_id.zip} ${object.worker_id.company_id.city}<br>
% endif
% if object.worker_id.company_id.country_id:
${object.worker_id.company_id.state_id and ('%s, ' % object.worker_id.company_id.state_id.name) or ''} ${object.worker_id.company_id.country_id.name or ''}<br>
% endif
% if object.worker_id.company_id.phone:
Phone:&nbsp; ${object.worker_id.company_id.phone}
% endif
% if object.worker_id.company_id.website:
<div>
Web :&nbsp;<a href="${object.worker_id.company_id.website}">${object.worker_id.company_id.website}</a>
</div>
%endif
% if object.worker_id.company_id.logo_url:
<div>
<img src="${object.worker_id.company_id.logo_url}">
</div>
%endif
</div>
]]></field>
</record>
<record id="email_template_non_validated_sheet" model="mail.template">
<field name="name">Non-validated sheet</field>
<field name="subject">[${object.day}] Non-validated sheet ${object.time_slot}</field>
<field name="model_id" ref="model_beesdoo_shift_sheet"/>
<field name="auto_delete" eval="True"/>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
<p>${object.day}
<br/><br/>The attendance sheet for ${object.time_slot} is not validated.
<br/><br/>Please, do it as soon as possible so as to update workers' status.
</p>
</div>
]]></field>
</record>
</data>
</odoo>

14
beesdoo_shift_attendance/data/system_parameter.xml

@ -0,0 +1,14 @@
<odoo noupdate="1">
<record id="card_support" model="ir.config_parameter">
<field name="key">beesdoo_shift_attendance.card_support</field>
<field name="value">False</field>
</record>
<record id="attendance_sheet_generation_interval" model="ir.config_parameter">
<field name="key">beesdoo_shift_attendance.attendance_sheet_generation_interval</field>
<field name="value">15</field>
</record>
<record id="default_task_type_id" model="ir.config_parameter">
<field name="key">beesdoo_shift_attendance.default_task_type_id</field>
<field name="value">1</field>
</record>
</odoo>

40
beesdoo_shift_attendance/demo/users.xml

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Coop IT Easy
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<!-- Generic and permanent account -->
<record id="beesdoo_shift_partner_1_demo" model="res.partner">
<field name="firstname">Generic Account</field>
<field name="lastname">Demo</field>
<field name="email">generic@demo.net</field>
</record>
<record id="beesdoo_shift_partner_2_demo" model="res.partner">
<field name="firstname">Permanent Member</field>
<field name="lastname">Demo</field>
<field name="is_company" eval="False"/>
<field name="email">permanent@demo.net</field>
<field name="city">Ixelles</field>
<field name="zip">1050</field>
<field name="country_id" ref="base.be"/>
</record>
<record id="beesdoo_shift_user_1_demo" model="res.users">
<field name="partner_id" ref="beesdoo_shift_partner_1_demo"/>
<field name="login">generic</field>
<field name="password">demo</field>
<field name="groups_id" eval="[(4,ref('beesdoo_shift_attendance.group_shift_attendance_sheet'))]"/>
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="beesdoo_shift_user_2_demo" model="res.users">
<field name="partner_id" ref="beesdoo_shift_partner_2_demo"/>
<field name="login">permanent</field>
<field name="password">demo</field>
<field name="groups_id" eval="[(4,ref('beesdoo_shift.group_shift_management'))]"/>
</record>
</odoo>

2
beesdoo_shift_attendance/models/__init__.py

@ -0,0 +1,2 @@
from . import attendance_sheet
from . import res_config_settings

656
beesdoo_shift_attendance/models/attendance_sheet.py

@ -0,0 +1,656 @@
from datetime import date, datetime, timedelta
from lxml import etree
from odoo import _, api, exceptions, fields, models
from odoo.exceptions import UserError, ValidationError
class AttendanceSheetShift(models.Model):
"""
Partial copy of Task class to use in AttendanceSheet,
actual Task is updated at validation.
Should be Abstract and not used alone (common code for
AttendanceSheetShiftAdded and AttendanceSheetShiftExpected),
but create() method from res.partner raise error
when class is Abstract.
"""
_name = "beesdoo.shift.sheet.shift"
_description = "Copy of an actual shift into an attendance sheet"
_order = "task_type_id, worker_name"
@api.model
def pre_filled_task_type_id(self):
parameters = self.env["ir.config_parameter"].sudo()
tasktype_id = int(
parameters.get_param(
"beesdoo_shift_attendance.pre_filled_task_type_id", default=1
)
)
task_types = self.env["beesdoo.shift.type"]
return task_types.browse(tasktype_id)
# Related actual shift
task_id = fields.Many2one("beesdoo.shift.shift", string="Task")
attendance_sheet_id = fields.Many2one(
"beesdoo.shift.sheet",
string="Attendance Sheet",
required=True,
ondelete="cascade",
)
state = fields.Selection(
[
("done", "Present"),
("absent_0", "Absent - 0 Compensation"),
("absent_1", "Absent - 1 Compensation"),
("absent_2", "Absent - 2 Compensations"),
],
string="Shift State",
required=True,
)
worker_id = fields.Many2one(
"res.partner",
string="Worker",
domain=[
("is_worker", "=", True),
("working_mode", "in", ("regular", "irregular")),
("state", "not in", ("unsubscribed", "resigning")),
],
required=True,
)
worker_name = fields.Char(related="worker_id.name", store=True)
task_type_id = fields.Many2one(
"beesdoo.shift.type", string="Task Type", default=pre_filled_task_type_id
)
working_mode = fields.Selection(
related="worker_id.working_mode", string="Working Mode"
)
# The two exclusive booleans are gathered in a simple one
is_compensation = fields.Boolean(
string="Compensation shift ?", help="Only for regular workers"
)
class AttendanceSheetShiftExpected(models.Model):
"""
Shifts already expected.
"""
_name = "beesdoo.shift.sheet.expected"
_description = "Expected Shift"
_inherit = ["beesdoo.shift.sheet.shift"]
super_coop_id = fields.Many2one(
related="task_id.super_coop_id", store=True
)
replaced_id = fields.Many2one(
"res.partner",
string="Replacement Worker",
help="Replacement Worker (must be regular)",
domain=[
("eater", "=", "worker_eater"),
("working_mode", "=", "regular"),
("state", "not in", ("unsubscribed", "resigning")),
],
)
@api.onchange("replaced_id")
def on_change_replacement_worker(self):
if self.replaced_id:
self.state = "done"
class AttendanceSheetShiftAdded(models.Model):
"""
Shifts added during time slot.
"""
_name = "beesdoo.shift.sheet.added"
_description = "Added Shift"
_inherit = ["beesdoo.shift.sheet.shift"]
state = fields.Selection(default="done")
@api.onchange("working_mode")
def on_change_working_mode(self):
self.state = "done"
self.is_compensation = self.working_mode == "regular"
class AttendanceSheet(models.Model):
_name = "beesdoo.shift.sheet"
_inherit = [
"mail.thread",
"barcodes.barcode_events_mixin",
]
_description = "Attendance sheet"
_order = "start_time"
name = fields.Char(string="Name", compute="_compute_name")
time_slot = fields.Char(
string="Time Slot",
compute="_compute_time_slot",
store=True,
readonly=True,
)
active = fields.Boolean(string="Active", default=1)
state = fields.Selection(
[("not_validated", "Not Validated"), ("validated", "Validated"),],
string="State",
readonly=True,
index=True,
default="not_validated",
track_visibility="onchange",
)
start_time = fields.Datetime(
string="Start Time", required=True, readonly=True
)
end_time = fields.Datetime(string="End Time", required=True, readonly=True)
day = fields.Date(string="Day", compute="_compute_day", store=True)
day_abbrevation = fields.Char(
string="Day Abbrevation", compute="_compute_day_abbrevation"
)
week = fields.Char(
string="Week",
help="Computed from planning name",
compute="_compute_week",
)
expected_shift_ids = fields.One2many(
"beesdoo.shift.sheet.expected",
"attendance_sheet_id",
string="Expected Shifts",
)
added_shift_ids = fields.One2many(
"beesdoo.shift.sheet.added",
"attendance_sheet_id",
string="Added Shifts",
)
max_worker_no = fields.Integer(
string="Maximum number of workers",
default=0,
readonly=True,
help="Indicative maximum number of workers.",
)
attended_worker_no = fields.Integer(
string="Number of workers present",
default=0,
readonly=True,
)
notes = fields.Text(
"Notes",
default="",
help="Notes about the attendance for the Members Office",
)
is_annotated = fields.Boolean(
compute="_compute_is_annotated",
string="Is annotated",
readonly=True,
store=True,
)
is_read = fields.Boolean(
string="Mark as read",
help="Has notes been read by an administrator ?",
default=False,
track_visibility="onchange",
)
feedback = fields.Text("Comments about the shift")
worker_nb_feedback = fields.Selection(
[
("not_enough", "Not enough workers"),
("enough", "Enough workers"),
("too_many", "Too many workers"),
("empty", "I was not there during the shift"),
],
string="Was your team big enough ? *",
)
validated_by = fields.Many2one(
"res.partner",
string="Validated by",
domain=[
("eater", "=", "worker_eater"),
("super", "=", True),
("working_mode", "=", "regular"),
("state", "not in", ("unsubscribed", "resigning")),
],
track_visibility="onchange",
readonly=True,
)
_sql_constraints = [
(
"check_not_annotated_mark_as_read",
"CHECK ((is_annotated=FALSE AND is_read=FALSE) OR is_annotated=TRUE)",
_("Non-annotated sheets can't be marked as read."),
)
]
@api.depends("start_time", "end_time")
def _compute_time_slot(self):
for rec in self:
start_time = fields.Datetime.context_timestamp(rec, rec.start_time)
end_time = fields.Datetime.context_timestamp(rec, rec.end_time)
rec.time_slot = (
start_time.strftime("%H:%M")
+ "-"
+ end_time.strftime("%H:%M")
)
@api.depends("start_time", "end_time", "week", "day_abbrevation")
def _compute_name(self):
for rec in self:
start_time = fields.Datetime.context_timestamp(rec, rec.start_time)
name = "[%s] " % fields.Date.to_string(start_time)
if rec.week:
name += rec.week + " "
if rec.day_abbrevation:
name += rec.day_abbrevation + " "
if rec.time_slot:
name += "(%s)" % rec.time_slot
rec.name = name
@api.depends("start_time")
def _compute_day(self):
for rec in self:
rec.day = rec.start_time.date()
@api.depends("expected_shift_ids")
def _compute_day_abbrevation(self):
"""
Compute Day Abbrevation from Planning Name
of first expected shift with one.
"""
for rec in self:
for shift in rec.expected_shift_ids:
if shift.task_id.task_template_id.day_nb_id.name:
rec.day_abbrevation = (
shift.task_id.task_template_id.day_nb_id.name
)
@api.depends("expected_shift_ids")
def _compute_week(self):
"""
Compute Week Name from Planning Name
of first expected shift with one.
"""
for rec in self:
for shift in rec.expected_shift_ids:
if shift.task_id.planning_id.name:
rec.week = shift.task_id.planning_id.name
@api.depends("notes")
def _compute_is_annotated(self):
for rec in self:
if rec.notes:
rec.is_annotated = bool(rec.notes.strip())
@api.constrains("expected_shift_ids", "added_shift_ids")
def _constrain_unique_worker(self):
# Warning : map return generator in python3 (for Odoo 12)
added_ids = [s.worker_id.id for s in self.added_shift_ids]
expected_ids = [s.worker_id.id for s in self.expected_shift_ids]
replacement_ids = [
s.replaced_id.id
for s in self.expected_shift_ids
if s.replaced_id.id
]
ids = added_ids + expected_ids + replacement_ids
if (len(ids) - len(set(ids))) > 0:
raise UserError(
_(
"You can't add the same worker more than once to an attendance sheet."
)
)
@api.constrains(
"expected_shift_ids",
"added_shift_ids",
"notes",
"feedback",
"worker_nb_feedback",
)
def _lock_after_validation(self):
if self.state == "validated":
raise UserError(
_("The sheet has already been validated and can't be edited.")
)
def on_barcode_scanned(self, barcode):
if self.env.user.has_group("beesdoo_shift_attendance.group_shift_attendance"):
raise UserError(
_("You must be logged as 'Attendance Sheet Generic Access' "
" if you want to scan cards.")
)
if self.state == "validated":
raise UserError(
_("A validated attendance sheet can't be modified")
)
worker = self.env["res.partner"].search([("barcode", "=", barcode)])
if not len(worker):
raise UserError(
_(
"Worker not found (invalid barcode or status). \nBarcode : %s"
)
% barcode
)
if len(worker) > 1:
raise UserError(
_(
"Multiple workers are corresponding this barcode. \nBarcode : %s"
)
% barcode
)
if worker.state == "unsubscribed":
shift_counter = (
worker.cooperative_status_ids.sc
+ worker.cooperative_status_ids.sr
)
raise UserError(
_(
"Beware, your account is frozen because your shift counter "
"is at %s. Please contact Members Office to unfreeze it. "
"If you want to attend this shift, your supercoop "
"can write your name in the notes field during validation."
)
% shift_counter
)
if worker.state == "resigning":
raise UserError(
_(
"Beware, you are recorded as resigning. "
"Please contact member's office if this is incorrect. Thank you."
)
)
if worker.working_mode not in ("regular", "irregular"):
raise UserError(
_("%s's working mode is %s and should be regular or irregular.")
% (worker.name, worker.working_mode)
)
# Expected shifts status update
for id in self.expected_shift_ids.ids:
shift = self.env["beesdoo.shift.sheet.expected"].browse(id)
if (
shift.worker_id == worker and not shift.replaced_id
) or shift.replaced_id == worker:
shift.state = "done"
return
if shift.worker_id == worker and shift.replaced_id:
raise UserError(
_("%s is registered as replaced.") % worker.name
)
is_compensation = worker.working_mode == "regular"
added_ids = [s.worker_id.id for s in self.added_shift_ids]
if worker.id not in added_ids:
# Added shift creation
self.added_shift_ids |= self.added_shift_ids.new(
{
"task_type_id": self.added_shift_ids.pre_filled_task_type_id(),
"state": "done",
"attendance_sheet_id": self._origin.id,
"worker_id": worker.id,
"is_compensation": is_compensation,
}
)
@api.model
def create(self, vals):
new_sheet = super(AttendanceSheet, self).create(vals)
# Creation and addition of the expected shifts corresponding
# to the time range
tasks = self.env["beesdoo.shift.shift"]
expected_shift = self.env["beesdoo.shift.sheet.expected"]
# Fix issues with equality check on datetime
# by searching on a small intervall instead
delta = timedelta(minutes=1)
tasks = tasks.search(
[
("start_time", ">", new_sheet.start_time - delta),
("start_time", "<", new_sheet.start_time + delta),
("end_time", ">", new_sheet.end_time - delta),
("end_time", "<", new_sheet.end_time + delta),
]
)
workers = []
for task in tasks:
# Only one shift is added if multiple similar exist
if task.worker_id and task.worker_id not in workers and (task.state != "cancel") :
expected_shift.create(
{
"attendance_sheet_id": new_sheet.id,
"task_id": task.id,
"worker_id": task.worker_id.id,
"replaced_id": task.replaced_id.id,
"task_type_id": task.task_type_id.id,
"state": "absent_2",
"working_mode": task.working_mode,
"is_compensation": task.is_compensation,
}
)
workers.append(task.worker_id)
# Maximum number of workers calculation (count empty shifts)
new_sheet.max_worker_no = len(tasks)
return new_sheet
@api.multi
def button_mark_as_read(self):
if self.is_read:
raise UserError(_("The sheet has already been marked as read."))
self.is_read = True
def _validate(self, user):
self.ensure_one()
if self.state == "validated":
raise UserError("The sheet has already been validated.")
# Expected shifts status update
for expected_shift in self.expected_shift_ids:
actual_shift = expected_shift.task_id
actual_shift.replaced_id = expected_shift.replaced_id
actual_shift.state = expected_shift.state
if expected_shift.state == "done":
self.attended_worker_no += 1
if expected_shift.state != "done":
mail_template = self.env.ref(
"beesdoo_shift_attendance.email_template_non_attendance", False
)
mail_template.send_mail(expected_shift.task_id.id, True)
# Added shifts status update
for added_shift in self.added_shift_ids:
is_regular_worker = added_shift.worker_id.working_mode == "regular"
is_compensation = added_shift.is_compensation
# Edit a non-assigned shift or create one if none
# Fix issues with equality check on datetime
# by searching on a small intervall instead
delta = timedelta(minutes=1)
non_assigned_shifts = self.env["beesdoo.shift.shift"].search(
[
("worker_id", "=", False),
("start_time", ">", self.start_time - delta),
("start_time", "<", self.start_time + delta),
("end_time", ">", self.end_time - delta),
("end_time", "<", self.end_time + delta),
("task_type_id", "=", added_shift.task_type_id.id),
],
limit=1,
)
if len(non_assigned_shifts):
actual_shift = non_assigned_shifts[0]
else:
actual_shift = self.env["beesdoo.shift.shift"].create(
{
"name": _("%s (added)" % self.name),
"task_type_id": added_shift.task_type_id.id,
"start_time": self.start_time,
"end_time": self.end_time,
}
)
actual_shift.write(
{
"state": added_shift.state,
"worker_id": added_shift.worker_id.id,
"is_regular": not is_compensation and is_regular_worker,
"is_compensation": is_compensation and is_regular_worker,
}
)
added_shift.task_id = actual_shift.id
if actual_shift.state == "done":
self.attended_worker_no += 1
self.validated_by = user
self.state = "validated"
return
@api.multi
def validate_with_checks(self):
self.ensure_one()
if self.state == "validated":
raise UserError(_("The sheet has already been validated."))
if self.start_time > datetime.now():
raise UserError(
_(
"Attendance sheet can only be validated once the shifts have started."
)
)
# Fields validation
for added_shift in self.added_shift_ids:
if not added_shift.worker_id:
raise UserError(
_("Worker name is missing for an added shift.")
)
if added_shift.state != "done":
raise UserError(
_("Shift State is missing or wrong for %s")
% added_shift.worker_id.name
)
if not added_shift.task_type_id:
raise UserError(
_("Task Type is missing for %s")
% added_shift.worker_id.name
)
if not added_shift.working_mode:
raise UserError(
_("Working mode is missing for %s")
% added_shift.worker_id.name
)
if added_shift.working_mode not in ["regular", "irregular"]:
raise UserError(
_("Warning : Working mode for %s is %s")
% (
added_shift.worker_id.name,
added_shift.worker_id.working_mode,
)
)
for expected_shift in self.expected_shift_ids:
if not expected_shift.state:
raise UserError(
_("Shift State is missing for %s")
% expected_shift.worker_id.name
)
if (
expected_shift.state == "absent"
and not expected_shift.compensation_no
):
raise UserError(
_("Compensation number is missing for %s")
% expected_shift.worker_id.name
)
# Open a validation wizard only if not admin
if self.env.user.has_group(
"beesdoo_shift_attendance.group_shift_attendance_sheet_validation"
):
if not self.worker_nb_feedback:
raise UserError(
_("Please give your feedback about the number of workers.")
)
self._validate(self.env.user.partner_id)
return
return {
"type": "ir.actions.act_window",
"res_model": "beesdoo.shift.sheet.validate",
"view_type": "form",
"view_mode": "form",
"target": "new",
}
@api.model
def _generate_attendance_sheet(self):
"""
Generate sheets with shifts in the time interval
defined from corresponding CRON time interval.
"""
tasks = self.env["beesdoo.shift.shift"]
sheets = self.env["beesdoo.shift.sheet"]
current_time = datetime.now()
generation_interval_setting = int(
self.env["ir.config_parameter"].sudo().get_param(
"beesdoo_shift_attendance.attendance_sheet_generation_interval"
)
)
allowed_time_range = timedelta(minutes=generation_interval_setting)
tasks = tasks.search(
[
("start_time", ">", str(current_time),),
("start_time", "<", str(current_time + allowed_time_range),),
]
)
for task in tasks:
start_time = task.start_time
end_time = task.end_time
sheets = sheets.search(
[("start_time", "=", start_time), ("end_time", "=", end_time)]
)
if not sheets:
sheet = sheets.create(
{"start_time": start_time, "end_time": end_time}
)
@api.model
def _cron_non_validated_sheets(self):
sheets = self.env["beesdoo.shift.sheet"]
non_validated_sheets = sheets.search(
[
("day", "=", date.today() - timedelta(days=1)),
("state", "=", "not_validated"),
]
)
if non_validated_sheets:
mail_template = self.env.ref(
"beesdoo_shift_attendance.email_template_non_validated_sheet",
False,
)
for rec in non_validated_sheets:
mail_template.send_mail(rec.id, True)

63
beesdoo_shift_attendance/models/res_config_settings.py

@ -0,0 +1,63 @@
# Copyright 2019-2020 Elouan Le Bars <elouan@coopiteasy.be>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import ast
from odoo import fields, models, api
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
card_support = fields.Boolean(
string="Scan cooperators cards instead of login for sheets validation",
config_parameter="beesdoo_shift_attendance.card_support",
)
pre_filled_task_type_id = fields.Many2one(
"beesdoo.shift.type",
string="Default Task Type",
help="Default task type for attendance sheet pre-filling",
required=True,
default=False,
)
attendance_sheet_generation_interval = fields.Integer(
string="Time interval for attendance sheet generation",
help="Time interval expressed in minutes",
required=True,
config_parameter=(
"beesdoo_shift_attendance.attendance_sheet_generation_interval"
),
)
@api.multi
def set_values(self):
super(ResConfigSettings, self).set_values()
parameters = self.env["ir.config_parameter"].sudo()
parameters.set_param(
"beesdoo_shift_attendance.card_support", str(self.card_support),
)
parameters.set_param(
"beesdoo_shift_attendance.pre_filled_task_type_id",
str(self.pre_filled_task_type_id.id),
)
parameters.set_param(
"beesdoo_shift_attendance.attendance_sheet_generation_interval",
str(self.attendance_sheet_generation_interval),
)
@api.multi
def get_values(self):
res = super(ResConfigSettings, self).get_values()
res.update(
card_support=ast.literal_eval(
self.env["ir.config_parameter"].get_param(
"beesdoo_shift_attendance.card_support"
),
),
pre_filled_task_type_id=int(
self.env["ir.config_parameter"].get_param(
"beesdoo_shift_attendance.pre_filled_task_type_id"
)
),
)
return res

14
beesdoo_shift_attendance/security/group.xml

@ -0,0 +1,14 @@
<odoo>
<record id="group_shift_attendance_sheet" model="res.groups">
<field name="name">Attendance Sheet Generic Access</field>
<field name="category_id" ref="base.module_category_cooperative_management"/>
</record>
<record id="group_shift_attendance_sheet_validation" model="res.groups">
<field name="name">Attendance Sheet Validation</field>
<field name="category_id" ref="base.module_category_cooperative_management"/>
<field name="implied_ids" eval="[(4, ref('group_shift_attendance_sheet'))]"/>
</record>
<record id="beesdoo_shift.group_shift_attendance" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_shift_attendance_sheet'))]"/>
</record>
</odoo>

12
beesdoo_shift_attendance/security/ir.model.access.csv

@ -0,0 +1,12 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
create_beesdoo_shift_shift,create_edit_beesdoo_shift_shift,beesdoo_shift.model_beesdoo_shift_shift,group_shift_attendance_sheet,1,1,1,0
read_beesdoo_shift_sheet_shift,read_beesdoo_shift_sheet_shift,model_beesdoo_shift_sheet_shift,group_shift_attendance_sheet,1,0,0,0
create_beesdoo_shift_sheet_shift,create_beesdoo_shift_sheet_shift,model_beesdoo_shift_sheet_shift,group_shift_attendance_sheet,1,1,1,0
create_beesdoo_shift_sheet_expected,create_beesdoo_shift_sheet_expected,model_beesdoo_shift_sheet_expected,group_shift_attendance_sheet,1,1,1,0
manage_beesdoo_shift_sheet_added,manage_beesdoo_shift_sheet_added,model_beesdoo_shift_sheet_added,group_shift_attendance_sheet,1,1,1,1
create_beesdoo_shift_sheet,create_beesdoo_shift_sheet,model_beesdoo_shift_sheet,group_shift_attendance_sheet,1,1,1,0
sheet_access_beesdoo_shift_template,sheet_access_beesdoo_shift_template,beesdoo_shift.model_beesdoo_shift_template,group_shift_attendance_sheet,1,0,0,0
sheet_access_beesdoo_shift_type,sheet_access_beesdoo_shift_type,beesdoo_shift.model_beesdoo_shift_type,group_shift_attendance_sheet,1,0,0,0
access_beesdoo_shift_daynumber,access_beesdoo_shift_daynumber,beesdoo_shift.model_beesdoo_shift_daynumber,group_shift_attendance_sheet,1,0,0,0
manage_beesdoo_shift_sheet_shift,beesdoo_shift_sheet_shift,model_beesdoo_shift_sheet_shift,beesdoo_shift.group_shift_attendance,1,1,1,1
manage_beesdoo_shift_sheet_expected,manage_beesdoo_shift_sheet_expected,model_beesdoo_shift_sheet_expected,beesdoo_shift.group_shift_attendance,1,1,1,1

1
beesdoo_shift_attendance/tests/__init__.py

@ -0,0 +1 @@
from . import test_beesdoo_shift

391
beesdoo_shift_attendance/tests/test_beesdoo_shift.py

@ -0,0 +1,391 @@
# Copyright 2019 - Today Coop IT Easy SCRLfs (<http://www.coopiteasy.be>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import time
from datetime import datetime, timedelta
from odoo import exceptions, fields
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import TransactionCase
class TestBeesdooShift(TransactionCase):
def setUp(self):
super(TestBeesdooShift, self).setUp()
self.shift_model = self.env["beesdoo.shift.shift"]
self.shift_template_model = self.env["beesdoo.shift.template"]
self.attendance_sheet_model = self.env["beesdoo.shift.sheet"]
self.attendance_sheet_shift_model = self.env[
"beesdoo.shift.sheet.shift"
]
self.shift_expected_model = self.env["beesdoo.shift.sheet.expected"]
self.shift_added_model = self.env["beesdoo.shift.sheet.added"]
self.pre_filled_task_type_id = self.env["ir.config_parameter"].sudo().get_param(
"beesdoo_shift.pre_filled_task_type_id"
)
self.current_time = datetime.now()
self.user_admin = self.env.ref("base.user_root")
self.user_generic = self.env.ref(
"beesdoo_shift_attendance.beesdoo_shift_user_1_demo"
)
self.user_permanent = self.env.ref(
"beesdoo_shift_attendance.beesdoo_shift_user_2_demo"
)
self.setting_wizard = self.env["res.config.settings"].sudo(
self.user_admin
)
self.worker_regular_1 = self.env.ref(
"beesdoo_shift.res_partner_cooperator_6_demo"
)
self.worker_regular_2 = self.env.ref(
"beesdoo_shift.res_partner_cooperator_5_demo"
)
self.worker_regular_3 = self.env.ref(
"beesdoo_shift.res_partner_cooperator_3_demo"
)
self.worker_regular_super_1 = self.env.ref(
"beesdoo_shift.res_partner_cooperator_1_demo"
)
self.worker_irregular_1 = self.env.ref(
"beesdoo_shift.res_partner_cooperator_2_demo"
)
self.worker_irregular_2 = self.env.ref(
"beesdoo_shift.res_partner_cooperator_4_demo"
)
self.task_type_1 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_type_1_demo"
)
self.task_type_2 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_type_2_demo"
)
self.task_type_3 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_type_3_demo"
)
self.task_template_1 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_template_1_demo"
)
self.task_template_2 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_template_2_demo"
)
# Set time in and out of generation interval parameter
self.start_in_1 = self.current_time + timedelta(seconds=2)
self.end_in_1 = self.current_time + timedelta(minutes=10)
self.start_in_2 = self.current_time + timedelta(minutes=9)
self.end_in_2 = self.current_time + timedelta(minutes=21)
self.start_out_1 = self.current_time - timedelta(minutes=50)
self.end_out_1 = self.current_time - timedelta(minutes=20)
self.start_out_2 = self.current_time + timedelta(minutes=40)
self.end_out_2 = self.current_time + timedelta(minutes=50)
self.shift_regular_regular_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_1.id,
"worker_id": self.worker_regular_1.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
"is_regular": True,
"is_compensation": False,
}
)
self.shift_regular_regular_2 = self.shift_model.create(
{
"task_type_id": self.task_type_2.id,
"worker_id": self.worker_regular_2.id,
"start_time": self.start_out_1,
"end_time": self.end_out_1,
"is_regular": True,
"is_compensation": False,
}
)
self.shift_regular_regular_replaced_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_3.id,
"worker_id": self.worker_regular_3.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
"is_regular": True,
"is_compensation": False,
"replaced_id": self.worker_regular_2.id,
}
)
self.future_shift_regular = self.shift_model.create(
{
"task_template_id": self.task_template_2.id,
"task_type_id": self.task_type_1.id,
"worker_id": self.worker_regular_super_1.id,
"start_time": self.start_in_2,
"end_time": self.end_in_2,
"is_regular": False,
"is_compensation": True,
}
)
self.shift_irregular_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_2.id,
"worker_id": self.worker_irregular_1.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
}
)
self.shift_irregular_2 = self.shift_model.create(
{
"task_type_id": self.task_type_3.id,
"worker_id": self.worker_irregular_2.id,
"start_time": self.start_out_2,
"end_time": self.end_out_2,
}
)
self.shift_empty_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_1.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
}
)
def search_sheets(self, start_time, end_time):
if (type(start_time) and type(end_time)) == datetime:
return self.attendance_sheet_model.search(
[("start_time", "=", start_time), ("end_time", "=", end_time)]
)
def test_attendance_sheet_creation(self):
"Test creation of an attendance sheet with all its expected shifts"
# Set generation interval setting
setting_wizard_1 = self.setting_wizard.create(
{
"pre_filled_task_type_id": self.task_type_1.id,
"attendance_sheet_generation_interval": 15,
}
)
setting_wizard_1.execute()
# Test attendance sheets creation
self.attendance_sheet_model._generate_attendance_sheet()
self.assertEqual(
len(self.search_sheets(self.start_in_1, self.end_in_1)), 1
)
self.assertEqual(
len(self.search_sheets(self.start_in_2, self.end_in_2)), 1
)
self.assertEqual(
len(self.search_sheets(self.start_out_1, self.end_out_1)), 0
)
self.assertEqual(
len(self.search_sheets(self.start_out_2, self.end_out_2)), 0
)
# Test expected shifts creation
# Sheet 1 starts at current time + 2 secs, ends at current time + 10 min
# Sheet 2 starts at current time + 9 min, ends at current time + 21 min
sheet_1 = self.search_sheets(self.start_in_1, self.end_in_1)
sheet_2 = self.search_sheets(self.start_in_2, self.end_in_2)
self.assertTrue(sheet_1.start_time)
self.assertTrue(sheet_1.end_time)
# Empty shift should not be added
self.assertEqual(len(sheet_1.expected_shift_ids), 3)
self.assertEqual(len(sheet_1.added_shift_ids), 0)
self.assertEqual(len(sheet_2.expected_shift_ids), 1)
# Test consistency with actual shift for sheet 1
for shift in sheet_1.expected_shift_ids:
self.assertEquals(shift.worker_id, shift.task_id.worker_id)
self.assertEquals(
shift.replaced_id, shift.task_id.replaced_id
)
self.assertEqual(shift.task_type_id, shift.task_id.task_type_id)
self.assertEqual(shift.super_coop_id, shift.task_id.super_coop_id)
self.assertEqual(shift.working_mode, shift.task_id.working_mode)
# Status should be "absent" for all shifts
self.assertEquals(shift.state, "absent_2")
# Empty shift should be considered in max worker number calculation
self.assertEqual(sheet_1.max_worker_no, 4)
# Test default values creation
self.assertTrue(sheet_1.time_slot)
self.assertEqual(sheet_1.day, self.start_in_1.date())
self.assertEqual(sheet_1.day_abbrevation, "Lundi")
self.assertEqual(sheet_1.week, "Semaine A")
self.assertTrue(sheet_1.name)
self.assertFalse(sheet_1.notes)
self.assertFalse(sheet_1.is_annotated)
def test_attendance_sheet_barcode_scan(self):
"""
Edition of an attendance sheet
with barcode scanner, as a generic user"
"""
# Attendance sheet generation
self.attendance_sheet_model.sudo(
self.user_generic
)._generate_attendance_sheet()
sheet_1 = self.search_sheets(self.start_in_1, self.end_in_1,)
sheet_1 = sheet_1.sudo(self.user_generic)
"""
Expected workers are :
worker_regular_1 (barcode : 421457731745)
worker_regular_3 replaced by worker_regular_2 (barcode : 421457731744))
worker_irregular_1 (barcode : 429919251493)
"""
# Scan barcode for expected workers
for barcode in [421457731745, 421457731744, 429919251493]:
sheet_1.on_barcode_scanned(barcode)
# Check expected shifts update
for id in sheet_1.expected_shift_ids.ids:
shift = sheet_1.expected_shift_ids.browse(id)
self.assertEqual(shift.state, "done")
"""
Added workers are :
worker_regular_super_1 (barcode : 421457731741)
worker_irregular_2 (barcode : 421457731743)
"""
# Workararound for _onchange method
# (not applying on temporary object in tests)
sheet_1._origin = sheet_1
# Scan barcode for added workers
sheet_1.on_barcode_scanned(421457731741)
self.assertEqual(len(sheet_1.added_shift_ids), 1)
sheet_1.on_barcode_scanned(421457731743)
# Scan an already added worker should not change anything
sheet_1.on_barcode_scanned(421457731743)
self.assertEqual(len(sheet_1.added_shift_ids), 2)
# Check added shifts fields
for id in sheet_1.added_shift_ids.ids:
shift = sheet_1.added_shift_ids.browse(id)
self.assertEqual(sheet_1, shift.attendance_sheet_id)
self.assertEqual(shift.state, "done")
self.assertEqual(
shift.task_type_id,
self.attendance_sheet_shift_model.pre_filled_task_type_id(),
)
if shift.working_mode == "regular":
self.assertTrue(shift.is_compensation)
else:
self.assertFalse(shift.is_compensation)
# Add a worker that should be replaced
with self.assertRaises(UserError) as e:
sheet_1.on_barcode_scanned(421457731742)
# Wrong barcode
with self.assertRaises(UserError) as e:
sheet_1.on_barcode_scanned(101010)
# Add an unsubscribed worker
self.worker_regular_1.cooperative_status_ids.sr = -2
self.worker_regular_1.cooperative_status_ids.sc = -2
with self.assertRaises(UserError) as e:
sheet_1.on_barcode_scanned(421457731745)
def test_attendance_sheet_edition(self):
# Attendance sheet generation
self.attendance_sheet_model.sudo(
self.user_generic
)._generate_attendance_sheet()
sheet_1 = self.search_sheets(self.start_in_1, self.end_in_1)
# Expected shifts edition
sheet_1.expected_shift_ids[1].state = "done"
sheet_1.expected_shift_ids[2].state = "absent_1"
# Added shits addition
sheet_1.added_shift_ids |= sheet_1.added_shift_ids.new(
{
"task_type_id": self.task_type_2.id,
"state": "done",
"attendance_sheet_id": sheet_1.id,
"worker_id": self.worker_irregular_2.id,
"is_compensation": False,
"is_regular": False,
}
)
# Same task type as empty shift (should edit it on validation)
sheet_1.added_shift_ids |= sheet_1.added_shift_ids.new(
{
"task_type_id": self.task_type_1.id,
"state": "done",
"attendance_sheet_id": sheet_1.id,
"worker_id": self.worker_regular_super_1.id,
"is_compensation": True,
"is_regular": False,
}
)
# TODO: test validation with wizard (as generic user)
# class odoo.tests.common.Form(recordp, view=None)
# is only available from version 12
# sheet_1 = sheet_1.sudo(self.user_generic)
# Validation without wizard (as admin user)
sheet_1 = sheet_1.sudo(self.user_admin)
# Wait necessary time for shifts to begin
waiting_time = (self.start_in_1 - datetime.now()).total_seconds()
if waiting_time > 0:
with self.assertRaises(UserError) as e:
sheet_1.validate_with_checks()
self.assertIn("wait", str(e.exception))
time.sleep(waiting_time)
sheet_1.worker_nb_feedback = "enough"
sheet_1.feedback = "Great session."
sheet_1.notes = "Important information."
sheet_1.validate_with_checks()
with self.assertRaises(UserError) as e:
sheet_1.validate_with_checks()
self.assertIn("already been validated", str(e.exception))
self.assertEqual(sheet_1.state, "validated")
self.assertEqual(sheet_1.validated_by, self.user_admin.partner_id)
self.assertTrue(sheet_1.is_annotated)
self.assertFalse(sheet_1.is_read)
# Check actual shifts update
workers = sheet_1.expected_shift_ids.mapped(
"worker_id"
) | sheet_1.added_shift_ids.mapped("worker_id")
self.assertEqual(len(workers), 5)
self.assertEqual(
sheet_1.expected_shift_ids[0].task_id.state, "absent_2"
)
self.assertEqual(sheet_1.expected_shift_ids[1].task_id.state, "done")
self.assertEqual(
sheet_1.expected_shift_ids[2].task_id.state, "absent_1"
)
self.assertEqual(sheet_1.added_shift_ids[0].task_id.state, "done")
self.assertEqual(sheet_1.added_shift_ids[1].task_id.state, "done")
# Empty shift should have been updated
self.assertEqual(
sheet_1.added_shift_ids[0].task_id, self.shift_empty_1
)
# sheet_1.expected_shift_ids[0].worker_id

263
beesdoo_shift_attendance/views/attendance_sheet.xml

@ -0,0 +1,263 @@
<odoo>
<!-- Attendance Sheet Shifts Views -->
<record model="ir.ui.view" id="sheet_view_search">
<field name="name">Attendance Sheet Search</field>
<field name="model">beesdoo.shift.sheet</field>
<field name="arch" type="xml">
<search>
<field name="day" />
<field name="validated_by" />
<filter string="Annotated (unread)"
name="annotated"
domain="[('is_annotated', '=', True),
('is_read', '=', False)]" />
<filter string="Annotated (read)"
name="annotated_read"
domain="[('is_annotated', '=', True),
('is_read', '=', True)]" />
<separator />
<filter string="Archived"
name="archived"
domain="[('active', '=', False)]" />
<group expand="1" string="Group By">
<filter string="Day" name="gb_day"
context="{'group_by' : 'start_time:day'}" />
<filter string="Feedback on number of workers"
name="gb_worker_nb_feedback"
context="{'group_by' : 'worker_nb_feedback'}" />
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="sheet_expected_view_tree">
<field name="name">Expected Shifts List</field>
<field name="model">beesdoo.shift.sheet.expected</field>
<field name="arch" type="xml">
<tree create="false"
decoration-danger="state in ['absent_0', 'absent_1', 'absent_2'] "
decoration-success="state == 'done'">
<field name="task_type_id" readonly="True" options="{'no_open': True}"/>
<field name="super_coop_id" readonly="True" options="{'no_open': True}"/>
<field name="worker_id" readonly="True" options="{'no_open': True}"/>
<field name="working_mode" />
<field name="replaced_id" readonly="True"/>
<field name="state" readonly="True" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="sheet_added_view_tree">
<field name="view_mode">tree</field>
<field name="name">Added Shifts List</field>
<field name="model">beesdoo.shift.sheet.added</field>
<field name="arch" type="xml">
<tree decoration-danger="state in ['absent_0', 'absent_1', 'absent_2'] "
decoration-success="state == 'done'">
<field name="task_type_id"
options="{'no_open': True, 'no_create': True, 'no_create_edit':True }" />
<field name="worker_id"
domain="[
('eater', '=', 'worker_eater'),
('working_mode', 'in', ('regular', 'irregular')),
('state', 'not in', ('unsubscribed', 'resigning')),
]"
options="{'no_open': True, 'no_create': True, 'no_create_edit':True }" />
<field name="working_mode" />
<field name="is_compensation"
attrs="{'invisible':
[('working_mode','=','irregular')]}"/>
<field name="state"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="sheet_expected_view_form">
<field name="view_mode">tree</field>
<field name="name">Expected Shifts Form</field>
<field name="model">beesdoo.shift.sheet.expected</field>
<field name="arch" type="xml">
<form readonly="True">
<group string="Expected Shift">
<field name="task_type_id" readonly="True" options="{'no_open': True}"/>
<field name="worker_id" readonly="True" options="{'no_open': True}"/>
<field name="working_mode" />
<field name="replaced_id"
attrs="{'invisible':
[('working_mode','=','irregular')]}"
options="{'no_create': True, 'no_create_edit':True}"/>
<field name="state" />
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="sheet_added_view_form">
<field name="view_mode">tree</field>
<field name="name">Added Shifts Form</field>
<field name="model">beesdoo.shift.sheet.added</field>
<field name="arch" type="xml">
<form readonly="True" >
<group>
<field name="task_type_id"
options="{'no_open': True, 'no_create': True, 'no_create_edit':True }" />
<field name="worker_id"
domain="[
('eater', '=', 'worker_eater'),
('working_mode', 'in', ('regular', 'irregular')),
('state', 'not in', ('unsubscribed', 'resigning')),
]"
options="{'no_open': True, 'no_create': True, 'no_create_edit':True }" />
<field name="working_mode" />
<field name="is_compensation"
attrs="{'invisible':
[('working_mode','=','irregular')]}"/>
<field name="state"/>
</group>
</form>
</field>
</record>
<!-- Attendance Sheets Views -->
<record model="ir.ui.view" id="sheet_view_tree">
<field name="name">Attendance Sheet List</field>
<field name="model">beesdoo.shift.sheet</field>
<field name="arch" type="xml">
<tree create="false" delete="false" decoration-danger="state == 'not_validated'">
<field name="week" />
<field name="day" />
<field name="time_slot" />
<field name="max_worker_no" type="char"/>
<field name="state" />
<field name="validated_by" />
<field name="attended_worker_no" type="char"/>
<field name="is_annotated" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="sheet_view_form">
<field name="name">Attendance Sheet Form</field>
<field name="model">beesdoo.shift.sheet</field>
<field name="arch" type="xml">
<form create="false" delete="false">
<field name="_barcode_scanned" widget="barcode_handler"/>
<header>
<field name="state" widget="statusbar" readonly="True"/>
<button type="object"
name="validate_with_checks"
string="Validate Sheet"
confirm="Beware : a validated sheet cannot be edited anymore
and you won't be able to add any latecomers.
The counters of those who didn't attend will be updated
and they will get warning emails."
attrs="{'invisible': [('state', '=', 'validated')]}"
/>
</header>
<sheet >
<div class="oe_button_box" name="button_box">
<button name="button_mark_as_read" type="object"
class="oe_stat_button" icon="fa-check"
string="Mark as read"
groups="beesdoo_shift.group_shift_attendance"
/>
<button name="toggle_active"
type="object"
groups="beesdoo_shift.group_shift_attendance"
class="oe_stat_button"
icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<separator string="Expected Shifts" style="position:relative;padding:0px;margin:0px;" />
<field name="expected_shift_ids" />
<separator string="Added Shifts" style="position:relative;padding:0px;margin-top:15px;margin-bottom:0px;" />
<field name="added_shift_ids" />
<group>
<field name="max_worker_no" />
</group>
<group col="1" string="Feedback"
groups="beesdoo_shift.group_shift_attendance">
<field name="attended_worker_no" />
<field name="notes" />
<field name="feedback" />
<field name="worker_nb_feedback" />
<field name="validated_by" readonly="True"/>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_ids"
widget="mail_thread"
groups="beesdoo_shift.group_shift_attendance"
/>
</div>
</form>
</field>
</record>
<!-- Actions -->
<act_window id="action_sheet_admin_non_validated"
name="Non-validated sheets"
res_model="beesdoo.shift.sheet"
view_mode="tree,form"
domain="[('state','=','not_validated')]"
/>
<!-- Annotated sheets should display only the both annotated and validated ones-->
<act_window id="action_sheet_admin_annotated"
name="Unread notes"
res_model="beesdoo.shift.sheet"
view_mode="tree,form"
domain="[('is_annotated','=',True),
('is_read','=',False)]"
/>
<act_window id="action_sheet_admin_list"
name="All sheets"
res_model="beesdoo.shift.sheet"
view_mode="tree,form"
/>
<act_window id="action_sheet_daily"
name="Daily attendance sheets"
res_model="beesdoo.shift.sheet"
view_mode="tree,form"
domain="[('end_time','&gt;', datetime.datetime.now().replace(hour=00, minute=00, second=10)),
('start_time','&lt;', datetime.datetime.now().replace(hour=23, minute=59, second=59))]"
/>
<!-- Top menu item -->
<menuitem id="menu_sheet_top"
name="Attendance Sheets"
parent="beesdoo_shift.menu_root"
groups="beesdoo_shift_attendance.group_shift_attendance_sheet"
sequence="1"
/>
<!-- Menu actions -->
<menuitem id="menu_sheet"
name="Daily attendance sheets"
parent="menu_sheet_top"
action="action_sheet_daily"
sequence="2"
/>
<menuitem id="menu_sheet_admin_non_validated"
name="Non-validated sheets"
parent="menu_sheet_top"
action="action_sheet_admin_non_validated"
groups="beesdoo_shift.group_shift_attendance"
/>
<menuitem id="menu_sheet_admin_annotated"
name="Unread notes"
parent="menu_sheet_top"
action="action_sheet_admin_annotated"
groups="beesdoo_shift.group_shift_attendance"
/>
<menuitem id="menu_sheet_admin_list"
name="All sheets"
parent="menu_sheet_top"
action="action_sheet_admin_list"
groups="beesdoo_shift.group_shift_attendance"
/>
</odoo>

141
beesdoo_shift_attendance/views/res_config_settings_view.xml

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019-2020 Elouan Le Bars <elouan@coopiteasy.be>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<data>
<record model="ir.ui.view" id="daynumber_view_tree">
<field name="name">Day Number List</field>
<field name="model">beesdoo.shift.daynumber</field>
<field name="arch" type="xml">
<tree editable="top">
<field name="name" />
<field name="number" />
<field name="active" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="type_view_tree">
<field name="name">Shift Type List</field>
<field name="model">beesdoo.shift.type</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="description" />
<field name="active" />
</tree>
</field>
</record>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.beesdoo.shift</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="50"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Shifts Management" string="Shifts Management" data-key="shifts_management" groups="beesdoo_shift.group_cooperative_admin">
<field name="has_accounting_entries" invisible="1"/>
<h2>Attendance Sheets</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="card_support"/>
</div>
<div class="o_setting_right_pane">
<label for="card_support" string="Scan cards for validation" />
<div class="text-muted">
If not checked, user credentials are asked.
</div>
</div>
</div>
</div>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Attendance Sheets Generation Interval</span>
<div class="text-muted">
Generate attendance sheets before shifts start.
</div>
<div class="content-group">
<div class="mt16 row">
<label for="attendance_sheet_generation_interval" string="Interval (minutes)" class="col-3 col-lg-3 o_light_label"/>
<field name="attendance_sheet_generation_interval" class="oe_inline" required="1"/>
</div>
</div>
</div>
</div>
</div>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Default Task Type</span>
<div class="text-muted">
For attendance sheets automatic pre-filling.
</div>
<div class="content-group">
<div class="mt16 row">
<label for="pre_filled_task_type_id" string="Default Task Type" class="col-3 col-lg-3 o_light_label"/>
<field name="pre_filled_task_type_id" class="oe_inline" required="1"/>
</div>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
<menuitem name="Configuration"
id="menu_configuration_top"
parent="beesdoo_shift.menu_root"
groups="beesdoo_shift.group_planning_management"
sequence="21"
/>
<record model="ir.actions.act_window" id="action_missing_attendance_sheets">
<field name="name">Generate missing past attendance sheets</field>
<field name="res_model">beesdoo.shift.generate_missing_attendance_sheets</field>
<field name="view_mode">form</field>
</record>
<record model="ir.actions.act_window" id="action_day_number">
<field name="name">Day Number</field>
<field name="res_model">beesdoo.shift.daynumber</field>
<field name="view_mode">tree</field>
</record>
<menuitem name="Shift Day" id="menu_configuration_day"
parent="menu_configuration_top" action="action_day_number" />
<record model="ir.actions.act_window" id="action_type">
<field name="name">Shift Type</field>
<field name="res_model">beesdoo.shift.type</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Shift Type" id="menu_configuration_type"
parent="menu_configuration_top" action="action_type" />
<record model="ir.actions.act_window" id="action_shift_settings">
<field name="name">Settings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'beesdoo_shift'}</field>
</record>
<menuitem name="Shift Settings"
id="menu_shift_settings"
parent="menu_configuration_top"
action="action_shift_settings"
groups="beesdoo_shift.group_cooperative_admin"
/>
</data>
</odoo>

2
beesdoo_shift_attendance/wizard/__init__.py

@ -0,0 +1,2 @@
from . import validate_attendance_sheet
from . import generate_missing_attendance_sheets

57
beesdoo_shift_attendance/wizard/generate_missing_attendance_sheets.py

@ -0,0 +1,57 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from datetime import datetime
class GenerateMissingAttendanceSheets(models.TransientModel):
"""
Generate missing past sheets
"""
_name = "beesdoo.shift.generate_missing_attendance_sheets"
date_start = fields.Datetime("Start date", required=True)
date_end = fields.Datetime("End date", required=True)
@api.multi
def generate_missing_attendance_sheets(self):
self.ensure_one()
tasks = self.env["beesdoo.shift.shift"]
sheets = self.env["beesdoo.shift.sheet"]
tasks = tasks.search(
[
("start_time", ">", self.date_start),
("start_time", "<", self.date_end),
]
)
# We should not loop on task with same start_time and end_time
# To improve performances
for task in tasks:
start_time = task.start_time
end_time = task.end_time
sheet = sheets.search(
[("start_time", "=", start_time), ("end_time", "=", end_time),]
)
if not sheet:
sheets |= sheets.create(
{"start_time": start_time, "end_time": end_time}
)
return {
"name": _("Generated Missing Sheets"),
"type": "ir.actions.act_window",
"view_type": "form",
"view_mode": "tree,form",
"res_model": "beesdoo.shift.sheet",
"target": "current",
"domain": [("id", "in", sheets.ids)],
}
@api.constrains("date_start", "date_end")
def constrains_dates(self):
if self.date_start > datetime.now() or self.date_end > datetime.now():
raise UserError(_("Only past attendance sheets can be generated"))

35
beesdoo_shift_attendance/wizard/generate_missing_attendance_sheets.xml

@ -0,0 +1,35 @@
<odoo>
<record model="ir.ui.view" id="validate_attendance_sheet_form">
<field name="name">Generate Missing Attendance Sheets</field>
<field name="model">beesdoo.shift.generate_missing_attendance_sheets</field>
<field name="arch" type="xml">
<form>
<sheet>
<label for="date_start" string="Generate missing attendance sheets in a given time interval" />
<group>
<field name="date_start" />
<field name="date_end" />
</group>
</sheet>
<footer>
<button type="object"
name="generate_missing_attendance_sheets"
string="Generate"
class="oe_highlight"
/>
</footer>
</form>
</field>
</record>
<act_window id="action_missing_attendance_sheets" name="Generate Missing Sheets"
res_model="beesdoo.shift.generate_missing_attendance_sheets"
view_mode="form" target="new" />
<menuitem id="menu_missing_attendance_sheets"
name="Generate missing past attendance sheets"
parent="menu_configuration_top"
action="action_missing_attendance_sheets"
sequence="2"
/>
</odoo>

138
beesdoo_shift_attendance/wizard/validate_attendance_sheet.py

@ -0,0 +1,138 @@
import ast
from odoo import _, api, exceptions, fields, models
from odoo.exceptions import UserError, ValidationError
class ValidateAttendanceSheet(models.TransientModel):
_name = "beesdoo.shift.sheet.validate"
_description = """Check the user name and validate sheet.
Useless for users in group_shift_attendance"""
_inherit = ["barcodes.barcode_events_mixin"]
@api.multi
def _get_active_sheet(self):
sheet_id = self._context.get("active_id")
sheet_model = self._context.get("active_model")
if sheet_id and sheet_model:
return self.env[sheet_model].browse(sheet_id)
def _get_card_support_setting(self):
return ast.literal_eval(
self.env["ir.config_parameter"].sudo().get_param(
"beesdoo_shift_attendance.card_support"
)
)
@api.multi
def _get_warning_regular_workers(self):
"""
A warning is shown if some regular workers were not expected
but should be doing their regular shifts. This warning is added
to sheet's notes at validation.
"""
sheet = self._get_active_sheet()
warning_message = ""
if sheet:
for added_shift in sheet.added_shift_ids:
is_regular_worker = (
added_shift.worker_id.working_mode == "regular"
)
is_compensation = added_shift.is_compensation
if is_regular_worker and not is_compensation:
warning_message += (
_(
"\n%s attended its shift as a normal one but was not expected. "
"Something may be wrong in his/her personnal informations.\n"
)
% added_shift.worker_id.name
)
return warning_message
active_sheet = fields.Many2one(
"beesdoo.shift.sheet", default=_get_active_sheet
)
card_support = fields.Boolean(
default=_get_card_support_setting, string="Card validation"
)
login = fields.Char(string="Login")
password = fields.Char(string="Password")
barcode = fields.Char(string="Barcode")
warning_regular_workers = fields.Text(
"Warning",
default=_get_warning_regular_workers,
help="Is any regular worker doing its regular shift as an added one ?",
)
notes = fields.Text(
related="active_sheet.notes",
string="Notes about the attendance for the Members Office",
default="",
readonly=False,
)
feedback = fields.Text(
related="active_sheet.feedback",
string="Comments about the shift",
default="",
readonly=False,
)
worker_nb_feedback = fields.Selection(
related="active_sheet.worker_nb_feedback", readonly=False
)
def on_barcode_scanned(self, barcode):
self.barcode = barcode
@api.multi
def save(self):
sheet = self.active_sheet
sheet.notes = self.notes
sheet.feedback = self.feedback
sheet.worker_nb_feedback = self.worker_nb_feedback
@api.multi
def validate_sheet(self):
sheet = self.active_sheet
if not self.worker_nb_feedback:
raise UserError(
_("Please give your feedback on the number of workers.")
)
if self.card_support:
# Login with barcode
card = self.env["member.card"].search(
[("barcode", "=", self.barcode)]
)
if not len(card):
raise UserError(_("Please set a correct barcode."))
partner = card[0].partner_id
else:
# Login with credentials
if not self.login:
raise UserError(_("Please enter your login."))
user = self.env["res.users"].search([("login", "=", self.login)])
user.sudo(user.id)._check_credentials(self.password)
partner = user.partner_id
can_validate = partner.user_ids.has_group(
"beesdoo_shift_attendance.group_shift_attendance_sheet_validation"
)
if not partner.super and not can_validate:
raise UserError(
_(
"Only super-cooperators and administrators can validate attendance sheets."
)
)
if self.notes and self.warning_regular_workers:
self.notes += self.warning_regular_workers
elif self.warning_regular_workers:
self.notes = self.warning_regular_workers
self.save()
sheet._validate(partner or self.env.user.partner_id)

48
beesdoo_shift_attendance/wizard/validate_attendance_sheet.xml

@ -0,0 +1,48 @@
<odoo>
<record model="ir.ui.view" id="validate_attendance_sheet_form">
<field name="name">Validate Attendance Sheet</field>
<field name="model">beesdoo.shift.sheet.validate</field>
<field name="arch" type="xml">
<form>
<sheet>
<separator string="Validation"/>
<field name="card_support" invisible="1" />
<field name="active_sheet" invisible="1" />
<field name="_barcode_scanned" widget="barcode_handler"/>
<label for="notes" />
<field name="notes" />
<label for="feedback" />
<field name="feedback" />
<group>
<field name="worker_nb_feedback"/>
</group>
<group string="Login"
attrs="{'invisible': [('card_support', '=', True)]}"
>
<field name="login" />
<field name="password" password="True"/>
</group>
<group string="Scan your card"
attrs="{'invisible': [('card_support', '=', False)]}"
>
<field name="barcode" />
</group>
<field name="warning_regular_workers"
readonly="1"
attrs="{'invisible': [('warning_regular_workers', '=', False)]}"
/>
</sheet>
<footer>
<button type="object"
name="validate_sheet"
string="Validate"
class="oe_highlight"
/>
<button type="object" name="save"
string="Save" />
<button special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
</odoo>

26
beesdoo_website_shift/__manifest__.py

@ -0,0 +1,26 @@
# Copyright 2017-2020 Coop IT Easy (http://coopiteasy.be)
# Rémy Taymans <remy@coopiteasy.be>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'BEES coop Website Shift',
'summary': """
Show available shifts for regular and irregular workers on the
website and let workers manage their shifts with an
easy web interface.
""",
'description': """
""",
"author": "Coop IT Easy SCRLfs",
"license": "AGPL-3",
"version": "12.0.1.0.0",
"website": "https://github.com/beescoop/Obeesdoo",
"category": "Cooperative management",
"depends": ["portal", "website", "beesdoo_shift"],
"data": [
"data/res_config_data.xml",
"views/shift_website_templates.xml",
"views/my_shift_website_templates.xml",
"views/res_config_views.xml",
],
}

32
beesdoo_website_shift/__openerp__.py

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2017-2018 Rémy Taymans <remytaymans@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'BEES coop Website Shift',
'summary': """
Show available shifts for regular and irregular workers on the
website and let workers manage their shifts with an
easy web interface.
""",
'description': """
""",
'author': 'Rémy Taymans',
'license': 'AGPL-3',
'version': '9.0.2.3.1',
'website': "https://github.com/beescoop/Obeesdoo",
'category': 'Cooperative management',
'depends': ['website', 'beesdoo_shift'],
'data': [
'data/res_config_data.xml',
'views/shift_website_templates.xml',
'views/my_shift_website_templates.xml',
'views/res_config_views.xml',
]
}

90
beesdoo_website_shift/controllers/main.py

@ -1,6 +1,5 @@
# -*- coding: utf8 -*-
# Copyright 2017-2018 Rémy Taymans <remytaymans@gmail.com>
# Copyright 2017-2020 Coop IT Easy (http://coopiteasy.be)
# Rémy Taymans <remy@coopiteasy.be>
# Copyright 2017-2018 Thibault François
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
@ -9,19 +8,17 @@ from datetime import datetime, timedelta
from itertools import groupby
from pytz import timezone, utc
from openerp import http, fields
from openerp.http import request
from odoo import http, fields
from odoo.http import request
from openerp.addons.beesdoo_shift.models.planning import float_to_time
from openerp.addons.beesdoo_shift.models.cooperative_status import PERIOD
from odoo.addons.beesdoo_shift.models.planning import float_to_time
class WebsiteShiftController(http.Controller):
def is_user_worker(self):
user = request.env['res.users'].browse(request.uid)
share_type = user.partner_id.cooperator_type
return share_type == 'share_a'
return user.partner_id.is_worker
def is_user_irregular(self):
user = request.env['res.users'].browse(request.uid)
@ -135,22 +132,26 @@ class WebsiteShiftController(http.Controller):
* the given shift exist
* the shift status is open
* the shift is free for subscription
* the shift is starting after the time interval
for attendance sheet generation defined in beesdoo_shift settings
"""
# Get current user
cur_user = request.env['res.users'].browse(request.uid)
# Get the shift
shift = request.env['beesdoo.shift.shift'].sudo().browse(shift_id)
# Get config
irregular_enable_sign_up = literal_eval(request.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.irregular_enable_sign_up'))
# Get open status
open_status = request.env.ref('beesdoo_shift.open')
irregular_enable_sign_up = request.website.irregular_enable_sign_up
# Set start time limit as defined in beesdoo_shift settings
# TODO: Move this into the attendance_sheet module
# setting = request.website.attendance_sheet_generation_interval
start_time_limit = datetime.now() # + timedelta(minutes=setting)
request.session['success'] = False
if (irregular_enable_sign_up
and self.user_can_subscribe()
and shift
and shift.stage_id == open_status
and shift.state == "open"
and shift.start_time > start_time_limit
and not shift.worker_id):
shift.worker_id = cur_user.partner_id
request.session['success'] = True
@ -185,11 +186,7 @@ class WebsiteShiftController(http.Controller):
task_templates = template.sudo().search([], order="planning_id, day_nb_id, start_time")
# Get config
regular_highlight_rule = literal_eval(
request.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.regular_highlight_rule'
)
)
regular_highlight_rule = request.website.regular_highlight_rule
task_tpls_data = []
for task_tpl in task_templates:
@ -213,8 +210,7 @@ class WebsiteShiftController(http.Controller):
Return template variables for 'beesdoo_website_shift.my_shift_irregular_worker' template
"""
# Get config
irregular_enable_sign_up = literal_eval(request.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.irregular_enable_sign_up'))
irregular_enable_sign_up = request.website.irregular_enable_sign_up
# Create template context
template_context = {}
@ -233,6 +229,14 @@ class WebsiteShiftController(http.Controller):
template_context['success'] = request.session.get('success')
del request.session['success']
# Add setting for subscription allowed time
# TODO: move this to the attendance_sheet module
# subscription_time_limit = (
# request.website.attendance_sheet_generation_interval
# )
subscription_time_limit = 0
template_context['subscription_time_limit'] = subscription_time_limit
return template_context
def my_shift_regular_worker_without_shift(self):
@ -280,11 +284,10 @@ class WebsiteShiftController(http.Controller):
# Get all the shifts in the future with no worker
now = datetime.now()
open_status = request.env.ref('beesdoo_shift.open')
shifts = request.env['beesdoo.shift.shift'].sudo().search(
[('start_time', '>', now.strftime("%Y-%m-%d %H:%M:%S")),
('worker_id', '=', False),
('stage_id', '=', open_status.id)],
('state', '=', 'open')],
order="start_time, task_template_id, task_type_id",
)
@ -296,18 +299,9 @@ class WebsiteShiftController(http.Controller):
)
# Get config
irregular_shift_limit = int(
request.env['ir.config_parameter']
.get_param('beesdoo_website_shift.irregular_shift_limit')
)
highlight_rule_pc = int(
request.env['ir.config_parameter']
.get_param('beesdoo_website_shift.highlight_rule_pc')
)
hide_rule = int(
request.env['ir.config_parameter']
.get_param('beesdoo_website_shift.hide_rule')
) / 100.0
irregular_shift_limit = request.website.irregular_shift_limit
highlight_rule_pc = request.website.highlight_rule_pc
hide_rule = request.website.hide_rule / 100.0
# Grouby task_template_id, if no task_template_id is specified
# then group by start_time, if no start_time specified sort by
@ -391,11 +385,9 @@ class WebsiteShiftController(http.Controller):
)
# Get config
regular_next_shift_limit = int(request.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.regular_next_shift_limit'))
# Get default status for fictive shifts
draft_status = request.env.ref('beesdoo_shift.draft')
regular_next_shift_limit = request.website.regular_next_shift_limit
shift_period = int(request.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.shift_period'))
for i in range(nb_subscribed_shifts, regular_next_shift_limit):
# Create the fictive shift
@ -405,7 +397,7 @@ class WebsiteShiftController(http.Controller):
shift.planning_id = main_shift.planning_id
shift.task_type_id = main_shift.task_type_id
shift.worker_id = main_shift.worker_id
shift.stage_id = draft_status
shift.state = "draft"
shift.super_coop_id = main_shift.super_coop_id
shift.color = main_shift.color
shift.is_regular = main_shift.is_regular
@ -413,12 +405,12 @@ class WebsiteShiftController(http.Controller):
shift.revert_info = main_shift.revert_info
# Set new date
shift.start_time = self.add_days(
fields.Datetime.from_string(main_shift.start_time),
days=i * PERIOD
main_shift.start_time,
days=i * shift_period
)
shift.end_time = self.add_days(
fields.Datetime.from_string(main_shift.end_time),
days=i * PERIOD
main_shift.end_time,
days=i * shift_period
)
# Add the fictive shift to the list of shift
subscribed_shifts.append(shift)
@ -437,11 +429,9 @@ class WebsiteShiftController(http.Controller):
# Get config
past_shift_limit = 0
if self.is_user_irregular():
past_shift_limit = int(request.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.irregular_past_shift_limit'))
past_shift_limit = request.website.irregular_past_shift_limit
if self.is_user_regular():
past_shift_limit = int(request.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.regular_past_shift_limit'))
past_shift_limit = request.website.regular_past_shift_limit
# Get shifts where user was subscribed
now = datetime.now()
if past_shift_limit > 0:

38
beesdoo_website_shift/data/res_config_data.xml

@ -3,39 +3,11 @@
Copyright 2017-2018 Rémy Taymans <remytaymans@gmail.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<openerp>
<odoo>
<data noupdate="1">
<record id="beesdoo_website_shift.irregular_shift_limit" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.irregular_shift_limit</field>
<field name="value">0</field>
</record>
<record id="beesdoo_website_shift.highlight_rule_pc" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.highlight_rule_pc</field>
<field name="value">30</field>
</record>
<record id="beesdoo_website_shift.hide_rule" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.hide_rule</field>
<field name="value">20</field>
</record>
<record id="beesdoo_website_shift.irregular_enable_sign_up" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.irregular_enable_sign_up</field>
<field name="value">True</field>
</record>
<record id="beesdoo_website_shift.irregular_past_shift_limit" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.irregular_past_shift_limit</field>
<field name="value">10</field>
</record>
<record id="beesdoo_website_shift.regular_past_shift_limit" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.regular_past_shift_limit</field>
<field name="value">10</field>
</record>
<record id="beesdoo_website_shift.regular_next_shift_limit" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.regular_next_shift_limit</field>
<field name="value">13</field>
</record>
<record id="beesdoo_website_shift.regular_highlight_rule" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.regular_highlight_rule</field>
<field name="value">20</field>
<record id="beesdoo_website_shift.shift_period" model="ir.config_parameter">
<field name="key">beesdoo_website_shift.shift_period</field>
<field name="value">28</field>
</record>
</data>
</openerp>
</odoo>

2
beesdoo_website_shift/i18n/fr_BE.po

@ -167,7 +167,7 @@ msgstr "Appliquer"
#. module: beesdoo_website_shift
#: model:ir.ui.view,arch_db:beesdoo_website_shift.available_shift_irregular_worker
msgid "Are you shure you want to subscribe to this shift?"
msgid "Please confirm subscription"
msgstr "Etes-vous sûr de vouloir vous inscrire à ce shift ?"
#. module: beesdoo_website_shift

11
beesdoo_website_shift/migrations/9.0.2.1.1/post-migrate.py

@ -1,11 +0,0 @@
# coding: utf-8
def migrate(cr, version):
"""Create a sequence for beesdoo_website_shift_config_settings."""
cr.execute(
"""
CREATE SEQUENCE IF NOT EXISTS
beesdoo_website_shift_config_settings_id_seq
"""
)

1
beesdoo_website_shift/models/__init__.py

@ -1 +1,2 @@
from . import website
from . import res_config

127
beesdoo_website_shift/models/res_config.py

@ -1,136 +1,45 @@
# -*- coding: utf-8 -*-
# Copyright 2017-2018 Rémy Taymans <remytaymans@gmail.com>
# Copyright 2017-2020 Rémy Taymans <remytaymans@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from ast import literal_eval
from openerp import fields, models, api
PARAMS = [
('irregular_shift_limit', 'beesdoo_website_shift.irregular_shift_limit'),
('highlight_rule_pc', 'beesdoo_website_shift.highlight_rule_pc'),
('hide_rule', 'beesdoo_website_shift.hide_rule'),
('irregular_enable_sign_up',
'beesdoo_website_shift.irregular_enable_sign_up'),
('irregular_past_shift_limit',
'beesdoo_website_shift.irregular_past_shift_limit'),
('regular_past_shift_limit',
'beesdoo_website_shift.regular_past_shift_limit'),
('regular_next_shift_limit',
'beesdoo_website_shift.regular_next_shift_limit'),
('regular_highlight_rule',
'beesdoo_website_shift.regular_highlight_rule'),
]
from odoo import fields, models, api
class WebsiteShiftConfigSettings(models.TransientModel):
_name = 'beesdoo.website.shift.config.settings'
_inherit = 'res.config.settings'
# Irregular worker settings
irregular_shift_limit = fields.Integer(
help="Maximum shift that will be shown"
related='website_id.irregular_shift_limit',
readonly=False,
)
highlight_rule_pc = fields.Integer(
help="Treshold (in %) of available space in a shift that trigger the "
"highlight of the shift"
related='website_id.highlight_rule_pc',
readonly=False,
)
hide_rule = fields.Integer(
help="Treshold ((available space)/(max space)) in percentage of "
"available space under wich the shift is hidden"
related='website_id.highlight_rule_pc',
readonly=False,
)
irregular_enable_sign_up = fields.Boolean(
help="Enable shift sign up for irregular worker"
related='website_id.irregular_enable_sign_up',
readonly=False,
)
irregular_past_shift_limit = fields.Integer(
help="Maximum past shift that will be shown for irregular worker"
related='website_id.irregular_past_shift_limit',
readonly=False,
)
# Regular worker settings
regular_past_shift_limit = fields.Integer(
help="Maximum past shift that will be shown for regular worker"
related='website_id.regular_past_shift_limit',
readonly=False,
)
regular_next_shift_limit = fields.Integer(
help="Maximun number of next shift that will be shown"
related='website_id.regular_next_shift_limit',
readonly=False,
)
regular_highlight_rule = fields.Integer(
help="Treshold (in %) of available space in a shift that trigger the "
"the highlight of a shift template."
related='website_id.regular_highlight_rule',
readonly=False,
)
@api.multi
def set_params(self):
self.ensure_one()
for field_name, key_name in PARAMS:
value = getattr(self, field_name)
self.env['ir.config_parameter'].set_param(key_name, str(value))
@api.multi
def get_default_irregular_shift_limit(self):
return {
'irregular_shift_limit': int(
self.env['ir.config_parameter']
.get_param("beesdoo_website_shift.irregular_shift_limit")
)
}
@api.multi
def get_default_highlight_rule_pc(self):
return {
'highlight_rule_pc': int(
self.env['ir.config_parameter']
.get_param("beesdoo_website_shift.highlight_rule_pc")
)
}
@api.multi
def get_default_hide_rule(self):
return {
'hide_rule': int(self.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.hide_rule'))
}
@api.multi
def get_default_irregular_shift_sign_up(self):
return {
'irregular_enable_sign_up':
literal_eval(self.env['ir.config_parameter'].get_param(
'beesdoo_website_shift.irregular_enable_sign_up'))
}
@api.multi
def get_default_irregular_past_shift_limit(self):
return {
'irregular_past_shift_limit': int(
self.env['ir.config_parameter']
.get_param("beesdoo_website_shift.irregular_past_shift_limit")
)
}
@api.multi
def get_default_regular_past_shift_limit(self):
return {
'regular_past_shift_limit': int(
self.env['ir.config_parameter']
.get_param('beesdoo_website_shift.regular_past_shift_limit')
)
}
@api.multi
def get_default_regular_next_shift_limit(self):
return {
'regular_next_shift_limit': int(
self.env['ir.config_parameter']
.get_param('beesdoo_website_shift.regular_next_shift_limit')
)
}
@api.multi
def get_default_regular_highlight_rule(self):
return {
'regular_highlight_rule': int(
self.env['ir.config_parameter']
.get_param('beesdoo_website_shift.regular_highlight_rule')
)
}

47
beesdoo_website_shift/models/website.py

@ -0,0 +1,47 @@
# Copyright 2017-2020 Rémy Taymans <remytaymans@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class Website(models.Model):
_inherit = 'website'
# Irregular worker settings
irregular_shift_limit = fields.Integer(
default=0,
help="Maximum shift that will be shown"
)
highlight_rule_pc = fields.Integer(
default=30,
help="Treshold (in %) of available space in a shift that trigger the "
"highlight of the shift"
)
hide_rule = fields.Integer(
default=20,
help="Treshold ((available space)/(max space)) in percentage of "
"available space under wich the shift is hidden"
)
irregular_enable_sign_up = fields.Boolean(
default=True,
help="Enable shift sign up for irregular worker"
)
irregular_past_shift_limit = fields.Integer(
default=10,
help="Maximum past shift that will be shown for irregular worker"
)
# Regular worker settings
regular_past_shift_limit = fields.Integer(
default=10,
help="Maximum past shift that will be shown for regular worker"
)
regular_next_shift_limit = fields.Integer(
default=13,
help="Maximun number of next shift that will be shown"
)
regular_highlight_rule = fields.Integer(
default=20,
help="Treshold (in %) of available space in a shift that trigger the "
"the highlight of a shift template."
)

1417
beesdoo_website_shift/views/my_shift_website_templates.xml
File diff suppressed because it is too large
View File

184
beesdoo_website_shift/views/res_config_views.xml

@ -3,102 +3,92 @@
Copyright 2017-2018 Rémy Taymans <remytaymans@gmail.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<openerp>
<data>
<odoo>
<record id="view_website_shift_config_irregular" model="ir.ui.view">
<field name="name">Website Shift Settings Irregular Worker</field>
<field name="model">beesdoo.website.shift.config.settings</field>
<field name="arch" type="xml">
<form string="Configure Website Shift Irregular Worker" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<div>
<label for="irregular_shift_limit"/>
<field name="irregular_shift_limit"/>
</div>
<div>
<label for="highlight_rule_pc"/>
<field name="highlight_rule_pc"/>
</div>
<div>
<label for="hide_rule"/>
<field name="hide_rule"/>
</div>
<div>
<label for="irregular_enable_sign_up"/>
<field name="irregular_enable_sign_up"/>
</div>
<div>
<label for="irregular_past_shift_limit"/>
<field name="irregular_past_shift_limit"/>
</div>
</form>
</field>
<field name="name">Website Shift Settings Irregular Worker</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="website.res_config_settings_view_form" />
<field name="arch" type="xml">
<div id="webmaster_settings" position="after">
<h2>Shift: Irregular Worker</h2>
<div class="row mt16 o_settings_container" id="shift_irregular_settings">
<div id="irregular_shift_limit_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="irregular_shift_limit"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="mt8">
<field name="irregular_shift_limit"/>
</div>
</div>
</div>
<div id="highlight_rule_pc_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="highlight_rule_pc"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="mt8">
<field name="highlight_rule_pc"/>
</div>
</div>
</div>
<div id="hide_rule_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="hide_rule"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="mt8">
<field name="hide_rule"/>
</div>
</div>
</div>
<div id="irregular_enable_sign_up_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="irregular_enable_sign_up"/>
</div>
<div class="o_setting_right_pane">
<label for="irregular_enable_sign_up"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
</div>
</div>
<div id="irregular_past_shift_limit_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="irregular_past_shift_limit"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="mt8">
<field name="irregular_past_shift_limit"/>
</div>
</div>
</div>
</div>
<h2>Shift: Regular Worker</h2>
<div class="row mt16 o_settings_container" id="shift_regular_settings">
<div id="regular_past_shift_limit_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="regular_past_shift_limit"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="mt8">
<field name="regular_past_shift_limit"/>
</div>
</div>
</div>
<div id="regular_next_shift_limit_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="regular_next_shift_limit"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="mt8">
<field name="regular_next_shift_limit"/>
</div>
</div>
</div>
<div id="regular_highlight_rule_settings" class="col-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="regular_highlight_rule"/>
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="mt8">
<field name="regular_highlight_rule"/>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
<record id="view_website_shift_config_regular" model="ir.ui.view">
<field name="name">Website Shift Settings Regular Worker</field>
<field name="model">beesdoo.website.shift.config.settings</field>
<field name="arch" type="xml">
<form string="Configure Website Shift Regular Worker" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<div>
<label for="regular_past_shift_limit"/>
<field name="regular_past_shift_limit"/>
</div>
<div>
<label for="regular_next_shift_limit"/>
<field name="regular_next_shift_limit"/>
</div>
<div>
<label for="regular_highlight_rule"/>
<field name="regular_highlight_rule"/>
</div>
</form>
</field>
</record>
<record id="action_website_shift_config_irregular" model="ir.actions.act_window">
<field name="name">Website Shift Settings Irregular Worker</field>
<field name="res_model">beesdoo.website.shift.config.settings</field>
<field name="view_id" ref="view_website_shift_config_irregular"/>
<field name="view_mode">form</field>
<field name="target">inline</field>
</record>
<record id="action_website_shift_config_regular" model="ir.actions.act_window">
<field name="name">Website Shift Settings Regular Worker</field>
<field name="res_model">beesdoo.website.shift.config.settings</field>
<field name="view_id" ref="view_website_shift_config_regular"/>
<field name="view_mode">form</field>
<field name="target">inline</field>
</record>
<menuitem
id="menu_website_shift_root"
name="Shift"
parent="website.menu_website_configuration"
sequence="20"/>
<menuitem
id="menu_website_shift_irregular"
name="Irregular Shift"
action="action_website_shift_config_irregular"
parent="menu_website_shift_root"
sequence="1"/>
<menuitem
id="menu_website_shift_regular"
name="Regular Shift"
action="action_website_shift_config_regular"
parent="menu_website_shift_root"
sequence="10"/>
</data>
</openerp>
</odoo>

335
beesdoo_website_shift/views/shift_website_templates.xml

@ -1,209 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017-2018 Rémy Taymans <remytaymans@gmail.com>
Copyright 2017-2020 Rémy Taymans <remytaymans@gmail.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<openerp>
<!-- Add menu entries -->
<data noupdate="1">
<record id="menu_work_irregular" model="website.menu">
<field name="name">Shifts Irregular</field>
<field name="url">/shift_irregular_worker</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">50</field>
</record>
<record id="menu_work_regular" model="website.menu">
<field name="name">Shifts Regular</field>
<field name="url">/shift_template_regular_worker</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">51</field>
</record>
</data>
<!-- Public Available Tasks Templates for Regular Workers -->
<template
id="public_shift_template_regular_worker"
name="Available Tasks Templates for Regular Workers"
page="True">
<t t-call="website.layout">
<div class="oe_structure"/>
<section class="wrap">
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">
Available Tasks Templates for Regular Workers
</h1>
</div>
</div>
</div>
</section>
<div class="oe_structure"/>
<section class="wrap">
<div class="container">
<div class="row">
<div class="col-md-12">
<p class="text-center">
Subscribe via the member office or via
<a href="mailto:membre@bees-coop.be">membre@bees-coop.be</a>
</p>
</div>
</div>
</div>
</section>
<section class="wrap">
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="oe_structure"/>
<div class="visible-xs" t-foreach="task_tpls_data" t-as="template_data">
<t t-set="template" t-value="template_data[0]"/>
<t t-set="has_enough_workers" t-value="template_data[1]"/>
<t t-set="highlight_class" t-value="'panel-warning' if not has_enough_workers else 'panel-default'"/>
<div t-att-class="'panel %s' % highlight_class">
<div class="panel-heading clearfix">
<div class="panel-title pull-left">
<t t-esc="template.planning_id.name"/> :
<t t-esc="template.day_nb_id.name"/>
<t t-esc='float_to_time(template.start_time)' /> -
<t t-esc='float_to_time(template.end_time)'/>
</div>
<div class="label label-default pull-right"
t-if="template.remaining_worker > 0">
<t t-esc="template.remaining_worker"/> space(s)
<odoo>
<!-- Add menu entries -->
<data noupdate="1">
<record id="menu_work_irregular" model="website.menu">
<field name="name">Shifts Irregular</field>
<field name="url">/shift_irregular_worker</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">50</field>
</record>
<record id="menu_work_regular" model="website.menu">
<field name="name">Shifts Regular</field>
<field name="url">/shift_template_regular_worker</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">51</field>
</record>
</data>
<!-- Help texts -->
<template
id="help_text_public_shift_template_regular_worker"
name="Help text for public available shifts template for irregular worker"
>
<p class="text-center">
Help text or information text.
</p>
</template>
<template
id="help_text_public_shift_irregular_worker"
name="Help text for public Available Shifts for Irregular Worker"
>
<p class="text-center">
Help text or information text.
</p>
</template>
<!-- Public Available Tasks Templates for Regular Workers -->
<template
id="public_shift_template_regular_worker"
name="Available Tasks Templates for Regular Workers"
>
<t t-call="portal.portal_layout">
<div class="o_regular_shift_template">
<div class="container mt32">
<div class="row mt4">
<div class="col">
<h1 class="text-center">
Available Tasks Templates for Regular Workers
</h1>
</div>
</div>
<div class="label label-default pull-right"
t-if="template.remaining_worker == 0">
full
<div class="row mt4">
<div class="col">
<t t-call="beesdoo_website_shift.help_text_public_shift_template_regular_worker"/>
</div>
</div>
</div>
<div class="panel-body clearfix">
<t t-esc="template.task_type_id.name"/>
<div class="label label-warning pull-right"
t-if="not template.super_coop_id">
Need Super Co-operator
</div>
<div class="container mb64">
<div class="row mt4 justify-content-center">
<div class="col-12 col-lg-6">
<t t-foreach="task_tpls_data" t-as="template_data">
<t t-set="template" t-value="template_data[0]"/>
<t t-set="has_enough_workers" t-value="template_data[1]"/>
<t t-set="highlight_header_class" t-value="'bg-warning' if not has_enough_workers else ''"/>
<t t-set="highlight_class" t-value="'border-warning' if not has_enough_workers else ''"/>
<div t-att-class="'card mt-4 %s' % highlight_class">
<div t-att-class="'card-header %s clearfix' % highlight_header_class">
<div class="pull-left">
<t t-esc="template.planning_id.name"/> :
<t t-esc="template.day_nb_id.name"/>
<t t-esc='float_to_time(template.start_time)' /> -
<t t-esc='float_to_time(template.end_time)'/>
</div>
<div class="badge badge-secondary pull-right"
t-if="template.remaining_worker > 0">
<t t-esc="template.remaining_worker"/> space(s)
</div>
<div class="badge badge-secondary pull-right"
t-if="template.remaining_worker == 0">
full
</div>
</div>
<div class="card-body clearfix">
<t t-esc="template.task_type_id.name"/>
<div class="badge badge-warning pull-right"
t-if="not template.super_coop_id">
Need Super Co-operator
</div>
</div>
</div>
</t>
</div>
</div>
</div>
</div>
</div>
<table class="hidden-xs table table-striped">
<thead>
<tr>
<th>Week</th>
<th>Day</th>
<th>Time</th>
<th>Type of Task</th>
<th>Super Co-operator</th>
<th class="text-center">Available Spaces</th>
</tr>
</thead>
<tbody>
<t t-foreach="task_tpls_data" t-as="template_data">
<t t-set="template" t-value="template_data[0]"/>
<t t-set="has_enough_workers" t-value="template_data[1]"/>
<!-- Row with no super coop will be shown in color -->
<tr t-attf-class="{{ 'warning' if not has_enough_workers else '' }}">
<td>
<t t-esc="template.planning_id.name"/>
</td>
<td>
<t t-esc="template.day_nb_id.name"/>
</td>
<td>
<t t-esc='float_to_time(template.start_time)' /> -
<t t-esc='float_to_time(template.end_time)'/>
</td>
<td>
<t t-esc="template.task_type_id.name"/>
</td>
<td>
<t t-if="template.super_coop_id">
Yes
</t>
<t t-if="not template.super_coop_id">
Not yet
</t>
</td>
<td class="text-center">
<t t-esc="template.remaining_worker"/>
</td>
</tr>
</t>
</tbody>
</table>
</div> <!-- col-md -->
</div> <!-- row -->
</div> <!-- container -->
</section>
<div class="oe_structure"/>
</t>
</template>
<!-- Public Available Shifts for Irregular Workers -->
<template
id="public_shift_irregular_worker"
name="Available Shifts for Irregular Workers"
page="True">
<t t-call="website.layout">
<div class="oe_structure"/>
<section class="wrap">
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">
Available Shifts for Irregular Workers
</h1>
</div>
</div>
</div>
</section>
<div class="oe_structure"/>
<section class="wrap">
<div class="container">
<div class="row">
<div class="col-md-12">
<p class="text-center">
Subscribe via <a href="mailto:volant@bees-coop.be">volant@bees-coop.be</a>
</p>
</div>
</div>
</div>
</section>
<div class="oe_structure"/>
</t>
</template>
<section class="wrap">
<div class="container">
<div class="row">
<div class="col-md-12">
<t t-call="beesdoo_website_shift.available_shift_irregular_worker"/>
<!-- Public Available Shifts for Irregular Workers -->
<template
id="public_shift_irregular_worker"
name="Available Shifts for Irregular Workers"
>
<t t-call="portal.portal_layout">
<t t-set="no_breadcrumbs" t-value="True"/>
</div> <!-- col-md -->
</div> <!-- row -->
</div> <!-- container -->
</section>
<div class="o_regular_shift_template">
<div class="container mt32">
<div class="row mt4">
<div class="col">
<h1 class="text-center">
Available Shifts for Irregular Workers
</h1>
</div>
</div>
<div class="oe_structure"/>
<div class="row mt4">
<div class="col">
<t t-call="beesdoo_website_shift.help_text_public_shift_irregular_worker "/>
</div>
</div>
</div>
<div class="container mb64">
<div class="row mt4 justify-content-center">
<div class="col-12 col-lg-6">
<t t-call="beesdoo_website_shift.available_shift_irregular_worker"/>
</div>
</div>
</div>
</div>
</t>
</template>
</t>
</template>
</openerp>
</odoo>

0
beesdoo_worker_status/__init__.py

27
beesdoo_worker_status/__manifest__.py

@ -0,0 +1,27 @@
{
'name': "Beescoop Worker Status manager",
'summary': """
Worker status management specific to beescoop""",
'description': """
""",
'author': "Thibault Francois, Elouan Le Bars, Coop It Easy",
'website': "https://github.com/beescoop/Obeesdoo",
'category': 'Cooperative management',
'version': '12.0.1.0.0',
'depends': [
'beesdoo_base',
'beesdoo_shift',
],
'data': [
"views/res_partner.xml",
],
'demo': [
]
}

1
beesdoo_worker_status/models/__init__.py

@ -0,0 +1 @@
from . import cooperative_status

280
beesdoo_worker_status/models/cooperative_status.py

@ -0,0 +1,280 @@
from odoo import models, fields, api, _
from odoo.addons.beesdoo_shift.models.cooperative_status import add_days_delta
from odoo.exceptions import ValidationError, UserError
from datetime import timedelta, datetime
import logging
class CooperativeStatus(models.Model):
_inherit = 'cooperative.status'
_period = 28
######################################################
# #
# Override of method to define status behavior #
# #
######################################################
future_alert_date = fields.Date(compute='_compute_future_alert_date')
next_countdown_date = fields.Date(compute='_compute_next_countdown_date')
@api.depends('today', 'irregular_start_date', 'sr', 'holiday_start_time',
'holiday_end_time', 'temporary_exempt_start_date',
'temporary_exempt_end_date')
def _compute_future_alert_date(self):
"""Compute date before which the worker is up to date"""
for rec in self:
# Only for irregular worker
if rec.working_mode != 'irregular' and not rec.irregular_start_date:
rec.future_alert_date = False
# Alert start time already set
elif rec.alert_start_time:
rec.future_alert_date = False
# Holidays are not set properly
elif bool(rec.holiday_start_time) != bool(rec.holiday_end_time):
rec.future_alert_date = False
# Exemption have not a start and end time
elif (bool(rec.temporary_exempt_start_date)
!= bool(rec.temporary_exempt_end_date)):
rec.future_alert_date = False
else:
date = rec.today
counter = rec.sr
# Simulate the countdown
while counter > 0:
date = self._next_countdown_date(
rec.irregular_start_date, date
)
# Check holidays
if (
rec.holiday_start_time and rec.holiday_end_time
and date >= rec.holiday_start_time
and date <= rec.holiday_end_time
):
date = add_days_delta(date, 1)
continue
# Check temporary exemption
if (
rec.temporary_exempt_start_date
and rec.temporary_exempt_end_date
and date >= rec.temporary_exempt_start_date
and date <= rec.temporary_exempt_end_date
):
date = add_days_delta(date, 1)
continue
# Otherwise
date = add_days_delta(date, 1)
counter -= 1
rec.future_alert_date = self._next_countdown_date(
rec.irregular_start_date, date
)
@api.depends('today', 'irregular_start_date', 'holiday_start_time',
'holiday_end_time', 'temporary_exempt_start_date',
'temporary_exempt_end_date')
def _compute_next_countdown_date(self):
"""
Compute the following countdown date. This date is the date when
the worker will see his counter changed du to the cron. This
date is like the birthday date of the worker that occurred each
PERIOD.
"""
for rec in self:
# Only for irregular worker
if rec.working_mode != 'irregular' and not rec.irregular_start_date:
rec.next_countdown_date = False
# Holidays are not set properly
elif bool(rec.holiday_start_time) != bool(rec.holiday_end_time):
rec.next_countdown_date = False
# Exemption have not a start and end time
elif (bool(rec.temporary_exempt_start_date)
!= bool(rec.temporary_exempt_end_date)):
rec.next_countdown_date = False
else:
date = rec.today
next_countdown_date = False
while not next_countdown_date:
date = self._next_countdown_date(
rec.irregular_start_date, date
)
# Check holidays
if (
rec.holiday_start_time and rec.holiday_end_time
and date >= rec.holiday_start_time
and date <= rec.holiday_end_time
):
date = add_days_delta(date, 1)
continue
# Check temporary exemption
if (
rec.temporary_exempt_start_date
and rec.temporary_exempt_end_date
and date >= rec.temporary_exempt_start_date
and date <= rec.temporary_exempt_end_date
):
date = add_days_delta(date, 1)
continue
# Otherwise
next_countdown_date = date
rec.next_countdown_date = next_countdown_date
#####################################
# Status Change implementation #
#####################################
def _set_regular_status(self, grace_delay, alert_delay):
self.ensure_one()
counter_unsubscribe = int(self.env['ir.config_parameter'].get_param('regular_counter_to_unsubscribe', -4))
ok = self.sr >= 0 and self.sc >= 0
grace_delay = grace_delay + self.time_extension
if (self.sr + self.sc) <= counter_unsubscribe or self.unsubscribed:
return 'unsubscribed'
#Check if exempted. Exempt end date is not required.
if self.temporary_exempt_start_date and self.today >= self.temporary_exempt_start_date:
if not self.temporary_exempt_end_date or self.today <= self.temporary_exempt_end_date:
return 'exempted'
#Transition to alert sr < 0 or stay in alert sr < 0 or sc < 0 and thus alert time is defined
if not ok and self.alert_start_time and self.extension_start_time and self.today <= add_days_delta(self.extension_start_time, grace_delay):
return 'extension'
if not ok and self.alert_start_time and self.extension_start_time and self.today > add_days_delta(self.extension_start_time, grace_delay):
return 'suspended'
if not ok and self.alert_start_time and self.today > add_days_delta(self.alert_start_time, alert_delay):
return 'suspended'
if (self.sr < 0) or (not ok and self.alert_start_time):
return 'alert'
if (
self.holiday_start_time
and self.holiday_end_time
and self.today >= self.holiday_start_time
and self.today <= self.holiday_end_time
):
return 'holiday'
elif ok or (not self.alert_start_time and self.sr >= 0):
return 'ok'
def _set_irregular_status(self, grace_delay, alert_delay):
counter_unsubscribe = int(self.env['ir.config_parameter'].get_param('irregular_counter_to_unsubscribe', -3))
self.ensure_one()
ok = self.sr >= 0
grace_delay = grace_delay + self.time_extension
if self.sr <= counter_unsubscribe or self.unsubscribed:
return 'unsubscribed'
#Check if exempted. Exempt end date is not required.
elif self.temporary_exempt_start_date and self.today >= self.temporary_exempt_start_date:
if not self.temporary_exempt_end_date or self.today <= self.temporary_exempt_end_date:
return 'exempted'
#Transition to alert sr < 0 or stay in alert sr < 0 or sc < 0 and thus alert time is defined
elif not ok and self.alert_start_time and self.extension_start_time and self.today <= add_days_delta(self.extension_start_time, grace_delay):
return 'extension'
elif not ok and self.alert_start_time and self.extension_start_time and self.today > add_days_delta(self.extension_start_time, grace_delay):
return 'suspended'
elif not ok and self.alert_start_time and self.today > add_days_delta(self.alert_start_time, alert_delay):
return 'suspended'
elif (self.sr < 0) or (not ok and self.alert_start_time):
return 'alert'
elif (
self.holiday_start_time
and self.holiday_end_time
and self.today >= self.holiday_start_time
and self.today <= self.holiday_end_time
):
return 'holiday'
elif ok or (not self.alert_start_time and self.sr >= 0):
return 'ok'
def _state_change(self, new_state):
self.ensure_one()
if new_state == 'alert':
self.write({'alert_start_time': self.today, 'extension_start_time': False, 'time_extension': 0})
if new_state == 'ok':
data = {'extension_start_time': False, 'time_extension': 0}
data['alert_start_time'] = False
self.write(data)
if new_state == 'unsubscribed' or new_state == 'resigning':
# Remove worker from task_templates
self.cooperator_id.sudo().write(
{'subscribed_shift_ids': [(5, 0, 0)]})
# Remove worker from supercoop in task_templates
task_tpls = self.env['beesdoo.shift.template'].search(
[('super_coop_id', 'in', self.cooperator_id.user_ids.ids)]
)
task_tpls.write({'super_coop_id': False})
# Remove worker for future tasks (remove also supercoop)
self.env['beesdoo.shift.shift'].sudo().unsubscribe_from_today(
[self.cooperator_id.id], now=fields.Datetime.now()
)
def _change_counter(self, data):
"""
Call when a shift state is changed
use data generated by _get_counter_date_state_change
"""
self.sc += data.get('sc', 0)
self.sr += data.get('sr', 0)
self.irregular_absence_counter += data.get('irregular_absence_counter', 0)
self.irregular_absence_date = data.get('irregular_absence_date', False)
###############################################
###### Irregular Cron implementation ##########
###############################################
def _get_irregular_worker_domain(self, **kwargs):
today = kwargs.get("today") or self.today
return ['&',
'&',
'&',
('status', 'not in', ['unsubscribed', 'exempted']),
('working_mode', '=', 'irregular'),
('irregular_start_date', '!=', False),
'|',
'|', ('holiday_start_time', '=', False), ('holiday_end_time', '=', False),
'|', ('holiday_start_time', '>', today), ('holiday_end_time', '<', today),
]
def _change_irregular_counter(self):
if self.sr > 0:
self.sr -= 1
elif self.alert_start_time:
self.sr -= 1
else:
self.sr -= 2
##################################
# Internal Implementation #
##################################
def _next_countdown_date(self, irregular_start_date, today=False):
"""
Return the next countdown date given irregular_start_date and
today dates.
This does not take holiday and other status into account.
"""
today = today or fields.Date.today()
delta = (today - irregular_start_date).days
if not delta % self._period:
return today
return add_days_delta(today, self._period - (delta % self._period))
class ResPartner(models.Model):
_inherit = 'res.partner'
"""
Override is_worker definition
You need have subscribe to a A Share
"""
is_worker = fields.Boolean(compute="_is_worker", search="_search_worker", readonly=True, related="")
def _is_worker(self):
for rec in self:
rec.is_worker = rec.cooperator_type == 'share_a'
def _search_worker(self, operator, value):
if (operator == '=' and value) or (operator == '!=' and not value):
return [('cooperator_type', '=', 'share_a')]
else:
return [('cooperator_type', '!=', 'share_a')]

81
beesdoo_worker_status/models/task.py

@ -0,0 +1,81 @@
import json
from datetime import datetime, timedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class Task(models.Model):
_inherit = 'beesdoo.shift.shift'
#################################
# State Definition #
#################################
def _get_selection_status(self):
return [
("open","Confirmed"),
("done","Attended"),
("absent_2","Absent - 2 compensations"),
("absent_1","Absent - 1 compensation"),
("absent_0","Absent - 0 compensation"),
("cancel","Cancelled")
]
def _get_color_mapping(state):
return {
"draft": 0,
"open": 1,
"done": 5,
"absent_2": 2,
"absent_1": 7,
"absent_0": 3,
"cancel": 9,
}[state]
def _get_final_state():
return ["done", "absent_2", "absent_1", "absent_0"]
##############################################
# Change counter when state change #
##############################################
def _get_counter_date_state_change(self, new_state):
data = {}
if self.worker_id.working_mode == 'regular':
if not self.replaced_id: #No replacement case
status = self.worker_id.cooperative_status_ids[0]
else:
status = self.replaced_id.cooperative_status_ids[0]
if new_state == "done" and not self.is_regular:
# Regular counter is always updated first
if status.sr < 0:
data['sr'] = 1
elif status.sc < 0:
data['sc'] = 1
# Bonus shift case
else:
data['sr'] = 1
if new_state == "absent_2":
data['sr'] = -1
data['sc'] = -1
if new_state == "absent_1":
data['sr'] = -1
elif self.worker_id.working_mode == 'irregular':
status = self.worker_id.cooperative_status_ids[0]
if new_state == "done" or new_state == "absent_0":
data['sr'] = 1
data['irregular_absence_date'] = False
data['irregular_absence_counter'] = 1 if status.irregular_absence_counter < 0 else 0
if new_state == "absent_2" or new_state == "absent_1":
if new_state == "absent_2":
data['sr'] = -1
data['irregular_absence_date'] = self.start_time.date()
data['irregular_absence_counter'] = -1
return data

1
beesdoo_worker_status/tests/__init__.py

@ -0,0 +1 @@
from . import test_beesdoo_shift

247
beesdoo_worker_status/tests/test_beesdoo_shift.py

@ -0,0 +1,247 @@
# Copyright 2019 - Today Coop IT Easy SCRLfs (<http://www.coopiteasy.be>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import time
from datetime import datetime, timedelta
from odoo import exceptions, fields
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import TransactionCase
class TestBeesdooShift(TransactionCase):
def setUp(self):
super(TestBeesdooShift, self).setUp()
self.shift_model = self.env["beesdoo.shift.shift"]
self.shift_template_model = self.env["beesdoo.shift.template"]
self.attendance_sheet_model = self.env["beesdoo.shift.sheet"]
self.attendance_sheet_shift_model = self.env[
"beesdoo.shift.sheet.shift"
]
self.shift_expected_model = self.env["beesdoo.shift.sheet.expected"]
self.shift_added_model = self.env["beesdoo.shift.sheet.added"]
self.task_type_default_id = self.env["ir.config_parameter"].sudo().get_param(
"beesdoo_shift.task_type_default_id"
)
self.current_time = datetime.now()
self.user_admin = self.env.ref("base.user_root")
self.user_generic = self.env.ref(
"beesdoo_base.beesdoo_shift_user_1_demo"
)
self.user_permanent = self.env.ref(
"beesdoo_base.beesdoo_shift_user_2_demo"
)
self.setting_wizard = self.env["beesdoo.shift.config.settings"].sudo(
self.user_admin
)
self.worker_regular_1 = self.env.ref(
"beesdoo_base.res_partner_cooperator_6_demo"
)
self.worker_regular_2 = self.env.ref(
"beesdoo_base.res_partner_cooperator_5_demo"
)
self.worker_regular_3 = self.env.ref(
"beesdoo_base.res_partner_cooperator_3_demo"
)
self.worker_regular_super_1 = self.env.ref(
"beesdoo_base.res_partner_cooperator_1_demo"
)
self.worker_irregular_1 = self.env.ref(
"beesdoo_base.res_partner_cooperator_2_demo"
)
self.worker_irregular_2 = self.env.ref(
"beesdoo_base.res_partner_cooperator_4_demo"
)
self.task_type_1 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_type_1_demo"
)
self.task_type_2 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_type_2_demo"
)
self.task_type_3 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_type_3_demo"
)
self.task_template_1 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_template_1_demo"
)
self.task_template_2 = self.env.ref(
"beesdoo_shift.beesdoo_shift_task_template_2_demo"
)
# Set time in and out of generation interval parameter
self.start_in_1 = self.current_time + timedelta(seconds=2)
self.end_in_1 = self.current_time + timedelta(minutes=10)
self.start_in_2 = self.current_time + timedelta(minutes=9)
self.end_in_2 = self.current_time + timedelta(minutes=21)
self.start_out_1 = self.current_time - timedelta(minutes=50)
self.end_out_1 = self.current_time - timedelta(minutes=20)
self.start_out_2 = self.current_time + timedelta(minutes=40)
self.end_out_2 = self.current_time + timedelta(minutes=50)
self.shift_regular_regular_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_1.id,
"worker_id": self.worker_regular_1.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
"is_regular": True,
"is_compensation": False,
}
)
self.shift_regular_regular_2 = self.shift_model.create(
{
"task_type_id": self.task_type_2.id,
"worker_id": self.worker_regular_2.id,
"start_time": self.start_out_1,
"end_time": self.end_out_1,
"is_regular": True,
"is_compensation": False,
}
)
self.shift_regular_regular_replaced_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_3.id,
"worker_id": self.worker_regular_3.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
"is_regular": True,
"is_compensation": False,
"replaced_id": self.worker_regular_2.id,
}
)
future_shift_regular = self.shift_model.create(
{
"task_template_id": self.task_template_2.id,
"task_type_id": self.task_type_1.id,
"worker_id": self.worker_regular_super_1.id,
"start_time": self.start_in_2,
"end_time": self.end_in_2,
"is_regular": False,
"is_compensation": True,
}
)
self.shift_irregular_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_2.id,
"worker_id": self.worker_irregular_1.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
}
)
self.shift_irregular_2 = self.shift_model.create(
{
"task_type_id": self.task_type_3.id,
"worker_id": self.worker_irregular_2.id,
"start_time": self.start_out_2,
"end_time": self.end_out_2,
}
)
self.shift_empty_1 = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_1.id,
"start_time": self.start_in_1,
"end_time": self.end_in_1,
}
)
def test_shift_counters(self):
"Test shift counters calculation and cooperative status update"
status_1 = self.worker_regular_1.cooperative_status_ids
status_2 = self.worker_regular_3.cooperative_status_ids
status_3 = self.worker_irregular_1.cooperative_status_ids
shift_regular = self.shift_model.create(
{
"task_template_id": self.task_template_1.id,
"task_type_id": self.task_type_1.id,
"worker_id": self.worker_regular_1.id,
"start_time": datetime.now() - timedelta(minutes=50),
"end_time": datetime.now() - timedelta(minutes=40),
"is_regular": True,
"is_compensation": False,
}
)
future_shift_regular = self.shift_model.create(
{
"task_template_id": self.task_template_2.id,
"task_type_id": self.task_type_2.id,
"worker_id": self.worker_regular_1.id,
"start_time": datetime.now() + timedelta(minutes=20),
"end_time": datetime.now() + timedelta(minutes=30),
"is_regular": True,
"is_compensation": False,
}
)
shift_irregular = self.shift_model.create(
{
"task_template_id": self.task_template_2.id,
"task_type_id": self.task_type_3.id,
"worker_id": self.worker_irregular_1.id,
"start_time": datetime.now() - timedelta(minutes=15),
"end_time": datetime.now() - timedelta(minutes=10),
}
)
# For a regular worker
status_1.sr = 0
status_1.sc = 0
self.assertEqual(status_1.status, "ok")
shift_regular.state = "absent_1"
self.assertEqual(status_1.sr, -1)
self.assertEqual(status_1.status, "alert")
shift_regular.state = "done"
self.assertEquals(status_1.sr, 0)
self.assertEquals(status_1.sc, 0)
shift_regular.state = "open"
shift_regular.write({"is_regular": False, "is_compensation": True})
shift_regular.state = "done"
self.assertEquals(status_1.sr, 1)
self.assertEquals(status_1.sc, 0)
# Check unsubscribed status
status_1.sr = -1
status_1.sc = -1
# Subscribe him to another future shift
future_shift_regular.worker_id = self.worker_regular_1
with self.assertRaises(ValidationError) as e:
future_shift_regular.state = "absent_2"
self.assertIn("future", str(e.exception))
status_1.sr = -2
status_1.sc = -2
self.assertEquals(status_1.status, "unsubscribed")
# Should be unsubscribed from future shift
self.assertFalse(future_shift_regular.worker_id)
# With replacement worker (self.worker_regular_3)
shift_regular.state = "open"
status_1.sr = 0
status_1.sc = 0
status_2.sr = 0
status_2.sc = 0
shift_regular.replaced_id = self.worker_regular_3
shift_regular.state = "absent_2"
self.assertEqual(status_1.sr, 0)
self.assertEqual(status_1.sc, 0)
self.assertEqual(status_2.sr, -1)
self.assertEqual(status_2.sc, -1)
# For an irregular worker
status_3.sr = 0
status_3.sc = 0
self.assertEqual(status_3.status, "ok")
shift_irregular.state = "done"
self.assertEqual(status_3.sr, 1)
shift_irregular.state = "absent_2"
self.assertEqual(status_3.sr, -1)

15
beesdoo_worker_status/views/res_partner.xml

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="beesdoo_partner_form_view">
<field name="name">beesdoo.partner.form.view</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="beesdoo_base.beesdoo_partner_form_view" />
<field name="arch" type="xml">
<field name="cooperator_type" position="before">
<field name="is_worker"/>
</field>
</field>
</record>
</odoo>

1
macavrac_base/__init__.py

@ -0,0 +1 @@
from . import models

23
macavrac_base/__manifest__.py

@ -0,0 +1,23 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': "Macavrac Base Module",
'summary': """
Module with basic customizations for the Macavrac cooperative.
""",
'description': """
""",
'author': "Patricia Daloze",
'category': 'Sales',
'version': '12.0.1.0.0',
'depends': ['beesdoo_shift', 'contacts'],
'data': [
'views/res_partner.xml',
],
'installable': True,
}

1
macavrac_base/models/__init__.py

@ -0,0 +1 @@
from . import res_partner

49
macavrac_base/models/res_partner.py

@ -0,0 +1,49 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class Partner(models.Model):
_inherit = 'res.partner'
date_stamp = fields.Date(string="Timestamp", help="Date de remplissage du formulaire")
birthdate = fields.Date(string="Date d'anniversaire")
payment_date = fields.Date(string="Date de paiement")
certificate_sent_date = fields.Date(string="Certificat envoyé le")
fiscal_certificate_sent_date = fields.Date(string="Attestation fiscale envoyée le")
coop_number = fields.Integer(string="Coop N°")
share_qty = fields.Integer(string="Nombre de part")
share_amount = fields.Float(string="Montant", compute="_compute_share_amount")
gender = fields.Selection([('female','Féminin'),('male','Masculin'),('other','Autre')], string="Genre")
cooperator_type = fields.Selection([('share_a', 'Part A'), ('share_b', 'Part B'), ('share_c', 'Part C'), ('share_d', 'Part D')], string="Type de Part")
state_request = fields.Selection([('ok',"En ordre"),('waiting_payment','En attente de paiement'),
('certificate_to_send', 'Certificat à envoyer'), ('resigning', 'Parts revendues')]) #TODO should we use the cooperative.status model instead?
national_register_number = fields.Char(string="Numéro de registre national") #TODO add constraint / check consistancy
share_numbers = fields.Char(string="Numéro de parts")
payment_details = fields.Char(string="Détail de paiement")
iban = fields.Char(string="IBAN") #TODO remove. Temp for import purpose.
comment_request = fields.Char(string="Commentaire")
email_sent = fields.Boolean(string="Email envoyé")
is_worker = fields.Boolean(compute="_compute_is_worker", search="_search_is_worker", string="is Worker", readonly=True, related="")
@api.depends('share_qty')
def _compute_share_amount(self):
for rec in self:
rec.share_amount = rec.share_qty * 25.0 #TODO add ir.config_parameter to make this amount editable
@api.depends('cooperator_type')
def _compute_is_worker(self):
for rec in self:
rec.is_worker = rec.cooperator_type == 'share_b'
def _search_is_worker(self, operator, value):
if (operator == '=' and value) or (operator == '!=' and not value):
return [('cooperator_type', '=', 'share_b')]
else:
return [('cooperator_type', '!=', 'share_b')]

35
macavrac_base/views/res_partner.xml

@ -0,0 +1,35 @@
<odoo>
<record model="ir.ui.view" id="macavrac_coop_partner_inherited_view_form">
<field name="name">Macavrac Coop</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority">99</field>
<field name="arch" type="xml">
<xpath expr="//notebook/page[1]" position="attributes">
<attribute name="autofocus">0</attribute>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Google Sheet Infos" name="google_sheet" autofocus="autofocus">
<group>
<field name="date_stamp"/>
<field name="coop_number"/>
<field name="gender"/>
<field name="birthdate"/>
<field name="national_register_number"/>
<field name="cooperator_type"/>
<field name="share_qty"/>
<field name="share_numbers"/>
<field name="share_amount"/>
<field name="state_request"/>
<field name="email_sent"/>
<field name="payment_date"/>
<field name="payment_details"/>
<field name="certificate_sent_date"/>
<field name="comment_request"/>
<field name="fiscal_certificate_sent_date"/>
</group>
</page>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save