diff --git a/bemade_sports_clinic/__init__.py b/bemade_sports_clinic/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/bemade_sports_clinic/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/bemade_sports_clinic/__manifest__.py b/bemade_sports_clinic/__manifest__.py new file mode 100644 index 0000000..cc6348f --- /dev/null +++ b/bemade_sports_clinic/__manifest__.py @@ -0,0 +1,46 @@ +# +# Bemade Inc. +# +# Copyright (C) October 2023 Bemade Inc. (). +# Author: Marc Durepos (Contact : marc@bemade.org) +# +# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1) +# It is forbidden to publish, distribute, sublicense, or sell copies of the Software +# or modified copies of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +{ + 'name': 'Sports Clinic Management', + 'version': '16.0.1.0.0', + 'summary': 'Manage the patients of a sports medicine clinic.', + 'description': """ + Adds the notion of sports teams, players (patients), coaches and treatment + professionals. The core purpose of this module is to keep track of the treatment + history of players and to make it appropriately accessible to the various + involved parties (medical practitioners, clinic staff, team staff, and patients). + + Internal users get access to patient data as appropriate, i.e. + treatment professionals get full access to their own patients' data, clinic + staff users get access to basic patient data needed for contacting patients, etc. + + 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': ['base', 'mail'], + 'data': [], + 'demo': [], + 'installable': True, + 'auto_install': False, + 'application': True, +} diff --git a/bemade_sports_clinic/models/__init__.py b/bemade_sports_clinic/models/__init__.py new file mode 100644 index 0000000..977ec36 --- /dev/null +++ b/bemade_sports_clinic/models/__init__.py @@ -0,0 +1,2 @@ +from . import patient +from . import sports_team diff --git a/bemade_sports_clinic/models/patient.py b/bemade_sports_clinic/models/patient.py new file mode 100644 index 0000000..b129111 --- /dev/null +++ b/bemade_sports_clinic/models/patient.py @@ -0,0 +1,80 @@ +from odoo import models, fields, _, api +from odoo.exceptions import ValidationError +from datetime import date +from dateutil import relativedelta + + +class Patient(models.Model): + _name = 'sports.patient' + + first_name = fields.Char(required=True) + last_name = fields.Char(required=True) + date_of_birth = fields.Date() + age = fields.Integer(compute='_compute_age') + phone = fields.Char(unaccent=False) + email = fields.Char() + contact_ids = fields.One2many(comodel_name='sports.patient.contact', + inverse_name='patient_id', string='Patient Contacts', ) + team_ids = fields.Many2many(comodel_name='res.partner', + relation='sports_team_patient_rel', + column1='patient_id', + column2='team_id', + string='Teams', + domain=[('type', '=', 'team')]) + player_status = fields.Selection([ + ('practice', 'Practice'), + ('match', 'Match'), + ]) + injury_ids = fields.One2many(comodel_name='sports.patient.injury', + inverse_name='patient_id', + string='Injuries') + predicted_return_date = fields.Date(compute='_compute_predicted_return_date') + + @api.depends('date_of_birth') + def _compute_age(self): + for rec in self: + if not rec.date_of_birth: + rec.age = False + rec.age = relativedelta(date.today() - rec.date_of_birth).years + + @api.depends('injury_ids.predicted_return_date') + def _compute_predicted_return_date(self): + for rec in self: + ongoing_injuries = rec.injury_ids.filtered( + lambda r: r.predicted_return_date > date.today()).sorted( + 'predicted_return_date') + if ongoing_injuries: + rec.predicted_return_date = ongoing_injuries[-1] + else: + rec.predicted_return_date = False + + +class PatientContact(models.Model): + _name = 'sports.patient.contact' + + sequence = fields.Integer(required=True, default=0) + name = fields.Char(unaccent=False) + contact_type = fields.Selection(selection=[ + ('Mother', 'mother'), + ('Father', 'father'), + ('other', 'Other'), + ]) + phone = fields.Char(unaccent=False) + patient_id = fields.Many2one(comodel_name='sports.patient', string="Patient") + + +class PatientInjury(models.Model): + _name = 'sports.patient.injury' + _inherit = ['mail.thread', 'mail.activity.mixin'] + patient_id = fields.Many2one(comodel_name='sports.patient', string="Patient") + diagnosis = fields.Char(tracking=True) + injury_date_time = fields.Datetime(string='Date and Time of Injury') + internal_notes = fields.Html(tracking=True) + treatment_professional_ids = fields.Many2many(comodel_name='res.partner', + relation='patient_injury_treatment_pro_rel', + column1='patient_injury_id', + column2='treatment_pro_id', + string='Treatment Professionals', + domain=[ + ('type', '=', 'treatment_pro')], ) + predicted_return_date = fields.Date(tracking=True) diff --git a/bemade_sports_clinic/models/res_partner.py b/bemade_sports_clinic/models/res_partner.py new file mode 100644 index 0000000..6dbe4d1 --- /dev/null +++ b/bemade_sports_clinic/models/res_partner.py @@ -0,0 +1,50 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class Partner(models.Model): + _inherit = 'res.partner' + + patient_ids = fields.Many2many(comodel_name='sports.patient', + relation='sports_team_patient_rel', + column1='team_id', + column2='patient_id', + string='Players') + type = fields.Selection(selection_add=[('team', 'Sports Team'), + ('treatment_pro', 'Treatment Professional'),]) + staff_partner_ids = fields.Many2many(comodel_name='res.partner', + relation='sports_team_staff_partner_rel', + column1='team_id', + column2='staff_partner_id', + string='Staff') + + def write(self, vals): + # Override to validate changes to the 'type' field + treatment_pros = self.filtered(lambda r: r.type == 'treatment_pro') + if treatment_pros and 'type' in vals and vals['type'] != 'treatment_pro': + injuries = self.env['sports.patient.injury'].search_count([('treatment_professional_ids', 'in', treatment_pros.ids)]) + if injuries: + raise UserError(_('Partner type cannot be changed since this treatment ' + 'professional appears on a patient injury record.')) + teams = self.filtered(lambda r: r.type == 'team') + if teams and 'type' in vals and vals['type'] != 'team': + if teams.mapped('staff_partner_ids') or teams.mapped('patient_ids'): + raise UserError(_('Sports team is related to patients and/or staff. ' + 'Its type cannot be changed until these relations are ' + 'removed.')) + return super().write(vals) + + def unlink(self): + treatment_pros = self.filtered(lambda r: r.type == 'treatment_pro') + if treatment_pros: + injuries = self.env['sports.patient.injury'].search_count([('treatment_professional_ids', 'in', treatment_pros.ids)]) + if injuries: + raise UserError(_('Treatment professional cannot be deleted since they ' + 'appears on a patient injury record.')) + teams = self.filtered(lambda r: r.type == 'team') + if teams: + if teams.mapped('staff_partner_ids') or teams.mapped('patient_ids'): + raise UserError(_('Sports team is related to patients and/or staff. ' + 'Its type cannot be deleted until these relations are ' + 'removed. You may try archiving it instead.')) + return super().unlink()