Browse Source

Sep 13: [FIX] Bug fixed 'gym_mgmt_system'

16.0
Risvana Cybro 2 days ago
parent
commit
15b3805453
  1. 9
      gym_mgmt_system/__manifest__.py
  2. 12
      gym_mgmt_system/data/gym_membership_cron.xml
  3. 57
      gym_mgmt_system/data/trainer_skill_data.xml
  4. 9
      gym_mgmt_system/doc/RELEASE_NOTES.md
  5. 3
      gym_mgmt_system/models/__init__.py
  6. 66
      gym_mgmt_system/models/account_move.py
  7. 64
      gym_mgmt_system/models/account_payment.py
  8. 218
      gym_mgmt_system/models/gym_attendance.py
  9. 397
      gym_mgmt_system/models/gym_membership.py
  10. 59
      gym_mgmt_system/models/hr_employee.py
  11. 10
      gym_mgmt_system/models/res_partner.py
  12. 23
      gym_mgmt_system/models/sale_order.py
  13. 4
      gym_mgmt_system/models/trainer_skill.py
  14. 16
      gym_mgmt_system/security/ir.model.access.csv
  15. 107
      gym_mgmt_system/views/gym_attendance_views.xml
  16. 93
      gym_mgmt_system/views/gym_membership_views.xml
  17. 6
      gym_mgmt_system/views/gym_menu.xml
  18. 108
      gym_mgmt_system/views/hr_employee_views.xml
  19. 14
      gym_mgmt_system/views/trainer_skill_views.xml
  20. 1
      gym_mgmt_system/wizard/__init__.py
  21. 212
      gym_mgmt_system/wizard/gym_membership_extension.py
  22. 57
      gym_mgmt_system/wizard/gym_membership_extension.xml

9
gym_mgmt_system/__manifest__.py

