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:
parent
ac73e5dc93
commit
d3d1fb1b32
10 changed files with 417 additions and 6 deletions
10
README.md
10
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
109
app.py
|
|
@ -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
1
gestiontablets.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
vibe --resume fa1bb230
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
63
templates/add_non_loanable_device.html
Normal file
63
templates/add_non_loanable_device.html
Normal 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 %}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
71
templates/edit_non_loanable_device.html
Normal file
71
templates/edit_non_loanable_device.html
Normal 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 %}
|
||||
43
templates/non_loanable_devices.html
Normal file
43
templates/non_loanable_devices.html
Normal 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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue