feat(non-loanable-devices): implement non-loanable device management

- Add non_loanable_devices table to database schema
- Add web interface routes: /non_loanable_devices, /add_non_loanable_device, /edit_non_loanable_device, /delete_non_loanable_device
- Add CLI menu options 8-10 for non-loanable device management
- Add templates for non-loanable device CRUD operations
- Update README.md with new feature documentation
- Update REQUIREMENTS.md marking non-loanable devices as implemented
- Add gestiontablets.sh script

Implements: #porDefinir Registrar nuevos dispositivos no prestables
This commit is contained in:
ijuanes 2026-06-09 23:54:38 +01:00
parent ac73e5dc93
commit d3d1fb1b32
10 changed files with 417 additions and 6 deletions

View file

@ -10,15 +10,16 @@ A simple SQLite-based system for managing tablet lending and returns.
- **Return Management**: Record when tablets are returned
- **History Tracking**: Complete history of all loans and returns
- **User Loans View**: See all tablets loaned to each user (demonstrates one-to-many relationship)
- **Non-Loanable Devices**: Track inventory devices that cannot be loaned (e.g., projectors, monitors)
- **Project Notes**: Markdown editor for development documentation
## Files
- `minimal_app.py` - Interactive command-line application
- `minimal_app.py` - Interactive command-line application (includes non-loanable device management)
- `test_app.py` - Test script that demonstrates functionality
- `tablets.db` - SQLite database (created automatically)
- `simple_app.py` - Web-based version (requires Flask)
- `app.py` - Alternative web version (requires Flask)
- `tablets.db` - SQLite database (created automatically, includes non_loanable_devices table)
- `simple_app.py` - Web-based version (requires Flask, includes non-loanable device management)
- `app.py` - Alternative web version (requires Flask, includes non-loanable device management)
## Quick Start
@ -144,6 +145,7 @@ python3 app.py
- **Loan Tablet**: Loan a tablet to a user (with search for large datasets)
- **User Loans**: View all tablets loaned to each user - demonstrates the **one-to-many relationship** (one user, multiple tablets)
- **Loan History**: Complete history of all loan and return transactions
- **Non-Loanable Devices**: Manage inventory of devices that cannot be loaned (add, edit, delete, view)
- **Project Management**: Markdown editor for development notes (saved to `notes_development/project_notes.md`)
## Database Backup

View file

@ -5,6 +5,17 @@
El sistema debe permitir la gestión del préstamo y devolución de dispositivos tablets entre un inventario y un conjunto de usuarios. El enfoque principal es el seguimiento del estado de cada dispositivo y su relación con los usuarios que los han tomado prestados.
---
## Requisitos Faltantes
### Gestión de dispositivos no prestables
**IMPLEMENTADO** - El sistema ahora permite:
- Registrar nuevos dispositivos no prestables en un inventario separado (tabla `non_loanable_devices`)
- Gestionar dispositivos no prestables a través de:
- Interfaz web (app.py): /non_loanable_devices, /add_non_loanable_device, /edit_non_loanable_device, /delete_non_loanable_device
- Interfaz CLI (minimal_app.py): Opciones 8, 9, 10 del menú
- La implementación utiliza una tabla separada en la misma base de datos con los siguientes campos:
- brand, model, serial_number (único), device_type, location, status, notes, purchase_date, purchase_cost
## Requisitos Funcionales

109
app.py
View file