@ -21,7 +21,7 @@
############################################################################# #############################################################################
{ {
'name': 'GYM Management System', 'name': 'GYM Management System',
'version': '16.0.1.0.0', 'version': '16.0.2.0.0',
'category': 'Industries', 'category': 'Industries',
'summary': 'GYM Management System For Managing ' 'summary': 'GYM Management System For Managing '
'Membership, Member, Workout Plan, etc', 'Membership, Member, Workout Plan, etc',
@ -34,14 +34,19 @@
'maintainer': 'Cybrosys Techno Solutions', 'maintainer': 'Cybrosys Techno Solutions',
'website': "https://www.cybrosys.com", 'website': "https://www.cybrosys.com",
'depends': [ 'mail', 'contacts', 'hr', 'depends': [ 'mail', 'contacts', 'hr',
'product', 'membership', 'sale_management', 'product', 'membership', 'sale_management','hr_skills',
], ],
'data': [ 'data': [
'security/gym_mgmt_system_groups.xml', 'security/gym_mgmt_system_groups.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/gym_mgmt_system_security.xml', 'security/gym_mgmt_system_security.xml',
'data/ir_sequence_data.xml', 'data/ir_sequence_data.xml',
'data/gym_membership_cron.xml',
'data/trainer_skill_data.xml',
'wizard/assign_workout.xml', 'wizard/assign_workout.xml',
'wizard/gym_membership_extension.xml',
'views/gym_menu.xml',
'views/gym_attendance_views.xml',
'views/product_template_views.xml', 'views/product_template_views.xml',
'views/res_partner_views.xml', 'views/res_partner_views.xml',
'views/exercise_for_views.xml', 'views/exercise_for_views.xml',

12
gym_mgmt_system/data/gym_membership_cron.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_cron_expire_memberships" model="ir.cron">
<field name="name">Auto Expire Gym Memberships</field>
<field name="model_id" ref="model_gym_membership"/>
<field name="state">code</field>
<field name="code">model._cron_expire_memberships()</field>
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
<field name="active" eval="True"/>
</record>
</odoo>

57
gym_mgmt_system/data/trainer_skill_data.xml

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="skill_type_gym" model="hr.skill.type">
<field name="name">Gym</field>
<field name="is_gym_skill">True</field>
</record>
<record id="skill_strength_training" model="hr.skill">
<field name="name">Strength Training</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_cardio_training" model="hr.skill">
<field name="name">Cardio Training</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_yoga" model="hr.skill">
<field name="name">Yoga</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_crossfit" model="hr.skill">
<field name="name">CrossFit</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_pilates" model="hr.skill">
<field name="name">Pilates</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_martial_arts" model="hr.skill">
<field name="name">Martial Arts</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_swimming" model="hr.skill">
<field name="name">Swimming</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_nutrition_coaching" model="hr.skill">
<field name="name">Nutrition Coaching</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_rehabilitation" model="hr.skill">
<field name="name">Rehabilitation Training</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
<record id="skill_group_fitness" model="hr.skill">
<field name="name">Group Fitness</field>
<field name="skill_type_id" ref="skill_type_gym"/>
</record>
</odoo>

9
gym_mgmt_system/doc/RELEASE_NOTES.md

@ -4,3 +4,12 @@
#### Version 16.0.1.0.0 #### Version 16.0.1.0.0
#### ADD #### ADD
- Initial commit for GYM Management System - Initial commit for GYM Management System
#### 08.09.2025
#### Version 16.0.2.0.0
#### ADD
- Membership States – Added states (Draft, Confirmed, Active, Paused, Expired) with automatic transitions.
- Pause & Resume – Implemented pause/resume functionality with auto checkout on pause.
- Extend Membership – Added extend button with options (same plan, custom days, upgrade via wizard).
- Attendance Section – Added Quick Check-in, Attendance Records, and state validations.
- Trainer Skills Integration – Linked skills to Employee (Trainer) form with dedicated tab.

3
gym_mgmt_system/models/__init__.py

@ -33,3 +33,6 @@ from . import trainer_skill
from . import workout_days from . import workout_days
from . import workout_plan from . import workout_plan
from . import workout_plan_option from . import workout_plan_option
from . import account_payment
from . import account_move
from . import gym_attendance

66
gym_mgmt_system/models/account_move.py

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Sahla Sherin (<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import models
class AccountMove(models.Model):
_inherit = "account.move"
def _activate_gym_memberships(self):
"""Activate gym memberships when invoice is paid"""
for move in self:
if move.move_type == "out_invoice" and move.payment_state == "paid":
sale_order = None
if move.invoice_origin:
sale_order = self.env["sale.order"].search([
("name", "=", move.invoice_origin)
], limit=1)
if not sale_order:
sale_lines = move.invoice_line_ids.mapped('sale_line_ids')
if sale_lines:
sale_order = sale_lines[0].order_id
if sale_order:
memberships = self.env["gym.membership"].search([
("sale_order_id", "=", sale_order.id),
("state", "=", "confirm")
])
for membership in memberships:
membership.state = "active"
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
def reconcile(self):
"""Check for membership activation after payment reconciliation"""
res = super().reconcile()
invoice_moves = self.mapped('move_id').filtered(
lambda m: m.move_type == 'out_invoice' and m.payment_state == 'paid'
)
invoice_moves._activate_gym_memberships()
return res

64
gym_mgmt_system/models/account_payment.py

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Sahla Sherin (<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import models
import logging
_logger = logging.getLogger(__name__)
class AccountPayment(models.Model):
_inherit = "account.payment"
def action_post(self):
"""Check for membership activation after payment is posted"""
res = super().action_post()
for payment in self:
if payment.partner_type == 'customer' and payment.state == 'posted':
_logger.info(f"Payment {payment.name} posted, checking for memberships to activate")
reconciled_invoice_lines = payment.line_ids.mapped(
'matched_debit_ids.debit_move_id') + payment.line_ids.mapped('matched_credit_ids.credit_move_id')
invoices = reconciled_invoice_lines.mapped('move_id').filtered(lambda m: m.move_type == 'out_invoice')
for invoice in invoices:
if invoice.payment_state == 'paid':
_logger.info(f"Invoice {invoice.name} is now fully paid")
sale_order = None
if invoice.invoice_origin:
sale_order = self.env["sale.order"].search([
("name", "=", invoice.invoice_origin)
], limit=1)
if sale_order:
memberships = self.env["gym.membership"].search([
("sale_order_id", "=", sale_order.id),
("state", "=", "confirm")
])
for membership in memberships:
membership.state = "active"
_logger.info(
f"Activated membership {membership.reference} due to payment {payment.name}")
return res

218
gym_mgmt_system/models/gym_attendance.py

@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Sahla Sherin (<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class GymAttendance(models.Model):
"""Simple Gym Attendance Model"""
_name = 'gym.attendance'
_description = 'Gym Attendance'
_order = 'check_in desc'
_rec_name = 'member_id'
member_id = fields.Many2one('res.partner', string='Member', required=True,
domain="[('gym_member', '=', True)]")
check_in = fields.Datetime(string='Check In', required=True,
default=fields.Datetime.now)
check_out = fields.Datetime(string='Check Out')
duration = fields.Float(string='Duration (Hours)',
compute='_compute_duration', store=True)
state = fields.Selection([
('checked_in', 'Checked In'),
('checked_out', 'Checked Out')
], string='State', compute='_compute_state', store=True)
@api.depends('check_out')
def _compute_state(self):
for record in self:
record.state = 'checked_out' if record.check_out else 'checked_in'
@api.depends('check_in', 'check_out')
def _compute_duration(self):
for record in self:
if record.check_in and record.check_out:
delta = record.check_out - record.check_in
record.duration = delta.total_seconds() / 3600
else:
record.duration = 0.0
@api.model
def create(self, vals):
"""Override create to validate BEFORE creating the record"""
if 'member_id' in vals:
member_id = vals['member_id']
existing_checkin = self.search([
('member_id', '=', member_id),
('check_out', '=', False)
])
if existing_checkin:
member_name = self.env['res.partner'].browse(member_id).name
raise UserError(_('%s is already checked in at %s. Please check out first.') %
(member_name, existing_checkin.check_in.strftime('%Y-%m-%d %H:%M:%S')))
member = self.env['res.partner'].browse(member_id)
validation = self._validate_member_can_checkin(member)
if not validation['can_checkin']:
raise UserError(validation['message'])
return super(GymAttendance, self).create(vals)
def write(self, vals):
"""Override write to validate member changes"""
if 'member_id' in vals:
existing_checkin = self.env['gym.attendance'].search([
('member_id', '=', vals['member_id']),
('check_out', '=', False),
('id', 'not in', self.ids) # Exclude current records
])
if existing_checkin:
member_name = self.env['res.partner'].browse(vals['member_id']).name
raise UserError(_('%s is already checked in. Cannot change to this member.') % member_name)
return super(GymAttendance, self).write(vals)
def action_check_in(self):
"""Check in a member - simplified since validation is now in create()"""
self.ensure_one()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': f'Welcome {self.member_id.name}! Check-in successful.',
'type': 'success',
'sticky': False,
}
}
def _validate_member_can_checkin(self, member):
"""Validate if member can check in - returns dict with can_checkin boolean and message"""
active_membership = self.env['gym.membership'].search([
('member_id', '=', member.id),
('state', '=', 'active')
], order='effective_end_date desc', limit=1)
if active_membership:
if active_membership.effective_end_date and active_membership.effective_end_date < fields.Date.today():
return {
'can_checkin': False,
'message': _(
'Cannot check in. Your active membership expired on %s.\n'
'Please renew your membership.'
) % active_membership.effective_end_date.strftime('%Y-%m-%d')
}
return {
'can_checkin': True,
'message': _('Check-in allowed with active membership.')
}
membership = self.env['gym.membership'].search([
('member_id', '=', member.id),
('state', 'in', ['paused', 'expired', 'confirmed'])
], order='id desc', limit=1)
if not membership:
return {
'can_checkin': False,
'message': _('No membership found for this member.')
}
if membership.state == 'paused':
return {
'can_checkin': False,
'message': _(
'Cannot check in. Membership is currently PAUSED.\n'
'Please resume your membership to check in.'
)
}
elif membership.state == 'expired':
return {
'can_checkin': False,
'message': _(
'Cannot check in. Membership has EXPIRED.\n'
'Please renew your membership to continue.'
)
}
elif membership.state == 'confirmed':
return {
'can_checkin': False,
'message': _(
'Cannot check in. Membership is confirmed but not yet active.\n'
'Please contact administration to activate your membership.'
)
}
else:
return {
'can_checkin': False,
'message': _(
'Cannot check in. Membership is not active.\n'
'Current status: %s'
) % membership.state.title()
}
def action_check_out(self):
"""Check out manually"""
self.ensure_one()
if self.check_out:
raise UserError(_('Already checked out.'))
self.check_out = fields.Datetime.now()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': f'Goodbye {self.member_id.name}! Duration: {self.duration:.2f} hours',
'type': 'success',
'sticky': False,
}
}
@api.model
def quick_checkin(self, member_id):
"""Method for quick check-in from external calls"""
member = self.env['res.partner'].browse(member_id)
if not member.exists():
raise UserError(_('Member not found.'))
attendance = self.create({
'member_id': member_id,
'check_in': fields.Datetime.now(),
})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': f'Welcome {member.name}! Check-in successful.',
'type': 'success',
'sticky': False,
}
}

397
gym_mgmt_system/models/gym_membership.py

@ -20,6 +20,11 @@
# #
############################################################################# #############################################################################
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from odoo.exceptions import UserError
from datetime import date, timedelta
import logging
_logger = logging.getLogger(__name__)
class GymMembership(models.Model): class GymMembership(models.Model):
@ -29,21 +34,33 @@ class GymMembership(models.Model):
_description = "Gym Membership" _description = "Gym Membership"
_rec_name = "reference" _rec_name = "reference"
reference = fields.Char(string='GYM reference',readonly=True, reference = fields.Char(string='GYM reference', readonly=True, default=lambda self: _('New'),
default=lambda self: _('New')) help="Member reference")
member_id = fields.Many2one('res.partner', string='Member', member_id = fields.Many2one('res.partner', string='Member',
required=True, tracking=True, required=True, tracking=True,
help="Member taken the membership",
domain="[('gym_member', '!=',False)]") domain="[('gym_member', '!=',False)]")
membership_scheme_id = fields.Many2one('product.product', membership_scheme_id = fields.Many2one('product.product',
string='Membership scheme', string='Membership scheme',
help="Member ship scheme",
required=True, tracking=True, required=True, tracking=True,
domain="[('membership_date_from', " domain="[('membership_date_from', "
"'!=',False)]") "'!=',False)]")
paid_amount = fields.Float(string="Paid Amount", tracking=True) membership_duration = fields.Integer(string='Plan Duration (Days)', compute='_compute_membership_duration')
paid_amount = fields.Monetary(
string="Paid Amount",
compute="_compute_paid_amount",
store=True,
currency_field="company_currency_id",
tracking=True,
)
company_currency_id = fields.Many2one("res.currency", related="company_id.currency_id", readonly=True)
membership_fees = fields.Float(string="Membership Fees", tracking=True, membership_fees = fields.Float(string="Membership Fees", tracking=True,
help="The membership fees",
related="membership_scheme_id.list_price") related="membership_scheme_id.list_price")
sale_order_id = fields.Many2one('sale.order', string='Sales Order', sale_order_id = fields.Many2one('sale.order', string='Sales Order',
ondelete='cascade', copy=False, ondelete='cascade', copy=False,
help="Order reference",
readonly=True) readonly=True)
membership_date_from = fields.Date(string='Membership Start Date', membership_date_from = fields.Date(string='Membership Start Date',
related="membership_scheme_id." related="membership_scheme_id."
@ -55,27 +72,383 @@ class GymMembership(models.Model):
"date_to", "date_to",
help='Date until which membership remains' help='Date until which membership remains'
'active.') 'active.')
effective_start_date = fields.Date(string='Effective Start Date',
compute='_compute_effective_dates',
help='Actual start date considering pauses')
effective_end_date = fields.Date(string='Effective End Date',
compute='_compute_effective_dates',
help='Actual end date considering pauses and extensions')
current_pause_start = fields.Date(string='Current Pause Start Date',
help='Start date of current pause period')
total_paused_days = fields.Integer(string='Total Paused Days', default=0,
help='Total number of days this membership has been paused')
pause_history_ids = fields.One2many('gym.membership.pause', 'membership_id',
string='Pause History')
extension_history_ids = fields.One2many('gym.membership.extension', 'membership_id',
string='Extension History')
total_extended_days = fields.Integer(string='Total Extended Days', default=0,
help='Total number of days this membership has been extended')
company_id = fields.Many2one('res.company', string='Company', company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.company, default=lambda self: self.env.company,
help='The field hold the company id') help='The field hold the company id')
state = fields.Selection([ state = fields.Selection([
('draft', 'Draft'), ('draft', 'Draft'),
('confirm', 'Confirm'), ('confirm', 'Confirmed'),
('active', 'Active'),
('paused', 'Paused'),
('expired', 'Expired'),
('cancelled', 'Cancelled') ('cancelled', 'Cancelled')
], default='draft', string='Status', ], default='draft', string='Status',
tracking=True,
help="The status of record defined here") help="The status of record defined here")
extension_count = fields.Integer(
string='Extension Count',
compute='_compute_extension_count',
help='Number of times this membership has been extended'
)
can_extend = fields.Boolean(
string='Can Extend',
compute='_compute_can_extend'
)
_sql_constraints = [ _sql_constraints = [
('membership_date_greater', ('membership_date_greater',
'check(membership_date_to >= membership_date_from)', 'check(membership_date_to >= membership_date_from)',
'Error ! Ending Date cannot be set before Beginning Date.') 'Error ! Ending Date cannot be set before Beginning Date.')
] ]
@api.model @api.depends('state', 'effective_end_date')
def create_multi(self, vals): def _compute_can_extend(self):
"""Compute if membership can be extended (only if expired)"""
for rec in self:
rec.can_extend = rec.state == 'expired'
@api.depends('membership_date_from', 'membership_date_to', 'total_paused_days', 'total_extended_days')
def _compute_effective_dates(self):
"""Compute effective start and end dates based on pauses and extensions"""
for rec in self:
rec.effective_start_date = rec.membership_date_from
if rec.membership_date_to:
additional_days = rec.total_paused_days + rec.total_extended_days
rec.effective_end_date = rec.membership_date_to + timedelta(days=additional_days)
else:
rec.effective_end_date = rec.membership_date_to
@api.model_create_multi
def create(self, vals_list):
"""Sequence number for membership """ """Sequence number for membership """
if vals.get('reference', ('New')) == ('New'): for vals in vals_list:
vals['reference'] = self.env['ir.sequence'].next_by_code( if vals.get('reference', 'New') == 'New':
'gym.membership') or ('New') vals['reference'] = self.env['ir.sequence'].next_by_code(
res = super(GymMembership, self).create(vals) 'gym.membership') or 'New'
return res return super(GymMembership, self).create(vals_list)
def action_confirm(self):
for rec in self:
rec.state = 'confirm'
def action_set_active(self):
for rec in self:
rec.state = 'active'
def action_pause(self):
"""Pause the membership - only from active state"""
for rec in self:
if rec.state != 'active':
raise UserError(_('Only active memberships can be paused.'))
current_attendance = self.env['gym.attendance'].search([
('member_id', '=', rec.member_id.id),
('check_out', '=', False)
], limit=1)
if current_attendance:
current_attendance.write({
'check_out': fields.Datetime.now()
})
rec.message_post(
body=_('Member automatically checked out at %s due to membership pause.') %
fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
message_type='notification'
)
rec.current_pause_start = date.today()
rec.state = 'paused'
rec.message_post(
body=_('Membership paused on %s') % date.today().strftime('%Y-%m-%d'),
message_type='notification'
)
def action_resume(self):
"""Resume the membership from paused state"""
for rec in self:
if rec.state != 'paused':
raise UserError(_('Only paused memberships can be resumed.'))
if not rec.current_pause_start:
raise UserError(_('No pause start date found.'))
pause_end = date.today()
paused_days = (pause_end - rec.current_pause_start).days
self.env['gym.membership.pause'].create({
'membership_id': rec.id,
'pause_start': rec.current_pause_start,
'pause_end': pause_end,
'days_paused': paused_days,
})
rec.total_paused_days += paused_days
rec.current_pause_start = False
rec.state = 'active'
rec.message_post(
body=_('Membership resumed on %s. Paused for %s days.') % (
pause_end.strftime('%Y-%m-%d'), paused_days
),
message_type='notification'
)
def _auto_checkout_member(self, reason="membership status change"):
"""Helper method to automatically check out a member"""
self.ensure_one()
current_attendance = self.env['gym.attendance'].search([
('member_id', '=', self.member_id.id),
('check_out', '=', False)
], limit=1)
if current_attendance:
current_attendance.write({
'check_out': fields.Datetime.now()
})
self.message_post(
body=_('Member automatically checked out at %s due to %s') %
(fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S'), reason),
message_type='notification'
)
return True
return False
def action_expire(self):
"""Expire the membership and auto check-out if needed"""
for rec in self:
rec._auto_checkout_member("membership expiry")
rec.state = 'expired'
rec.message_post(
body=_('Membership expired on %s') % date.today().strftime('%Y-%m-%d'),
message_type='notification'
)
def action_cancel(self):
"""Cancel the membership and auto check-out if needed"""
for rec in self:
rec._auto_checkout_member("membership cancellation")
rec.state = 'cancelled'
def action_extend_membership(self):
"""Open wizard to extend membership - only for expired memberships"""
self.ensure_one()
if self.state != 'expired':
raise UserError(_('Only expired memberships can be extended.'))
recent_extension = self.env['gym.membership.extension'].search([
('membership_id', '=', self.id),
('extension_date', '>', fields.Date.today() - timedelta(days=30))
], limit=1)
if recent_extension:
raise UserError(_(
'This membership was already extended on %s. '
'You cannot extend again within 30 days of the last extension.'
) % recent_extension.extension_date.strftime('%Y-%m-%d'))
return {
'name': _('Extend Membership'),
'type': 'ir.actions.act_window',
'res_model': 'gym.membership.extend.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_membership_id': self.id,
'default_member_id': self.member_id.id,
}
}
def force_checkout_inactive_members(self):
"""
Manual method to force check-out all members who are checked in
but have inactive memberships (paused, expired, cancelled)
"""
inactive_memberships = self.search([
('state', 'in', ['paused', 'expired', 'cancelled'])
])
checkout_count = 0
for membership in inactive_memberships:
current_attendance = self.env['gym.attendance'].search([
('member_id', '=', membership.member_id.id),
('check_out', '=', False)
])
if current_attendance:
current_attendance.write({
'check_out': fields.Datetime.now()
})
checkout_count += 1
membership.message_post(
body=_('Forced check-out at %s due to inactive membership status: %s') %
(fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S'), membership.state),
message_type='notification'
)
if checkout_count > 0:
message = _('%s members were automatically checked out due to inactive memberships.') % checkout_count
notification_type = 'success'
else:
message = _('No members needed to be checked out.')
notification_type = 'info'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': message,
'type': notification_type,
'sticky': False,
}
}
def _check_and_activate_membership(self):
for membership in self:
invoices = membership.sale_order_id.invoice_ids.filtered(lambda inv: inv.move_type == 'out_invoice')
if invoices and all(inv.payment_state == "paid" for inv in invoices):
membership.state = "active"
@api.depends('sale_order_id.invoice_ids.payment_state', 'sale_order_id.invoice_ids.amount_residual')
def _compute_paid_amount(self):
for membership in self:
total_paid = 0.0
if membership.sale_order_id:
invoices = membership.sale_order_id.invoice_ids.filtered(
lambda inv: inv.move_type == "out_invoice" and inv.state == "posted"
)
for invoice in invoices:
total_paid += (invoice.amount_total - invoice.amount_residual)
membership.paid_amount = total_paid
@api.model
def _cron_expire_memberships(self):
"""Cron job to automatically set memberships to expired when effective end date passes"""
today = fields.Date.today()
_logger.info(f"Starting membership expiry cron job for date: {today}")
active_memberships = self.search([
('state', 'in', ['active', 'paused']),
('effective_end_date', '!=', False),
('effective_end_date', '<', today)
])
expired_count = 0
_logger.info(f"Found {len(active_memberships)} memberships to check for expiry")
for membership in active_memberships:
try:
if membership.effective_end_date and membership.effective_end_date < today:
_logger.info(
f"Expiring membership {membership.reference} - End date: {membership.effective_end_date}")
membership._auto_checkout_member("automatic membership expiry")
membership.state = 'expired'
membership.message_post(
body=_('Membership automatically expired on %s (End date: %s)') %
(today.strftime('%Y-%m-%d'), membership.effective_end_date.strftime('%Y-%m-%d')),
message_type='notification'
)
expired_count += 1
except Exception as e:
_logger.error(f"Error expiring membership {membership.reference}: {str(e)}")
continue
_logger.info(f"Cron job completed. Expired {expired_count} memberships")
@api.depends('membership_date_from', 'membership_date_to')
def _compute_membership_duration(self):
for rec in self:
if rec.membership_date_from and rec.membership_date_to:
rec.membership_duration = (rec.membership_date_to - rec.membership_date_from).days + 1
else:
rec.membership_duration = 0
def complete_extension(self, days_extended, extension_amount, sale_order_id=None):
"""Complete the extension process and reactivate membership"""
self.ensure_one()
if self.state != 'expired':
raise UserError(_('Only expired memberships can be extended.'))
if days_extended <= 0:
raise UserError(_('Days extended must be greater than 0.'))
extension_record = self.env['gym.membership.extension'].create({
'membership_id': self.id,
'extension_date': fields.Date.today(),
'days_extended': days_extended,
'extension_amount': extension_amount,
'sale_order_id': sale_order_id,
'notes': f'Membership extended by {days_extended} days for ${extension_amount}'
})
self.total_extended_days += days_extended
self.state = 'active'
self._compute_effective_dates()
self.message_post(
body=_('Membership extended by %s days on %s. Amount paid: %s. Membership reactivated. New end date: %s') %
(days_extended, fields.Date.today().strftime('%Y-%m-%d'),
extension_amount, self.effective_end_date.strftime('%Y-%m-%d')),
message_type='notification'
)
return extension_record
@api.depends('extension_history_ids')
def _compute_extension_count(self):
for rec in self:
rec.extension_count = len(rec.extension_history_ids)
@api.depends('state', 'extension_count')
def _compute_can_extend(self):
"""Compute if membership can be extended"""
for rec in self:
max_extensions = 3
rec.can_extend = (
rec.state == 'expired' and
rec.extension_count < max_extensions
)
class GymMembershipPause(models.Model):
"""Model to track pause history"""
_name = 'gym.membership.pause'
_description = 'Gym Membership Pause History'
_order = 'pause_start desc'
membership_id = fields.Many2one('gym.membership', string='Membership', required=True, ondelete='cascade')
pause_start = fields.Date(string='Pause Start Date', required=True)
pause_end = fields.Date(string='Pause End Date', required=True)
days_paused = fields.Integer(string='Days Paused', required=True)
notes = fields.Text(string='Notes')
class GymMembershipExtension(models.Model):
"""Model to track extension history"""
_name = 'gym.membership.extension'
_description = 'Gym Membership Extension History'
_order = 'extension_date desc'
membership_id = fields.Many2one('gym.membership', string='Membership', required=True, ondelete='cascade')
extension_date = fields.Date(string='Extension Date', required=True, default=fields.Date.today)
days_extended = fields.Integer(string='Days Extended', required=True)
extension_amount = fields.Float(string='Extension Amount', required=True)
sale_order_id = fields.Many2one('sale.order', string='Extension Sale Order')
notes = fields.Text(string='Notes')

