22 changed files with 1422 additions and 87 deletions
			
			
		@ -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">1</field> | 
				
			|||
        <field name="interval_type">days</field> | 
				
			|||
        <field name="active" eval="True"/> | 
				
			|||
    </record> | 
				
			|||
</odoo> | 
				
			|||
@ -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> | 
				
			|||
@ -0,0 +1,66 @@ | 
				
			|||
# -*- coding: utf-8 -*- | 
				
			|||
############################################################################# | 
				
			|||
# | 
				
			|||
#    Cybrosys Technologies Pvt. Ltd. | 
				
			|||
# | 
				
			|||
#    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) | 
				
			|||
#    Author: Gayathri V (<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 | 
				
			|||
@ -0,0 +1,63 @@ | 
				
			|||
# -*- coding: utf-8 -*- | 
				
			|||
############################################################################# | 
				
			|||
# | 
				
			|||
#    Cybrosys Technologies Pvt. Ltd. | 
				
			|||
# | 
				
			|||
#    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) | 
				
			|||
#    Author: Gayathri V (<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 | 
				
			|||
@ -0,0 +1,200 @@ | 
				
			|||
# -*- coding: utf-8 -*- | 
				
			|||
############################################################################# | 
				
			|||
# | 
				
			|||
#    Cybrosys Technologies Pvt. Ltd. | 
				
			|||
# | 
				
			|||
#    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) | 
				
			|||
