Files
alro65 5b7b41aa50 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>
2026-05-05 02:54:10 -04:00

597 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)