@ -63,6 +63,22 @@ def init_db():
)
''')
# Create non_loanable_devices table for devices that cannot be loaned
cursor.execute('''
CREATE TABLE IF NOT EXISTS non_loanable_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
brand TEXT NOT NULL,
model TEXT NOT NULL,
serial_number TEXT UNIQUE NOT NULL,
device_type TEXT NOT NULL,
location TEXT,
status TEXT DEFAULT 'available',
notes TEXT,
purchase_date TEXT,
purchase_cost REAL
)
''')
conn.commit()
@app.route('/')
@ -319,6 +335,99 @@ def user_loans():
return render_template('user_loans.html', users_with_loans=user_loans_list)
@app.route('/non_loanable_devices')
def non_loanable_devices():
"""Show all non-loanable devices"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM non_loanable_devices ORDER BY device_type, brand, model")
devices = cursor.fetchall()
return render_template('non_loanable_devices.html', devices=devices)
@app.route('/add_non_loanable_device', methods=['GET', 'POST'])
def add_non_loanable_device():
"""Add a new non-loanable device to inventory"""
if request.method == 'POST':
brand = request.form['brand']
model = request.form['model']
serial_number = request.form['serial_number']
device_type = request.form['device_type']
location = request.form['location']
notes = request.form['notes']
purchase_date = request.form.get('purchase_date', '')
purchase_cost = request.form.get('purchase_cost', '')
try:
with get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO non_loanable_devices
(brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost)
VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?)
''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost))
conn.commit()
flash('Non-loanable device added successfully!', 'success')
except sqlite3.IntegrityError:
flash('Error: Serial number already exists!', 'error')
return redirect(url_for('non_loanable_devices'))
return render_template('add_non_loanable_device.html')
@app.route('/edit_non_loanable_device/<int:device_id>', methods=['GET', 'POST'])
def edit_non_loanable_device(device_id):
"""Edit a non-loanable device"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM non_loanable_devices WHERE id = ?", (device_id,))
device = cursor.fetchone()
if not device:
flash('Error: Device not found!', 'error')
return redirect(url_for('non_loanable_devices'))
if request.method == 'POST':
brand = request.form['brand']
model = request.form['model']
serial_number = request.form['serial_number']
device_type = request.form['device_type']
location = request.form['location']
status = request.form['status']
notes = request.form['notes']
purchase_date = request.form.get('purchase_date', '')
purchase_cost = request.form.get('purchase_cost', '')
try:
cursor.execute('''
UPDATE non_loanable_devices SET
brand = ?, model = ?, serial_number = ?, device_type = ?,
location = ?, status = ?, notes = ?, purchase_date = ?, purchase_cost = ?
WHERE id = ?
''', (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost, device_id))
conn.commit()
flash('Device updated successfully!', 'success')
return redirect(url_for('non_loanable_devices'))
except sqlite3.IntegrityError:
flash('Error: Serial number already exists!', 'error')
return render_template('edit_non_loanable_device.html', device=device)
@app.route('/delete_non_loanable_device/<int:device_id>')
def delete_non_loanable_device(device_id):
"""Delete a non-loanable device"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,))
conn.commit()
flash('Device deleted successfully!', 'success')
return redirect(url_for('non_loanable_devices'))
if __name__ == '__main__':
# Initialize database
init_db()

1
gestiontablets.sh Normal file
View file

@ -0,0 +1 @@
vibe --resume fa1bb230

View file

@ -44,6 +44,22 @@ def init_db():
)
''')
# Create non_loanable_devices table
cursor.execute('''
CREATE TABLE IF NOT EXISTS non_loanable_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
brand TEXT NOT NULL,
model TEXT NOT NULL,
serial_number TEXT UNIQUE NOT NULL,
device_type TEXT NOT NULL,
location TEXT,
status TEXT DEFAULT 'available',
notes TEXT,
purchase_date TEXT,
purchase_cost REAL
)
''')
conn.commit()
conn.close()
@ -202,6 +218,59 @@ def show_loan_history():
conn.close()
def add_non_loanable_device(brand, model, serial_number, device_type, location='', notes='', purchase_date='', purchase_cost=None):
"""Add a new non-loanable device to inventory"""
conn = sqlite3.connect('tablets.db')
cursor = conn.cursor()
try:
cursor.execute('''
INSERT INTO non_loanable_devices
(brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost)
VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?)
''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost))
conn.commit()
print(f"✓ Added non-loanable device: {brand} {model} ({serial_number}) - Type: {device_type}")
except sqlite3.IntegrityError:
print(f"✗ Error: Serial number {serial_number} already exists")
finally:
conn.close()
def show_non_loanable_devices():
"""Show all non-loanable devices"""
conn = sqlite3.connect('tablets.db')
cursor = conn.cursor()
cursor.execute("SELECT id, brand, model, serial_number, device_type, location, status FROM non_loanable_devices")
devices = cursor.fetchall()
print("\n=== Non-Loanable Devices ===")
if devices:
for device in devices:
print(f"ID: {device[0]}, Type: {device[4]}, {device[1]} {device[2]} ({device[3]}), Location: {device[5] or 'N/A'}, Status: {device[6]}")
else:
print("No non-loanable devices")
conn.close()
def delete_non_loanable_device(device_id):
"""Delete a non-loanable device"""
conn = sqlite3.connect('tablets.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,))
conn.commit()
if cursor.rowcount > 0:
print(f"✓ Non-loanable device {device_id} deleted")
else:
print(f"✗ Error: Device {device_id} not found")
conn.close()
def main():
"""Main menu"""
init_db()
@ -218,9 +287,12 @@ def main():
print("5. Show Available Tablets")
print("6. Show Active Loans")
print("7. Show Loan History")
print("8. Exit")
print("8. Add Non-Loanable Device")
print("9. Show Non-Loanable Devices")
print("10. Delete Non-Loanable Device")
print("11. Exit")
choice = input("Enter your choice (1-8): ")
choice = input("Enter your choice (1-11): ")
if choice == '1':
print("\n=== Add Tablet ===")
@ -269,6 +341,28 @@ def main():
show_loan_history()
elif choice == '8':
print("\n=== Add Non-Loanable Device ===")
brand = input("Brand: ")
model = input("Model: ")
serial_number = input("Serial Number: ")
device_type = input("Device Type (e.g., projector, monitor): ")
location = input("Location (optional): ")
notes = input("Notes (optional): ")
add_non_loanable_device(brand, model, serial_number, device_type, location, notes)
elif choice == '9':
show_non_loanable_devices()
elif choice == '10':
print("\n=== Delete Non-Loanable Device ===")
show_non_loanable_devices()
device_id = input("Enter Device ID to delete: ")
try:
delete_non_loanable_device(int(device_id))
except ValueError:
print("✗ Error: Invalid ID format")
elif choice == '11':
print("Goodbye!")
break

View file

@ -62,6 +62,22 @@ def init_db():
)
''')
# Create non_loanable_devices table
cursor.execute('''
CREATE TABLE IF NOT EXISTS non_loanable_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
brand TEXT NOT NULL,
model TEXT NOT NULL,
serial_number TEXT UNIQUE NOT NULL,
device_type TEXT NOT NULL,
location TEXT,
status TEXT DEFAULT 'available',
notes TEXT,
purchase_date TEXT,
purchase_cost REAL
)
''')
conn.commit()
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):

