@@ -1,8 +1,9 @@ | |||||
from fastapi import Depends, APIRouter, HTTPException, status | from fastapi import Depends, APIRouter, HTTPException, status | ||||
from fastapi.responses import StreamingResponse | |||||
from requests import Session | from requests import Session | ||||
from apis.v1.route_auth import get_current_user | from apis.v1.route_auth import get_current_user | ||||
from db.models.user import User | from db.models.user import User | ||||
from db.repository.report import get_repot_jobsdone_by_driver | |||||
from db.repository.report import get_repot_jobsdone_by_driver, get_pdf | |||||
from db.session import get_db | from db.session import get_db | ||||
@@ -20,3 +21,16 @@ def get_report_jobsdone(driver_id: int, db: Session = Depends(get_db), current_u | |||||
status_code=404, detail=f"Driver with id {driver_id} not found" | status_code=404, detail=f"Driver with id {driver_id} not found" | ||||
) | ) | ||||
return report | return report | ||||
@router.get("/pdf/{driver_id}", status_code=status.HTTP_200_OK) | |||||
def get_report_pdf(driver_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): | |||||
if current_user.Role != "Admin": | |||||
raise HTTPException( | |||||
status_code=403, detail="You are not authorized to perform this action" | |||||
) | |||||
report = get_pdf(driver_id, db) | |||||
if report == "notdriver": | |||||
raise HTTPException( | |||||
status_code=404, detail=f"Driver with id {driver_id} not found" | |||||
) | |||||
return StreamingResponse(open(report, "rb"), media_type="application/pdf") |
@@ -3,7 +3,7 @@ class Settings: | |||||
PROJECT_VERSION: str = "1.0.0" | PROJECT_VERSION: str = "1.0.0" | ||||
POSTGRES_USER: str = "VMSBase" | POSTGRES_USER: str = "VMSBase" | ||||
POSTGRES_PASSWORD = "VMSBasePass" | POSTGRES_PASSWORD = "VMSBasePass" | ||||
POSTGRES_SERVER: str = "db" | |||||
POSTGRES_SERVER: str = "localhost" | |||||
POSTGRES_PORT: str = "5432" | POSTGRES_PORT: str = "5432" | ||||
POSTGRES_DB: str = "VMSData" | POSTGRES_DB: str = "VMSData" | ||||
DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:{POSTGRES_PORT}/{POSTGRES_DB}" | DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:{POSTGRES_PORT}/{POSTGRES_DB}" | ||||
@@ -1,4 +1,5 @@ | |||||
from datetime import datetime | from datetime import datetime | ||||
from fastapi.responses import StreamingResponse | |||||
from sqlalchemy.orm import Session | from sqlalchemy.orm import Session | ||||
from db.models.drivetask import DriveTask | from db.models.drivetask import DriveTask | ||||
@@ -7,6 +8,18 @@ from db.models.fuelingtask import FuelingTask | |||||
from db.models.maintenancejob import MaintenanceJob | from db.models.maintenancejob import MaintenanceJob | ||||
from db.repository.maintenancejob import calculate_total_cost | from db.repository.maintenancejob import calculate_total_cost | ||||
from reportlab.lib import colors | |||||
from reportlab.lib.pagesizes import letter | |||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Image, PageBreak | |||||
from reportlab.lib.styles import getSampleStyleSheet | |||||
from reportlab.lib.units import inch | |||||
from reportlab.graphics.shapes import Drawing | |||||
from reportlab.graphics.charts.linecharts import HorizontalLineChart | |||||
from reportlab.graphics.widgets.markers import makeMarker | |||||
from reportlab.platypus import Spacer | |||||
from reportlab.graphics.charts.axes import CategoryAxis | |||||
from dateutil import parser | |||||
def get_repot_jobsdone_by_driver(driver_id: int, db: Session): | def get_repot_jobsdone_by_driver(driver_id: int, db: Session): | ||||
driver = db.query(User).filter(User.Id == driver_id).first() | driver = db.query(User).filter(User.Id == driver_id).first() | ||||
@@ -75,4 +88,291 @@ def get_repot_jobsdone_by_driver(driver_id: int, db: Session): | |||||
maintenspent.append(maintenpoint) | maintenspent.append(maintenpoint) | ||||
res["MaintenanceSpent"] = maintenspent | res["MaintenanceSpent"] = maintenspent | ||||
res["TotalMaintenanceSpent"] = totalmaintenspent | res["TotalMaintenanceSpent"] = totalmaintenspent | ||||
return res | |||||
return res | |||||
def get_pdf(driver_id: int, db: Session): | |||||
report_data = get_repot_jobsdone_by_driver(driver_id, db) | |||||
if report_data == "notdriver": | |||||
return "notdriver" | |||||
# Assuming your report data is stored in a variable named 'report_data' | |||||
# Create a PDF document | |||||
pdf_filename = "report1.pdf" | |||||
document = SimpleDocTemplate(pdf_filename, pagesize=letter) | |||||
# Create a list to hold the contents of the PDF | |||||
content = [] | |||||
# Add a title to the PDF | |||||
title_style = getSampleStyleSheet()['Title'] | |||||
title = Paragraph("Driver Report", title_style) | |||||
content.append(title) | |||||
content.append(Paragraph("<br/>", title_style)) | |||||
print(report_data["Driver"]) | |||||
# Add detailed information about the driver | |||||
driver_info = [ | |||||
["Driver Name", report_data["Driver"]["Name"]], | |||||
["Last Name", report_data["Driver"]["LastName"]], | |||||
["Government ID", report_data["Driver"]["GovernmentId"]], | |||||
["Email", report_data["Driver"]["Email"]], | |||||
["Contact Number", report_data["Driver"]["ContactNumber"]], | |||||
["Address", report_data["Driver"]["Address"]], | |||||
["Role", report_data["Driver"]["Role"]], | |||||
] | |||||
driver_table_style = TableStyle([ | |||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |||||
('BOX', (0, 0), (-1, -1), 1, colors.black), | |||||
]) | |||||
driver_table = Table(driver_info, style=driver_table_style) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
vehicle = report_data["Driver"]["AssignedVehicle"].__dict__ | |||||
print(vehicle) | |||||
loc = vehicle["CurrentLocation"][0] | |||||
if len(vehicle["CurrentLocation"]) > 1: | |||||
loc += ", " + vehicle["CurrentLocation"][1] | |||||
# Add vehicle details | |||||
vehicle_info = [ | |||||
["Vehicle Type", vehicle["Type"]], | |||||
["Vehicle Model", vehicle["Model"]], | |||||
["Year", vehicle["Year"]], | |||||
["License Plate", vehicle["LicensePlate"]], | |||||
["Current Location", loc], | |||||
["Mileage", vehicle["Mileage"]], | |||||
["Capacity", vehicle["Capacity"]], | |||||
] | |||||
vehicle_table_style = TableStyle([ | |||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |||||
('BOX', (0, 0), (-1, -1), 1, colors.black), | |||||
]) | |||||
vehicle_table = Table(vehicle_info, style=vehicle_table_style) | |||||
content.append(Spacer(1, 12)) # Add space between the tables | |||||
content.append(Table([[driver_table, vehicle_table]])) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
total_info = [ | |||||
["Total Tasks Done", report_data["tasks"].__len__()], | |||||
["Total Fuel Spent", report_data["TotalFuelSpent"]], | |||||
["Total Time Spent", report_data["TotalTime"]], | |||||
["Total Distance Driven", report_data["TotalDistance"]], | |||||
["Total Money Spent", report_data["TotalMaintenanceSpent"]], | |||||
] | |||||
total_table_style = TableStyle([ | |||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |||||
('BOX', (0, 0), (-1, -1), 1, colors.black), | |||||
]) | |||||
total_table = Table(total_info, style=total_table_style) | |||||
content.append(total_table) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
content.append(PageBreak()) | |||||
title_style = getSampleStyleSheet()["Heading1"] | |||||
content.append(Paragraph("Driving Tasks", title_style)) | |||||
title_style = getSampleStyleSheet()["Heading2"] | |||||
content.append(Paragraph("Distance Driven over Time", title_style)) | |||||
tasks_data = report_data["tasks"] | |||||
task_values = [entry["DistanceAtTime"] for entry in tasks_data] | |||||
task_dates = [parser.parse(entry['EndDateTime']).strftime("%m/%d/%Y, %H:%M:%S") for entry in tasks_data] | |||||
drawing = Drawing(width=400, height=200) | |||||
chart = HorizontalLineChart() | |||||
chart.x = 0 | |||||
chart.y = 50 | |||||
chart.width = 500 | |||||
chart.height = 125 | |||||
chart.data = [task_values] | |||||
chart.categoryAxis.labels.boxAnchor = 'e' | |||||
chart.valueAxis.valueMin = 0 | |||||
chart.valueAxis.valueMax = max(task_values) + 10 | |||||
chart.lines[0].strokeColor = colors.blue | |||||
chart.lines[0].strokeWidth = 2 | |||||
chart.categoryAxis.categoryNames = [str(date) for date in task_dates] | |||||
chart.categoryAxis.labels.angle = 90 | |||||
drawing.add(chart) | |||||
# Add space between the tables | |||||
content.append(drawing) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
# Add tasks information | |||||
title_style = getSampleStyleSheet()["Heading2"] | |||||
content.append(Paragraph("All DriveTasks", title_style)) | |||||
tasks_header = ["Task ID", "Description", "Start Location", "End Location", "Distance Covered", "Status", "Start DateTime", "End DateTime"] | |||||
tasks_info = [] | |||||
for task in tasks_data: | |||||
task_info = [f"{task['Id']}", task["Description"], f"{task['StartLocation'][0]}, {task['StartLocation'][1]}", f"{task['EndLocation'][0]}, {task['EndLocation'][1]}", | |||||
"{} km".format(task["DistanceCovered"]), task["Status"], task['StartDateTime'].strftime("%m/%d/%Y, %H:%M:%S"), parser.parse(task['EndDateTime']).strftime("%m/%d/%Y, %H:%M:%S")] | |||||
tasks_info.append(task_info) | |||||
# Define the styles | |||||
styles = getSampleStyleSheet() | |||||
normal_style = styles["Normal"] | |||||
# Convert strings to Paragraphs to allow line breaks | |||||
for row in tasks_info: | |||||
for i, cell in enumerate(row): | |||||
row[i] = Paragraph(cell, normal_style) | |||||
tasks_table_style = TableStyle([ | |||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey), | |||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'), | |||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12), | |||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige), | |||||
('GRID', (0, 0), (-1, -1), 1, colors.black), | |||||
]) | |||||
tasks_table = Table([tasks_header] + tasks_info, style=tasks_table_style) | |||||
content.append(tasks_table) | |||||
content.append(PageBreak()) | |||||
title_style = getSampleStyleSheet()["Heading1"] | |||||
content.append(Paragraph("Fuel Spent Over Time", title_style)) | |||||
# Add chart of fuel spendings | |||||
fuel_spent_data = report_data["FuelSpent"] | |||||
fuel_values = [entry["total"] for entry in fuel_spent_data] | |||||
fuel_dates = [entry["date"] for entry in fuel_spent_data] | |||||
drawing = Drawing(width=400, height=200) | |||||
chart = HorizontalLineChart() | |||||
chart.x = 0 | |||||
chart.y = 50 | |||||
chart.width = 500 | |||||
chart.height = 125 | |||||
chart.data = [fuel_values] | |||||
chart.categoryAxis.labels.boxAnchor = 'e' | |||||
chart.valueAxis.valueMin = 0 | |||||
chart.valueAxis.valueMax = max(fuel_values) + 10 | |||||
chart.lines[0].strokeColor = colors.blue | |||||
chart.lines[0].strokeWidth = 2 | |||||
chart.categoryAxis.categoryNames = [str(date) for date in fuel_dates] | |||||
chart.categoryAxis.labels.angle = 90 | |||||
drawing.add(chart) | |||||
# Add space between the tables | |||||
content.append(drawing) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
fuel_header = ["Date", "Fuel Refilled", "Total Fuel Spent"] | |||||
fuel_info = [] | |||||
for fuel in fuel_spent_data: | |||||
fuel_info1 = [fuel["date"].strftime("%m/%d/%Y, %H:%M:%S"), "{} L".format(fuel["fuelrefilled"]), "{} L".format(fuel["total"])] | |||||
fuel_info.append(fuel_info1) | |||||
# Define the styles | |||||
styles = getSampleStyleSheet() | |||||
normal_style = styles["Normal"] | |||||
# Convert strings to Paragraphs to allow line breaks | |||||
for row in fuel_info: | |||||
for i, cell in enumerate(row): | |||||
row[i] = Paragraph(cell, normal_style) | |||||
fuel_table_style = TableStyle([ | |||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey), | |||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'), | |||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12), | |||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige), | |||||
('GRID', (0, 0), (-1, -1), 1, colors.black), | |||||
]) | |||||
fuel_table = Table([fuel_header] + fuel_info, style=fuel_table_style) | |||||
content.append(fuel_table) | |||||
content.append(PageBreak()) | |||||
title_style = getSampleStyleSheet()["Heading1"] | |||||
content.append(Paragraph("Maintenance Over Time", title_style)) | |||||
# Add chart of fuel spendings | |||||
maintenance_data = report_data["MaintenanceSpent"] | |||||
maintenance_values = [entry["total"] for entry in maintenance_data] | |||||
maintenance_dates = [entry["date"] for entry in maintenance_data] | |||||
drawing = Drawing(width=400, height=200) | |||||
chart = HorizontalLineChart() | |||||
chart.x = 0 | |||||
chart.y = 50 | |||||
chart.width = 500 | |||||
chart.height = 125 | |||||
chart.data = [maintenance_values] | |||||
chart.categoryAxis.labels.boxAnchor = 'e' | |||||
chart.valueAxis.valueMin = 0 | |||||
chart.valueAxis.valueMax = max(maintenance_values) + 10 | |||||
chart.lines[0].strokeColor = colors.blue | |||||
chart.lines[0].strokeWidth = 2 | |||||
chart.categoryAxis.categoryNames = [str(date) for date in maintenance_dates] | |||||
chart.categoryAxis.labels.angle = 90 | |||||
drawing.add(chart) | |||||
# Add space between the tables | |||||
content.append(drawing) | |||||
content.append(Paragraph("<br/><br/>", title_style)) | |||||
maintenance_header = ["Date", "Maintenance Cost", "Total Maintenance Cost"] | |||||
maintenance_info = [] | |||||
for fuel in maintenance_data: | |||||
fuel_info1 = [fuel["date"].strftime("%m/%d/%Y, %H:%M:%S"), "{} L".format(fuel["cost"]), "{} L".format(fuel["total"])] | |||||
maintenance_info.append(fuel_info1) | |||||
# Define the styles | |||||
styles = getSampleStyleSheet() | |||||
normal_style = styles["Normal"] | |||||
# Convert strings to Paragraphs to allow line breaks | |||||
for row in maintenance_info: | |||||
for i, cell in enumerate(row): | |||||
row[i] = Paragraph(cell, normal_style) | |||||
maintenance_table_style = TableStyle([ | |||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey), | |||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'), | |||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12), | |||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige), | |||||
('GRID', (0, 0), (-1, -1), 1, colors.black), | |||||
]) | |||||
maintenance_table = Table([maintenance_header] + maintenance_info, style=maintenance_table_style) | |||||
content.append(maintenance_table) | |||||
document.build(content) | |||||
print(f"PDF generated successfully: {pdf_filename}") | |||||
return pdf_filename |
@@ -0,0 +1,74 @@ | |||||
%PDF-1.4 | |||||
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com | |||||
1 0 obj | |||||
<< | |||||
/F1 2 0 R /F2 3 0 R | |||||
>> | |||||
endobj | |||||
2 0 obj | |||||
<< | |||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font | |||||
>> | |||||
endobj | |||||
3 0 obj | |||||
<< | |||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font | |||||
>> | |||||
endobj | |||||
4 0 obj | |||||
<< | |||||
/Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources << | |||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |||||
>> /Rotate 0 /Trans << | |||||
>> | |||||
/Type /Page | |||||
>> | |||||
endobj | |||||
5 0 obj | |||||
<< | |||||
/PageMode /UseNone /Pages 7 0 R /Type /Catalog | |||||
>> | |||||
endobj | |||||
6 0 obj | |||||
<< | |||||
/Author (\(anonymous\)) /CreationDate (D:20231118170352-06'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20231118170352-06'00') /Producer (ReportLab PDF Library - www.reportlab.com) | |||||
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False | |||||
>> | |||||
endobj | |||||
7 0 obj | |||||
<< | |||||
/Count 1 /Kids [ 4 0 R ] /Type /Pages | |||||
>> | |||||
endobj | |||||
8 0 obj | |||||
<< | |||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 615 | |||||
>> | |||||
stream | |||||
Gas1\bAQ&g&A7<Zp<Z/F^atgjqAA8o'Q&Dsq>haJ=p:4^b&[CV>tUhb)-0")>`Eo=G.BF2VhN%[\H02hiQF@c$7K6^'iRqXK=eGa;rEmS*>gFXR4pg4h62ED3%GT=+Vb%AU5BLZ""e0.e(=-oY&:lJDs6+.]^NK31R)fS?Ge*c+j3AZ93Z&'m5\q:8&^:h(i"m</If[(4bP_8<?GOThek`!+ZG2rWYb&gbH2OR#\fMp[9Y=R:<!Z+*4d5/::@,[Lgh/(d93?S"tfePatuV,&C,K7YU,%u5SZ';@3[=F!FDr;jpedp_7ViSXO;r7`CutEKei&U0b-RpcAejLR[&Ju"<%V)S4<@MC-m)W=Z.2CZs0D?RJHL#a0cToSf%jg%m&gdYBFkh+8W!c??=]=N73W8S9)]DWH[<?R34*Q()1?jA\He6eUqpd<;sQqFpCiU0PQUHRZ1,p%P`%85DC3VHk1)/ed7n@eVPP_[+j,ZkZ05CkY1u&<O[Bij%4[_@MH.`@X\qHAtC<g/,PT\Dpue!*pD]m\s#k"@MD`k"3aOh?\&'=21N@d5dtlj.8I*J-^`lr965\6:,)XWB:.Zj7Npg]U?rXi[ZA=Xi]iN9+S#~>endstream | |||||
endobj | |||||
xref | |||||
0 9 | |||||
0000000000 65535 f | |||||
0000000073 00000 n | |||||
0000000114 00000 n | |||||
0000000221 00000 n | |||||
0000000333 00000 n | |||||
0000000526 00000 n | |||||
0000000594 00000 n | |||||
0000000877 00000 n | |||||
0000000936 00000 n | |||||
trailer | |||||
<< | |||||
/ID | |||||
[<5763d16e9cd2e7059c1e01e0f37189ef><5763d16e9cd2e7059c1e01e0f37189ef>] | |||||
% ReportLab generated PDF document -- digest (http://www.reportlab.com) | |||||
/Info 6 0 R | |||||
/Root 5 0 R | |||||
/Size 9 | |||||
>> | |||||
startxref | |||||
1641 | |||||
%%EOF |
@@ -0,0 +1,137 @@ | |||||
%PDF-1.4 | |||||
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com | |||||
1 0 obj | |||||
<< | |||||
/F1 2 0 R /F2 3 0 R /F3 5 0 R | |||||
>> | |||||
endobj | |||||
2 0 obj | |||||
<< | |||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font | |||||
>> | |||||
endobj | |||||
3 0 obj | |||||
<< | |||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font | |||||
>> | |||||
endobj | |||||
4 0 obj | |||||
<< | |||||
/Contents 12 0 R /MediaBox [ 0 0 612 792 ] /Parent 11 0 R /Resources << | |||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |||||
>> /Rotate 0 /Trans << | |||||
>> | |||||
/Type /Page | |||||
>> | |||||
endobj | |||||
5 0 obj | |||||
<< | |||||
/BaseFont /Times-Roman /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font | |||||
>> | |||||
endobj | |||||
6 0 obj | |||||
<< | |||||
/Contents 13 0 R /MediaBox [ 0 0 612 792 ] /Parent 11 0 R /Resources << | |||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |||||
>> /Rotate 0 /Trans << | |||||
>> | |||||
/Type /Page | |||||
>> | |||||
endobj | |||||
7 0 obj | |||||
<< | |||||
/Contents 14 0 R /MediaBox [ 0 0 612 792 ] /Parent 11 0 R /Resources << | |||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |||||
>> /Rotate 0 /Trans << | |||||
>> | |||||
/Type /Page | |||||
>> | |||||
endobj | |||||
8 0 obj | |||||
<< | |||||
/Contents 15 0 R /MediaBox [ 0 0 612 792 ] /Parent 11 0 R /Resources << | |||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |||||
>> /Rotate 0 /Trans << | |||||
>> | |||||
/Type /Page | |||||
>> | |||||
endobj | |||||
9 0 obj | |||||
<< | |||||
/PageMode /UseNone /Pages 11 0 R /Type /Catalog | |||||
>> | |||||
endobj | |||||
10 0 obj | |||||
<< | |||||
/Author (\(anonymous\)) /CreationDate (D:20231118192517-06'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20231118192517-06'00') /Producer (ReportLab PDF Library - www.reportlab.com) | |||||
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False | |||||
>> | |||||
endobj | |||||
11 0 obj | |||||
<< | |||||
/Count 4 /Kids [ 4 0 R 6 0 R 7 0 R 8 0 R ] /Type /Pages | |||||
>> | |||||
endobj | |||||
12 0 obj | |||||
<< | |||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 880 | |||||
>> | |||||
stream | |||||
GauHI?#u2p'Re<2\<#o'mKX;J;j4YbVJ9>41\PZ3CJ"ag!i_B5mq#X86F%0CdFehWbIM9SSiq:k5]BPJNAH*/&\L<*5T)uIJ\$jLd/qUfaj18NaCtX'a:O:bU1;/WJn_T%_OM$;(snpf@BAUa;rY;1^5\HJCgJBJZ@H`$<^l[$U'HK\^oL`H:3hP'J(h]AVsR!bh\<Cu"7th&(^h7nVM;";g;9PTQo#Xc6^Mf"!),R-^^^3/27/0-MoZARoRWX!Z*PVr.-:,b>4"M19X`+26UTi94nN`e&##me$W0r7KEg0"WCIofFK/ac$;l'qo*X@##?VG*0ZNEED7!WJEG(\1dK)B2>O!Ei1k/9%1)m>g21DFn9#N-o-MqBg9Cj*M=VXNBC9BI0f'*-W\L_8o%WE?E0O#e$[7Z4_a?.,r[I6<;n%mZDq>qaf:%C=WgEa\f$lko(LD4Xu$h=uYHU4q>lMq/(=2!I!`-%sFhmiYdhC!:m+kj)9&l8goV&"P*QQdNVIEQo8QI=nF9ks0[g8[c\9GK$O`CL5]Q,;Z/c"UQB%,$I$(J-1L;Snc"<.esFplBWPZJ3";P`OLH;\XA0/lXC%6s8Sc\4m=j%co=lK<poSBcNbU%ZGMHAWfZj)I.eYHCj;]iD,bs[>:#]4",kR\pr-7bk2sgl$%^cgH2)2*)O#5d-lee]A^'qo&6=:kAUsAHeN,tR$f2kI0u)bJ!;Wg\RcGZpDg@ue&Q:\E1Utnb-XVW%W\!XcAC?p`hK?],a0VFRN,jlHaP:pE.,CDP'g2"bG!`0\s[6'Pa0="$2WGboT5Kc,Z6r5QJq4m?_Z-+;rf[XanBBQllKoB1@"B*G0fJ8fXH/3C1$_@O4]qcIq`hgr!#XFAF]~>endstream | |||||
endobj | |||||
13 0 obj | |||||
<< | |||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1144 | |||||
>> | |||||
stream | |||||
GauHL966RV&AJ$Cka5ssXORE0gtFkOmGu]M>Gh^e9eoKA88U<aqd#m#?iq_amW]/gCA6K5lB><M6Lb<VFg`LTF%Rd'5_,!hOar5j8ajc<3r1u@Vl;M0'8FXXUUb4<1dWu(88,=r'Lk>,'ZLR$`Q`Sr[TGbVFLlhPI3@c18M:PDGPkIgj,W=IO&9-R`GRnGcSAtbA8AJrbI/[%XKj`[6H7Ri<3kF%s7h3:^Y&@:s6%eU?RVCFJstJ7!]5si\NJD8O$*%I["Yp-X/'9[Qb9RK%gV]S*t7:73fm8U9KF-Ak\TA?ZiLE4L4Fi69co`ZWk%U^l)u>[=N;"'N,q$c!)0>[0Zs/\1qjUpMT[7,KK-L<IrPa;&4]*NqCXS&'=mR^16r+*:7EXD.ER7OL8!5L4@p_g73\jZk]J`f-6ae%Yi_Y2fj#d7V+K[jIBTufifT+dp[3A[$P7QGkBIid8P_;!GWM-lAf.EW]JA4+2_=4?IRUm+^kJK\`[C\1&e450TCD1"2iM*5_]_L1*4MFj@($GagBGaS`>nWKm:oDP@`OEK!q>`$SJJ"QI%!*,jm%PoIdhD%-JG=`J]Dk?5[_J21>S^=qQsM]HE"S)!uCD0D&bo_G<uoE3P;+_F2Vc]UfJ)QA]tkAB&rK&'I>J6>**^T0iRiMFXEJ]Dh?4<'6j29(TX70%lhS8.qbTE(\rAURTW3Tnjk76H'@W-gVrK-Z!#[o"/D".?N":sh<rD`Ai=M2^k5=5\%#_i:ZsS\,EO&k?dN,K;Dt#rgMi-L'h0%krm.`R<H*a,<LpqMFIsQfalNY6r@^Tb!]Bi)H5N)$hjAB>7T!bH>",q4NNBM1f*?@Uh9Dkh(:rZZh$4f7#7akIR<u7B98pRp(ujSh&`3&V%m3%T,"c0BNS%K%KWf$:Ycg!df4DD:RM5-,g("Ue4u#Lbm.'U+FN%Bg+b)cH+W].K3L'6Qq\.SodV]G_D$QSX".Y_X4V!saoKO`#LZ9RIo';M#+-cXmDl^mM/aFmi7]oQiq<'rMbra95f*P>Y&^iurN7S*K-#m0Tn<0/cYu#$;R1\0\0OV2]o+BS*L(\CVRh+1a(h.JoW0_uBZ-=u@@qhlOL0QMC#q!p>k>BlD0i=sq,2)E1H.*5UWgA\D!O.e++o~>endstream | |||||
endobj | |||||
14 0 obj | |||||
<< | |||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1154 | |||||
>> | |||||
stream | |||||
Gb!$H95bIu&BF88'QYf?EBN:5\jlll6Br+!DMUYK;q*9G:_id%N7Vi`dQD^)U`kVYh4=5_ci3f%$A.=Lf"YQ.!,n_g3Cc>dkq/PD)Q">?DE@VS_PX-0^j>"WCm29on_8lU0LoTE"\0`O.RYEe0i$#Dc/s!<A+oBp++MTX@=#DM]n5K39MYcK'7X%*]!;p".ogDQQg`W,LEA_LpO;c1$eoQ"l8(G%J?j%s23$7)>pC%$G#djZIuT*bs3s!/_mB%H$c<UH-:jJYalNFb-l]('e))[LE!V@7Mh\GaZ"OD9-'fId;Mh6)Y]O@=+Y90dH3h^8V;[]Q[L=gBEHifD9%LEoF(NgR+'2W*JoNs@:UhMF=P>JJ5]rsKR?D?l4P.Fj74n!g`5(D0K8r;.3Y"$sEN#Rk6OeG`C?Y\r`g2=dF<_<[ra1]gA()JK\m6d9SCJ+E@t*_/Nhh=OYG6GnlKrJci:7=?5OFOTG?!D@rUdWlrOkD9QHE`Tp&i9-j`As)DAIBP>jC7EhUjbgX`8OnG7:7%RaHbgV`q=1:m!LqI!FX1/Z9Q"@1VQ#;Vjta*r2qAC)+Y@YbrX:@4eB&o6P=@,2qP9Yf<Hd_NPFm0s%d]SWK]UKu9@7`XDFogj-U,TL2T.@QqBGbumhM1h@ufQBD]?`13dNOck5+1tn6-&c!8?(l@)n4MPnAKK2NMna^@%9F`+VE-jVYhF^j`aOeRtit\q@^e.(OE3mNY\Ye/>2@POD<GM`CP3?EE^V,)":@shZ,'OXN0bpcG`66j$h0_Dug$GCh$C#Dol1_+A3OGdIP1OG$Ltf7"$_b!jnl^kGltV=iBd[R@@Dd?'IETbb2,tM_.3[YZCKuSa7H3#T!tM"r$)N\0-#p'SdNhQ&QdqmbdRTLa\DDFDJp7Pq8go*E.>c50:kpH**C,f$8.S9a_X\>hTnkeK$<mSrTh%c-YdNk!`#N67/V3OjUa.K?lqg(j@Wa8RoMD>//>$30'VRmqXX0&MMNic?n%-N[+bn(]N0'4$E_G&P)9#]PZCObiMu"hDmQshM:`"sH\RL[oA`DHec]AZ*CUhPg9?CqJ[;)86>.7PXG8`!begd_2PuY9h"S44g-'W1:-@nk6;$j"`-lJ0bBF*U)'XZ@b,rNWU"a%2mHk&Vq!WE@Q`W~>endstream | |||||
endobj | |||||
15 0 obj | |||||
<< | |||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 742 | |||||
>> | |||||
stream | |||||
Gatm:9lJc?%))O>n<auL,#[!IHe_*r$lt)*^(!.,SQiJ">q?Al^HYnbPrOU62_^Cs&];XZ;D4mZ,eq]5mq[IJ,qQ22OJ$t$bXE$'%=BY3h\emZlbF4Qc>8^$[X-.7L+*aFC5tW2Y)Pct`gD0[4uMZ+=Z6!hI/@-MTR<c?$RhX@]PJI'q:/usSI$k"b;#0/s311d]_TF:kNp5b[f,J;[('.edapdiMSn".L9h</dVAQW-.I>4?XDZ.Vr"n:i?%;%j:idn#3pj[TLP<XHd^d$)rK)b6ZSP=>/:i=HWA9\P%#*>6/!j-/$d#aPZ$p?@3EU3qh=`3kp)qHp#bU96`_&`%(FUddr$]V,mpEH#k52?%HkIf[SRRmm7,"$&kZmTiSEom#Ldtq-;>^*h5P!f*'Lm_g55GO4'$co2BBn-[&I0_q_.Se]<e#tpI)]c#c?a\Z2ad,A^OBi/T-KT]kO/hKN#F83kJ%Ip'45cWJCuiR5UNgX>[tsb8o=/L*i080E`(n@UTgk=0B4C2sjihR$35("V6rf.#bMcLU9*0QG'"WVr7N!*Uo,*2t6%HJTQ/U/^S0-jI?-g\_\CA>CPeC+A@gMA,F10TClWS+sicF0]hVHq#X'Jr@s;L"5/2cp7t"58Y[A8L:H/c4)*QJ+Z$'5A4<-,6&L>KS&)=d]dF7.eD@fU7nM6;,]U1JhBA7scQY;#iQ")$\nFGID5bQ<[VKn_1AAe3h0Pa(bollomXZ@[~>endstream | |||||
endobj | |||||
xref | |||||
0 16 | |||||
0000000000 65535 f | |||||
0000000073 00000 n | |||||
0000000124 00000 n | |||||
0000000231 00000 n | |||||
0000000343 00000 n | |||||
0000000538 00000 n | |||||
0000000647 00000 n | |||||
0000000842 00000 n | |||||
0000001037 00000 n | |||||
0000001232 00000 n | |||||
0000001301 00000 n | |||||
0000001585 00000 n | |||||
0000001663 00000 n | |||||
0000002634 00000 n | |||||
0000003870 00000 n | |||||
0000005116 00000 n | |||||
trailer | |||||
<< | |||||
/ID | |||||
[<ee664bded2e591a430d712699aba8e65><ee664bded2e591a430d712699aba8e65>] | |||||
% ReportLab generated PDF document -- digest (http://www.reportlab.com) | |||||
/Info 10 0 R | |||||
/Root 9 0 R | |||||
/Size 16 | |||||
>> | |||||
startxref | |||||
5949 | |||||
%%EOF |
@@ -6,4 +6,5 @@ alembic==1.12.0 | |||||
passlib | passlib | ||||
python-jose==3.3.0 | python-jose==3.3.0 | ||||
python-multipart==0.0.6 | python-multipart==0.0.6 | ||||
requests | |||||
requests | |||||
reportlab |