59
gym_mgmt_system/models/hr_employee.py

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################# #############################################################################
# #
@ -19,16 +20,58 @@
# If not, see <http://www.gnu.org/licenses/>. # If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################# #############################################################################
from odoo import fields, models from odoo import api, fields, models, _
class HrEmployee(models.Model): class HrEmployee(models.Model):
"""Inherited the model hr employee for adding some field to check whether he """Inherited the model hr employee for adding gym trainer field."""
is trainer or not."""
_inherit = 'hr.employee' _inherit = 'hr.employee'
trainer = fields.Boolean(string='Gym Trainer', is_trainer = fields.Boolean(string='Gym Trainer',
help="The employee is trainer ") help="The employee is trainer")
exercise_for_ids = fields.Many2many("trainer.skill",
string="Specialization", gym_skill_ids = fields.Many2many(
help="Skill of the trainer") 'hr.skill',
string='Gym Specializations',
compute='_compute_gym_skills',
help="Gym-related skills of the trainer (read-only display)"
)
@api.depends('skill_ids')
def _compute_gym_skills(self):
"""Get only gym-related skills from all employee skills"""
for employee in self:
try:
if employee.skill_ids:
gym_skills = employee.skill_ids.filtered(
lambda emp_skill: emp_skill.skill_id and emp_skill.skill_id.skill_type_id.is_gym_skill
)
employee.gym_skill_ids = gym_skills.mapped('skill_id')
else:
employee.gym_skill_ids = False
except Exception:
employee.gym_skill_ids = False
class HrSkillType(models.Model):
"""Extend HR Skill Type for gym category"""
_inherit = 'hr.skill.type'
is_gym_skill = fields.Boolean(string='Is Gym Skill', default=False)
class HrSkill(models.Model):
"""Extend HR Skill for gym skills"""
_inherit = 'hr.skill'
is_gym_skill = fields.Boolean(related='skill_type_id.is_gym_skill', store=True)
class HrSkillLevel(models.Model):
_inherit = 'hr.skill.level'
skill_type_id = fields.Many2one(
'hr.skill.type',
string="Skill Type",
required=True,
help="The skill type this level belongs to (e.g. Gym, Dev, Music)."
)