View file

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block content %}
<h2>Add Non-Loanable Device</h2>
<p>Add a device that will be tracked in inventory but cannot be loaned to users (e.g., projectors, monitors, etc.)</p>
<form method="POST" action="/add_non_loanable_device">
<div class="form-group">
<label for="device_type">Device Type:</label>
<select id="device_type" name="device_type" required>
<option value="">Select device type...</option>
<option value="projector">Projector</option>
<option value="monitor">Monitor</option>
<option value="laptop">Laptop (non-loanable)</option>
<option value="printer">Printer</option>
<option value="camera">Camera</option>
<option value="audio">Audio Equipment</option>
<option value="network">Network Equipment</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="brand">Brand:</label>
<input type="text" id="brand" name="brand" required>
</div>
<div class="form-group">
<label for="model">Model:</label>
<input type="text" id="model" name="model" required>
</div>
<div class="form-group">
<label for="serial_number">Serial Number:</label>
<input type="text" id="serial_number" name="serial_number" required>
</div>
<div class="form-group">
<label for="location">Location:</label>
<input type="text" id="location" name="location">
</div>
<div class="form-group">
<label for="purchase_date">Purchase Date:</label>
<input type="date" id="purchase_date" name="purchase_date">
</div>
<div class="form-group">
<label for="purchase_cost">Purchase Cost:</label>
<input type="number" id="purchase_cost" name="purchase_cost" step="0.01">
</div>
<div class="form-group">
<label for="notes">Notes:</label>
<textarea id="notes" name="notes"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn">Add Device</button>
<a href="/non_loanable_devices" class="btn">Cancel</a>
</div>
</form>
{% endblock %}

View file

