PPR ACK and Bulk Logging start

This commit is contained in:
2026-06-15 15:45:58 -04:00
parent 7b2de645db
commit 1952b89ecf
14 changed files with 1710 additions and 19 deletions
+593 -6
View File
@@ -1,12 +1,22 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from datetime import date
from app.api.deps import get_db, get_current_read_user
from sqlalchemy import func, or_
from datetime import date, datetime, time
from app.api.deps import get_db, get_current_operator_user, get_current_read_user
from app.crud.crud_movement import movement as crud_movement
from app.schemas.movement import Movement
from app.crud.crud_journal import journal as crud_journal
from app.schemas.movement import BulkMovementContext, BulkMovementLog, BulkMovementResult, Movement, MovementCreate
from app.models.ppr import User
from app.models.movement import MovementType
from app.models.arrival import Arrival, ArrivalStatus, SubmissionSource as ArrivalSubmissionSource
from app.models.circuit import Circuit
from app.models.departure import Departure, DepartureStatus, SubmissionSource as DepartureSubmissionSource
from app.models.journal import EntityType
from app.models.local_flight import LocalFlight, LocalFlightStatus, LocalFlightType, SubmissionSource as LocalSubmissionSource
from app.models.movement import Movement as MovementModel, MovementType
from app.models.overflight import Overflight, OverflightStatus
from app.models.ppr import PPRRecord, PPRStatus
from app.core.utils import get_client_ip
router = APIRouter()
@@ -32,6 +42,583 @@ async def get_movements(
return movements
def _clean_reg(registration: str) -> str:
return (registration or "").strip().upper()
def _clean_alnum(value: str) -> str:
return "".join(char for char in (value or "").upper() if char.isalnum())
def _sql_clean_alnum(column):
return func.upper(func.replace(func.replace(column, "-", ""), " ", ""))
def _combine_date_time(movement_date: date, movement_time: str) -> datetime:
if not movement_time:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A time is required for this strip"
)
try:
parsed_time = time.fromisoformat(movement_time)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="movement_time must be HH:MM or HH:MM:SS"
)
return datetime.combine(movement_date, parsed_time)
def _kind_to_movement_type(flight_kind: str) -> MovementType:
kind = (flight_kind or "").strip().upper()
if kind == "ARRIVAL":
return MovementType.LANDING
if kind == "DEPARTURE":
return MovementType.TAKEOFF
if kind == "LOCAL":
return MovementType.TAKEOFF
if kind == "OVERFLIGHT":
return MovementType.OVERFLIGHT
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="flight_kind must be ARRIVAL, DEPARTURE, LOCAL or OVERFLIGHT"
)
def _strip_entity_type(flight_kind: str) -> Optional[str]:
kind = (flight_kind or "").strip().upper()
return {
"LOCAL": "LOCAL_FLIGHT",
"OVERFLIGHT": "OVERFLIGHT",
}.get(kind)
def _compatible_entity_types(flight_kind: str) -> List[str]:
kind = (flight_kind or "").strip().upper()
if kind == "ARRIVAL":
return ["PPR", "ARRIVAL"]
if kind == "DEPARTURE":
return ["PPR", "DEPARTURE"]
if kind == "LOCAL":
return ["LOCAL_FLIGHT"]
if kind == "OVERFLIGHT":
return ["OVERFLIGHT"]
return []
def _ppr_to_dict(ppr: PPRRecord) -> dict:
return {
"id": ppr.id,
"status": ppr.status.value,
"aircraft_registration": ppr.ac_reg,
"aircraft_type": ppr.ac_type,
"callsign": ppr.ac_call,
"from_location": ppr.in_from,
"to_location": ppr.out_to,
"eta": ppr.eta.isoformat() if ppr.eta else None,
"etd": ppr.etd.isoformat() if ppr.etd else None,
"pob_in": ppr.pob_in,
"pob_out": ppr.pob_out,
"notes": ppr.notes,
}
def _movement_to_dict(movement: MovementModel) -> dict:
return {
"id": movement.id,
"movement_type": movement.movement_type.value,
"aircraft_registration": movement.aircraft_registration,
"aircraft_type": movement.aircraft_type,
"callsign": movement.callsign,
"timestamp": movement.timestamp.isoformat() if movement.timestamp else None,
"entity_type": movement.entity_type,
"entity_id": movement.entity_id,
"from_location": movement.from_location,
"to_location": movement.to_location,
"runway": movement.runway,
"wind": movement.wind,
"pressure_setting": movement.pressure_setting,
"notes": movement.notes,
}
def _build_suggestion(pprs: List[PPRRecord], movements: List[MovementModel], flight_kind: str) -> dict:
if movements:
movement = movements[0]
return {
"source": "movement",
"movement_id": movement.id,
"aircraft_registration": movement.aircraft_registration,
"aircraft_type": movement.aircraft_type,
"callsign": movement.callsign,
"movement_time": movement.timestamp.strftime("%H:%M") if movement.timestamp else None,
"from_location": movement.from_location,
"to_location": movement.to_location,
"runway": movement.runway,
"wind": movement.wind,
"pressure_setting": movement.pressure_setting,
"notes": movement.notes,
}
if pprs:
ppr = pprs[0]
is_arrival = flight_kind.upper() == "ARRIVAL"
timestamp = ppr.eta if is_arrival else ppr.etd
return {
"source": "ppr",
"ppr_id": ppr.id,
"aircraft_registration": ppr.ac_reg,
"aircraft_type": ppr.ac_type,
"callsign": ppr.ac_call,
"movement_time": timestamp.strftime("%H:%M") if timestamp else None,
"from_location": ppr.in_from,
"to_location": ppr.out_to,
"pob": ppr.pob_in if is_arrival else (ppr.pob_out or ppr.pob_in),
"notes": ppr.notes,
}
return {}
def _movement_for_entity(
db: Session,
entity_type: str,
entity_id: int,
movement_type: MovementType
) -> Optional[MovementModel]:
return db.query(MovementModel).filter(
MovementModel.entity_type == entity_type,
MovementModel.entity_id == entity_id,
MovementModel.movement_type == movement_type
).order_by(MovementModel.timestamp).first()
def _create_or_update_movement(db: Session, movement_data: MovementCreate) -> MovementModel:
existing = _movement_for_entity(
db,
movement_data.entity_type,
movement_data.entity_id,
movement_data.movement_type
)
if existing:
return crud_movement.update(db, existing, movement_data)
return crud_movement.create(db, movement_data)
def _clear_local_circuit_details(
db: Session,
local_flight: LocalFlight
) -> None:
db.query(Circuit).filter(Circuit.local_flight_id == local_flight.id).delete()
db.query(MovementModel).filter(
MovementModel.entity_type == "LOCAL_FLIGHT",
MovementModel.entity_id == local_flight.id,
MovementModel.movement_type == MovementType.TOUCH_AND_GO
).delete()
db.commit()
@router.get("/bulk-context", response_model=BulkMovementContext)
async def get_bulk_movement_context(
target_date: date,
aircraft_registration: str,
flight_kind: str = "ARRIVAL",
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Find same-day PPRs and movements that may match a bulk entry strip."""
clean_reg = _clean_reg(aircraft_registration)
clean_lookup = _clean_alnum(aircraft_registration)
movement_type = _kind_to_movement_type(flight_kind)
entity_type_filter = _strip_entity_type(flight_kind)
pprs = []
if clean_lookup:
pprs = db.query(PPRRecord).filter(
_sql_clean_alnum(PPRRecord.ac_reg).like(f"{clean_lookup}%"),
or_(
func.date(PPRRecord.eta) == target_date,
func.date(PPRRecord.etd) == target_date
),
PPRRecord.status != PPRStatus.DELETED
).order_by(PPRRecord.eta).limit(10).all()
movements = []
if clean_lookup:
movements = db.query(MovementModel).filter(
func.date(MovementModel.timestamp) == target_date,
_sql_clean_alnum(MovementModel.aircraft_registration).like(f"{clean_lookup}%"),
MovementModel.movement_type == movement_type
).order_by(MovementModel.timestamp.desc()).limit(10).all()
compatible_types = _compatible_entity_types(flight_kind)
if compatible_types:
movements = [movement for movement in movements if movement.entity_type in compatible_types]
return BulkMovementContext(
pprs=[_ppr_to_dict(ppr) for ppr in pprs],
movements=[_movement_to_dict(movement) for movement in movements],
suggested=_build_suggestion(pprs, movements, flight_kind)
)
@router.post("/bulk-log", response_model=BulkMovementResult)
async def bulk_log_movement(
request: Request,
entry: BulkMovementLog,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Create or update one same-day movement from a paper strip."""
client_ip = get_client_ip(request)
username = current_user.username
clean_reg = _clean_reg(entry.aircraft_registration)
clean_lookup = _clean_alnum(entry.aircraft_registration)
if not clean_reg:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Aircraft registration is required")
movement_type = _kind_to_movement_type(entry.flight_kind)
flight_kind = entry.flight_kind.strip().upper()
primary_time = (
entry.landing_time if flight_kind == "ARRIVAL"
else entry.takeoff_time if flight_kind in ("DEPARTURE", "LOCAL")
else entry.contact_time if flight_kind == "OVERFLIGHT"
else entry.movement_time
) or entry.movement_time
timestamp = _combine_date_time(entry.movement_date, primary_time)
existing_movement = crud_movement.get(db, entry.movement_id) if entry.movement_id else None
if not existing_movement:
existing_movement = crud_movement.find_daily_match(
db,
entry.movement_date,
clean_reg,
movement_type,
entity_type=_strip_entity_type(flight_kind),
entity_types=None if _strip_entity_type(flight_kind) else _compatible_entity_types(flight_kind)
)
if existing_movement and existing_movement.entity_type not in _compatible_entity_types(flight_kind):
existing_movement = None
if flight_kind == "LOCAL":
takeoff_dt = _combine_date_time(entry.movement_date, entry.takeoff_time)
landing_dt = _combine_date_time(entry.movement_date, entry.landing_time)
local_type = LocalFlightType.CIRCUITS if (entry.local_nature or "").upper() == "CIRCUITS" else LocalFlightType.LOCAL
local = None
if existing_movement and existing_movement.entity_type == "LOCAL_FLIGHT":
local = db.query(LocalFlight).filter(LocalFlight.id == existing_movement.entity_id).first()
if not local:
local = db.query(LocalFlight).filter(
_sql_clean_alnum(LocalFlight.registration) == clean_lookup,
func.date(LocalFlight.takeoff_dt) == entry.movement_date
).first()
if not local:
local = LocalFlight(
registration=clean_reg,
type=entry.aircraft_type or "",
callsign=entry.callsign,
pob=entry.pob or 1,
flight_type=local_type,
status=LocalFlightStatus.LANDED,
duration=int((landing_dt - takeoff_dt).total_seconds() / 60) if landing_dt > takeoff_dt else None,
circuits=entry.circuits or 0,
notes=entry.notes,
etd=takeoff_dt,
departed_dt=takeoff_dt,
takeoff_dt=takeoff_dt,
landed_dt=landing_dt,
created_by=username,
submitted_via=LocalSubmissionSource.ADMIN
)
db.add(local)
db.commit()
db.refresh(local)
action = "created"
crud_journal.log_change(db, EntityType.LOCAL_FLIGHT, local.id, "Local strip created from bulk flight log", username, client_ip)
else:
local.registration = clean_reg
local.type = entry.aircraft_type or local.type
local.callsign = entry.callsign
local.pob = entry.pob or local.pob
local.flight_type = local_type
local.status = LocalFlightStatus.LANDED
local.duration = int((landing_dt - takeoff_dt).total_seconds() / 60) if landing_dt > takeoff_dt else local.duration
local.circuits = entry.circuits or 0
local.notes = entry.notes
local.etd = takeoff_dt
local.departed_dt = takeoff_dt
local.takeoff_dt = takeoff_dt
local.landed_dt = landing_dt
db.add(local)
db.commit()
db.refresh(local)
action = "updated"
takeoff_movement = _create_or_update_movement(db, MovementCreate(
movement_type=MovementType.TAKEOFF,
aircraft_registration=clean_reg,
aircraft_type=local.type,
callsign=local.callsign,
timestamp=takeoff_dt,
entity_type="LOCAL_FLIGHT",
entity_id=local.id,
runway=entry.runway,
wind=entry.wind,
pressure_setting=entry.pressure_setting,
created_by=username,
ip_address=client_ip,
notes=entry.notes
))
_create_or_update_movement(db, MovementCreate(
movement_type=MovementType.LANDING,
aircraft_registration=clean_reg,
aircraft_type=local.type,
callsign=local.callsign,
timestamp=landing_dt,
entity_type="LOCAL_FLIGHT",
entity_id=local.id,
runway=entry.runway,
wind=entry.wind,
pressure_setting=entry.pressure_setting,
created_by=username,
ip_address=client_ip,
notes=entry.notes
))
_clear_local_circuit_details(db, local)
crud_journal.log_change(db, EntityType.LOCAL_FLIGHT, local.id, f"Bulk local strip {action}: takeoff {takeoff_dt.strftime('%H:%M')}, landing {landing_dt.strftime('%H:%M')}, circuits {entry.circuits or 0}", username, client_ip)
return BulkMovementResult(action=action, movement=takeoff_movement, entity_type="LOCAL_FLIGHT", entity_id=local.id, message=f"Local strip {action} for {clean_reg}")
if flight_kind == "OVERFLIGHT":
contact_dt = _combine_date_time(entry.movement_date, entry.contact_time)
qsy_dt = _combine_date_time(entry.movement_date, entry.qsy_time) if entry.qsy_time else None
overflight = None
if existing_movement and existing_movement.entity_type == "OVERFLIGHT":
overflight = db.query(Overflight).filter(Overflight.id == existing_movement.entity_id).first()
if not overflight:
overflight = db.query(Overflight).filter(
_sql_clean_alnum(Overflight.registration) == clean_lookup,
func.date(Overflight.call_dt) == entry.movement_date
).first()
if not overflight:
overflight = Overflight(
registration=clean_reg,
pob=entry.pob,
type=entry.aircraft_type,
departure_airfield=entry.from_location,
destination_airfield=entry.to_location,
status=OverflightStatus.INACTIVE if qsy_dt else OverflightStatus.ACTIVE,
call_dt=contact_dt,
qsy_dt=qsy_dt,
notes=entry.notes,
created_by=username
)
db.add(overflight)
db.commit()
db.refresh(overflight)
action = "created"
crud_journal.log_change(db, EntityType.OVERFLIGHT, overflight.id, "Overflight strip created from bulk flight log", username, client_ip)
else:
overflight.registration = clean_reg
overflight.pob = entry.pob
overflight.type = entry.aircraft_type
overflight.departure_airfield = entry.from_location
overflight.destination_airfield = entry.to_location
overflight.status = OverflightStatus.INACTIVE if qsy_dt else OverflightStatus.ACTIVE
overflight.call_dt = contact_dt
overflight.qsy_dt = qsy_dt
overflight.notes = entry.notes
db.add(overflight)
db.commit()
db.refresh(overflight)
action = "updated"
movement = _create_or_update_movement(db, MovementCreate(
movement_type=MovementType.OVERFLIGHT,
aircraft_registration=clean_reg,
aircraft_type=entry.aircraft_type,
callsign=entry.callsign,
timestamp=contact_dt,
entity_type="OVERFLIGHT",
entity_id=overflight.id,
from_location=entry.from_location,
to_location=entry.to_location,
runway=entry.runway,
wind=entry.wind,
pressure_setting=entry.pressure_setting,
created_by=username,
ip_address=client_ip,
notes=entry.notes
))
crud_journal.log_change(db, EntityType.OVERFLIGHT, overflight.id, f"Bulk overflight strip {action}: contact {contact_dt.strftime('%H:%M')}" + (f", QSY {qsy_dt.strftime('%H:%M')}" if qsy_dt else ""), username, client_ip)
return BulkMovementResult(action=action, movement=movement, entity_type="OVERFLIGHT", entity_id=overflight.id, message=f"Overflight strip {action} for {clean_reg}")
ppr = None
if entry.ppr_id:
ppr = db.query(PPRRecord).filter(PPRRecord.id == entry.ppr_id).first()
if not ppr and existing_movement and existing_movement.entity_type == "PPR":
ppr = db.query(PPRRecord).filter(PPRRecord.id == existing_movement.entity_id).first()
if not ppr:
ppr = db.query(PPRRecord).filter(
_sql_clean_alnum(PPRRecord.ac_reg) == clean_lookup,
or_(
func.date(PPRRecord.eta) == entry.movement_date,
func.date(PPRRecord.etd) == entry.movement_date
),
PPRRecord.status != PPRStatus.DELETED
).order_by(PPRRecord.eta).first()
entity_type = existing_movement.entity_type if existing_movement else None
entity_id = existing_movement.entity_id if existing_movement else None
if not entity_type:
if ppr:
entity_type = "PPR"
entity_id = ppr.id
elif movement_type == MovementType.LANDING:
arrival = Arrival(
registration=clean_reg,
type=entry.aircraft_type,
callsign=entry.callsign,
pob=entry.pob or 1,
in_from=entry.from_location or "ZZZZ",
status=ArrivalStatus.LANDED,
notes=entry.notes,
eta=timestamp,
landed_dt=timestamp,
created_by=username,
submitted_via=ArrivalSubmissionSource.ADMIN
)
db.add(arrival)
db.commit()
db.refresh(arrival)
entity_type = "ARRIVAL"
entity_id = arrival.id
crud_journal.log_change(db, EntityType.ARRIVAL, arrival.id, "Arrival created from bulk flight log", username, client_ip)
else:
departure = Departure(
registration=clean_reg,
type=entry.aircraft_type,
callsign=entry.callsign,
pob=entry.pob or 1,
out_to=entry.to_location or "ZZZZ",
status=DepartureStatus.DEPARTED,
notes=entry.notes,
etd=timestamp,
takeoff_dt=timestamp,
departed_dt=timestamp,
created_by=username,
submitted_via=DepartureSubmissionSource.ADMIN
)
db.add(departure)
db.commit()
db.refresh(departure)
entity_type = "DEPARTURE"
entity_id = departure.id
crud_journal.log_change(db, EntityType.DEPARTURE, departure.id, "Departure created from bulk flight log", username, client_ip)
movement_data = MovementCreate(
movement_type=movement_type,
aircraft_registration=clean_reg,
aircraft_type=entry.aircraft_type,
callsign=entry.callsign,
timestamp=timestamp,
entity_type=entity_type,
entity_id=entity_id,
from_location=entry.from_location,
to_location=entry.to_location,
runway=entry.runway,
wind=entry.wind,
pressure_setting=entry.pressure_setting,
created_by=username,
ip_address=client_ip,
notes=entry.notes
)
if existing_movement:
movement = crud_movement.update(db, existing_movement, movement_data)
action = "updated"
else:
movement = crud_movement.create(db, movement_data)
action = "created"
if entity_type == "PPR" and ppr:
ppr.ac_type = entry.aircraft_type or ppr.ac_type
ppr.ac_call = entry.callsign or ppr.ac_call
if movement_type == MovementType.LANDING:
ppr.in_from = entry.from_location or ppr.in_from
ppr.pob_in = entry.pob or ppr.pob_in
ppr.landed_dt = timestamp
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED, PPRStatus.DEPARTED):
ppr.status = PPRStatus.LANDED
else:
ppr.out_to = entry.to_location or ppr.out_to
ppr.pob_out = entry.pob or ppr.pob_out
ppr.departed_dt = timestamp
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED):
ppr.status = PPRStatus.DEPARTED
if entry.notes:
ppr.notes = entry.notes
db.add(ppr)
db.commit()
crud_journal.log_change(
db,
EntityType.PPR,
ppr.id,
f"Bulk flight log {action}: {movement_type.value} at {timestamp.strftime('%Y-%m-%d %H:%M')}",
username,
client_ip
)
elif entity_type == "ARRIVAL":
arrival = db.query(Arrival).filter(Arrival.id == entity_id).first()
if arrival:
arrival.registration = clean_reg
arrival.type = entry.aircraft_type
arrival.callsign = entry.callsign
arrival.pob = entry.pob or arrival.pob
arrival.in_from = entry.from_location or arrival.in_from
arrival.eta = timestamp
arrival.landed_dt = timestamp
arrival.status = ArrivalStatus.LANDED
arrival.notes = entry.notes
db.add(arrival)
db.commit()
crud_journal.log_change(db, EntityType.ARRIVAL, arrival.id, f"Bulk flight log {action}: landing at {timestamp.strftime('%Y-%m-%d %H:%M')}", username, client_ip)
elif entity_type == "DEPARTURE":
departure = db.query(Departure).filter(Departure.id == entity_id).first()
if departure:
departure.registration = clean_reg
departure.type = entry.aircraft_type
departure.callsign = entry.callsign
departure.pob = entry.pob or departure.pob
departure.out_to = entry.to_location or departure.out_to
departure.etd = timestamp
departure.takeoff_dt = timestamp
departure.departed_dt = timestamp
departure.status = DepartureStatus.DEPARTED
departure.notes = entry.notes
db.add(departure)
db.commit()
crud_journal.log_change(db, EntityType.DEPARTURE, departure.id, f"Bulk flight log {action}: takeoff at {timestamp.strftime('%Y-%m-%d %H:%M')}", username, client_ip)
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "movement_bulk_logged",
"data": {
"id": movement.id,
"aircraft_registration": movement.aircraft_registration,
"movement_type": movement.movement_type.value,
"action": action
}
})
return BulkMovementResult(
action=action,
movement=movement,
entity_type=entity_type,
entity_id=entity_id,
message=f"Movement {action} for {clean_reg}"
)
@router.get("/{movement_id}", response_model=Movement)
async def get_movement(
movement_id: int,
@@ -45,4 +632,4 @@ async def get_movement(
status_code=status.HTTP_404_NOT_FOUND,
detail="Movement record not found"
)
return movement
return movement
+36 -1
View File
@@ -239,6 +239,41 @@ async def update_ppr_status(
return ppr
@router.post("/{ppr_id}/acknowledge", response_model=PPR)
async def acknowledge_ppr_strip(
request: Request,
ppr_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Acknowledge that the operator has created the paper strip for a PPR."""
client_ip = get_client_ip(request)
ppr = crud_ppr.acknowledge_strip(
db,
ppr_id=ppr_id,
user=current_user.username,
user_ip=client_ip
)
if not ppr:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="PPR record not found"
)
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "ppr_acknowledged",
"data": {
"id": ppr.id,
"ac_reg": ppr.ac_reg,
"acknowledged_dt": ppr.acknowledged_dt.isoformat() if ppr.acknowledged_dt else None,
"acknowledged_by": ppr.acknowledged_by
}
})
return ppr
@router.delete("/{ppr_id}", response_model=PPR)
async def delete_ppr(
request: Request,
@@ -451,4 +486,4 @@ async def activate_ppr(
f"PPR activated: arrival #{new_arrival.id} created"
+ (f", departure #{new_departure.id} queued (will appear when aircraft lands)" if new_departure else "")
)
}
}
+32 -1
View File
@@ -47,6 +47,37 @@ class CRUDMovement:
db.refresh(db_obj)
return db_obj
def update(self, db: Session, db_obj: Movement, obj_in: MovementCreate) -> Movement:
update_data = obj_in.dict()
for field, value in update_data.items():
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def find_daily_match(
self,
db: Session,
target_date: date,
aircraft_registration: str,
movement_type: MovementType,
entity_type: Optional[str] = None,
entity_types: Optional[List[str]] = None
) -> Optional[Movement]:
clean_reg = "".join(char for char in aircraft_registration.upper() if char.isalnum())
clean_column = func.upper(func.replace(func.replace(Movement.aircraft_registration, "-", ""), " ", ""))
query = db.query(Movement).filter(
func.date(Movement.timestamp) == target_date,
clean_column == clean_reg,
Movement.movement_type == movement_type
)
if entity_type:
query = query.filter(Movement.entity_type == entity_type)
if entity_types:
query = query.filter(Movement.entity_type.in_(entity_types))
return query.order_by(Movement.timestamp.desc()).first()
def get_movements_by_entity(self, db: Session, entity_type: str, entity_id: int) -> List[Movement]:
return db.query(Movement).filter(
and_(Movement.entity_type == entity_type, Movement.entity_id == entity_id)
@@ -58,4 +89,4 @@ class CRUDMovement:
).order_by(Movement.timestamp).all()
movement = CRUDMovement()
movement = CRUDMovement()
+31 -1
View File
@@ -169,6 +169,36 @@ class CRUDPPR:
return db_obj
def acknowledge_strip(
self,
db: Session,
ppr_id: int,
user: str = "system",
user_ip: str = "127.0.0.1"
) -> Optional[PPRRecord]:
db_obj = self.get(db, ppr_id)
if not db_obj:
return None
if db_obj.acknowledged_dt:
return db_obj
db_obj.acknowledged_dt = datetime.utcnow()
db_obj.acknowledged_by = user
db.add(db_obj)
db.commit()
db.refresh(db_obj)
crud_journal.log_ppr_change(
db,
db_obj.id,
f"Paper strip acknowledged by {user}",
user,
user_ip
)
return db_obj
def delete(self, db: Session, ppr_id: int, user: str = "system", user_ip: str = "127.0.0.1") -> Optional[PPRRecord]:
db_obj = self.get(db, ppr_id)
if db_obj:
@@ -190,4 +220,4 @@ class CRUDPPR:
return db_obj
ppr = CRUDPPR()
ppr = CRUDPPR()
+3 -1
View File
@@ -43,6 +43,8 @@ class PPRRecord(Base):
departed_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
acknowledged_dt = Column(DateTime, nullable=True)
acknowledged_by = Column(String(50), nullable=True)
public_token = Column(String(128), nullable=True, unique=True, index=True)
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
@@ -101,4 +103,4 @@ class UserAircraft(Base):
clean_reg = Column(String(25), nullable=False, index=True)
created_by = Column(String(16), nullable=False, index=True)
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
+41 -3
View File
@@ -1,6 +1,6 @@
from typing import Optional
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
from datetime import date, datetime
from app.models.movement import MovementType
@@ -26,9 +26,47 @@ class MovementCreate(MovementBase):
pass
class BulkMovementLog(BaseModel):
flight_kind: str
movement_date: date
movement_time: Optional[str] = None
takeoff_time: Optional[str] = None
landing_time: Optional[str] = None
contact_time: Optional[str] = None
qsy_time: Optional[str] = None
aircraft_registration: str
aircraft_type: Optional[str] = None
callsign: Optional[str] = None
from_location: Optional[str] = None
to_location: Optional[str] = None
pob: Optional[int] = None
local_nature: Optional[str] = None
circuits: Optional[int] = None
runway: Optional[str] = None
wind: Optional[str] = None
pressure_setting: Optional[str] = None
notes: Optional[str] = None
ppr_id: Optional[int] = None
movement_id: Optional[int] = None
class BulkMovementContext(BaseModel):
pprs: List[dict]
movements: List[dict]
suggested: dict
class Movement(MovementBase):
id: int
created_at: datetime
class Config:
from_attributes = True
from_attributes = True
class BulkMovementResult(BaseModel):
action: str
movement: Movement
entity_type: str
entity_id: int
message: str
+3 -1
View File
@@ -88,6 +88,8 @@ class PPRInDBBase(PPRBase):
departed_dt: Optional[datetime] = None
created_by: Optional[str] = None
submitted_dt: datetime
acknowledged_dt: Optional[datetime] = None
acknowledged_by: Optional[str] = None
class Config:
from_attributes = True
@@ -235,4 +237,4 @@ class UserAircraft(UserAircraftBase):
class UserAircraftCreate(BaseModel):
registration: str
type_code: str
type_code: str