10
gym_mgmt_system/models/res_partner.py

@ -26,13 +26,13 @@ class ResPartner(models.Model):
"""Inherited the partner model for adding gym related fields.""" """Inherited the partner model for adding gym related fields."""
_inherit = 'res.partner' _inherit = 'res.partner'
gym_member = fields.Boolean(string='Gym Member', default=True, gym_member = fields.Boolean(string='Gym Member', default=False,
help='This field define the whether is member' help='This field define the whether is '
'of gym') 'member of gym')
membership_count = fields.Integer('membership_count', membership_count = fields.Integer(string='Membership Count',
compute='_compute_membership_count', compute='_compute_membership_count',
help='This help to count the membership') help='This help to count the membership')
measurement_count = fields.Integer('measurement_count', measurement_count = fields.Integer(string='Measurement Count',
compute='_compute_measurement_count', compute='_compute_measurement_count',
help='This helps to get the umber of ' help='This helps to get the umber of '
'measurements for gym members') 'measurements for gym members')

23
gym_mgmt_system/models/sale_order.py

@ -27,17 +27,18 @@ class SaleOrder(models.Model):
_inherit = "sale.order" _inherit = "sale.order"
def action_confirm(self): def action_confirm(self):
""" Membership created directly from sale order confirmed """ """Membership created directly from sale order confirmed"""
product = self.env['product.product'].search([ product = self.env['product.product'].search([
('membership_date_from', '!=', False), ('membership_date_from', '!=', False),
('id', '=', self.order_line.product_id.id)]) ('id', 'in', self.order_line.product_id.ids)
])
for record in product: for record in product:
self.env['gym.membership'].create([ membership = self.env['gym.membership'].create({
{'member_id': self.partner_id.id, 'member_id': self.partner_id.id,
'membership_date_from': record.membership_date_from, 'membership_date_from': record.membership_date_from,
'membership_scheme_id': self.order_line.product_id.id, 'membership_scheme_id': record.id,
'sale_order_id': self.id, 'sale_order_id': self.id,
}]) 'state': 'confirm',
})
res = super(SaleOrder, self).action_confirm() self.partner_id.gym_member = True
return res return super().action_confirm()

4
gym_mgmt_system/models/trainer_skill.py

@ -28,8 +28,8 @@ class TrainerSkill(models.Model):
_inherit = ["mail.thread", "mail.activity.mixin"] _inherit = ["mail.thread", "mail.activity.mixin"]
_description = "Trainer Skill" _description = "Trainer Skill"
name = fields.Char(string="Name", help="Name") name = fields.Char(string="Name", help="Name of the trainer", required=True)
code = fields.Char(string="Code", help="Code") code = fields.Char(string="Code", help="Code for the trainer")
company_id = fields.Many2one('res.company', string='Company', company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.company, default=lambda self: self.env.company,
help="This field hold company id") help="This field hold company id")

