Initial commit: Fleet Management app with security hardening and background launcher

- Flask app with SQLAlchemy, Flask-Login, Flask-Mail
- Admin/owner roles, vessel management, charters, work orders
- Background launcher (Iniciar.vbs) runs server without terminal window
- Root redirect fixed: / → /login
- debug=False, use_reloader=False for pythonw.exe compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 02:54:10 -04:00
parent 0b976a1ec0
commit 5b7b41aa50
23 changed files with 6375 additions and 0 deletions
+596
View File
@@ -0,0 +1,596 @@
"""
seed_data.py — Datos de demo completos para Fleet Management
Ejecutar: python seed_data.py
Cuentas creadas:
Admin: admin@fleet.com / admin123
Owner 1: rmitchell@email.com / owner123 (Robert Mitchell - 2 botes)
Owner 2: msantos@email.com / owner123 (Maria Santos - 2 botes)
Owner 3: cvega@email.com / owner123 (Carlos Vega - 1 bote)
Owner 4: jwilliams@email.com / owner123 (Jennifer Williams - 1 bote)
"""
from app import create_app, db
from app.models import Company, User, Vessel, Captain, Charter, WorkOrder, Voucher, AccountingEntry, FuelEntry
from werkzeug.security import generate_password_hash
from datetime import date
from datetime import datetime, timedelta
import random
app = create_app()
def get_or_create(model, defaults=None, **kwargs):
instance = model.query.filter_by(**kwargs).first()
if instance:
return instance, False
params = {**kwargs, **(defaults or {})}
instance = model(**params)
db.session.add(instance)
db.session.flush()
return instance, True
with app.app_context():
print("=" * 55)
print(" Fleet Management — Cargando datos de demo")
print("=" * 55)
# ============================================================
# MANAGEMENT COMPANY + ADMIN USER
# ============================================================
mgmt, _ = get_or_create(Company,
defaults={'type': 'management', 'phone': '305-900-0001'},
name='Al & Al Management LLC', email='admin@fleet.com')
admin_user, created = get_or_create(User,
defaults={
'name': 'Admin Fleet',
'password_hash': generate_password_hash('admin123'),
'company_id': mgmt.id,
'role': 'admin',
'is_active': True,
'is_super_admin': True
},
email='admin@fleet.com')
# Ensure existing admin is marked as super_admin
if not admin_user.is_super_admin:
admin_user.is_super_admin = True
if created:
print(" [+] Admin creado: admin@fleet.com / admin123")
else:
print(" [=] Admin ya existe")
db.session.commit()
# ============================================================
# OWNERS (companies + portal users)
# ============================================================
owners_data = [
{
'name': 'Robert Mitchell',
'email': 'rmitchell@email.com',
'phone': '305-555-2001',
},
{
'name': 'Maria Santos',
'email': 'msantos@email.com',
'phone': '305-555-2002',
},
{
'name': 'Carlos Vega',
'email': 'cvega@email.com',
'phone': '786-555-2003',
},
{
'name': 'Jennifer Williams',
'email': 'jwilliams@email.com',
'phone': '786-555-2004',
},
]
owner_companies = {}
for od in owners_data:
company, created = get_or_create(Company,
defaults={'type': 'owner', 'phone': od['phone']},
name=od['name'], email=od['email'])
owner_companies[od['name']] = company
user, ucreated = get_or_create(User,
defaults={
'name': od['name'],
'password_hash': generate_password_hash('owner123'),
'company_id': company.id,
'role': 'owner',
'is_active': True
},
email=od['email'])
if ucreated:
print(f" [+] Owner: {od['email']} / owner123")
else:
print(f" [=] Owner ya existe: {od['email']}")
db.session.commit()
# ============================================================
# CAPTAINS
# ============================================================
captains_data = [
{'name': 'Carlos Pérez', 'phone': '305-555-5001', 'license_number': 'USCG-REC-12345', 'hourly_rate': 55},
{'name': 'Miguel Torres', 'phone': '305-555-5002', 'license_number': 'USCG-REC-67890', 'hourly_rate': 60},
{'name': 'Roberto Díaz', 'phone': '786-555-5003', 'license_number': 'USCG-REC-11223', 'hourly_rate': 50},
]
captains = {}
for cd in captains_data:
cap, created = get_or_create(Captain,
defaults={
'phone': cd['phone'],
'license_number': cd['license_number'],
'hourly_rate': cd['hourly_rate'],
'company_id': mgmt.id
},
name=cd['name'])
captains[cd['name']] = cap
if created:
print(f" [+] Capitán: {cd['name']}")
db.session.commit()
# ============================================================
# VESSELS (6 embarcaciones)
# ============================================================
# plan_id: 1=Básico $199, 2=Estándar $399, 3=Mantenimiento $299, 4=Plus $599
vessels_data = [
{
'name': 'Blue Horizon',
'make': 'Sea Ray',
'model': '430 Sundancer',
'engines': '2 x Mercruiser 496 MAG',
'length': 43,
'fuel_consumption_14knots': 65,
'base_rate_4h': 1200,
'hourly_rate_extra': 300,
'charter_percentage': 25,
'plan_id': 4,
'owner': 'Robert Mitchell',
'hin': 'SRAY43001A222'
},
{
'name': 'Sol y Mar',
'make': 'Azimut',
'model': '38 Flybridge',
'engines': '2 x Volvo Penta D6-370',
'length': 38,
'fuel_consumption_14knots': 55,
'base_rate_4h': 950,
'hourly_rate_extra': 250,
'charter_percentage': 25,
'plan_id': 2,
'owner': 'Robert Mitchell',
'hin': 'AZMT38002B222'
},
{
'name': 'La Perla',
'make': 'Contender',
'model': '32 ST',
'engines': '2 x Yamaha F350',
'length': 32,
'fuel_consumption_14knots': 42,
'base_rate_4h': 750,
'hourly_rate_extra': 190,
'charter_percentage': 25,
'plan_id': 2,
'owner': 'Maria Santos',
'hin': 'CONT32003C222'
},
{
'name': 'Brisa Marina',
'make': 'Grady-White',
'model': 'Freedom 285',
'engines': '2 x Yamaha F150',
'length': 28,
'fuel_consumption_14knots': 30,
'base_rate_4h': 550,
'hourly_rate_extra': 140,
'charter_percentage': 25,
'plan_id': 1,
'owner': 'Maria Santos',
'hin': 'GRDY28004D222'
},
{
'name': 'Veloce',
'make': 'Scarab',
'model': '35 Sport',
'engines': '2 x Mercury Verado 400R',
'length': 35,
'fuel_consumption_14knots': 50,
'base_rate_4h': 850,
'hourly_rate_extra': 220,
'charter_percentage': 25,
'plan_id': 1,
'owner': 'Carlos Vega',
'hin': 'SCRB35005E222'
},
{
'name': 'Lady J',
'make': 'Sea Ray',
'model': '480 Sundancer',
'engines': '2 x Zeus Pod 600',
'length': 48,
'fuel_consumption_14knots': 75,
'base_rate_4h': 1500,
'hourly_rate_extra': 375,
'charter_percentage': 25,
'plan_id': 4,
'owner': 'Jennifer Williams',
'hin': 'SRAY48006F222'
},
]
vessels = {}
for vd in vessels_data:
owner_co = owner_companies[vd['owner']]
vessel, created = get_or_create(Vessel,
defaults={
'make': vd['make'],
'model': vd['model'],
'engines': vd['engines'],
'length': vd['length'],
'fuel_consumption_14knots': vd['fuel_consumption_14knots'],
'base_rate_4h': vd['base_rate_4h'],
'hourly_rate_extra': vd['hourly_rate_extra'],
'charter_percentage': vd['charter_percentage'],
'plan_id': vd['plan_id'],
'owner_company_id': owner_co.id,
'management_company_id': mgmt.id,
'hin': vd['hin'],
'is_active': True
},
name=vd['name'])
vessels[vd['name']] = vessel
if created:
print(f" [+] Bote: {vd['name']} ({vd['make']} {vd['model']}) — {vd['owner']}")
db.session.commit()
# ============================================================
# CHARTERS
# ============================================================
# Clientes reales con datos
charterers = [
{'name': 'Andrew Lawson', 'phone': '305-600-1001', 'email': 'alawson@gmail.com'},
{'name': 'Sophia Chen', 'phone': '305-600-1002', 'email': 'sophia.chen@mail.com'},
{'name': 'Marcus Johnson', 'phone': '786-600-1003', 'email': 'mjohnson@corp.com'},
{'name': 'Isabella Gomez', 'phone': '305-600-1004', 'email': 'igomez@gmail.com'},
{'name': 'David Park', 'phone': '786-600-1005', 'email': 'dpark@hotmail.com'},
{'name': 'Nicole Fontaine', 'phone': '305-600-1006', 'email': 'nfontaine@gmail.com'},
{'name': 'Tyler Brooks', 'phone': '786-600-1007', 'email': 'tbrooks@corp.com'},
{'name': 'Valentina Cruz', 'phone': '305-600-1008', 'email': 'vcruz@gmail.com'},
{'name': 'James Whitfield', 'phone': '305-600-1009', 'email': 'jwhitfield@mail.com'},
{'name': 'Camila Restrepo', 'phone': '786-600-1010', 'email': 'crestrepo@gmail.com'},
]
def calc_charter(vessel, hours):
base = vessel.base_rate_4h
extra = vessel.hourly_rate_extra
total = base if hours <= 4 else base + (hours - 4) * extra
pct = vessel.charter_percentage
mgmt_earn = round(total * pct / 100, 2)
owner_earn = round(total - mgmt_earn, 2)
return round(total, 2), mgmt_earn, owner_earn
now = datetime.utcnow()
charters_def = [
# Blue Horizon — 4 charters completados, 1 próximo
{'vessel': 'Blue Horizon', 'days_ago': 45, 'hours': 4, 'charterer': 0, 'status': 'completed'},
{'vessel': 'Blue Horizon', 'days_ago': 32, 'hours': 6, 'charterer': 3, 'status': 'completed'},
{'vessel': 'Blue Horizon', 'days_ago': 18, 'hours': 8, 'charterer': 6, 'status': 'completed'},
{'vessel': 'Blue Horizon', 'days_ago': 7, 'hours': 4, 'charterer': 9, 'status': 'completed'},
{'vessel': 'Blue Horizon', 'days_ago': -5, 'hours': 6, 'charterer': 1, 'status': 'signed'},
# Sol y Mar — 3 completados
{'vessel': 'Sol y Mar', 'days_ago': 38, 'hours': 5, 'charterer': 2, 'status': 'completed'},
{'vessel': 'Sol y Mar', 'days_ago': 21, 'hours': 4, 'charterer': 5, 'status': 'completed'},
{'vessel': 'Sol y Mar', 'days_ago': 10, 'hours': 6, 'charterer': 8, 'status': 'completed'},
# La Perla — 3 completados, 1 draft
{'vessel': 'La Perla', 'days_ago': 50, 'hours': 4, 'charterer': 1, 'status': 'completed'},
{'vessel': 'La Perla', 'days_ago': 29, 'hours': 4, 'charterer': 4, 'status': 'completed'},
{'vessel': 'La Perla', 'days_ago': 14, 'hours': 5, 'charterer': 7, 'status': 'completed'},
{'vessel': 'La Perla', 'days_ago': -3, 'hours': 4, 'charterer': 0, 'status': 'draft'},
# Brisa Marina — 2 completados
{'vessel': 'Brisa Marina', 'days_ago': 42, 'hours': 4, 'charterer': 3, 'status': 'completed'},
{'vessel': 'Brisa Marina', 'days_ago': 15, 'hours': 4, 'charterer': 6, 'status': 'completed'},
# Veloce — 3 completados, 1 signed
{'vessel': 'Veloce', 'days_ago': 55, 'hours': 4, 'charterer': 5, 'status': 'completed'},
{'vessel': 'Veloce', 'days_ago': 35, 'hours': 6, 'charterer': 2, 'status': 'completed'},
{'vessel': 'Veloce', 'days_ago': 12, 'hours': 4, 'charterer': 9, 'status': 'completed'},
{'vessel': 'Veloce', 'days_ago': -7, 'hours': 5, 'charterer': 4, 'status': 'signed'},
# Lady J — 4 completados, 1 próximo (el más caro)
{'vessel': 'Lady J', 'days_ago': 60, 'hours': 8, 'charterer': 7, 'status': 'completed'},
{'vessel': 'Lady J', 'days_ago': 40, 'hours': 6, 'charterer': 0, 'status': 'completed'},
{'vessel': 'Lady J', 'days_ago': 22, 'hours': 4, 'charterer': 3, 'status': 'completed'},
{'vessel': 'Lady J', 'days_ago': 8, 'hours': 10, 'charterer': 8, 'status': 'completed'},
{'vessel': 'Lady J', 'days_ago': -10,'hours': 6, 'charterer': 1, 'status': 'signed'},
]
charters_created = 0
vouchers_created = 0
for cd in charters_def:
vessel = vessels[cd['vessel']]
ch_data = charterers[cd['charterer']]
start_dt = now - timedelta(days=cd['days_ago'], hours=10)
total, mgmt_earn, owner_earn = calc_charter(vessel, cd['hours'])
existing = Charter.query.filter_by(
vessel_id=vessel.id,
charterer_name=ch_data['name'],
hours=cd['hours']
).filter(
Charter.start_datetime >= start_dt - timedelta(hours=2),
Charter.start_datetime <= start_dt + timedelta(hours=2)
).first()
if existing:
continue
charter = Charter(
vessel_id=vessel.id,
charterer_name=ch_data['name'],
charterer_phone=ch_data['phone'],
charterer_email=ch_data['email'],
start_datetime=start_dt,
hours=cd['hours'],
total_base_rate=total,
management_percentage=vessel.charter_percentage,
management_earnings=mgmt_earn,
owner_earnings=owner_earn,
status=cd['status'],
completed_at=start_dt + timedelta(hours=cd['hours']) if cd['status'] == 'completed' else None
)
db.session.add(charter)
db.session.flush()
charters_created += 1
# Generar voucher para charters completados
if cd['status'] == 'completed':
fuel = round(vessel.fuel_consumption_14knots * cd['hours'] * random.uniform(0.8, 1.0))
tip_pct = random.choice([15, 18, 20])
voucher = Voucher(
charter_id=charter.id,
total_charged=total,
fuel_actual_liters=fuel,
deviation_charged=0,
tip_amount=round(total * tip_pct / 100, 2),
tip_percentage=tip_pct,
issued_at=charter.completed_at,
paid_at=charter.completed_at + timedelta(days=1)
)
db.session.add(voucher)
vouchers_created += 1
db.session.commit()
print(f"\n [+] Charters creados: {charters_created} ({vouchers_created} con voucher)")
# ============================================================
# WORK ORDERS
# ============================================================
wo_data = [
{
'vessel': 'Blue Horizon',
'description': 'Cambio de aceite y filtros motores - servicio 200h',
'estimated_cost': 850, 'actual_cost': 820,
'status': 'done', 'priority': 'normal', 'days_ago': 40
},
{
'vessel': 'Blue Horizon',
'description': 'Revisión sistema de dirección hidráulica - pérdida de fluido detectada',
'estimated_cost': 450, 'actual_cost': None,
'status': 'approved', 'priority': 'urgente', 'days_ago': 5
},
{
'vessel': 'Sol y Mar',
'description': 'Limpieza de teca y aplicación de teak oil',
'estimated_cost': 600, 'actual_cost': 580,
'status': 'done', 'priority': 'normal', 'days_ago': 25
},
{
'vessel': 'Sol y Mar',
'description': 'Actualización software GPS Garmin y radar',
'estimated_cost': 200, 'actual_cost': None,
'status': 'pending', 'priority': 'normal', 'days_ago': 3
},
{
'vessel': 'La Perla',
'description': 'Reemplazo baterías principales (banco 4 x AGM)',
'estimated_cost': 1200, 'actual_cost': 1150,
'status': 'done', 'priority': 'normal', 'days_ago': 30
},
{
'vessel': 'La Perla',
'description': 'Pintura fondo antifouling - temporada anual',
'estimated_cost': 1800, 'actual_cost': None,
'status': 'pending', 'priority': 'normal', 'days_ago': 2
},
{
'vessel': 'Brisa Marina',
'description': 'Cambio impellers bombas de agua dulce y salada',
'estimated_cost': 320, 'actual_cost': 310,
'status': 'done', 'priority': 'normal', 'days_ago': 20
},
{
'vessel': 'Veloce',
'description': 'Servicio anual motores Mercury Verado (aceite, filtros, bujías)',
'estimated_cost': 1400, 'actual_cost': 1380,
'status': 'done', 'priority': 'normal', 'days_ago': 35
},
{
'vessel': 'Veloce',
'description': 'Falla en sistema eléctrico principal — luces de navegación inoperativas. BOTE NO PUEDE SALIR A CHARTER.',
'estimated_cost': 1100, 'actual_cost': None,
'status': 'pending', 'priority': 'emergencia', 'days_ago': 1
},
{
'vessel': 'Lady J',
'description': 'Detailing completo exterior e interior - plan Plus anual',
'estimated_cost': 2200, 'actual_cost': 2200,
'status': 'done', 'priority': 'normal', 'days_ago': 50
},
{
'vessel': 'Lady J',
'description': 'Revisión y ajuste sistema Zeus Pod - vibración anormal a más de 12 nudos',
'estimated_cost': 950, 'actual_cost': None,
'status': 'pending', 'priority': 'urgente', 'days_ago': 1
},
]
wo_created = 0
for wd in wo_data:
vessel = vessels[wd['vessel']]
existing = WorkOrder.query.filter_by(
vessel_id=vessel.id,
description=wd['description']
).first()
if existing:
continue
created_date = now - timedelta(days=wd['days_ago'])
wo = WorkOrder(
vessel_id=vessel.id,
requested_by_company_id=mgmt.id,
description=wd['description'],
estimated_cost=wd['estimated_cost'],
actual_cost=wd['actual_cost'],
status=wd['status'],
priority=wd.get('priority', 'normal'),
created_at=created_date,
completed_at=created_date + timedelta(days=3) if wd['status'] == 'done' else None,
approved_by_owner_id=1 if wd['status'] in ('approved', 'done') else None
)
db.session.add(wo)
wo_created += 1
db.session.commit()
print(f" [+] Work Orders creadas: {wo_created}")
# ============================================================
# ACCOUNTING ENTRIES (desde charters y WOs existentes)
# ============================================================
acc_created = 0
# Income: charters completados → 75% va al owner
all_charters = Charter.query.filter_by(status='completed').all()
for ch in all_charters:
exists = AccountingEntry.query.filter_by(
vessel_id=ch.vessel_id, reference_type='charter', reference_id=ch.id
).first()
if exists:
continue
if not ch.owner_earnings:
continue
entry_date = ch.completed_at.date() if ch.completed_at else (
ch.start_datetime.date() if ch.start_datetime else date.today())
db.session.add(AccountingEntry(
vessel_id=ch.vessel_id,
date=entry_date,
entry_type='income',
category='charter',
description=f'Charter {ch.charterer_name} {ch.hours}h',
amount=round(ch.owner_earnings, 2),
reference_type='charter',
reference_id=ch.id,
invoice_number=f'CHR-{ch.id:04d}'
))
acc_created += 1
# Expense: WOs completadas
all_wos = WorkOrder.query.filter_by(status='done').all()
for wo in all_wos:
exists = AccountingEntry.query.filter_by(
vessel_id=wo.vessel_id, reference_type='work_order', reference_id=wo.id
).first()
if exists:
continue
cost = wo.actual_cost or wo.estimated_cost or 0
if cost <= 0:
continue
entry_date = wo.completed_at.date() if wo.completed_at else date.today()
db.session.add(AccountingEntry(
vessel_id=wo.vessel_id,
date=entry_date,
entry_type='expense',
category='work_order',
description=f'Mantenimiento {wo.description[:80]}',
amount=round(cost, 2),
reference_type='work_order',
reference_id=wo.id,
invoice_number=f'WO-{wo.id:04d}'
))
acc_created += 1
# Expense: combustible de vouchers (si tiene fuel_actual_liters)
fuel_price = 1.45 # $/litro aprox
all_vouchers = Voucher.query.filter(Voucher.fuel_actual_liters > 0).all()
for v in all_vouchers:
ch = Charter.query.get(v.charter_id)
if not ch:
continue
exists = AccountingEntry.query.filter_by(
vessel_id=ch.vessel_id, reference_type='fuel_entry',
reference_id=v.id
).first()
if exists:
continue
fuel_cost = round(v.fuel_actual_liters * fuel_price, 2)
entry_date = v.issued_at.date() if v.issued_at else date.today()
# FuelEntry record
fuel_rec = FuelEntry(
vessel_id=ch.vessel_id,
charter_id=ch.id,
date=entry_date,
liters=v.fuel_actual_liters,
price_per_liter=fuel_price,
total_cost=fuel_cost,
supplier='Marina Fuel Station',
invoice_number=f'FUEL-V{v.id:04d}'
)
db.session.add(fuel_rec)
db.session.flush()
db.session.add(AccountingEntry(
vessel_id=ch.vessel_id,
date=entry_date,
entry_type='expense',
category='fuel',
description=f'Combustible {v.fuel_actual_liters:.0f}L charter {ch.charterer_name}',
amount=fuel_cost,
reference_type='fuel_entry',
reference_id=fuel_rec.id,
invoice_number=f'FUEL-V{v.id:04d}'
))
acc_created += 1
db.session.commit()
print(f" [+] Asientos contables generados: {acc_created}")
# ============================================================
# RESUMEN
# ============================================================
print()
print("=" * 55)
print(" DATOS CARGADOS EXITOSAMENTE")
print("=" * 55)
print()
print(" ACCESOS:")
print(" Admin: admin@fleet.com / admin123")
print(" Owner 1: rmitchell@email.com / owner123")
print(" Owner 2: msantos@email.com / owner123")
print(" Owner 3: cvega@email.com / owner123")
print(" Owner 4: jwilliams@email.com / owner123")
print()
print(" DATOS:")
print(f" - {len(owner_companies)} duenos | 6 embarcaciones | 3 capitanes")
print(f" - {Charter.query.count()} charters | {WorkOrder.query.count()} work orders | {Voucher.query.count()} vouchers")
print(f" - {AccountingEntry.query.count()} asientos contables | {FuelEntry.query.count()} registros de combustible")
print()
print(" URL: http://localhost:5010")
print("=" * 55)