diff --git a/bemade_sports_clinic/__manifest__.py b/bemade_sports_clinic/__manifest__.py index 96f75f6..6cc1415 100644 --- a/bemade_sports_clinic/__manifest__.py +++ b/bemade_sports_clinic/__manifest__.py @@ -18,7 +18,7 @@ # { 'name': 'Sports Clinic Management', - 'version': '17.0.1.5.2', + 'version': '17.0.1.6.2', 'summary': 'Manage the patients of a sports medicine clinic.', 'description': """ Adds the notion of sports teams, players (patients), coaches and treatment @@ -33,25 +33,31 @@ External users (portal users) can be added to give coaches and other team personnel access to limited player data such as estimated return-to-play dates. """, - 'category': 'Services/Medical', - 'author': 'Bemade Inc.', - 'website': 'https://www.bemade.org', - 'license': 'OPL-1', - 'depends': ['portal', 'contacts'], - 'data': [ - 'security/sports_clinic_groups.xml', - 'security/ir.model.access.csv', - 'security/sports_clinic_rules.xml', - 'data/sports_clinic_data.xml', - 'views/sports_team_views.xml', - 'views/sports_clinic_menus.xml', - 'views/sports_patient_injury_views.xml', - 'views/sports_patient_views.xml', - 'views/sports_clinic_portal_views.xml', - 'views/res_partner_views.xml', + "category": "Services/Medical", + "author": "Bemade Inc.", + "website": "https://www.bemade.org", + "license": "OPL-1", + "depends": ["portal", "contacts"], + "external_dependencies": { + "python": [ + "openupgradelib", + ], + }, + "data": [ + "security/sports_clinic_groups.xml", + "security/ir.model.access.csv", + "security/sports_clinic_rules.xml", + "data/sports_clinic_data.xml", + "views/sports_team_views.xml", + "views/sports_clinic_menus.xml", + "views/sports_patient_injury_views.xml", + "views/sports_patient_views.xml", + "views/sports_clinic_portal_views.xml", + "views/res_partner_views.xml", + "views/res_users_views.xml", ], - 'demo': ['data/demo/sports_clinic_demo_data.xml'], - 'installable': True, - 'auto_install': False, - 'application': True, + "demo": ["data/demo/sports_clinic_demo_data.xml"], + "installable": True, + "auto_install": False, + "application": True, } diff --git a/bemade_sports_clinic/controllers/team_staff_portal.py b/bemade_sports_clinic/controllers/team_staff_portal.py index 0487317..fc3797e 100644 --- a/bemade_sports_clinic/controllers/team_staff_portal.py +++ b/bemade_sports_clinic/controllers/team_staff_portal.py @@ -16,9 +16,8 @@ class TeamStaffPortal(CustomerPortal): @classmethod def _prepare_teams_domain(cls): user = http.request.env.user - partner = http.request.env.user.partner_id return [ - ('staff_ids', 'in', partner.team_staff_rel_ids.ids), + ('staff_ids.user_ids', 'in', user.id), ] @classmethod diff --git a/bemade_sports_clinic/i18n/fr_CA.po b/bemade_sports_clinic/i18n/fr_CA.po index 6608832..a7fe0ed 100644 --- a/bemade_sports_clinic/i18n/fr_CA.po +++ b/bemade_sports_clinic/i18n/fr_CA.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-21 00:52+0000\n" -"PO-Revision-Date: 2024-02-21 00:52+0000\n" +"POT-Creation-Date: 2024-09-05 18:11+0000\n" +"PO-Revision-Date: 2024-09-05 18:11+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -15,15 +15,98 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: bemade_sports_clinic +#. odoo-python +#: code:addons/bemade_sports_clinic/models/patient_injury.py:0 +#: code:addons/bemade_sports_clinic/models/patient_injury.py:0 +#, python-format +msgid " Diagnosis: %s." +msgstr " Diagnostic : %s." + #. module: bemade_sports_clinic #: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.portal_my_home msgid "/my/players" -msgstr "" +msgstr "/my/players" #. module: bemade_sports_clinic #: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.portal_my_home msgid "/my/teams" +msgstr "/my/teams" + +#. module: bemade_sports_clinic +#: model:mail.template,body_html:bemade_sports_clinic.mail_template_patient_new_internal_note +msgid "" +"

A new internal note has been posted to the record for 's.\n" +"

\n" +"
\n" +"

New Note:

\n" +"
\n" +"
\n" +" " msgstr "" +"

Une nouvelle note interne a été ajouté au dossier pour ." +"

\n" +"
\n" +"

Nouvelle note :

\n" +"
\n" +"
\n" +" " + +#. module: bemade_sports_clinic +#: model:mail.template,body_html:bemade_sports_clinic.mail_template_patient_injury_new_internal_note +msgid "" +"

A new internal note has been posted to the injury record for 's injurywith\n" +" the diagnosis of .\n" +"

\n" +"
\n" +"

New Note:

\n" +"
\n" +"
\n" +" " +msgstr "" +"

Une nouvelle note interne a été publiée dans le dossier du patient avec\n" +" pour la blessure .\n" +"

\n" +"
\n" +"

Nouvelle note :

\n" +"
\n" +"
\n" +" " + +#. module: bemade_sports_clinic +#: model:mail.template,body_html:bemade_sports_clinic.mail_template_patient_status_update +msgid "" +"

An update has been posted to the record for 's. Click\n" +" here to view the details.\n" +"

\n" +" " +msgstr "" +"

Une mise à jour a été publiée dans le dossier de 's. Cliquez\n" +" ici pour les détails.\n" +"

\n" +" " + +#. module: bemade_sports_clinic +#: model:mail.template,body_html:bemade_sports_clinic.mail_template_patient_injury_status_update +msgid "" +"

An update has been posted to the injury record for 's injurywith\n" +" the diagnosis of . Click\n" +" here to view the injury details.\n" +"

\n" +" " +msgstr "" +"

Une mise à jour a été publiée par rapport à la blessure de avec\n" +" avec le diagnostic . Cliquez\n" +" ici pour voir les détails de la blessure.\n" +"