16
gym_mgmt_system/security/ir.model.access.csv

@ -8,9 +8,6 @@ access_hr_employee_operator,access.hr.employee,model_hr_employee,gym_mgmt_system
access_sale_order_trainer,access.sale.order,model_sale_order,gym_mgmt_system.group_gym_trainer,1,1,1,0 access_sale_order_trainer,access.sale.order,model_sale_order,gym_mgmt_system.group_gym_trainer,1,1,1,0
access_sale_order_member,access.sale.order,model_sale_order,gym_mgmt_system.group_gym_member,1,0,0,0 access_sale_order_member,access.sale.order,model_sale_order,gym_mgmt_system.group_gym_member,1,0,0,0
access_sale_order_operator,access.sale.order,model_sale_order,gym_mgmt_system.group_gym_operator,1,1,1,1 access_sale_order_operator,access.sale.order,model_sale_order,gym_mgmt_system.group_gym_operator,1,1,1,1
access_product_template_trainer,access.product.template,model_product_template,gym_mgmt_system.group_gym_trainer,1,1,1,0
access_product_template_member,access.product.template,model_product_template,gym_mgmt_system.group_gym_member,1,0,0,0
access_product_template_operator,access.product.template,model_product_template,gym_mgmt_system.group_gym_operator,1,1,1,1
access_res_partner_trainer,access.res.partner,model_res_partner,gym_mgmt_system.group_gym_trainer,1,0,0,0 access_res_partner_trainer,access.res.partner,model_res_partner,gym_mgmt_system.group_gym_trainer,1,0,0,0
access_res_partner_operator,access.res.partner,model_res_partner,gym_mgmt_system.group_gym_operator,1,1,1,1 access_res_partner_operator,access.res.partner,model_res_partner,gym_mgmt_system.group_gym_operator,1,1,1,1
access_res_partner_member,access.res.partner,model_res_partner,gym_mgmt_system.group_gym_member,1,0,0,0 access_res_partner_member,access.res.partner,model_res_partner,gym_mgmt_system.group_gym_member,1,0,0,0
@ -20,9 +17,6 @@ access_gym_membership_member,access.gym.membership,model_gym_membership,gym_mgmt
access_measurement_history_operator,access.measurement.history,model_measurement_history,gym_mgmt_system.group_gym_operator,1,1,1,1 access_measurement_history_operator,access.measurement.history,model_measurement_history,gym_mgmt_system.group_gym_operator,1,1,1,1
access_measurement_history_trainer,access.measurement.history,model_measurement_history,gym_mgmt_system.group_gym_trainer,1,1,1,1 access_measurement_history_trainer,access.measurement.history,model_measurement_history,gym_mgmt_system.group_gym_trainer,1,1,1,1
access_measurement_history_member,access.measurement.history,model_measurement_history,gym_mgmt_system.group_gym_member,1,0,0,0 access_measurement_history_member,access.measurement.history,model_measurement_history,gym_mgmt_system.group_gym_member,1,0,0,0
access_trainer_skill_operator,access.trainer_skill,model_trainer_skill,gym_mgmt_system.group_gym_operator,1,1,1,1
access_trainer_skill_member,access.trainer_skill,model_trainer_skill,gym_mgmt_system.group_gym_member,1,0,0,0
access_trainer_skill_trainer,access.trainer_skill,model_trainer_skill,gym_mgmt_system.group_gym_trainer,1,1,1,1
access_product_template_trainer,access.product_template,model_product_template,gym_mgmt_system.group_gym_trainer,1,1,1,1 access_product_template_trainer,access.product_template,model_product_template,gym_mgmt_system.group_gym_trainer,1,1,1,1
access_product_template_member,access.product_template,model_product_template,gym_mgmt_system.group_gym_member,1,0,0,0 access_product_template_member,access.product_template,model_product_template,gym_mgmt_system.group_gym_member,1,0,0,0
access_product_template_operator,access.product_template,model_product_template,gym_mgmt_system.group_gym_operator,1,1,1,1 access_product_template_operator,access.product_template,model_product_template,gym_mgmt_system.group_gym_operator,1,1,1,1
@ -47,3 +41,13 @@ access_trainer_skill_member,access.trainer.skill,model_trainer_skill,gym_mgmt_sy
access_my_workout_plan_operator,access.my.workout.plan,model_my_workout_plan,gym_mgmt_system.group_gym_operator,1,1,1,1 access_my_workout_plan_operator,access.my.workout.plan,model_my_workout_plan,gym_mgmt_system.group_gym_operator,1,1,1,1
access_my_workout_plan_trainer,access.my.workout.plan,model_my_workout_plan,gym_mgmt_system.group_gym_trainer,1,1,1,1 access_my_workout_plan_trainer,access.my.workout.plan,model_my_workout_plan,gym_mgmt_system.group_gym_trainer,1,1,1,1
access_my_workout_plan_member,access.my.workout.plan,model_my_workout_plan,gym_mgmt_system.group_gym_member,1,0,0,0 access_my_workout_plan_member,access.my.workout.plan,model_my_workout_plan,gym_mgmt_system.group_gym_member,1,0,0,0
access_gym_membership_pause_operator,access.gym.membership.pause operator,model_gym_membership_pause,gym_mgmt_system.group_gym_operator,1,1,1,1
access_gym_membership_pause_trainer,access.gym.membership.pause trainer,model_gym_membership_pause,gym_mgmt_system.group_gym_trainer,1,0,0,0
access_membership_pause_member,access.gym.membership.pause member,model_gym_membership_pause,gym_mgmt_system.group_gym_member,1,0,0,0
access_membership_extension_operator,access.gym.membership.extension operator,model_gym_membership_extension,gym_mgmt_system.group_gym_operator,1,1,1,1
access_membership_extension_trainer,access.gym.membership.extension trainer,model_gym_membership_extension,gym_mgmt_system.group_gym_trainer,1,0,0,0
access_membership_extension_member,access.gym.membership.extension member,model_gym_membership_extension,gym_mgmt_system.group_gym_member,1,0,0,0
access_membership_extend_wizard_operator,access.gym.membership.extend.wizard operator,model_gym_membership_extend_wizard,gym_mgmt_system.group_gym_operator,1,1,1,1
access_gym_attendance_operator,access.gym.attendance.operator,model_gym_attendance,gym_mgmt_system.group_gym_operator,1,1,1,1
access_gym_attendance_trainer,access.gym.attendance.trainer,model_gym_attendance,gym_mgmt_system.group_gym_trainer,1,0,0,0
access_gym_attendance_member,access.gym.attendance.member,model_gym_attendance,gym_mgmt_system.group_gym_member,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_sale_order_trainer access.sale.order model_sale_order gym_mgmt_system.group_gym_trainer 1 1 1 0
9 access_sale_order_member access.sale.order model_sale_order gym_mgmt_system.group_gym_member 1 0 0 0
10 access_sale_order_operator access.sale.order model_sale_order gym_mgmt_system.group_gym_operator 1 1 1 1
access_product_template_trainer access.product.template model_product_template gym_mgmt_system.group_gym_trainer 1 1 1 0
access_product_template_member access.product.template model_product_template gym_mgmt_system.group_gym_member 1 0 0 0
access_product_template_operator access.product.template model_product_template gym_mgmt_system.group_gym_operator 1 1 1 1
11 access_res_partner_trainer access.res.partner model_res_partner gym_mgmt_system.group_gym_trainer 1 0 0 0
12 access_res_partner_operator access.res.partner model_res_partner gym_mgmt_system.group_gym_operator 1 1 1 1
13 access_res_partner_member access.res.partner model_res_partner gym_mgmt_system.group_gym_member 1 0 0 0
17 access_measurement_history_operator access.measurement.history model_measurement_history gym_mgmt_system.group_gym_operator 1 1 1 1
18 access_measurement_history_trainer access.measurement.history model_measurement_history gym_mgmt_system.group_gym_trainer 1 1 1 1
19 access_measurement_history_member access.measurement.history model_measurement_history gym_mgmt_system.group_gym_member 1 0 0 0
access_trainer_skill_operator access.trainer_skill model_trainer_skill gym_mgmt_system.group_gym_operator 1 1 1 1
access_trainer_skill_member access.trainer_skill model_trainer_skill gym_mgmt_system.group_gym_member 1 0 0 0
access_trainer_skill_trainer access.trainer_skill model_trainer_skill gym_mgmt_system.group_gym_trainer 1 1 1 1
20 access_product_template_trainer access.product_template model_product_template gym_mgmt_system.group_gym_trainer 1 1 1 1
21 access_product_template_member access.product_template model_product_template gym_mgmt_system.group_gym_member 1 0 0 0
22 access_product_template_operator access.product_template model_product_template gym_mgmt_system.group_gym_operator 1 1 1 1
41 access_my_workout_plan_operator access.my.workout.plan model_my_workout_plan gym_mgmt_system.group_gym_operator 1 1 1 1
42 access_my_workout_plan_trainer access.my.workout.plan model_my_workout_plan gym_mgmt_system.group_gym_trainer 1 1 1 1
43 access_my_workout_plan_member access.my.workout.plan model_my_workout_plan gym_mgmt_system.group_gym_member 1 0 0 0
44 access_gym_membership_pause_operator access.gym.membership.pause operator model_gym_membership_pause gym_mgmt_system.group_gym_operator 1 1 1 1
45 access_gym_membership_pause_trainer access.gym.membership.pause trainer model_gym_membership_pause gym_mgmt_system.group_gym_trainer 1 0 0 0
46 access_membership_pause_member access.gym.membership.pause member model_gym_membership_pause gym_mgmt_system.group_gym_member 1 0 0 0
47 access_membership_extension_operator access.gym.membership.extension operator model_gym_membership_extension gym_mgmt_system.group_gym_operator 1 1 1 1
48 access_membership_extension_trainer access.gym.membership.extension trainer model_gym_membership_extension gym_mgmt_system.group_gym_trainer 1 0 0 0
49 access_membership_extension_member access.gym.membership.extension member model_gym_membership_extension gym_mgmt_system.group_gym_member 1 0 0 0
50 access_membership_extend_wizard_operator access.gym.membership.extend.wizard operator model_gym_membership_extend_wizard gym_mgmt_system.group_gym_operator 1 1 1 1
51 access_gym_attendance_operator access.gym.attendance.operator model_gym_attendance gym_mgmt_system.group_gym_operator 1 1 1 1
52 access_gym_attendance_trainer access.gym.attendance.trainer model_gym_attendance gym_mgmt_system.group_gym_trainer 1 0 0 0
53 access_gym_attendance_member access.gym.attendance.member model_gym_attendance gym_mgmt_system.group_gym_member 1 0 0 0