@ -144,6 +144,7 @@
<a href="/loan_tablet">Loan Tablet</a>
<a href="/history">Loan History</a>
<a href="/user_loans">User Loans</a>
<a href="/non_loanable_devices">Non-Loanable Devices</a>
<a href="/project_management">Project Management</a>
</div>

View file

@ -0,0 +1,71 @@
{% extends "base.html" %}
{% block content %}
<h2>Edit Non-Loanable Device</h2>
<form method="POST" action="/edit_non_loanable_device/{{ device.id }}">
<div class="form-group">
<label for="device_type">Device Type:</label>
<select id="device_type" name="device_type" required>
<option value="projector" {% if device.device_type == 'projector' %}selected{% endif %}>Projector</option>
<option value="monitor" {% if device.device_type == 'monitor' %}selected{% endif %}>Monitor</option>
<option value="laptop" {% if device.device_type == 'laptop' %}selected{% endif %}>Laptop (non-loanable)</option>
<option value="printer" {% if device.device_type == 'printer' %}selected{% endif %}>Printer</option>
<option value="camera" {% if device.device_type == 'camera' %}selected{% endif %}>Camera</option>
<option value="audio" {% if device.device_type == 'audio' %}selected{% endif %}>Audio Equipment</option>
<option value="network" {% if device.device_type == 'network' %}selected{% endif %}>Network Equipment</option>
<option value="other" {% if device.device_type == 'other' %}selected{% endif %}>Other</option>
</select>
</div>
<div class="form-group">
<label for="brand">Brand:</label>
<input type="text" id="brand" name="brand" value="{{ device.brand }}" required>
</div>
<div class="form-group">
<label for="model">Model:</label>
<input type="text" id="model" name="model" value="{{ device.model }}" required>
</div>
<div class="form-group">
<label for="serial_number">Serial Number:</label>
<input type="text" id="serial_number" name="serial_number" value="{{ device.serial_number }}" required>
</div>
<div class="form-group">
<label for="location">Location:</label>
<input type="text" id="location" name="location" value="{{ device.location or '' }}">
</div>
<div class="form-group">
<label for="status">Status:</label>
<select id="status" name="status" required>
<option value="available" {% if device.status == 'available' %}selected{% endif %}>Available</option>
<option value="in_use" {% if device.status == 'in_use' %}selected{% endif %}>In Use</option>
<option value="maintenance" {% if device.status == 'maintenance' %}selected{% endif %}>Maintenance</option>
<option value="retired" {% if device.status == 'retired' %}selected{% endif %}>Retired</option>
</select>
</div>
<div class="form-group">
<label for="purchase_date">Purchase Date:</label>
<input type="date" id="purchase_date" name="purchase_date" value="{{ device.purchase_date or '' }}">
</div>
<div class="form-group">
<label for="purchase_cost">Purchase Cost:</label>
<input type="number" id="purchase_cost" name="purchase_cost" step="0.01" value="{{ device.purchase_cost or '' }}">
</div>
<div class="form-group">
<label for="notes">Notes:</label>
<textarea id="notes" name="notes">{{ device.notes or '' }}</textarea>
</div>
<div class="form-group">
<button type="submit" class="btn">Update Device</button>
<a href="/non_loanable_devices" class="btn">Cancel</a>
</div>
</form>
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="section">
<h2>Non-Loanable Devices Inventory</h2>
<p>These devices are tracked in inventory but cannot be loaned to users.</p>
<a href="/add_non_loanable_device" class="btn">Add Non-Loanable Device</a>
{% if devices %}
<table>
<thead>
<tr>
<th>Type</th>
<th>Brand</th>
<th>Model</th>
<th>Serial Number</th>
<th>Location</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for device in devices %}
<tr>
<td>{{ device.device_type }}</td>
<td>{{ device.brand }}</td>
<td>{{ device.model }}</td>
<td>{{ device.serial_number }}</td>
<td>{{ device.location or '-' }}</td>
<td>{{ device.status }}</td>
<td>
<a href="/edit_non_loanable_device/{{ device.id }}" class="btn">Edit</a>
<a href="/delete_non_loanable_device/{{ device.id }}" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this device?')">Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No non-loanable devices registered.</p>
{% endif %}
</div>
{% endblock %}