\n" +" " + +#. module: bemade_sports_clinic +#. odoo-python +#: code:addons/bemade_sports_clinic/models/patient_injury.py:0 +#, python-format +msgid "A new injury was created for this patient." +msgstr "Une nouvelle blessure a été créée pour ce patient." #. module: bemade_sports_clinic #. odoo-python @@ -39,9 +122,15 @@ msgstr "Une équipe ne peut avoir qu'un seul entraineur chef." msgid "A team can have only one head therapist." msgstr "Une équipe ne peut avoir qu'un seul thérapeute en chef." +#. module: bemade_sports_clinic +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_res_users__accessible_team_ids +msgid "Accessible Sports Teams" +msgstr "Équipes accessibles" + #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_needaction #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_needaction +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_needaction msgid "Action Needed" msgstr "Action requise" @@ -69,24 +158,28 @@ msgstr "Nombre de blessures actives" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_ids #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_ids +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_ids msgid "Activities" msgstr "Activités" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_exception_decoration #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_exception_decoration +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_exception_decoration msgid "Activity Exception Decoration" msgstr "Décoration d'activité en exception" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_state #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_state +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_state msgid "Activity State" msgstr "État de l'activité" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_type_icon #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_type_icon +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_type_icon msgid "Activity Type Icon" msgstr "Icône du type d'activité" @@ -105,9 +198,22 @@ msgstr "Âge" msgid "Allergies" msgstr "" +#. module: bemade_sports_clinic +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__allowed_user_ids +msgid "Allowed Users" +msgstr "Utilisateurs autorisés" + +#. module: bemade_sports_clinic +#. odoo-python +#: code:addons/bemade_sports_clinic/models/patient_injury.py:0 +#, python-format +msgid "An injury was deleted." +msgstr "Une blessure a été supprimée." + #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_attachment_count #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_attachment_count +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_attachment_count msgid "Attachment Count" msgstr "Nombre de pièces jointes" @@ -121,6 +227,11 @@ msgstr "Ville" msgid "Coach" msgstr "Entraîneur" +#. module: bemade_sports_clinic +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__parental_consent +msgid "Consent for Disclosure to Parent" +msgstr "Consentement pour divulgation au parent" + #. module: bemade_sports_clinic #: model:ir.model,name:bemade_sports_clinic.model_res_partner #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__partner_id @@ -200,6 +311,11 @@ msgstr "Nom pour affichage" msgid "Doctor" msgstr "Docteur" +#. module: bemade_sports_clinic +#: model:ir.model,name:bemade_sports_clinic.model_mail_followers +msgid "Document Followers" +msgstr "Abonnés au document" + #. module: bemade_sports_clinic #: model:ir.model.constraint,message:bemade_sports_clinic.constraint_sports_team_staff_team_staff_unique msgid "Each partner can only be related to a given team once." @@ -242,18 +358,21 @@ msgstr "Prénom" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_follower_ids #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_follower_ids +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_follower_ids msgid "Followers" msgstr "Abonnés" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_partner_ids #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_partner_ids +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_partner_ids msgid "Followers (Partners)" msgstr "Abonnés (partenaires)" #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__activity_type_icon #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__activity_type_icon +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__activity_type_icon msgid "Font awesome icon e.g. fa-tasks" msgstr "Icône Font Awesome ex. fa-tasks" @@ -265,6 +384,7 @@ msgstr "Donner accès au portail" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__has_message #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__has_message +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__has_message msgid "Has Message" msgstr "A un message" @@ -317,18 +437,21 @@ msgstr "" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_exception_icon #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_exception_icon +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_exception_icon msgid "Icon" msgstr "Icône" #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__activity_exception_icon #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__activity_exception_icon +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__activity_exception_icon msgid "Icon to indicate an exception activity." msgstr "Icône pour indiquer une activité en exception." #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__message_needaction #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__message_needaction +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__message_needaction msgid "If checked, new messages require your attention." msgstr "Si coché, des nouveaux messages nécessitent votre attention." @@ -337,6 +460,8 @@ msgstr "Si coché, des nouveaux messages nécessitent votre attention." #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__message_has_sms_error #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__message_has_error #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__message_has_sms_error +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__message_has_error +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__message_has_sms_error msgid "If checked, some messages have a delivery error." msgstr "Si coché, certains messages ont une erreur de livraison." @@ -345,7 +470,8 @@ msgstr "Si coché, certains messages ont une erreur de livraison." #: code:addons/bemade_sports_clinic/models/patient_injury.py:0 #, python-format msgid "If injury date is not set, the N/A box must be checked." -msgstr "Sa la date de la blessure n'est pas remplie, la case S/O doit être cochée." +msgstr "" +"Sa la date de la blessure n'est pas remplie, la case S/O doit être cochée." #. module: bemade_sports_clinic #: model:ir.model.fields.selection,name:bemade_sports_clinic.selection__sports_patient__stage__no_play @@ -384,6 +510,16 @@ msgstr "Détails des blessures pour" msgid "Injury Record for" msgstr "Historique des blessures pour" +#. module: bemade_sports_clinic +#: model:mail.template,subject:bemade_sports_clinic.mail_template_patient_new_internal_note +msgid "Internal Note Updated for Patient {{ object.name}}" +msgstr "" + +#. module: bemade_sports_clinic +#: model:mail.template,subject:bemade_sports_clinic.mail_template_patient_injury_new_internal_note +msgid "Internal Note Updated for Patient {{ object.patient_name }}" +msgstr "Note interne mise à jour pour le patient {{ object.patient_name }}" + #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__internal_notes msgid "Internal Notes" @@ -404,6 +540,7 @@ msgstr "Combinaison invalide des statuts de match et de pratique." #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_is_follower #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_is_follower +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_is_follower msgid "Is Follower" msgstr "Est abonné" @@ -461,6 +598,7 @@ msgstr "Dernière mise à jour le" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_main_attachment_id #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_main_attachment_id +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_main_attachment_id msgid "Main Attachment" msgstr "Pièce jointe principale" @@ -474,12 +612,14 @@ msgstr "Statut match" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_has_error #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_has_error +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_has_error msgid "Message Delivery error" msgstr "Erreur de livraison du message" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_ids #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_ids +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_ids msgid "Messages" msgstr "" @@ -498,6 +638,7 @@ msgstr "Mère" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__my_activity_date_deadline #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__my_activity_date_deadline +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__my_activity_date_deadline msgid "My Activity Deadline" msgstr "Date limite de mon activité" @@ -521,18 +662,21 @@ msgstr "Nom" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_date_deadline #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_date_deadline +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_date_deadline msgid "Next Activity Deadline" msgstr "Échéance de la prochaine activité" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_summary #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_summary +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_summary msgid "Next Activity Summary" msgstr "Résumé de la prochaine activité" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_type_id #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_type_id +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_type_id msgid "Next Activity Type" msgstr "Type de la prochaine activité" @@ -552,24 +696,28 @@ msgstr "" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_needaction_counter #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_needaction_counter +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_needaction_counter msgid "Number of Actions" msgstr "Nombre d'actions" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_has_error_counter #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_has_error_counter +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_has_error_counter msgid "Number of errors" msgstr "Nombre d'erreurs" #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__message_needaction_counter #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__message_needaction_counter +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__message_needaction_counter msgid "Number of messages requiring action" msgstr "Nombre de messages nécessitant une action" #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__message_has_error_counter #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__message_has_error_counter +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__message_has_error_counter msgid "Number of messages with delivery error" msgstr "Nombre de messages avec une erreur de livraison" @@ -608,11 +756,6 @@ msgstr "Équipe détenue" msgid "Parent Organization" msgstr "Organisation parente" -#. module: bemade_sports_clinic -#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__parental_consent -msgid "Consent for Disclosure to Parent" -msgstr "Consentement pour divulgation au parent" - #. module: bemade_sports_clinic #: model:ir.model,name:bemade_sports_clinic.model_sports_patient #: model:ir.model.fields,field_description:bemade_sports_clinic.field_res_partner__patient_ids @@ -633,9 +776,16 @@ msgid "Patient Contacts" msgstr "Contacts du patient" #. module: bemade_sports_clinic -#: model:mail.message.subtype,name:bemade_sports_clinic.subtype_patient_update -msgid "Patient File Update" -msgstr "Mise à jour du dossier du patient" +#: model:mail.message.subtype,name:bemade_sports_clinic.subtype_patient_external_update +#: model:mail.message.subtype,name:bemade_sports_clinic.subtype_patient_injury_external_update +msgid "Patient File Update (External)" +msgstr "Mise à jour du dossier patient (externe)" + +#. module: bemade_sports_clinic +#: model:mail.message.subtype,name:bemade_sports_clinic.subtype_patient_injury_internal_update +#: model:mail.message.subtype,name:bemade_sports_clinic.subtype_patient_internal_update +msgid "Patient File Update (Internal)" +msgstr "Mise à jour du dossier patient (interne)" #. module: bemade_sports_clinic #: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_view_form @@ -653,9 +803,9 @@ msgid "Patient Injury History" msgstr "Historique des blessures du patient" #. module: bemade_sports_clinic -#: model:mail.message.subtype,name:bemade_sports_clinic.subtype_patient_injury_update -msgid "Patient Injury Status Update" -msgstr "Mise à jour du statut de la blessure d'un patient" +#: model:mail.template,subject:bemade_sports_clinic.mail_template_patient_injury_status_update +msgid "Patient Injury Status Update for {{ object.patient_name }}" +msgstr "Mise à jour du dossier de la blessure de {{ object.patient_name }}" #. module: bemade_sports_clinic #: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_view_form @@ -667,21 +817,24 @@ msgstr "Téléphone et courriel du patient" msgid "Patient Record" msgstr "Dossier du patient" +#. module: bemade_sports_clinic +#: model:mail.template,name:bemade_sports_clinic.mail_template_patient_injury_new_internal_note +#: model:mail.template,name:bemade_sports_clinic.mail_template_patient_injury_status_update +#: model:mail.template,name:bemade_sports_clinic.mail_template_patient_new_internal_note +#: model:mail.template,name:bemade_sports_clinic.mail_template_patient_status_update +msgid "Patient Status Update" +msgstr "Mise à jour sur le patient" + +#. module: bemade_sports_clinic +#: model:mail.template,subject:bemade_sports_clinic.mail_template_patient_status_update +msgid "Patient Status Update for {{ object.name}}" +msgstr "Mise à jour pour le patient {{ object.name }}" + #. module: bemade_sports_clinic #: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_view_form msgid "Patient's Contacts" msgstr "Contacts du patient" -#. module: bemade_sports_clinic -#: model:mail.message.subtype,description:bemade_sports_clinic.subtype_patient_update -msgid "Patient's file has been updated." -msgstr "Le dossier du patient a été mis à jour." - -#. module: bemade_sports_clinic -#: model:mail.message.subtype,description:bemade_sports_clinic.subtype_patient_injury_update -msgid "Patient's injury was updated." -msgstr "Le dossier de la blessure du patient a été mis à jour." - #. module: bemade_sports_clinic #: model:ir.actions.act_window,name:bemade_sports_clinic.action_view_patient #: model:ir.ui.menu,name:bemade_sports_clinic.sports_clinic_patients @@ -749,6 +902,7 @@ msgstr "Résolu" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__activity_user_id #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__activity_user_id +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__activity_user_id msgid "Responsible User" msgstr "Utilisateur responsable" @@ -770,6 +924,7 @@ msgstr "Rôle" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__message_has_sms_error #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__message_has_sms_error +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__message_has_sms_error msgid "SMS Delivery error" msgstr "Erreur de livraison SMS" @@ -836,6 +991,7 @@ msgstr "État" #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__activity_state #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__activity_state +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__activity_state msgid "" "Status based on activities\n" "Overdue: Due date is already passed\n" @@ -932,7 +1088,8 @@ msgstr "Cette équipe n'a pu être trouvée." #: code:addons/bemade_sports_clinic/models/res_partner.py:0 #, python-format msgid "To change a patient's name, change it from the patient form." -msgstr "Pour changer le nom d'un patient, changez le à partir du formulaire patient." +msgstr "" +"Pour changer le nom d'un patient, changez le à partir du formulaire patient." #. module: bemade_sports_clinic #: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.portal_my_teams @@ -952,6 +1109,7 @@ msgstr "Professionnels de la santé" #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__activity_exception_decoration #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__activity_exception_decoration +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__activity_exception_decoration msgid "Type of the exception activity on record." msgstr "Type d'exception de l'activité enregistrée." @@ -960,6 +1118,11 @@ msgstr "Type d'exception de l'activité enregistrée." msgid "User" msgstr "Utilisateur" +#. module: bemade_sports_clinic +#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_team_view_form +msgid "User Access" +msgstr "Accès utilisateur" + #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team_staff__user_ids msgid "Users" @@ -973,12 +1136,14 @@ msgstr "Site web" #. module: bemade_sports_clinic #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__website_message_ids #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient_injury__website_message_ids +#: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_team__website_message_ids msgid "Website Messages" msgstr "Messages du site web" #. module: bemade_sports_clinic #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient__website_message_ids #: model:ir.model.fields,help:bemade_sports_clinic.field_sports_patient_injury__website_message_ids +#: model:ir.model.fields,help:bemade_sports_clinic.field_sports_team__website_message_ids msgid "Website communication history" msgstr "Historique de communication du site web" @@ -994,8 +1159,8 @@ msgid "" "Whether the patient has given their consent to share injury details with " "their parents." msgstr "" -"Indique si le patient a donné son consentement pour que les détails de la blessure soient partagés avec " -"ses parents." +"Indique si le patient a donné son consentement pour que les détails de la " +"blessure soient partagés avec ses parents." #. module: bemade_sports_clinic #: model:ir.model.fields.selection,name:bemade_sports_clinic.selection__sports_patient__match_status__yes @@ -1023,3 +1188,13 @@ msgstr "Vos équipes" #: model:ir.model.fields,field_description:bemade_sports_clinic.field_sports_patient__zip msgid "Zip" msgstr "Code postal" + +#. module: bemade_sports_clinic +#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.portal_my_home +msgid "players_count" +msgstr "players_count" + +#. module: bemade_sports_clinic +#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.portal_my_home +msgid "teams_count" +msgstr "teams_count" diff --git a/bemade_sports_clinic/migrations/16.0.1.5.8/post-migrate.py b/bemade_sports_clinic/migrations/16.0.1.5.8/post-migrate.py new file mode 100644 index 0000000..62a8c9c --- /dev/null +++ b/bemade_sports_clinic/migrations/16.0.1.5.8/post-migrate.py @@ -0,0 +1,7 @@ +from odoo import api, SUPERUSER_ID + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + env["sports.team"].search([])._allow_access_for_staff_internal_users() + cr.execute('CREATE EXTENSION IF NOT EXISTS "unaccent"') diff --git a/bemade_sports_clinic/migrations/16.0.1.6.0/post-migrate.py b/bemade_sports_clinic/migrations/16.0.1.6.0/post-migrate.py new file mode 100644 index 0000000..29f6e35 --- /dev/null +++ b/bemade_sports_clinic/migrations/16.0.1.6.0/post-migrate.py @@ -0,0 +1,6 @@ +from odoo import api, SUPERUSER_ID + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + env["sports.patient"].search([]).recompute_followers() diff --git a/bemade_sports_clinic/migrations/16.0.1.7.0/post-migrate.py b/bemade_sports_clinic/migrations/16.0.1.7.0/post-migrate.py new file mode 100644 index 0000000..140cc3d --- /dev/null +++ b/bemade_sports_clinic/migrations/16.0.1.7.0/post-migrate.py @@ -0,0 +1,23 @@ +""" Patient access revised to make the team staff relationship central to +access. Everything is calculated from there. Inverse functions deal with +sports.team.staff records instead of having a separate table for storing +access rights.""" + +from odoo import api, SUPERUSER_ID, Command + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + rules = ( + env.ref("bemade_sports_clinic.restrict_staff_access_to_teams") + + env.ref("bemade_sports_clinic.restrict_staff_access_to_team_injuries") + + env.ref("bemade_sports_clinic.restrict_staff_access_to_team_players") + ) + rules.write({ + "groups": [Command.link(env.ref("base.group_user").id)] + }) + env.ref("bemade_sports_clinic.group_sports_clinic_user").write({ + "implied_ids": [Command.link(env.ref("base.group_partner_manager").id)] + }) + cr.execute("DROP TABLE sports_team_res_users_rel") + # Check the groups are OK diff --git a/bemade_sports_clinic/migrations/16.0.1.7.0/pre-migrate.py b/bemade_sports_clinic/migrations/16.0.1.7.0/pre-migrate.py new file mode 100644 index 0000000..49ef708 --- /dev/null +++ b/bemade_sports_clinic/migrations/16.0.1.7.0/pre-migrate.py @@ -0,0 +1,10 @@ +import openupgradelib.openupgrade as ou +from odoo import SUPERUSER_ID, api + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + ou.delete_records_safely_by_xml_id(env, [ + "bemade_sports_clinic.restrict_team_access_to_allowed_internal_users", + "bemade_sports_clinic.restrict_patient_access_to_allowed_internal_users", + "bemade_sports_clinic.restrict_injury_access_to_allowed_internal_users", + ]) \ No newline at end of file diff --git a/bemade_sports_clinic/models/patient.py b/bemade_sports_clinic/models/patient.py index f2296eb..c28ccb3 100644 --- a/bemade_sports_clinic/models/patient.py +++ b/bemade_sports_clinic/models/patient.py @@ -3,105 +3,113 @@ from odoo.exceptions import ValidationError from datetime import date from dateutil.relativedelta import relativedelta from odoo.addons.phone_validation.tools import phone_validation -from typing import Set, Tuple +import logging + +_logger = logging.getLogger(__name__) external_tracking_fields = { - 'last_consultation_date', - 'match_status', - 'practice_status', - 'predicted_return_date', - 'return_date', + "last_consultation_date", + "match_status", + "practice_status", + "predicted_return_date", + "return_date", } internal_tracking_fields = { - 'team_info_notes', - 'age', - 'date_of_birth', + "team_info_notes", + "age", + "date_of_birth", } class Patient(models.Model): - _name = 'sports.patient' + _name = "sports.patient" _description = "Patient" - _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = 'last_name, first_name' + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "last_name, first_name" # res.partner fields - partner_id = fields.Many2one(comodel_name='res.partner', string='Contact', ondelete='restrict', compute_sudo=True) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Contact", + ondelete="restrict", + compute_sudo=True, + ) first_name = fields.Char(required=True, tracking=True) last_name = fields.Char(required=True, tracking=True) - name = fields.Char(related='partner_id.name', compute="_compute_name", compute_sudo=True) - phone = fields.Char(related='partner_id.phone', readonly=False) - mobile = fields.Char(related='partner_id.mobile', readonly=False) - street = fields.Char(related='partner_id.street', readonly=False) - street2 = fields.Char(related='partner_id.street2', readonly=False) - city = fields.Char(related='partner_id.city', readonly=False) - state_id = fields.Many2one(related='partner_id.state_id', readonly=False) - zip = fields.Char(related='partner_id.zip', readonly=False) - country_id = fields.Many2one(related='partner_id.country_id', readonly=False) - email = fields.Char(related='partner_id.email', readonly=False) + name = fields.Char( + related="partner_id.name", + ) + phone = fields.Char(related="partner_id.phone", readonly=False) + mobile = fields.Char(related="partner_id.mobile", readonly=False) + street = fields.Char(related="partner_id.street", readonly=False) + street2 = fields.Char(related="partner_id.street2", readonly=False) + city = fields.Char(related="partner_id.city", readonly=False) + state_id = fields.Many2one(related="partner_id.state_id", readonly=False) + zip = fields.Char(related="partner_id.zip", readonly=False) + country_id = fields.Many2one(related="partner_id.country_id", readonly=False) + email = fields.Char(related="partner_id.email", readonly=False) # Patient fields date_of_birth = fields.Date( groups="bemade_sports_clinic.group_sports_clinic_treatment_professional", - tracking=True) + tracking=True, + ) age = fields.Integer( - compute='_compute_age', - groups="bemade_sports_clinic.group_sports_clinic_treatment_professional" + compute="_compute_age", + groups="bemade_sports_clinic.group_sports_clinic_treatment_professional", ) contact_ids = fields.One2many( - comodel_name='sports.patient.contact', - inverse_name='patient_id', - string='Patient Contacts', - groups="bemade_sports_clinic.group_sports_clinic_user" + comodel_name="sports.patient.contact", + inverse_name="patient_id", + string="Patient Contacts", + groups="bemade_sports_clinic.group_sports_clinic_user", ) team_ids = fields.Many2many( - comodel_name='sports.team', - relation='sports_team_patient_rel', - column1='patient_id', - column2='team_id', - string='Teams', + comodel_name="sports.team", + relation="sports_team_patient_rel", + column1="patient_id", + column2="team_id", + string="Teams", ) - match_status = fields.Selection( # Selection rather than bool for easy expansion later + match_status = fields.Selection( + # Selection rather than bool for easy expansion later selection=[ - ('yes', 'Yes'), - ('no', 'No'), + ("yes", "Yes"), + ("no", "No"), ], required=True, - default='yes', - tracking=True) + default="yes", + tracking=True, + ) practice_status = fields.Selection( - selection=[ - ('yes', 'Yes'), - ('no_contact', 'Yes, no contact'), - ('no', 'No') - ], + selection=[("yes", "Yes"), ("no_contact", "Yes, no contact"), ("no", "No")], tracking=True, required=True, - default='yes', + default="yes", ) injury_ids = fields.One2many( - comodel_name='sports.patient.injury', - inverse_name='patient_id', - string='Injuries', + comodel_name="sports.patient.injury", + inverse_name="patient_id", + string="Injuries", ) - injured_since = fields.Date(compute='_compute_is_injured') + injured_since = fields.Date(compute="_compute_is_injured") predicted_return_date = fields.Date(tracking=True) return_date = fields.Date( tracking=True, - help="When the player was cleared by medical staff to " - "return to match play." + help="When the player was cleared by medical staff to " "return to match play.", ) is_injured = fields.Boolean(compute="_compute_is_injured") stage = fields.Selection( selection=[ - ('no_play', 'Injured'), - ('practice_ok', 'Practice OK'), - ('healthy', 'Play OK') + ("no_play", "Injured"), + ("practice_ok", "Practice OK"), + ("healthy", "Play OK"), ], - compute='_compute_stage') + compute="_compute_stage", + ) last_consultation_date = fields.Date(tracking=True) - active_injury_count = fields.Integer(compute='_compute_active_injury_count') + active_injury_count = fields.Integer(compute="_compute_active_injury_count") allergies = fields.Text() team_info_notes = fields.Html( string="Notes", @@ -110,55 +118,92 @@ class Patient(models.Model): def default_get(self, fields_list): res = super().default_get(fields_list) - if 'team_ids' in fields_list and 'params' in self.env.context \ - and self.env.context.get('params')['model'] == 'sports.team': - team = self.env['sports.team'].browse(self.env.context.get('params')['id']) + if ( + "team_ids" in fields_list + and "params" in self.env.context + and self.env.context.get("params")["model"] == "sports.team" + ): + team = self.env["sports.team"].browse(self.env.context.get("params")["id"]) team_ids = [Command.set([team.id])] if team_ids: - res.update({'team_ids': team_ids}) + res.update({"team_ids": team_ids}) return res + def write(self, values): + res = super().write(values) + if "team_ids" in values: + self.sudo().recompute_followers() + if "first_name" in values or "last_name" in values: + self._recompute_name() + return res + + def _recompute_name(self): + for rec in self: + rec.partner_id.with_context(patient_update=True).name = ( + rec._get_name_from_first_and_last(rec.first_name, rec.last_name) + ) + @api.model_create_multi def create(self, vals_list): for row in vals_list: - if 'partner_id' not in row: - row['partner_id'] = self.env['res.partner'].create({ - 'name': self._get_name_from_first_and_last(row['first_name'], row['last_name']) - }).id - return super().create(vals_list) + if "partner_id" not in row: + row["partner_id"] = ( + self.env["res.partner"] + .create( + { + "name": self._get_name_from_first_and_last( + row["first_name"], row["last_name"] + ) + } + ) + .id + ) + res = super().create(vals_list) + res.sudo().recompute_followers() + return res - @api.constrains('match_status', 'practice_status') + @api.constrains("match_status", "practice_status") def constrain_match_and_practice_status(self): - """ Avoid invalid combinations of match and practice status: - - Yes (match), No (practice) - - Yes (match), No Contact (practice) + """Avoid invalid combinations of match and practice status: + - Yes (match), No (practice) + - Yes (match), No Contact (practice) """ # combinations of (match_status, practice_status) that are valid - valid_combinations = [('yes', 'yes'), ('no', 'yes'), ('no', 'no_contact'), ('no', 'no')] + valid_combinations = [ + ("yes", "yes"), + ("no", "yes"), + ("no", "no_contact"), + ("no", "no"), + ] for rec in self: if (rec.match_status, rec.practice_status) not in valid_combinations: - raise ValidationError(_("Invalid combination of match and practice status.")) + raise ValidationError( + _("Invalid combination of match and practice status.") + ) - @api.depends('injury_ids.stage') + @api.depends("injury_ids.stage") def _compute_active_injury_count(self): for rec in self: - rec.active_injury_count = len(rec.injury_ids.filtered(lambda r: r.stage == 'active')) + rec.active_injury_count = len( + rec.injury_ids.filtered(lambda r: r.stage == "active") + ) - @api.depends('match_status', 'practice_status') + @api.depends("match_status", "practice_status") def _compute_stage(self): stage_map = { - ('yes', 'yes'): 'healthy', - ('no', 'yes'): 'practice_ok', - ('no', 'no_contact'): 'practice_ok', - ('no', 'no'): 'no_play', + ("yes", "yes"): "healthy", + ("no", "yes"): "practice_ok", + ("no", "no_contact"): "practice_ok", + ("no", "no"): "no_play", } for rec in self: + # not a valid combination, will be caught by constraint if save is attempted if (rec.match_status, rec.practice_status) not in stage_map: - rec.stage = False # not a valid combination, will be caught by constraint if save is attempted + rec.stage = False continue rec.stage = stage_map[(rec.match_status, rec.practice_status)] - @api.depends('date_of_birth') + @api.depends("date_of_birth") def _compute_age(self): for rec in self: if not rec.date_of_birth: @@ -166,56 +211,60 @@ class Patient(models.Model): else: rec.age = relativedelta(date.today(), rec.date_of_birth).years - @api.depends('first_name', 'last_name') - def _compute_name(self): - for rec in self: - rec.name = self._get_name_from_first_and_last(rec.first_name, rec.last_name) - @api.model def _get_name_from_first_and_last(self, first_name, last_name): - return ((first_name or "") + " " + (last_name or "")).strip() + names = [] + if first_name: + names.append(first_name) + if last_name: + names.append(last_name) + return " ".join(names) - @api.depends('practice_status', 'match_status', 'injury_ids.injury_date') + @api.depends("practice_status", "match_status", "injury_ids.injury_date") def _compute_is_injured(self): for rec in self: - rec.is_injured = rec.practice_status != 'yes' or rec.match_status != 'yes' + rec.is_injured = rec.practice_status != "yes" or rec.match_status != "yes" if rec.is_injured: - unresolved_injuries = rec.injury_ids.filtered(lambda r: not r.stage == 'resolved') - rec.injured_since = unresolved_injuries and unresolved_injuries[0].injury_date + unresolved_injuries = rec.injury_ids.filtered( + lambda r: not r.stage == "resolved" + ) + rec.injured_since = ( + unresolved_injuries and unresolved_injuries[0].injury_date + ) else: rec.injured_since = False def action_view_patient_form(self): self.ensure_one() return { - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'res_model': 'sports.patient', - 'res_id': self.id, - 'context': self._context, + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "sports.patient", + "res_id": self.id, + "context": self._context, } def action_consulted_today(self): self.ensure_one() # should just be called from form view self.last_consultation_date = date.today() return { - 'view_mode': 'form', - 'res_model': 'sports.patient', - 'context': self._context, - 'res_id': self.id, + "view_mode": "form", + "res_model": "sports.patient", + "context": self._context, + "res_id": self.id, } - @api.onchange('mobile', 'country_id') + @api.onchange("mobile", "country_id") def _onchange_mobile_validation(self): if self.mobile: self.mobile = self._phone_format(self.mobile, force_format="INTERNATIONAL") - @api.onchange('phone', 'country_id') + @api.onchange("phone", "country_id") def _onchange_phone_validation(self): if self.phone: self.phone = self._phone_format(self.phone, force_format="INTERNATIONAL") - def _phone_format(self, number, force_format='E164'): + def _phone_format(self, number, force_format="E164"): country = self.country_id or self.env.company.country_id if not country or not number: return number @@ -224,11 +273,11 @@ class Patient(models.Model): country.code if country else None, country.phone_code if country else None, force_format=force_format, - raise_exception=False + raise_exception=False, ) def _track_subtype(self, init_values): - return self.env.ref('mail.mt_note') + return self.env.ref("mail.mt_note") def _track_template(self, changes): res = super()._track_template(changes) @@ -237,18 +286,47 @@ class Patient(models.Model): if external: first_external_field = (external_tracking_fields & params).pop() res[first_external_field] = ( - self.env.ref('bemade_sports_clinic.mail_template_patient_status_update'), { - 'auto_delete_message': False, - 'subtype_id': self.env.ref('bemade_sports_clinic.subtype_patient_external_update').id, - 'email_layout_xmlid': 'mail.mail_notification_light', - } + self.env.ref( + "bemade_sports_clinic.mail_template_patient_status_update" + ), + { + "auto_delete_message": False, + "subtype_id": self.env.ref( + "bemade_sports_clinic.subtype_patient_external_update" + ).id, + "email_layout_xmlid": "mail.mail_notification_light", + }, ) - if 'team_info_notes' in changes: - res['team_info_notes'] = ( - self.env.ref('bemade_sports_clinic.mail_template_patient_new_internal_note'), { - 'auto_delete_message': False, - 'subtype_id': self.env.ref('bemade_sports_clinic.subtype_patient_internal_update').id, - 'email_layout_xmlid': 'mail.mail_notification_light', - } + if "team_info_notes" in changes: + res["team_info_notes"] = ( + self.env.ref( + "bemade_sports_clinic.mail_template_patient_new_internal_note" + ), + { + "auto_delete_message": False, + "subtype_id": self.env.ref( + "bemade_sports_clinic.subtype_patient_internal_update" + ).id, + "email_layout_xmlid": "mail.mail_notification_light", + }, ) return res + + def recompute_followers(self): + """Recompute the followers for this patient (and its injuries) based on the + changes to a specific team's staff members. Ignoring manually unsubscribed + followers, the set of followers should be the set of staff on all teams the + patient is part of.""" + for patient in self: + patient = patient.sudo() + current_followers = patient.message_partner_ids + future_followers = patient.team_ids.mapped("staff_ids").mapped("partner_id") + removed_followers = current_followers - future_followers + if removed_followers: + _logger.debug(f"{self} unsubscribing {removed_followers}") + patient.message_unsubscribe(removed_followers.ids) + patient.injury_ids.message_unsubscribe(removed_followers.ids) + if future_followers: + _logger.debug(f"{self} subscribing {future_followers}") + patient.message_subscribe(future_followers.ids) + patient.injury_ids.message_subscribe(future_followers.ids) diff --git a/bemade_sports_clinic/models/patient_contact.py b/bemade_sports_clinic/models/patient_contact.py index 36e70cc..90420b4 100644 --- a/bemade_sports_clinic/models/patient_contact.py +++ b/bemade_sports_clinic/models/patient_contact.py @@ -7,13 +7,13 @@ class PatientContact(models.Model): _description = "Emergency or other contacts for a patient." sequence = fields.Integer(required=True, default=0) - name = fields.Char(unaccent=False) + name = fields.Char(unaccent=True) contact_type = fields.Selection(selection=[ ('mother', 'Mother'), ('father', 'Father'), ('other', 'Other'), ], required=True) - mobile = fields.Char(unaccent=False, required=True) + mobile = fields.Char(unaccent=False) # TODO: add email here and on views patient_id = fields.Many2one(comodel_name='sports.patient', string='Patient') diff --git a/bemade_sports_clinic/models/patient_injury.py b/bemade_sports_clinic/models/patient_injury.py index f3c20c5..06fa51e 100644 --- a/bemade_sports_clinic/models/patient_injury.py +++ b/bemade_sports_clinic/models/patient_injury.py @@ -2,132 +2,138 @@ from odoo import models, fields, api, _ from datetime import datetime, date import pytz from odoo.exceptions import ValidationError -from typing import Set, Tuple +import logging + +_logger = logging.getLogger(__name__) external_tracking_fields = { - 'diagnosis', - 'predicted_resolution_date', - 'resolution_date', - 'external_notes', + "diagnosis", + "predicted_resolution_date", + "resolution_date", + "external_notes", } # Include only fields not already included in external_tracking_fields here internal_tracking_fields = { - 'internal_notes', - 'parental_consent', + "internal_notes", + "parental_consent", } class PatientInjury(models.Model): - _name = 'sports.patient.injury' + _name = "sports.patient.injury" _description = "Patient Injury" - _inherit = ['mail.thread', 'mail.activity.mixin'] - _rec_name = 'diagnosis' + _inherit = ["mail.thread", "mail.activity.mixin"] + _rec_name = "diagnosis" @api.model def _today(self): """Get the current date in the user's time zone.""" - return datetime.now(pytz.timezone(self.env.user.tz or 'GMT')) + return datetime.now(pytz.timezone(self.env.user.tz or "GMT")) # TODO: Find a way to improve notifications send about tracking injury details # TODO: Add field consentement_parental = fields.Selection(oui, non, non-applicable) patient_id = fields.Many2one( - comodel_name='sports.patient', + comodel_name="sports.patient", string="Patient", readonly=True, - required=True + required=True, + ondelete="cascade", ) patient_name = fields.Char(related="patient_id.name") diagnosis = fields.Char(tracking=True) injury_date = fields.Date( - string='Date of Injury', + string="Date of Injury", default=_today, ) injury_date_na = fields.Boolean(string="N/A", default=False) internal_notes = fields.Html(tracking=True) external_notes = fields.Html(tracking=True) treatment_professional_ids = fields.Many2many( - comodel_name='res.users', - relation='patient_injury_treatment_pro_rel', - column1='patient_injury_id', - column2='treatment_pro_id', string='Treatment Professionals', - domain=[ - ('is_treatment_professional', '=', - True)], tracking=True + comodel_name="res.users", + relation="patient_injury_treatment_pro_rel", + column1="patient_injury_id", + column2="treatment_pro_id", + string="Treatment Professionals", + domain=[("is_treatment_professional", "=", True)], + tracking=True, ) predicted_resolution_date = fields.Date(tracking=True) resolution_date = fields.Date( - tracking=True, - help="The date when the injury was actually resolved." + tracking=True, help="The date when the injury was actually resolved." + ) + stage = fields.Selection( + selection=[("active", "Active"), ("resolved", "Resolved")], + compute="_compute_stage", ) - stage = fields.Selection(selection=[('active', 'Active'), ('resolved', 'Resolved')], compute='_compute_stage') parental_consent = fields.Selection( string="Consent for Disclosure to Parent", - selection=[ - ('yes', 'Yes'), - ('no', 'No'), - ('na', 'N/A') - ], + selection=[("yes", "Yes"), ("no", "No"), ("na", "N/A")], help="Whether the patient has given their consent to share injury details with their parents.", tracking=True, ) - @api.constrains('injury_date_na', 'injury_date') + @api.constrains("injury_date_na", "injury_date") def constrain_date_blank_only_if_na(self): for rec in self: if not rec.injury_date_na and not rec.injury_date: - raise ValidationError(_("If injury date is not set, the N/A box must be checked.")) + raise ValidationError( + _("If injury date is not set, the N/A box must be checked.") + ) - @api.onchange('injury_date_na') + @api.onchange("injury_date_na") def _onchange_injury_date_na(self): for rec in self: if rec.injury_date_na: rec.injury_date = None - @api.onchange('injury_date') + @api.onchange("injury_date") def _onchange_injury_date(self): for rec in self: if rec.injury_date: rec.injury_date_na = False - @api.depends('resolution_date') + @api.depends("resolution_date") def _compute_stage(self): for rec in self: if rec.resolution_date and rec.resolution_date <= date.today(): - rec.stage = 'resolved' + rec.stage = "resolved" else: - rec.stage = 'active' - - def write(self, vals): - super().write(vals) - if 'treatment_professional_ids' in vals: - to_subscribe = (self.treatment_professional_ids.mapped('partner_id') - - self.message_follower_ids.mapped('partner_id')) - self.message_subscribe(to_subscribe.ids) + rec.stage = "active" @api.model_create_multi def create(self, vals_list): res = super().create(vals_list) - for rec in res: - to_subscribe = (rec.treatment_professional_ids.mapped('partner_id') - - rec.message_follower_ids.mapped('partner_id')) - rec.message_subscribe(to_subscribe.ids) + for rec in res.sudo(): + rec.message_subscribe(rec.patient_id.message_partner_ids) + msg_body = _("A new injury was created for this patient.") + if rec.diagnosis: + msg_body += _(" Diagnosis: %s." % rec.diagnosis) + rec.patient_id.message_post(body=msg_body, message_type="comment") return res + def unlink(self): + for rec in self: + msg_body = _("An injury was deleted.") + if rec.diagnosis: + msg_body += _(" Diagnosis: %s." % rec.diagnosis) + rec.patient_id.message_post(body=msg_body, message_type="comment") + return super().unlink() + def action_view_injury_form(self): self.ensure_one() return { - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'res_model': 'sports.patient.injury', - 'res_id': self.id, - 'context': self._context, + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "sports.patient.injury", + "res_id": self.id, + "context": self._context, } def _track_subtype(self, init_values): - return self.env.ref('mail.mt_note') + return self.env.ref("mail.mt_note") def _track_template(self, changes): res = super()._track_template(changes) @@ -136,18 +142,34 @@ class PatientInjury(models.Model): if external: first_external_field = (external_tracking_fields & params).pop() res[first_external_field] = ( - self.env.ref('bemade_sports_clinic.mail_template_patient_injury_status_update'), { - 'auto_delete_message': False, - 'subtype_id': self.env.ref('bemade_sports_clinic.subtype_patient_injury_external_update').id, - 'email_layout_xmlid': 'mail.mail_notification_light', - } + self.env.ref( + "bemade_sports_clinic.mail_template_patient_injury_status_update" + ), + { + "auto_delete_message": False, + "subtype_id": self.env.ref( + "bemade_sports_clinic.subtype_patient_injury_external_update" + ).id, + "email_layout_xmlid": "mail.mail_notification_light", + }, ) - if 'internal_notes' in changes: - res['internal_notes'] = ( - self.env.ref('bemade_sports_clinic.mail_template_patient_injury_new_internal_note'), { - 'auto_delete_message': False, - 'subtype_id': self.env.ref('bemade_sports_clinic.subtype_patient_injury_internal_update').id, - 'email_layout_xmlid': 'mail.mail_notification_light', - } + if "internal_notes" in changes: + res["internal_notes"] = ( + self.env.ref( + "bemade_sports_clinic.mail_template_patient_injury_new_internal_note" + ), + { + "auto_delete_message": False, + "subtype_id": self.env.ref( + "bemade_sports_clinic.subtype_patient_injury_internal_update" + ).id, + "email_layout_xmlid": "mail.mail_notification_light", + }, ) return res + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + res.patient_id.recompute_followers() + return res diff --git a/bemade_sports_clinic/models/res_partner.py b/bemade_sports_clinic/models/res_partner.py index 6ff7a0d..7698b7d 100644 --- a/bemade_sports_clinic/models/res_partner.py +++ b/bemade_sports_clinic/models/res_partner.py @@ -4,25 +4,54 @@ from odoo.exceptions import ValidationError class Partner(models.Model): - _inherit = 'res.partner' + _inherit = "res.partner" - owned_team_ids = fields.One2many(comodel_name='sports.team', - inverse_name='parent_id') - staff_ids = fields.One2many(comodel_name='sports.team.staff', - inverse_name='team_id') - team_staff_rel_ids = fields.One2many(comodel_name='sports.team.staff', - inverse_name='partner_id', - string='Teams Served', - help='The teams this person works for.') - teams_served_ids = fields.One2many(comodel_name='sports.team', compute='_compute_teams_served') - patient_ids = fields.One2many(comodel_name='sports.patient', inverse_name='partner_id') + owned_team_ids = fields.One2many( + comodel_name="sports.team", inverse_name="parent_id" + ) + staff_ids = fields.One2many( + comodel_name="sports.team.staff", inverse_name="team_id" + ) + team_staff_rel_ids = fields.One2many( + comodel_name="sports.team.staff", + inverse_name="partner_id", + string="Employer(s)", + help="The teams this person works for.", + ) + teams_served_ids = fields.One2many( + comodel_name="sports.team", + compute="_compute_teams_served", + inverse="_inverse_teams_served", + ) + patient_ids = fields.One2many( + comodel_name="sports.patient", inverse_name="partner_id" + ) def write(self, vals): - if self.patient_ids and 'name' in vals: - raise ValidationError(_("To change a patient's name, change it from the patient form.")) + if ( + self.patient_ids + and "name" in vals + and not self._context.get("patient_update") + ): + raise ValidationError( + _("To change a patient's name, change it from the patient form.") + ) return super().write(vals) - @api.depends('team_staff_rel_ids.team_id') + @api.depends("team_staff_rel_ids.team_id") def _compute_teams_served(self): for rec in self: - rec.teams_served_ids = rec.team_staff_rel_ids.mapped('team_id') + rec.teams_served_ids = rec.team_staff_rel_ids.mapped("team_id") + + @api.depends("team_staff_rel_ids.team_id") + def _inverse_teams_served(self): + for rec in self: + for staff in rec.team_staff_rel_ids: + if staff.team_id not in rec.teams_served_ids: + staff.unlink() + served_teams = rec.team_staff_rel_ids.mapped("team_id") + for team in rec.teams_served_ids: + if team not in served_teams: + raise UserError( + _("To add a staff member to a team, use the team view.") + ) diff --git a/bemade_sports_clinic/models/res_users.py b/bemade_sports_clinic/models/res_users.py index 1dcd258..0fa392b 100644 --- a/bemade_sports_clinic/models/res_users.py +++ b/bemade_sports_clinic/models/res_users.py @@ -1,14 +1,45 @@ -from odoo import models, fields, api, _ +from odoo import models, fields, api, _, Command class User(models.Model): - _inherit = 'res.users' + _inherit = "res.users" is_treatment_professional = fields.Boolean( - compute="_compute_is_treatment_professional", store=True) + compute="_compute_is_treatment_professional", store=True + ) - @api.depends('groups_id') + accessible_team_ids = fields.Many2many( + comodel_name="sports.team", + compute="_compute_accessible_team_ids", + inverse="_inverse_accessible_team_ids", + ) + + @api.depends("groups_id") def _compute_is_treatment_professional(self): for rec in self: rec.is_treatment_professional = rec.has_group( - 'bemade_sports_clinic.group_sports_clinic_treatment_professional') + "bemade_sports_clinic.group_sports_clinic_treatment_professional" + ) + + def _compute_accessible_team_ids(self): + for rec in self: + rec.accessible_team_ids = rec.partner_id.teams_served_ids + + def _inverse_accessible_team_ids(self): + for rec in self: + removed_teams = rec.partner_id.teams_served_ids - rec.accessible_team_ids + added_teams = rec.accessible_team_ids - rec.partner_id.teams_served_ids + removed_teams = rec.partner_id.teams_served_ids.filtered( + lambda team: team in removed_teams + ) + removed_teams.remove_access(self) + self.env["sports.team.staff"].create( + [ + { + "team_id": team.id, + "partner_id": rec.partner_id.id, + "role": "other", + } + for team in added_teams + ] + ) diff --git a/bemade_sports_clinic/models/sports_team.py b/bemade_sports_clinic/models/sports_team.py index 6f7d589..91300bd 100644 --- a/bemade_sports_clinic/models/sports_team.py +++ b/bemade_sports_clinic/models/sports_team.py @@ -5,105 +5,240 @@ from odoo.exceptions import ValidationError class SportsTeam(models.Model): _name = "sports.team" _description = "Sports Team" + _inherit = ["mail.thread", "mail.activity.mixin"] name = fields.Char() - patient_ids = fields.Many2many(comodel_name='sports.patient', - relation='sports_team_patient_rel', - column1='team_id', - column2='patient_id', - string='Players') + patient_ids = fields.Many2many( + comodel_name="sports.patient", + relation="sports_team_patient_rel", + column1="team_id", + column2="patient_id", + string="Players", + tracking=True, + ) player_count = fields.Integer(compute="_compute_player_counts") injured_count = fields.Integer(compute="_compute_player_counts") healthy_count = fields.Integer(compute="_compute_player_counts") - parent_id = fields.Many2one(comodel_name='res.partner', string='Parent Organization', - ondelete='restrict') - staff_ids = fields.One2many(comodel_name='sports.team.staff', - inverse_name='team_id') - head_coach_id = fields.Many2one(comodel_name='res.partner', - compute='_compute_head_coach', - store=True) - head_coach_name = fields.Char(related='head_coach_id.name') - head_therapist_id = fields.Many2one(comodel_name='res.partner', - compute='_compute_head_therapist', - store=True) - head_therapist_name = fields.Char(related='head_therapist_id.name') + parent_id = fields.Many2one( + comodel_name="res.partner", + string="Parent Organization", + ondelete="restrict", + tracking=True, + ) + staff_ids = fields.One2many( + comodel_name="sports.team.staff", + inverse_name="team_id", + tracking=True, + ) + head_coach_id = fields.Many2one( + comodel_name="res.partner", + compute="_compute_head_coach", + store=True, + ) + head_coach_name = fields.Char( + related="head_coach_id.name", + string="Head Coach Name", + ) + head_therapist_id = fields.Many2one( + comodel_name="res.partner", + compute="_compute_head_therapist", + store=True, + string="Head Therapist", + ) + head_therapist_name = fields.Char( + related="head_therapist_id.name", + string="Head Therapist Name", + ) website = fields.Char() + allowed_user_ids = fields.Many2many( + comodel_name="res.users", + compute="_compute_allowed_user_ids", + inverse="_inverse_allowed_user_ids", + ) - @api.depends('patient_ids.is_injured') + def write(self, vals): + previous_patient_ids = self.sudo().patient_ids + res = super().write(vals) + if "staff_ids" in vals or "patient_ids" in vals: + (self.sudo().patient_ids | previous_patient_ids).recompute_followers() + return res + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for index, rec in enumerate(res): + if "staff_ids" in vals_list[index]: + rec.sudo().patient_ids.recompute_followers() + return res + + def unlink(self): + to_recompute = self.patient_ids + res = super().unlink() + to_recompute.recompute_followers() + return res + + @api.depends("patient_ids.is_injured") def _compute_player_counts(self): for rec in self: rec.player_count = len(rec.patient_ids) rec.injured_count = len(rec.patient_ids.filtered(lambda p: p.is_injured)) rec.healthy_count = rec.player_count - rec.injured_count - @api.depends('staff_ids.role') + @api.depends("staff_ids.role") def _compute_head_coach(self): for rec in self: - staff = rec.staff_ids.filtered(lambda r: r.role == 'head_coach') + staff = rec.staff_ids.filtered(lambda r: r.role == "head_coach") rec.head_coach_id = staff.partner_id if staff else False - @api.depends('staff_ids.role') + @api.depends("staff_ids.role") def _compute_head_therapist(self): for rec in self: - staff = rec.staff_ids.filtered(lambda r: r.role == 'head_therapist') + staff = rec.staff_ids.filtered(lambda r: r.role == "head_therapist") rec.head_therapist_id = staff.partner_id if staff else False + def _compute_allowed_user_ids(self): + for rec in self: + rec.allowed_user_ids = rec.staff_ids.user_ids + + def _inverse_allowed_user_ids(self): + for rec in self: + removed_staff = rec.staff_ids.filtered( + lambda staff: staff.user_ids not in rec.allowed_user_ids + ) + added_users = rec.allowed_user_ids - rec.staff_ids.user_ids + removed_staff.unlink() + self.env["sports.team.staff"].create( + [ + { + "team_id": rec.id, + "partner_id": user.partner_id.id, + "role": "other", + } + for user in added_users + ] + ) + + def remove_access(self, user): + self.staff_ids.filtered(lambda staff: user in staff.user_ids).unlink() + class TeamStaff(models.Model): _name = "sports.team.staff" _description = "Sports Team Staff" sequence = fields.Integer() - team_id = fields.Many2one(comodel_name='sports.team', string='Team', required=True) - partner_id = fields.Many2one(comodel_name='res.partner', string='Staff Member', - required=True, domain=[('is_company', '=', False)]) - role = fields.Selection(selection=[ - ('head_coach', 'Head Coach'), - ('head_therapist', 'Head Therapist'), - ('coach', 'Coach'), - ('therapist', 'Therapist'), - ('doctor', 'Doctor'), - ('other', 'Other') - ], required=True) - mobile = fields.Char(related='partner_id.mobile', readonly=False) - name = fields.Char(related='partner_id.name', readonly=False) - parent_id = fields.Many2one(related='partner_id.parent_id', readonly=False, string="Organization", - domain=[('is_company', '=', True)]) - email = fields.Char(related='partner_id.email', readonly=False) - user_ids = fields.One2many(related='partner_id.user_ids', readonly=True) - has_portal_access = fields.Boolean(compute='_compute_has_portal_access', compute_sudo=True) + team_id = fields.Many2one( + comodel_name="sports.team", + string="Team", + required=True, + ondelete="cascade", + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Staff Member", + required=True, + domain=[("is_company", "=", False)], + ondelete="cascade", + ) + active = fields.Boolean(related="partner_id.active") + role = fields.Selection( + selection=[ + ("head_coach", "Head Coach"), + ("head_therapist", "Head Therapist"), + ("coach", "Coach"), + ("therapist", "Therapist"), + ("doctor", "Doctor"), + ("other", "Other"), + ], + required=True, + ) + mobile = fields.Char(related="partner_id.mobile", readonly=False) + name = fields.Char(related="partner_id.name", readonly=False) + parent_id = fields.Many2one( + related="partner_id.parent_id", + readonly=False, + string="Organization", + domain=[("is_company", "=", True)], + ) + email = fields.Char(related="partner_id.email", readonly=False) + user_ids = fields.One2many(related="partner_id.user_ids", readonly=True) + has_portal_access = fields.Boolean( + compute="_compute_has_portal_access", compute_sudo=True + ) - _sql_constraints = [('team_staff_unique', 'unique(team_id, partner_id)', - 'Each partner can only be related to a given team once.')] + _sql_constraints = [ + ( + "team_staff_unique", + "unique(team_id, partner_id)", + "Each partner can only be related to a given team once.", + ) + ] - @api.constrains('role') + @api.constrains("role") def _constrain_role(self): - teams = self.mapped('team_id') + teams = self.mapped("team_id") for team in teams: - if len(team.staff_ids.filtered(lambda r: r.role == 'head_coach')) > 1: + if len(team.staff_ids.filtered(lambda r: r.role == "head_coach")) > 1: raise ValidationError(_("A team can have only one head coach.")) - if len(team.staff_ids.filtered(lambda r: r.role == 'head_therapist')) > 1: + if len(team.staff_ids.filtered(lambda r: r.role == "head_therapist")) > 1: raise ValidationError(_("A team can have only one head therapist.")) - @api.onchange('mobile') + @api.onchange("mobile") def _onchange_mobile_validation(self): if self.mobile: - self.mobile = self.partner_id._phone_format(self.mobile, force_format='INTERNATIONAL') + self.mobile = self.partner_id._phone_format( + self.mobile, force_format="INTERNATIONAL" + ) - @api.depends('user_ids', 'user_ids.groups_id') + @api.depends("user_ids", "user_ids.groups_id") def _compute_has_portal_access(self): for rec in self: - rec.has_portal_access = bool(rec.user_ids.filtered(lambda r: r.has_group('base.group_portal'))) or bool( - rec.user_ids.filtered(lambda r: r.has_group('base.group_user'))) or bool(rec.partner_id.signup_token) + rec.has_portal_access = ( + bool(rec.user_ids.filtered(lambda r: r.has_group("base.group_portal"))) + or bool(rec.user_ids.filtered(lambda r: r.has_group("base.group_user"))) + or bool(rec.partner_id.signup_token) + ) def action_revoke_portal_access(self): - group_portal = self.env.ref('base.group_portal') - group_public = self.env.ref('base.group_public') + group_portal = self.env.ref("base.group_portal") + group_public = self.env.ref("base.group_public") self.user_ids.write( - {'groups_id': [Command.unlink(group_portal.id), Command.link(group_public.id)], 'active': False}) + { + "groups_id": [ + Command.unlink(group_portal.id), + Command.link(group_public.id), + ], + "active": False, + } + ) # Remove the signup token, so it cannot be used self.partner_id.sudo().signup_token = False def action_grant_portal_access(self): - wiz = self.env['portal.wizard'].create({'partner_ids': [(4, self.partner_id.id)]}) + wiz = self.env["portal.wizard"].create( + {"partner_ids": [(4, self.partner_id.id)]} + ) return wiz._action_open_modal() + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + res.team_id.mapped("patient_ids").recompute_followers() + return res + + def unlink(self): + patients = self.team_id.mapped("patient_ids") + res = super().unlink() + patients.recompute_followers() + return res + + def write(self, values): + if "team_id" in values: + to_recompute = self.env["sports.patient"] + for rec in self: + if rec.team_id.id != values["team_id"]: + to_recompute |= rec.team_id.patient_ids + res = super().write(values) + to_recompute.recompute_followers() + return res + return super().write(values) diff --git a/bemade_sports_clinic/security/sports_clinic_groups.xml b/bemade_sports_clinic/security/sports_clinic_groups.xml index 13a58f2..455b2fe 100644 --- a/bemade_sports_clinic/security/sports_clinic_groups.xml +++ b/bemade_sports_clinic/security/sports_clinic_groups.xml @@ -1,12 +1,20 @@ - + Sports Medicine Clinic Management Internal User + Treatment Professional diff --git a/bemade_sports_clinic/security/sports_clinic_rules.xml b/bemade_sports_clinic/security/sports_clinic_rules.xml index 41bf382..9cfe05b 100644 --- a/bemade_sports_clinic/security/sports_clinic_rules.xml +++ b/bemade_sports_clinic/security/sports_clinic_rules.xml @@ -2,13 +2,111 @@ - + Restrict Team Staff Access to Their Players Only - + + + + + - [('id', 'in', - user.partner_id.team_staff_rel_ids.team_id.patient_ids.ids)] + [('team_ids.staff_ids.user_ids', 'in', user.id)] + + + + Restrict Team Staff Access to Their Teams Only + + + + + + + + [('staff_ids.user_ids', 'in', user.id)] + + + + Restrict Team Staff Access to Their Teams Only + + + + + + + + [('patient_id.team_ids.staff_ids.user_ids', 'in', user.id)] + + + + Restrict Patient Contact Access to Allowed Internal + Users + + + + + + + + + [('patient_id.team_ids.staff_ids.user_ids', 'in', user.id)] + + + + Allow Team Access to Administrators + + + + + + + + [(1, '=', 1)] + + + + Allow Patient Access to Administrators + + + + + + + + [(1, '=', 1)] + + + + Allow Injury Access to Administrators + + + + + + + + [(1, '=', 1)] + + + + Allow Sports Patient Contact Access to Administrators + + + + + + + + + [(1, '=', 1)] diff --git a/bemade_sports_clinic/tests/__init__.py b/bemade_sports_clinic/tests/__init__.py new file mode 100644 index 0000000..e5e9a1e --- /dev/null +++ b/bemade_sports_clinic/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_patient +from . import test_rights +from . import test_users diff --git a/bemade_sports_clinic/tests/test_patient.py b/bemade_sports_clinic/tests/test_patient.py new file mode 100644 index 0000000..69c3c52 --- /dev/null +++ b/bemade_sports_clinic/tests/test_patient.py @@ -0,0 +1,222 @@ +from odoo.tests import TransactionCase, tagged, Form +from odoo import fields, Command +from datetime import timedelta + + +@tagged("-at_install", "post_install") +class TestPatient(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + organization = cls.env["res.partner"].create( + {"name": "Test Org"}, + ) + team1 = cls.env["sports.team"].create( + { + "name": "Test team", + "parent_id": organization.id, + } + ) + coach = cls.env["sports.team.staff"].create( + { + "partner_id": cls.env["res.partner"] + .create( + { + "name": "Test Coach", + } + ) + .id, + "team_id": team1.id, + "role": "head_coach", + } + ) + patient1 = cls.env["sports.patient"].create( + { + "first_name": "Test", + "last_name": "Patient 1", + "team_ids": [Command.set(team1.ids)], + "date_of_birth": fields.Date.today() - timedelta(days=18 * 365), + } + ) + patient1_injury = cls.env["sports.patient.injury"].create( + { + "patient_id": patient1.id, + } + ) + patient2 = cls.env["sports.patient"].create( + { + "first_name": "Test", + "last_name": "Patient2", + "team_ids": [Command.set(team1.ids)], + "date_of_birth": fields.Date.today() - timedelta(days=21 * 365), + } + ) + patient2_injury = cls.env["sports.patient.injury"].create( + { + "patient_id": patient2.id, + } + ) + ( + cls.organization, + cls.team1, + cls.coach, + cls.patient1, + cls.patient1_injury, + cls.patient2, + cls.patient2_injury, + ) = ( + organization, + team1, + coach, + patient1, + patient1_injury, + patient2, + patient2_injury, + ) + + def test_adding_staff_adds_follower_to_patient_and_injury(self): + therapist = self.env["sports.team.staff"].create( + { + "team_id": self.team1.id, + "partner_id": self.env["res.partner"] + .create( + {"name": "Tester"}, + ) + .id, + "role": "therapist", + } + ) + + therapist = therapist.partner_id + self.assertIn(therapist, self.patient1.message_partner_ids) + self.assertIn(therapist, self.patient2.message_partner_ids) + self.assertIn(therapist, self.patient1_injury.message_partner_ids) + self.assertIn(therapist, self.patient2_injury.message_partner_ids) + + def test_removing_staff_removes_follower_from_patient_and_injury(self): + coach = self.coach.partner_id + self.team1.staff_ids = False + self.assertNotIn(coach, self.patient1.message_partner_ids) + self.assertNotIn(coach, self.patient1_injury.message_partner_ids) + self.assertNotIn(coach, self.patient2.message_partner_ids) + self.assertNotIn(coach, self.patient2_injury.message_partner_ids) + + def test_deleting_team_removes_follower_from_patient_and_injury(self): + coach = self.coach.partner_id + self.team1.unlink() + self.assertNotIn(coach, self.patient1.message_partner_ids) + self.assertNotIn(coach, self.patient1_injury.message_partner_ids) + self.assertNotIn(coach, self.patient2.message_partner_ids) + self.assertNotIn(coach, self.patient2_injury.message_partner_ids) + + def test_adding_second_team_subscribes_new_staff(self): + team2, therapist, coach = self._generate_second_team_and_staff() + + team2.patient_ids = self.patient1 + + self.assertIn(therapist, self.patient1.message_partner_ids) + self.assertIn(coach, self.patient1.message_partner_ids) + self.assertIn(therapist, self.patient1_injury.message_partner_ids) + self.assertIn(coach, self.patient1_injury.message_partner_ids) + self.assertEqual(len(self.patient1_injury.message_partner_ids), 2) + self.assertEqual(len(self.patient1.message_partner_ids), 2) + + def test_creating_patient_in_team_assigns_followers(self): + patient = self.env["sports.patient"].create( + { + "first_name": "Test", + "last_name": "Patient", + "date_of_birth": fields.Date.today() - timedelta(days=365 * 20), + "team_ids": [(6, 0, self.team1.ids)], + } + ) + + cp_id = self.coach.partner_id + self.assertIn(cp_id, patient.message_partner_ids) + + injury = self.env["sports.patient.injury"].create( + { + "patient_id": patient.id, + "diagnosis": "Something", + } + ) + self.assertIn(cp_id, injury.message_partner_ids) + + def test_removing_second_team_correctly_adjusts_staff(self): + """Tests both removing from the team side and from the patient side.""" + team2, therapist, coach = self._generate_second_team_and_staff() + self.patient1.write({"team_ids": [Command.link(team2.id)]}) + self.assertIn(self.patient1, team2.patient_ids) + self.assertIn(therapist, self.patient1.message_partner_ids) + + team2.write({"patient_ids": [Command.unlink(self.patient1.id)]}) + + self.assertNotIn(self.patient1, team2.patient_ids) + self.assertEqual(self.patient1.message_partner_ids, coach) + self.assertEqual(self.patient1_injury.message_partner_ids, coach) + + self.patient1.write({"team_ids": [Command.link(team2.id)]}) + + self.assertIn(self.patient1, team2.patient_ids) + self.assertIn(therapist, self.patient1.message_partner_ids) + + self.patient1.write({"team_ids": [Command.unlink(team2.id)]}) + + self.assertNotIn(self.patient1, team2.patient_ids) + self.assertEqual(self.patient1.message_partner_ids, coach) + self.assertEqual(self.patient1_injury.message_partner_ids, coach) + + def test_adding_patient_injury_sets_followers(self): + injury2 = self.env["sports.patient.injury"].create( + { + "patient_id": self.patient1.id, + "diagnosis": "some other injury", + } + ) + + self.assertEqual(injury2.message_partner_ids, self.coach.partner_id) + + def _generate_second_team_and_staff(self): + team2 = self.env["sports.team"].create( + { + "parent_id": self.organization.id, + "name": "Test team 2", + } + ) + therapist = ( + self.env["sports.team.staff"] + .create( + { + "team_id": team2.id, + "partner_id": self.env["res.partner"] + .create( + {"name": "Tester"}, + ) + .id, + "role": "therapist", + } + ) + .partner_id + ) + coach = ( + self.env["sports.team.staff"] + .create( + { + "team_id": team2.id, + "partner_id": self.coach.partner_id.id, + "role": "coach", + } + ) + .partner_id + ) + return team2, therapist, coach + + def test_changing_patient_name_changes_on_partner(self): + new_last_name = "New last name" + new_first_name = "New first name" + with Form(self.patient1) as patient: + patient.last_name = new_last_name + patient.first_name = new_first_name + self.assertEqual( + self.patient1.partner_id.name, " ".join([new_first_name, new_last_name]) + ) diff --git a/bemade_sports_clinic/tests/test_rights.py b/bemade_sports_clinic/tests/test_rights.py new file mode 100644 index 0000000..d27783b --- /dev/null +++ b/bemade_sports_clinic/tests/test_rights.py @@ -0,0 +1,114 @@ +from odoo.tests import TransactionCase, Form, tagged +from odoo.fields import Date +from datetime import timedelta +from odoo.exceptions import AccessError +from odoo import Command +import logging + +_logger = logging.getLogger(__name__) + + +@tagged("-at_install", "post_install") +class TestRights(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create one admin user + cls.admin_user = cls.env["res.users"].create( + { + "name": "Admin User", + "login": "sports_admin", + "password": "sports_admin", + "groups_id": [ + Command.set( + cls.env.ref( + "bemade_sports_clinic.group_sports_clinic_admin" + ).ids, + ), + ], + } + ) + # Create one treatment professional user + cls.treatment_professional_user = cls.env["res.users"].create( + { + "name": "Treatment Professional User", + "login": "treatment_professional", + "password": "treatment_professional", + "groups_id": [ + Command.set( + cls.env.ref( + "bemade_sports_clinic.group_sports_clinic_treatment_professional" + ).ids, + ), + ], + } + ) + # _logger.info( + # f"Treatment Pro Groups: " + # f"{cls.treatment_professional_user.groups_id.mapped('name')}" + # ) + + def test_treatment_pro_has_access_only_to_staffed_teams(self): + """A treatment professional should only have access to teams and, + by extension, patients for which they are a team staff member.""" + team, patients = self._generate_team_with_patient(self.admin_user) + with self.assertRaises(AccessError): + Form( + self.env["sports.team"] + .with_user(self.treatment_professional_user) + .browse(team.id) + ) + with self.assertRaises(AccessError): + Form( + self.env["sports.patient"] + .with_user(self.treatment_professional_user) + .browse(patients[0].id) + ) + + def test_treatment_pro_can_remove_patient_from_team(self): + team, patients = self._generate_team_with_patient(self.admin_user) + self.env["sports.team.staff"].with_user(self.admin_user).create( + { + "team_id": team.id, + "partner_id": self.treatment_professional_user.partner_id.id, + "role": "head_therapist", + } + ) + # Test removing the patient since we are team staff + # Should not throw an error... + with Form(team.with_user(self.treatment_professional_user)) as team: + team.patient_ids.remove(index=0) + self.assertEqual(len(team.patient_ids), 1) + + def _generate_team_with_patient(self, user=None): + user = user or self.env.user + team = ( + self.env["sports.team"] + .with_user(user) + .create( + { + "name": "Test Team", + } + ) + ) + patients = ( + self.env["sports.patient"] + .with_user(user) + .create( + [ + { + "first_name": "Test", + "last_name": "Patient One", + "date_of_birth": Date.today() - timedelta(days=-365 * 18), + "team_ids": [(6, 0, team.ids)], + }, + { + "first_name": "Test", + "last_name": "Patient Two", + "date_of_birth": Date.today() - timedelta(days=-365 * 18), + "team_ids": [(6, 0, team.ids)], + }, + ] + ) + ) + return team, patients diff --git a/bemade_sports_clinic/tests/test_users.py b/bemade_sports_clinic/tests/test_users.py new file mode 100644 index 0000000..3f57c50 --- /dev/null +++ b/bemade_sports_clinic/tests/test_users.py @@ -0,0 +1,35 @@ +from odoo.tests import TransactionCase, tagged, Form +from odoo import Command + + +@tagged("-at_install", "post_install") +class TestUsers(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_add_team_access_to_user(self): + user = self.env["res.users"].create( + { + "name": "test", + "login": "test", + "password": "test", + "groups_id": [ + Command.set( + self.env.ref( + "bemade_sports_clinic.group_sports_clinic_treatment_professional" + ).ids + ) + ], + } + ) + team = self.env["sports.team"].create( + { + "name": "Test", + } + ) + + self.assertNotIn(user, team.staff_ids.user_ids) + user.write({"accessible_team_ids": [Command.link(team.id)]}) + # user._inverse_accessible_team_ids() + self.assertIn(user, team.staff_ids.user_ids) diff --git a/bemade_sports_clinic/views/res_partner_views.xml b/bemade_sports_clinic/views/res_partner_views.xml index e2c77af..1716aa9 100644 --- a/bemade_sports_clinic/views/res_partner_views.xml +++ b/bemade_sports_clinic/views/res_partner_views.xml @@ -8,7 +8,16 @@ - + + + + + + + + + + diff --git a/bemade_sports_clinic/views/res_users_views.xml b/bemade_sports_clinic/views/res_users_views.xml new file mode 100644 index 0000000..cbef072 --- /dev/null +++ b/bemade_sports_clinic/views/res_users_views.xml @@ -0,0 +1,16 @@ + + + + view.users.form.inherit + + res.users + + + + + + + + + \ No newline at end of file diff --git a/bemade_sports_clinic/views/sports_clinic_portal_views.xml b/bemade_sports_clinic/views/sports_clinic_portal_views.xml index 63d0cb7..cca1435 100644 --- a/bemade_sports_clinic/views/sports_clinic_portal_views.xml +++ b/bemade_sports_clinic/views/sports_clinic_portal_views.xml @@ -128,7 +128,7 @@