107
gym_mgmt_system/views/gym_attendance_views.xml

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="gym_attendance_view_form" model="ir.ui.view">
<field name="name">gym.attendance.view.form</field>
<field name="model">gym.attendance</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_check_in" string="Check In" type="object"
class="btn-primary" attrs="{'invisible': [('check_in', '!=', False)]}"/>
<button name="action_check_out" string="Check Out" type="object"
class="btn-warning"
attrs="{'invisible': [('state', '=', 'checked_out')]}"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<group>
<field name="member_id"/>
<field name="check_in"/>
</group>
<group>
<field name="check_out"/>
<field name="duration" widget="float_time"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="gym_attendance_view_tree" model="ir.ui.view">
<field name="name">gym.attendance.view.tree</field>
<field name="model">gym.attendance</field>
<field name="arch" type="xml">
<tree>
<field name="member_id"/>
<field name="check_in"/>
<field name="check_out"/>
<field name="duration" widget="float_time"/>
<field name="state" decoration-success="state == 'checked_out'"
decoration-info="state == 'checked_in'"/>
<button name="action_check_out" type="object" string="Check Out"
class="btn-sm btn-warning" icon="fa-sign-out"
attrs="{'invisible': [('check_out', '!=', False)]}"/>
</tree>
</field>
</record>
<record id="gym_attendance_quick_checkin_form" model="ir.ui.view">
<field name="name">gym.attendance.quick.checkin.form</field>
<field name="model">gym.attendance</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<h1>Quick Check-In</h1>
</div>
<group>
<field name="member_id" placeholder="Select Member..."/>
</group>
<footer>
<button name="action_check_in" string="Check In" type="object" class="btn-primary"/>
<button string="Cancel" special="cancel" class="btn-secondary"/>
</footer>
</sheet>
</form>
</field>
</record>
<record id="gym_attendance_action" model="ir.actions.act_window">
<field name="name">Gym Attendance</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">gym.attendance</field>
<field name="view_mode">tree,form</field>
</record>
<record id="gym_attendance_quick_checkin_action" model="ir.actions.act_window">
<field name="name">Quick Check-In</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">gym.attendance</field>
<field name="view_mode">form</field>
<field name="view_id" ref="gym_attendance_quick_checkin_form"/>
<field name="target">new</field>
</record>
<menuitem
id="gym_attendance_menu"
name="Attendance"
parent="gym_member_root"
sequence="45"/>
<menuitem
id="gym_attendance_records_menu"
name="Attendance Records"
parent="gym_attendance_menu"
action="gym_attendance_action"
sequence="10"/>
<menuitem
id="gym_attendance_quick_checkin_menu"
name="Quick Check-In"
parent="gym_attendance_menu"
action="gym_attendance_quick_checkin_action"
sequence="5"/>
</odoo>

93
gym_mgmt_system/views/gym_membership_views.xml

@ -1,14 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- form view for gym membership -->
<record id="gym_membership_view_form" model="ir.ui.view"> <record id="gym_membership_view_form" model="ir.ui.view">
<field name="name">gym.membership.form</field> <field name="name">gym.membership.view.form</field>
<field name="model">gym.membership</field> <field name="model">gym.membership</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form create="false" edit="false" delete="false">
<header> <header>
<field name="state" widget="statusbar" <field name="state" widget="statusbar"
options="{'clickable':'1'}"/> options="{'clickable': true}"/>
<button name="action_set_active" string="Activate" type="object"
class="btn-primary"
attrs="{'invisible': [('state', '!=', 'confirm')]}"/>
<button name="action_pause" string="Pause" type="object"
class="btn-primary"
attrs="{'invisible': [('state', '!=', 'active')]}"/>
<button name="action_resume" string="Resume" type="object"
class="btn-primary"
attrs="{'invisible': [('state', '!=', 'paused')]}"/>
<button name="action_extend_membership" string="Extend" type="object"
class="btn-primary"
attrs="{'invisible': [('can_extend', '=', False)]}"
help="Extend this expired membership"/>
</header> </header>
<sheet> <sheet>
<div class="oe_title"> <div class="oe_title">
@ -21,42 +33,89 @@
<field name="member_id"/> <field name="member_id"/>
<field name="membership_scheme_id"/> <field name="membership_scheme_id"/>
<field name="paid_amount"/> <field name="paid_amount"/>
<field name="membership_duration" readonly="1"/>
</group> </group>
<group> <group>
<field name="membership_fees"/> <field name="membership_fees"/>
<field name="sale_order_id" readonly="1"/> <field name="sale_order_id" readonly="1"/>
<label for="membership_date_from" string="Membership Duration"/> <field name="extension_count" readonly="1" attrs="{'invisible': [('extension_count', '=', 0)]}"/>
<field name="can_extend" attrs="{'invisible': [('id', '!=', False)]}"/>
<label for="membership_date_from" string="Original Duration"/>
<div class="o_row"> <div class="o_row">
<field name="membership_date_from" required="1"/> <field name="membership_date_from" required="1"/>
- -
<field name="membership_date_to" required="1"/> <field name="membership_date_to" required="1"/>
</div> </div>
<label for="effective_start_date" string="Effective Duration"/>
<div class="o_row">
<field name="effective_start_date" readonly="1"/>
-
<field name="effective_end_date" readonly="1"/>
</div>
</group>
</group>
<group string="Pause &amp; Extension Information"
attrs="{'invisible': [('state', 'not in', ['paused', 'active', 'expired'])]}">
<group>
<field name="current_pause_start" readonly="1" attrs="{'invisible': [('state', '!=', 'paused')]}"/>
<field name="total_paused_days" readonly="1"/>
</group>
<group>
<field name="total_extended_days" readonly="1"/>
<field name="can_extend" readonly="1"/>
</group> </group>
</group> </group>
<notebook>
<page string="Pause History" attrs="{'invisible': [('pause_history_ids', '=', [])]}">
<field name="pause_history_ids" readonly="1">
<tree create="false" edit="false">
<field name="pause_start"/>
<field name="pause_end"/>
<field name="days_paused"/>
<field name="notes"/>
</tree>
</field>
</page>
<page string="Extension History" attrs="{'invisible': [('extension_history_ids', '=', [])]}">
<field name="extension_history_ids" readonly="1">
<tree create="false" edit="false">
<field name="extension_date"/>
<field name="days_extended"/>
<field name="extension_amount"/>
<field name="notes"/>
</tree>
</field>
</page>
</notebook>
</sheet> </sheet>
<div class="oe_chatter"> <chatter/>
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form> </form>
</field> </field>
</record> </record>
<!--Tree view for gym membership-->
<record id="view_membership_tree" model="ir.ui.view"> <record id="gym_membership_view_tree" model="ir.ui.view">
<field name="name">gym.membership.tree</field> <field name="name">gym.membership.view.tree</field>
<field name="model">gym.membership</field> <field name="model">gym.membership</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree default_order="reference desc"> <tree default_order="reference desc" create="false" edit="false" delete="false">
<field name="reference"/> <field name="reference"/>
<field name="member_id"/> <field name="member_id"/>
<field name="membership_scheme_id"/> <field name="membership_scheme_id"/>
<field name="membership_fees"/> <field name="membership_fees"/>
<field name="state"/> <field name="effective_end_date"/>
<field name="total_paused_days"/>
<field name="state" decoration-info="state == 'active'"
decoration-warning="state == 'paused'"
decoration-danger="state == 'expired'"/>
</tree> </tree>
</field> </field>
</record> </record>
<!--Action for gym membership-->
<record id="gym_membership_action" model="ir.actions.act_window"> <record id="gym_membership_action" model="ir.actions.act_window">
<field name="name">Gym Membership</field> <field name="name">Gym Membership</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
@ -68,7 +127,7 @@
</p> </p>
</field> </field>
</record> </record>
<!-- Menu -->
<menuitem <menuitem
id="gym_membership_menu_action" id="gym_membership_menu_action"
name="Membership" name="Membership"

