import json from datetime import date from flask import (Blueprint, render_template, redirect, url_for, flash, request, current_app) from flask_login import login_required, current_user from app.extensions import db from app.models.assignment import Assignment from app.models.asset import Asset from app.models.user import User from app.models.audit_log import AuditLog bp = Blueprint('assignments', __name__, url_prefix='/assignments') def _log(action, record_id, description, old=None, new=None): entry = AuditLog( table_name='assignments', record_id=record_id, action=action, old_values=json.dumps(old) if old else None, new_values=json.dumps(new) if new else None, performed_by_id=current_user.id, ip_address=request.remote_addr, description=description, ) db.session.add(entry) @bp.route('/') @login_required def index(): page = request.args.get('page', 1, type=int) active_only = request.args.get('active', '1') == '1' q = request.args.get('q', '').strip() query = Assignment.query if active_only: query = query.filter_by(is_active=True) pagination = query.order_by(Assignment.assigned_date.desc()).paginate( page=page, per_page=current_app.config['ITEMS_PER_PAGE'], error_out=False ) return render_template('assignments/index.html', pagination=pagination, active_only=active_only, q=q) @bp.route('/new', methods=['GET', 'POST']) @login_required def create(): # Pre-fill from query params (used from asset / user detail pages) preselect_asset_id = request.args.get('asset_id', type=int) preselect_user_id = request.args.get('user_id', type=int) if request.method == 'POST': user_id = request.form.get('user_id', type=int) asset_id = request.form.get('asset_id', type=int) assigned_date_str = request.form.get('assigned_date', '') notes = request.form.get('notes', '').strip() or None user = User.query.get(user_id) if user_id else None asset = Asset.query.get(asset_id) if asset_id else None errors = [] if not user: errors.append('User is required.') if not asset: errors.append('Asset is required.') if user and user.is_masked: errors.append('Cannot assign assets to a masked user.') if asset and asset.status == 'assigned': errors.append(f'Asset {asset.serial_number} is already assigned.') if asset and asset.status in ('retired', 'lost'): errors.append(f'Asset {asset.serial_number} is {asset.status} and cannot be assigned.') if errors: for e in errors: flash(e, 'danger') return render_template('assignments/form.html', preselect_asset_id=asset_id, preselect_user_id=user_id) try: assigned_date = date.fromisoformat(assigned_date_str) if assigned_date_str else date.today() except ValueError: assigned_date = date.today() assignment = Assignment( asset_id=asset.id, user_id=user.id, assigned_date=assigned_date, assigned_by_id=current_user.id, notes=notes, is_active=True, ) asset.status = 'assigned' db.session.add(assignment) db.session.flush() _log('assign', assignment.id, f'Assigned asset SN={asset.serial_number} to WID={user.windows_id}', new={'asset_sn': asset.serial_number, 'user_wid': user.windows_id, 'date': str(assigned_date)}) db.session.commit() flash(f'Asset {asset.serial_number} assigned to {user.display_name}.', 'success') return redirect(url_for('assignments.index')) return render_template('assignments/form.html', preselect_asset_id=preselect_asset_id, preselect_user_id=preselect_user_id) @bp.route('//return', methods=['POST']) @login_required def return_asset(assignment_id): assignment = Assignment.query.get_or_404(assignment_id) if not assignment.is_active: flash('This assignment is already closed.', 'info') return redirect(url_for('assignments.index')) returned_date_str = request.form.get('returned_date', '') try: returned_date = date.fromisoformat(returned_date_str) if returned_date_str else date.today() except ValueError: returned_date = date.today() assignment.returned_date = returned_date assignment.returned_by_id = current_user.id assignment.is_active = False assignment.notes = (assignment.notes or '') + ('\n' + request.form.get('return_notes', '').strip() if request.form.get('return_notes') else '') # Only set asset back to available if no other active assignment (safety check) other_active = Assignment.query.filter( Assignment.asset_id == assignment.asset_id, Assignment.is_active == True, # noqa: E712 Assignment.id != assignment.id, ).first() if not other_active: assignment.asset.status = 'available' _log('return', assignment.id, f'Returned asset SN={assignment.asset.serial_number} from WID={assignment.user.windows_id}', new={'returned_date': str(returned_date)}) db.session.commit() flash(f'Asset {assignment.asset.serial_number} returned.', 'success') return redirect(url_for('assignments.index'))