#    Author: Gayathri V (<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 datetime | 
				
			|||
 | 
				
			|||
 | 
				
			|||
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="[('is_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) | 
				
			|||
            ]) | 
				
			|||
 | 
				
			|||
            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""" | 
				
			|||
        membership = self.env['gym.membership'].search([ | 
				
			|||
            ('member_id', '=', member.id), | 
				
			|||
            ('state', 'in', ['active', 'paused', 'expired']) | 
				
			|||
        ], 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 != 'active': | 
				
			|||
            return { | 
				
			|||
                'can_checkin': False, | 
				
			|||
                'message': _( | 
				
			|||
                    'Cannot check in. Membership is not active.\n' | 
				
			|||
                    'Current status: %s' | 
				
			|||
                ) % membership.state.title() | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
        if membership.effective_end_date and membership.effective_end_date < fields.Date.today(): | 
				
			|||
            return { | 
				
			|||
                'can_checkin': False, | 
				
			|||
                'message': _( | 
				
			|||
                    'Cannot check in. Membership expired on %s.\n' | 
				
			|||
                    'Please renew your membership.' | 
				
			|||
                ) % membership.effective_end_date.strftime('%Y-%m-%d') | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
        return { | 
				
			|||
            'can_checkin': True, | 
				
			|||
            'message': _('Check-in allowed.') | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
    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, | 
				
			|||
            } | 
				
			|||
        } | 
				
			|||
		
		
			
  | 
| 
		 Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB  | 
@ -0,0 +1,105 @@ | 
				
			|||
<?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" invisible="check_in"/> | 
				
			|||
                    <button name="action_check_out" string="Check Out" type="object" | 
				
			|||
                            class="btn-warning" invisible="check_out or not check_in"/> | 
				
			|||
                    <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" | 
				
			|||
                        invisible="check_out or not check_in"/> | 
				
			|||
            </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> | 
				
			|||
@ -0,0 +1,6 @@ | 
				
			|||
<?xml version="1.0" encoding="utf-8"?> | 
				
			|||
<odoo> | 
				
			|||
    <menuitem id="gym_member_root" | 
				
			|||
              name="Gym Management" | 
				
			|||
              sequence="50"/> | 
				
			|||
</odoo> | 
				
			|||
@ -1,47 +1,93 @@ | 
				
			|||
<?xml version="1.0" encoding="utf-8"?> | 
				
			|||
<odoo> | 
				
			|||
    <!-- Action for gym trainer --> | 
				
			|||
    <record id="hr_employee_action" model="ir.actions.act_window"> | 
				
			|||
    <record id="view_employee_form_trainer" model="ir.ui.view"> | 
				
			|||
        <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" invisible="not is_trainer"> | 
				
			|||
                    <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="type">ir.actions.act_window</field> | 
				
			|||
        <field name="res_model">hr.employee</field> | 
				
			|||
        <field name="view_mode">kanban,tree,form</field> | 
				
			|||
        <field name="domain">[('is_trainer','=','True')]</field> | 
				
			|||
        <field name="domain">[('is_trainer','=',True)]</field> | 
				
			|||
        <field name="context">{'default_is_trainer': True}</field> | 
				
			|||
        <field name="help" type="html"> | 
				
			|||
            <p class="o_view_nocontent_smiling_face"> | 
				
			|||
                Create your first Trainer! | 
				
			|||
            </p> | 
				
			|||
        </field> | 
				
			|||
    </record> | 
				
			|||
    <!--form view for trainer --> | 
				
			|||
    <record id="view_employee_form" model="ir.ui.view"> | 
				
			|||
        <field name="name">hr.employee.view.form.inherited.gym.mgmt.system | 
				
			|||
        </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"/> | 
				
			|||
                <field name="exercise_for_ids" widget="many2many_tags" | 
				
			|||
                       invisible="is_trainer == False"/> | 
				
			|||
            </xpath> | 
				
			|||
        </field> | 
				
			|||
    </record> | 
				
			|||
    <!-- Menu and Submenu --> | 
				
			|||
    <menuitem | 
				
			|||
            id="gym_trainer_root" | 
				
			|||
 | 
				
			|||
    <!-- Menu items --> | 
				
			|||
    <menuitem id="gym_trainer_root" | 
				
			|||
              name="Trainers" | 
				
			|||
              parent="gym_mgmt_system_menu_root" | 
				
			|||
              sequence="20"/> | 
				
			|||
    <menuitem | 
				
			|||
            id="gym_trainer_menu" | 
				
			|||
 | 
				
			|||
    <menuitem id="gym_trainer_menu" | 
				
			|||
              name="Trainers" | 
				
			|||
              parent="gym_trainer_root" | 
				
			|||
            action="hr_employee_action" | 
				
			|||
              action="hr_employee_trainer_action" | 
				
			|||
              sequence="10"/> | 
				
			|||
    <menuitem id="gym_trainer_skill_menu" | 
				
			|||
              name="Trainer skill" | 
				
			|||
 | 
				
			|||
    <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">tree,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="trainer_skill_action" | 
				
			|||
              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"> | 
				
			|||
            <tree string="Trainer Skill Levels" editable="bottom"> | 
				
			|||
                <field name="name"/> | 
				
			|||
                <field name="skill_type_id"/> | 
				
			|||
                <field name="level_progress" widget="progressbar" options="{'editable': true}"/> | 
				
			|||
                <field name="default_level" widget="boolean_toggle"/> | 
				
			|||
            </tree> | 
				
			|||
        </field> | 
				
			|||
    </record> | 
				
			|||
 | 
				
			|||
    <!-- Add the missing action for skill levels --> | 
				
			|||
    <record id="gym_skill_levels_action" model="ir.actions.act_window"> | 
				
			|||
        <field name="name">Trainer Skill Levels</field> | 
				
			|||
        <field name="res_model">hr.skill.level</field> | 
				
			|||
        <field name="view_mode">tree,form</field> | 
				
			|||
        <field name="view_id" ref="view_gym_skill_level_tree"/> | 
				
			|||
        <field name="context">{}</field> | 
				
			|||
    </record> | 
				
			|||
 | 
				
			|||
    <menuitem id="gym_skill_levels_menu" | 
				
			|||
              name="Trainer Skill Levels" | 
				
			|||
              parent="gym_trainer_root" | 
				
			|||
              action="gym_skill_levels_action" | 
				
			|||
              sequence="30"/> | 
				
			|||
</odoo> | 
				
			|||
@ -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 invisible="extension_type != 'same_plan'"> | 
				
			|||
                        <field name="same_plan_duration" readonly="1" string="Plan Duration (Days)"/> | 
				
			|||
                    </group> | 
				
			|||
 | 
				
			|||
                    <group invisible="extension_type != 'custom_days'"> | 
				
			|||
                        <field name="custom_days"/> | 
				
			|||
                    </group> | 
				
			|||
 | 
				
			|||
                    <group 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> | 
				
			|||
@ -0,0 +1,173 @@ | 
				
			|||
# -*- coding: utf-8 -*- | 
				
			|||
############################################################################# | 
				
			|||
# | 
				
			|||
#    Cybrosys Technologies Pvt. Ltd. | 
				
			|||
# | 
				
			|||
#    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) | 
				
			|||
#    Author: Gayathri V (<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 date, 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.')) | 
				
			|||
 | 
				
			|||
        sale_order = self._create_extension_sale_order() | 
				
			|||
 | 
				
			|||
        self.membership_id.complete_extension( | 
				
			|||
            days_extended=self.extension_days, | 
				
			|||
            extension_amount=self.extension_amount, | 
				
			|||
            sale_order_id=sale_order.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 | 
				
			|||
 | 
				
			|||
        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""" | 
				
			|||
        if self.extension_type == 'new_plan' and self.new_membership_plan_id: | 
				
			|||
            product = self.new_membership_plan_id | 
				
			|||
            product_name = f"Extension - {product.name}" | 
				
			|||
        else: | 
				
			|||
            product = self.membership_id.membership_scheme_id | 
				
			|||
            if self.extension_type == 'same_plan': | 
				
			|||
                product_name = f"Extension - {product.name}" | 
				
			|||
            else: | 
				
			|||
                product_name = f"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': product.id, | 
				
			|||
                'name': product_name, | 
				
			|||
                'product_uom_qty': 1, | 
				
			|||
                'price_unit': self.extension_amount, | 
				
			|||
            })] | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        sale_order = self.env['sale.order'].create(sale_order_vals) | 
				
			|||
        return sale_order | 
				
			|||
					Loading…
					
					
				
		Reference in new issue