6
gym_mgmt_system/views/gym_menu.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="gym_member_root"
name="Gym Management"
sequence="50"/>
</odoo>

108
gym_mgmt_system/views/hr_employee_views.xml

@ -1,46 +1,94 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Action for gym trainer --> <record id="view_employee_form_trainer" model="ir.ui.view">
<record id="gym_trainer_emp_action" model="ir.actions.act_window"> <field name="name">hr.employee.view.form.inherited.gym.trainer</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='mobile_phone']" position="after">
<field name="is_trainer"/>
</xpath>
<xpath expr="//page[@name='hr_settings']" position="after">
<page name="trainer_skills" string="Trainer Skills" attrs="{'invisible': [('is_trainer', '=', False)]}">
<group string="Current Gym Skills">
<field name="gym_skill_ids" widget="many2many_tags" readonly="1"/>
</group>
<group string="All Employee Skills">
<field name="skill_ids" nolabel="1" context="{'default_skill_type_id': False}"/>
</group>
</page>
</xpath>
</field>
</record>
<record id="hr_employee_trainer_action" model="ir.actions.act_window">
<field name="name">Trainers</field> <field name="name">Trainers</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">hr.employee</field> <field name="res_model">hr.employee</field>
<field name="view_mode">kanban,tree,form</field> <field name="view_mode">kanban,list,form</field>
<field name="domain">[('trainer','=','True')]</field> <field name="domain">[('is_trainer','=',True)]</field>
<field name="context">{'default_is_trainer': True}</field>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create your first Trainer! Create your first Trainer!
</p> </p>
</field> </field>
</record> </record>
<!--form view for trainer -->
<record id="view_employee_form" model="ir.ui.view"> <!-- Menu items -->
<field name="name">hr.employee.view.form.inherited.gym.mgmt.system</field> <menuitem id="gym_trainer_root"
<field name="model">hr.employee</field> name="Trainers"
<field name="inherit_id" ref="hr.view_employee_form"/> parent="gym_mgmt_system_menu_root"
sequence="20"/>
<menuitem id="gym_trainer_menu"
name="Trainers"
parent="gym_trainer_root"
action="hr_employee_trainer_action"
sequence="10"/>
<record id="gym_skills_action" model="ir.actions.act_window">
<field name="name">Trainer Skills</field>
<field name="res_model">hr.skill</field>
<field name="view_mode">list,form</field>
<field name="domain">[('skill_type_id.is_gym_skill', '=', True)]</field>
<field name="context">{}</field>
</record>
<menuitem id="gym_skills_menu"
name="Trainer Skills"
parent="gym_trainer_root"
action="gym_skills_action"
sequence="20"/>
<record id="view_gym_skill_level_tree" model="ir.ui.view">
<field name="name">hr.skill.level.view.tree.gym</field>
<field name="model">hr.skill.level</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='mobile_phone']" position="after"> <list string="Trainer Skill Levels" editable="bottom">
<field name="trainer"/> <field name="name"/>
<field name="exercise_for_ids" widget="many2many_tags" <field name="skill_type_id"/>
attrs="{'invisible': [('trainer', '=', False)]}"/> <field name="level_progress" widget="progressbar" options="{'editable': true}"/>
</xpath> <field name="default_level" widget="boolean_toggle"/>
</list>
</field> </field>
</record> </record>
<!-- Menu and Submenu -->
<menuitem
id="gym_trainer_root"
name="Trainers" <record id="gym_skill_levels_action" model="ir.actions.act_window">
parent="gym_mgmt_system_menu_root" <field name="name">Trainer Skill Levels</field>
sequence="20"/> <field name="res_model">hr.skill.level</field>
<menuitem <field name="view_mode">list,form</field>
id="gym_trainer_menu" <field name="view_id" ref="view_gym_skill_level_tree"/>
name="Trainers" <field name="context">{}</field>
parent="gym_trainer_root" </record>
action="gym_trainer_emp_action"
sequence="10"/> <menuitem id="gym_skill_levels_menu"
<menuitem id="gym_trainer_skill_menu" name="Trainer Skill Levels"
name="Trainer skill"
parent="gym_trainer_root" parent="gym_trainer_root"
action="trainer_skill_action" action="gym_skill_levels_action"
sequence="20"/> sequence="30"/>
</odoo> </odoo>

14
gym_mgmt_system/views/trainer_skill_views.xml

@ -1,23 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Tree view for trainer skill -->
<record id="trainer_skill_view_tree" model="ir.ui.view"> <record id="trainer_skill_view_tree" model="ir.ui.view">
<field name="name">trainer.skill.view.tree</field> <field name="name">trainer.skill.view.tree</field>
<field name="model">trainer.skill</field> <field name="model">trainer.skill</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Trainer skill Tags" editable="bottom"> <list string="Trainer skill Tags" editable="bottom">
<field name="name"/> <field name="name"/>
<field name="code"/> <field name="code"/>
<field name="company_id" invisible="1"/> <field name="company_id" readonly="1"/>
</tree> </list>
</field> </field>
</record> </record>
<!-- Action for trainer skill -->
<record id="trainer_skill_action" model="ir.actions.act_window"> <record id="trainer_skill_action" model="ir.actions.act_window">
<field name="name">Trainer skill</field> <field name="name">Trainer skill</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">trainer.skill</field> <field name="res_model">trainer.skill</field>
<field name="view_mode">tree</field> <field name="view_mode">list</field>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create your first Trainer skill! Create your first Trainer skill!

1
gym_mgmt_system/wizard/__init__.py

@ -20,3 +20,4 @@
# #
############################################################################# #############################################################################
from . import assign_workout from . import assign_workout
from . import gym_membership_extension

212
gym_mgmt_system/wizard/gym_membership_extension.py

