The SWL Loan System manages the complete lifecycle of item borrowing, from initial request through approval, active lending, overdue detection, and final return. The system tracks penalties for late returns and enforces business rules based on item categories and user roles.
@bp.route('/request/book', methods=['GET', 'POST'])@role_required('premium', 'cliente')def request_book(): form = RequestItemForm() available_books = CatalogService.get_catalog_with_counts(category_filter='libro') if form.validate_on_submit(): catalog_id = form.catalog_id.data success, reserved_ids, msg = InventoryService.reserve_instances(catalog_id, 1) if not success: flash(msg, 'danger') return redirect(url_for('main.request_book')) try: LoanService.create_loan(user_id=current_user.id, instance_id=reserved_ids[0]) db.session.commit() flash('Solicitud de libro registrada. Acércate al mostrador.', 'success') except Exception as e: db.session.rollback() flash('Ocurrió un error al registrar la solicitud.', 'danger')
class LoanService: @staticmethod def can_request_laptop(user_id): """Users can only have one computer loan active at a time""" active_loans = Loan.query.join(ItemInstance).join(Catalog).filter( Loan.user_id == user_id, Loan.status.in_(['pendiente', 'activo', 'atrasado']), Catalog.category == 'computo' ).count() if active_loans > 0: return False, "Ya tienes un equipo pendiente, en uso o atrasado." return True, "Ok" @staticmethod def can_request_accessory(user_id): """Users can have up to 2 accessories simultaneously""" active_accessories = Loan.query.join(ItemInstance).join(Catalog).filter( Loan.user_id == user_id, Catalog.category != 'computo', Catalog.category != 'libro', Loan.status.in_(['pendiente', 'activo', 'atrasado']) ).count() if active_accessories >= 2: return False, "Has alcanzado el límite de 2 accesorios simultáneos." return True, "Ok"
The create_loan method does NOT commit the transaction. The calling controller is responsible for committing, allowing for atomic multi-item reservations.
Librarians review pending requests and approve them:
app/services/loan_service.py
@staticmethoddef approve_loan(loan_id): loan = Loan.query.get(loan_id) if not loan or loan.status != 'pendiente': return False, "Préstamo no válido o ya procesado." loan.status = 'activo' loan.approval_date = datetime.utcnow() db.session.commit() return True, "Préstamo aprobado con éxito."
Librarians can reject invalid or problematic requests:
app/admin/routes.py
@bp.route('/reject/<int:id>', methods=['POST'])@role_required('bibliotecario')def reject_loan(id): loan = Loan.query.get_or_404(id) if loan.status == 'pendiente': loan.status = 'rechazado' loan.observation = 'Rechazado por el bibliotecario.' # Release the reserved instance back to inventory if loan.item_instance: loan.item_instance.status = 'disponible' db.session.commit() flash('Solicitud rechazada. Se liberó la reserva física.', 'success') else: flash('Solo puedes rechazar solicitudes que estén pendientes.', 'warning')
The system runs a scheduled job to detect overdue loans:
app/services/loan_service.py
@staticmethoddef check_overdue_loans(): """Find all active loans past their due date""" overdue_loans = Loan.query.filter( Loan.status == 'activo', Loan.due_date < datetime.utcnow() ).all() count = 0 for loan in overdue_loans: loan.status = 'atrasado' count += 1 if count > 0: db.session.commit() return count
This method is typically called by a background scheduler (e.g., APScheduler) to run periodically.
@propertydef is_overdue(self): if self.status not in ['devuelto', 'rechazado'] and self.due_date: return datetime.utcnow() > self.due_date return False
When a loan is returned, the current penalty is frozen:
app/services/loan_service.py
@staticmethoddef return_loan(loan_id): loan = Loan.query.get(loan_id) if not loan or loan.status not in ['activo', 'atrasado']: return False, "Préstamo no válido o no está activo." # Save the final penalty amount if loan.is_overdue: loan.final_penalty = loan.penalty_fee loan.status = 'devuelto' loan.return_date = datetime.utcnow() # Release the physical instance if loan.item_instance: loan.item_instance.status = 'disponible' db.session.commit() return True, "Artículo devuelto exitosamente al inventario."