@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Sahla Sherin (<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from datetime import timedelta
class GymMembershipExtendWizard(models.TransientModel):
"""Wizard to extend gym membership"""
_name = 'gym.membership.extend.wizard'
_description = 'Extend Gym Membership Wizard'
membership_id = fields.Many2one('gym.membership', string='Membership', required=True)
member_id = fields.Many2one('res.partner', string='Member', required=True)
current_end_date = fields.Date(string='Current End Date', related='membership_id.effective_end_date')
extension_type = fields.Selection([
('same_plan', 'Extend with Same Plan Duration'),
('custom_days', 'Custom Number of Days'),
('new_plan', 'Change to New Membership Plan')
], string='Extension Type', required=True, default='same_plan')
same_plan_duration = fields.Integer(string='Plan Duration (Days)', compute='_compute_same_plan_duration',
store=True)
custom_days = fields.Integer(string='Number of Days to Extend', default=30)
new_membership_plan_id = fields.Many2one('product.product', string='New Membership Plan',
domain="[('membership_date_from', '!=', False)]")
new_plan_duration = fields.Integer(string='New Plan Duration (Days)')
extension_days = fields.Integer(string='Total Extension Days', compute='_compute_extension_details', store=True)
new_end_date = fields.Date(string='New End Date', compute='_compute_extension_details', store=True)
extension_amount = fields.Float(string='Extension Amount', compute='_compute_extension_details', store=True)
notes = fields.Text(string='Notes')
@api.depends('membership_id.membership_duration')
def _compute_same_plan_duration(self):
"""Compute the same plan duration from membership"""
for wizard in self:
wizard.same_plan_duration = wizard.membership_id.membership_duration or 0
@api.depends('extension_type', 'custom_days', 'new_membership_plan_id', 'same_plan_duration')
def _compute_extension_details(self):
"""Compute extension days, new end date and amount"""
for wizard in self:
extension_days = 0
extension_amount = 0.0
if wizard.extension_type == 'same_plan':
if wizard.membership_id.membership_scheme_id:
original_plan = wizard.membership_id.membership_scheme_id
if original_plan.membership_date_from and original_plan.membership_date_to:
extension_days = (original_plan.membership_date_to - original_plan.membership_date_from).days
extension_amount = original_plan.list_price
elif wizard.extension_type == 'custom_days':
extension_days = wizard.custom_days
if wizard.membership_id.membership_scheme_id and extension_days > 0:
original_plan = wizard.membership_id.membership_scheme_id
if original_plan.membership_date_from and original_plan.membership_date_to:
original_days = (original_plan.membership_date_to - original_plan.membership_date_from).days
if original_days > 0:
daily_rate = original_plan.list_price / original_days
extension_amount = daily_rate * extension_days
elif wizard.extension_type == 'new_plan':
if wizard.new_membership_plan_id:
new_plan = wizard.new_membership_plan_id
if new_plan.membership_date_from and new_plan.membership_date_to:
extension_days = (new_plan.membership_date_to - new_plan.membership_date_from).days
extension_amount = new_plan.list_price
wizard.extension_days = extension_days
wizard.extension_amount = extension_amount
if wizard.current_end_date and extension_days > 0:
wizard.new_end_date = wizard.current_end_date + timedelta(days=extension_days)
else:
wizard.new_end_date = wizard.current_end_date
@api.onchange('new_membership_plan_id')
def _onchange_new_membership_plan_id(self):
"""Update new plan duration when plan changes"""
if self.new_membership_plan_id:
plan = self.new_membership_plan_id
if plan.membership_date_from and plan.membership_date_to:
self.new_plan_duration = (plan.membership_date_to - plan.membership_date_from).days
def action_extend_membership(self):
"""Process the membership extension"""
self.ensure_one()
if self.extension_days <= 0:
raise UserError(_('Extension days must be greater than 0.'))
if self.extension_amount <= 0:
raise UserError(_('Extension amount must be greater than 0.'))
# Create sale order for extension payment
sale_order = self._create_extension_sale_order()
# Complete the extension on existing membership
self.membership_id.complete_extension(
days_extended=self.extension_days,
extension_amount=self.extension_amount,
sale_order_id=sale_order.id
)
# CASE 1: New plan → replace membership_scheme_id
if self.extension_type == 'new_plan' and self.new_membership_plan_id:
self.membership_id.membership_scheme_id = self.new_membership_plan_id.id
# CASE 2: Custom days → create/update a custom plan product
elif self.extension_type == 'custom_days' and self.custom_days > 0:
custom_plan_name = f"Custom Plan - {self.custom_days} days"
custom_plan = self.env['product.product'].search([
('name', '=', custom_plan_name),
('detailed_type', '=', 'service')
], limit=1)
if not custom_plan:
custom_plan = self.env['product.product'].create({
'name': custom_plan_name,
'type': 'service',
'detailed_type': 'service',
'list_price': self.extension_amount,
'membership_date_from': fields.Date.today(),
'membership_date_to': fields.Date.today() + timedelta(days=self.custom_days),
'sale_ok': False,
'purchase_ok': False,
})
self.membership_id.membership_scheme_id = custom_plan.id
return {
'name': _('Extension Sale Order'),
'type': 'ir.actions.act_window',
'res_model': 'sale.order',
'res_id': sale_order.id,
'view_mode': 'form',
'target': 'current',
}
def _create_extension_sale_order(self):
"""Create sale order for membership extension without auto-creating membership"""
extension_product = self._get_or_create_extension_service_product()
if self.extension_type == 'new_plan' and self.new_membership_plan_id:
product_name = f"Membership Extension - {self.new_membership_plan_id.name} ({self.extension_days} days)"
elif self.extension_type == 'same_plan':
product_name = f"Membership Extension - {self.membership_id.membership_scheme_id.name} ({self.extension_days} days)"
else:
product_name = f"Membership Extension - {self.extension_days} days"
sale_order_vals = {
'partner_id': self.member_id.id,
'date_order': fields.Datetime.now(),
'origin': f'Extension of {self.membership_id.reference}',
'order_line': [(0, 0, {
'product_id': extension_product.id,
'name': product_name,
'product_uom_qty': 1,
'price_unit': self.extension_amount,
})]
}
sale_order = self.env['sale.order'].create(sale_order_vals)
sale_order.action_confirm()
return sale_order
def _get_or_create_extension_service_product(self):
"""Get or create a service product for membership extensions"""
extension_product = self.env['product.product'].search([
('name', '=', 'Membership Extension Service'),
('type', '=', 'service'),
('detailed_type', '=', 'service')
], limit=1)
if not extension_product:
extension_product = self.env['product.product'].create({
'name': 'Membership Extension Service',
'type': 'service',
'detailed_type': 'service',
'categ_id': self.env.ref('product.product_category_all').id,
'list_price': 0.0,
'sale_ok': True,
'purchase_ok': False,
'invoice_policy': 'order',
'membership_date_from': False,
'membership_date_to': False,
})
return extension_product

57
gym_mgmt_system/wizard/gym_membership_extension.xml

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="gym_membership_extend_wizard_view_form" model="ir.ui.view">
<field name="name">gym.membership.extend.wizard.view.form</field>
<field name="model">gym.membership.extend.wizard</field>
<field name="arch" type="xml">
<form string="Extend Membership">
<group>
<group>
<field name="membership_id" readonly="1"/>
<field name="member_id" readonly="1"/>
<field name="current_end_date" readonly="1"/>
</group>
<group>
<field name="extension_type" widget="radio"/>
</group>
</group>
<group>
<group attrs="{'invisible': [('extension_type','!=','same_plan')]}">
<field name="same_plan_duration" readonly="1" string="Plan Duration (Days)"/>
</group>
<group attrs="{'invisible': [('extension_type','!=','custom_days')]}">
<field name="custom_days"/>
</group>
<group attrs="{'invisible': [('extension_type','!=','new_plan')]}">
<field name="new_membership_plan_id"/>
<field name="new_plan_duration" readonly="1"/>
</group>
</group>
<separator string="Extension Summary"/>
<group>
<group>
<field name="extension_days" readonly="1"/>
<field name="new_end_date" readonly="1"/>
</group>
<group>
<field name="extension_amount" readonly="1"/>
</group>
</group>
<group>
<field name="notes" placeholder="Optional notes about this extension..."/>
</group>
<footer>
<button name="action_extend_membership" string="Create Extension Order"
type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
Loading…
Cancel
Save