> Car Rental Fleet & Reservation Management System
// Created at: 04-08-2026
[Project Overview]
A high-concurrency, low-latency Fleet Resource Planning (ERP) platform and Cross-Platform Synchronizer that unifies an on-premise back-office management system with a live, remote WordPress/WooCommerce platform. Technical Stack Architecture • Backend Core:** Python / Flask / Object-Oriented Programming • Persistence & Resource Pooling: MySQL, Thread-Safe Multi-Tenant Database Connection Pooling (`mysql.connector.pooling`) • AI & Computer Vision: Google Cloud Vision API (REST Transport layer), Regular Expressions (`re` structural matching matrices) • Document Processing: ReportLab Vector Canvas, WSGI Binary Streaming Pipes, PIL/Pillow (Alpha-Channel manipulation) • Data Structures Efficiency: In-Memory Hash Tables O(N+M) Hash Joins), Python Sets (Deduplication matrices) • Frontend Interface: HTML5 / CSS3 / JavaScript (For interactive timeline layouts, data representation, and reservation editing) A custom back-office web application for a car rental business, using Python and Flask. It sits alongside the company’s WordPress rental plugin: it reads bookings from the WordPress database, keeps its own MySQL database for everything the plugin doesn’t handle, and automates the daily work around each rental. What it does? • Daily operations: a live calendar of pick-ups and returns from WordPress, manual and local bookings, car swaps with conflict detection, and automatic alerts for expiring inspection, insurance, road tax and CASCO dates. • OCR-assisted contracts: customers’ ID cards, passports and driving licences are read with Google Cloud Vision, processed in parallel, and parsed with country-specific logic for 10 European countries (RO, IT, ES, UK, BE, DK, FR, DE, IE, CH). Extracted data pre-fills the rental contract, which removes most manual typing. • Contract lifecycle: bilingual PDF contracts generated with ReportLab, digital signatures extracted from signed PDFs and reused on contract extensions, completion reports with damage records, and automatic emailing to customer and company. • Pricing and offers: a pricing engine that reproduces the website’s rules (day-based rounding, price plans, discounts, extras, out-of-hours fees, deposit vs. insurance regimes), exposed as an API. It powers a lead pipeline that turns commercial offers into WhatsApp messages and converts accepted offers into real WordPress bookings. • Fleet and maintenance: vehicle records, service logs, history per car, and availability search across dates and locations. • Archive and reporting: paginated contract archive, bulk ZIP export of contracts and documents, customer database, and fleet reports covering performance, efficiency, profitability and seasonality. Derives advanced asset performance indicators, tracking Average Daily Rate (ADR) against Revenue Per Available Vehicle (RevPAV) over long-term, multi-year macro trends to maximize fleet yields. Technical highlights: • Dual MySQL connection pools (remote WordPress DB + local application DB) with local override tables, so business data can be corrected without touching the live site. • Parallel OCR with ThreadPoolExecutor and rule-based multi-country document parsing. • Programmatic PDF generation and PDF signature extraction (ReportLab, PyMuPDF). • Session-based authentication with CSRF protection, SMTP email, and WhatsApp integration.
[_Case Study]
Pulling bookings from the existing booking site
The app doesn't replace the company's WordPress booking plugin; it sits on top of it. A dedicated connection pool reads bookings, customers, cars and locations straight from the plugin's tables and converts the stored timestamps into the business's local time (the summer/winter offset is a setting, not hardcoded).
query_live = """
SELECT
b.booking_id, b.booking_timestamp, b.pickup_timestamp, b.return_timestamp,
i.model_name AS car_name, i.item_id AS car_id,
l1.location_name AS pickup_location_real,
c.first_name, c.last_name, c.phone, c.email, ...
FROM wp_car_rental_bookings b
LEFT JOIN wp_car_rental_booking_options o ON b.booking_id = o.booking_id
LEFT JOIN wp_car_rental_items i ON o.item_sku = i.item_sku
LEFT JOIN wp_car_rental_locations l1 ON b.pickup_location_code = l1.location_code
LEFT JOIN wp_car_rental_customers c ON b.customer_id = c.customer_id
WHERE b.is_cancelled = 0
AND (b.return_timestamp >= %s OR b.booking_id IN ({ids}))
GROUP BY b.booking_id
""".format(ids=placeholders_str)
The home screen: one timeline, with alerts
The home page shows upcoming pick-ups and returns in one list, merged from website bookings, manual bookings and contracts, grouped by date. Each entry is enriched with the vehicle's expiring documents: inspection, insurance, road tax, technical revision and CASCO, flagged when they expire within the next 30 days.
warning_limit = today_date + timedelta(days=30)
...
e['alerts'] = []
if lookup_key in cars_lookup:
car_data = cars_lookup[lookup_key]
date_fields = ['technical_inspection_validity', 'insurance_validity', 'vignete_validity',
'tehnical_revision_validity', 'casco_insurance_validity']
for field in date_fields:
val = car_data.get(field)
if val and val != "N/A":
try:
dt_obj = val if isinstance(val, date) else datetime.strptime(str(val), '%Y-%m-%d').date()
if dt_obj <= warning_limit:
label = field.replace('_validity', '').replace('_', ' ').upper()
e['alerts'].append(f"{label}: {dt_obj.strftime('%d-%m')}")
except: continue
Editing reservations without touching the live site
Staff can change nearly everything about a booking (car, plate, dates, locations, price, extras, fees) but the website's data is never modified. Edits go into a local booking_overrides table, saved with an upsert, and are merged over the website data every time a booking is read. When several sources disagree, the priority is fixed: amendment > manual edit > contract > website booking. Before saving, the app checks that the edited interval doesn't collide with another booking of the same car.
# Override on top of the website data: only real values, never None
if int(bid) in overrides:
ov = overrides[int(bid)]
if ov.get('override_car_name'): event['car_name'] = ov['override_car_name']
if ov.get('override_car_plate'):
event['car_plate'] = ov['override_car_plate']
event['registration_plate'] = ov['override_car_plate']
if ov.get('override_pickup_date'): event['pickup_timestamp'] = ov['override_pickup_date']
if ov.get('override_return_date'): event['return_timestamp'] = ov['override_return_date']
if ov.get('override_total_price'): event['grand_total'] = ov['override_total_price']
...
if bid in contracted_ids:
# a recent manual edit must not be overwritten by the contract's data
if bid not in overrides:
r_date = contracted_ids[bid]['return_timestamp']
r_hour = contracted_ids[bid]['return_hour_timestamp']
r_loc = contracted_ids[bid]['return_location']
# an extension has the highest priority over the contract
if bid in latest_ext:
r_date = latest_ext[bid]['new_return_date']
r_hour = latest_ext[bid]['new_return_hour']
r_loc = latest_ext[bid]['new_return_location']
...
INSERT INTO booking_overrides
(external_booked_id, override_car_name, override_car_plate,
override_pickup_date, override_pickup_hour, ..., override_guarantee)
VALUES (...)
ON DUPLICATE KEY UPDATE
override_pickup_date=VALUES(override_pickup_date),
override_return_date=VALUES(override_return_date),
override_total_price=VALUES(override_total_price),
...
Allocating a vehicle (plate number) to a booking
The website knows the car model a customer booked, not the physical vehicle. In the event screen the operator picks a plate from the fleet cars of that model, and the app refuses the choice if that plate is already taken for an overlapping interval, telling the user which booking blocks it and when.
def check_collision(current_event, selected_plate, all_reservations):
try:
cur_start = datetime.strptime(f"{current_event['pickup_timestamp']} {current_event.get('pickup_hour_timestamp', '00:00')}", "%d-%m-%Y %H:%M")
cur_end = datetime.strptime(f"{current_event['return_timestamp']} {current_event.get('return_hour_timestamp', '00:00')}", "%d-%m-%Y %H:%M")
except:
return False, None, None
for res in all_reservations:
res_plate = res.get('car_number') or res.get('car_plate')
if int(res['booked_id']) != int(current_event['booked_id']) and res_plate == selected_plate:
try:
other_start = datetime.strptime(...)
other_end = datetime.strptime(...)
if cur_start < other_end and cur_end > other_start:
interval_info = f"{other_start.strftime('%d.%m %H:%M')} - {other_end.strftime('%d.%m %H:%M')}"
return True, res['booked_id'], interval_info
except:
continue
return False, None, None
Changing the car
From the event screen the operator can swap the assigned car. The page offers only cars that are free for that booking's interval. Availability uses the real return time, so an extended contract is counted with its new return date instead of the original one. The swap itself is two upserts: the override (source of truth) and the plate shown on the home screen.
SELECT c.booked_id,
COALESCE(e.new_return_date, c.return_timestamp) AS final_return_date,
COALESCE(e.new_return_hour, c.return_hour_timestamp) AS final_return_hour
FROM contracts_data c
LEFT JOIN (
SELECT booked_id, new_return_date, new_return_hour
FROM contract_extensions
WHERE id IN (SELECT MAX(id) FROM contract_extensions GROUP BY booked_id)
) e ON c.booked_id = e.booked_id
WHERE c.is_completed IS NULL OR c.is_completed != 1
...
sql = '''
INSERT INTO booking_overrides (external_booked_id, override_car_name, override_car_plate)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE
override_car_name = VALUES(override_car_name),
override_car_plate = VALUES(override_car_plate)
'''
cursor.execute(sql, (booking_id, new_model, new_plate))
cursor.execute("""
INSERT INTO events_details_added (booked_id, car_number)
VALUES (%s, %s)
ON DUPLICATE KEY UPDATE car_number = VALUES(car_number)
""", (booking_id, new_plate))
Searching for free cars in the fleet
Given a date range, the app tells which fleet cars are free. It merges every source of occupancy (pending bookings, current bookings, open contracts and active extensions), applies overrides and assigned plates, and classifies each overlap. A car released on the first day of the search or taken again on the last day is still offered, with the exact hour it becomes free or busy. A car overlapped in the middle of the period is excluded. There is also an extended search that adds pricing for the free cars, used by the offers module.
raw_active = list(data.pending_bookings.values()) + \
list(data.current_bookings.values()) + \
[b for b in data.contracted_bookings_data.values() if b.get('is_completed') != 1]
...
if b_start < end_search and b_end > start_search: # overlaps the searched period
# CASE A: the old booking ends on the first day of the search (car gets released)
if b_end.date() == start_search.date():
car_availability_data[plate] = {'type': 'start', 'time': r_hour}
# CASE B: a new booking starts on the last day of the search (car is taken again)
elif b_start.date() == end_search.date():
car_availability_data[plate] = {'type': 'end', 'time': p_hour}
# CASE C: total overlap inside the period -> not available
else:
busy_car_plates.add(plate)
...
for car in all_cars:
clean_p = str(car['car_number']).strip().upper()
# busy for the whole period
if clean_p in {str(plate).strip().upper() for plate in busy_car_plates}:
continue
available_cars.append({'id': car['id'], 'model': car['car_model'], 'plate': car['car_number']})
Blocking a reservation
Some website bookings must disappear from the operation, for example duplicates or no-shows. Blocking records the ID in a local list that every data loader respects, and releases the car by deleting its allocation and overrides, so it immediately becomes available in searches and swaps. The website's data stays untouched.
# A. Add the ID to the block list (visual filtering)
cursor.execute("INSERT IGNORE INTO local_blocked_bookings (booked_id) VALUES (%s)", (bid,))
# B. RELEASE THE CAR (delete allocations from the swap/details tables)
cursor.execute("DELETE FROM events_details_added WHERE booked_id = %s", (bid,))
cursor.execute("DELETE FROM booking_overrides WHERE external_booked_id = %s", (bid,))
db_conn.commit()
OCR: from photos of documents to a pre-filled contract
Up to six photos per rental (ID front/back and driving licence, for the main and the secondary driver) are sent to Google Cloud Vision in parallel. Each result is handled by a rule-based pipeline: it decides which country's document it is from marker words and MRZ lines, runs a dedicated parser for 10 countries and merges the results into one set of fields (personal number, document series, licence number, expiry date, address, birth date and place). Where the same field appears on several documents, a merge rule keeps the most complete reading. The result pre-fills the contract form.
valid_files = [f for f in files_to_process if f and str(f).strip().lower() not in ['none', '']]
if not valid_files:
return extracted
with ThreadPoolExecutor(max_workers=len(valid_files)) as executor:
ocr_results = list(executor.map(worker_ocr, valid_files))
...
is_uk_authority = any(k in u_text for k in ["DVLA", "SWANSEA", "GREAT BRITAIN", "S99 1BN"])
is_italian = any(k in u_text for k in ["ITALIANA", "PATENTE", "REPUBBLICA", "CARTA DI IDENTIT", "RESIDENZA", "CODICE FISCALE"])
is_spain = any(k in u_text for k in ["ESPANA", "CONDUCCION", "REINO DE ESPA", "DNI ", "DIRECCION GENERAL DE TRAFICO"])
...
res = {}
if is_uk_authority: res = extract_uk(text, u_text, u_no_spaces, lines)
elif is_italian: res = extract_italia(text, u_text, u_no_spaces, lines)
elif is_spain: res = extract_spain(text, u_text, u_no_spaces, lines)
elif is_ro_language or ("ROMANIA" in u_text and "DRIVING LICENCE" not in u_text): res = extract_romania(text, u_text, u_no_spaces, lines)
...
cnp_m = re.search(r'\b[1-9]\d{12}\b', text)
if cnp_m:
extracted['cnp'] = cnp_m.group(0)
c = extracted['cnp']
pref = "19" if c[0] in ["1", "2"] else "20"
extracted['birth_date'] = f"{c[5:7]}.{c[3:5]}.{pref}{c[1:3]}"
# ...and the licence number is located right after it, with OCR-error correction
pattern = extracted['cnp'] + r'[.\s5]*([I10B][0-9]{8}[A-Z0-9]{1,2})'
lic_m = re.search(pattern, u_no_spaces)
if lic_m:
res = lic_m.group(1).upper()
if len(res) > 10: res = res[:10]
if res.startswith('1'): res = 'I' + res[1:] # "1" read instead of "I"
if res.endswith('5'): res = res[:-1] + 'S' # "5" read instead of "S"
extracted['license_no'] = res
...
# merge across documents: keep the most complete licence number
if k == 'license_no':
clean_val = re.sub(r'[^A-Z0-9]', '', val.upper())
if len(clean_val) >= 7:
if not extracted.get(key) or len(clean_val) >= len(str(extracted.get(key))):
extracted[key] = clean_val
Creating contracts
A contract is created from a booking, with data from three places: the booking, the OCR result and the operator's edits. The form carries both drivers, billing details (person or company), options and the financial breakdown, with the amount converted into local currency at the exchange rate stored in settings. The PDF is drawn directly with ReportLab, signatures included, stored in the database as a binary and emailed to the customer and the company.
...
cursor.execute("SELECT rent_contract_number FROM contracts_data WHERE CAST(rent_contract_number AS UNSIGNED) >= 500 ORDER BY CAST(rent_contract_number AS UNSIGNED) DESC LIMIT 1")
row = cursor.fetchone()
if row and row.get('rent_contract_number') is not None:
contract_num_str = str(int(row['rent_contract_number']) + 1)
else:
# empty database: take the number the agent typed in the form
contract_num_str = str(rent_form.rent_contract_number.data)
...
total_seconds = int(return_ts - pickup_ts)
full_days, remainder_seconds = divmod(total_seconds, 86400)
rent_days = full_days + 1 if remainder_seconds >= 1800 else max(1, full_days)
...
buffer = BytesIO()
p = Canvas(buffer, pagesize=A4)
p.setFont("ArialUnicode", 8)
...
p.drawImage(header_img, 20, 770, 120, 60)
p.line(20, 770, 580, 770)
Contract amendments (additional acts)
When a customer extends a rental, the operator creates an additional act: new return date and hour, new location and the extra price. Before saving, the app checks that the extension doesn't collide with the next booking of the same car. The customer signs on screen, the act is generated as a PDF numbered per contract (act no. 1, 2, …) and stored. From that moment the new return date wins everywhere: home screen, search and swap availability.
check_event = event_requested.copy()
check_event['return_timestamp'] = new_date_str
check_event['return_hour_timestamp'] = new_hour_str
collision, conflicting_id, conflict_period = check_collision(check_event, current_plate, all_reservations)
if collision:
flash(f"⚠️ Car {current_plate} is busy with booking #{conflicting_id} ({conflict_period})!", "danger")
return render_template('extension_form.html', form=form, event=event_requested, booked_id=booked_id)
sql = """
INSERT INTO contract_extensions
(booked_id, new_return_date, new_return_hour, new_return_location, extension_price)
VALUES (%s, %s, %s, %s, %s)
"""
Return reports and contract finalization
When the car comes back, the operator fills in a return report: mileage and fuel at return, exterior and interior damage, the customer signs on screen, and a PDF is generated. Closing the contract is guarded: it is refused while any amendment is missing its signed PDF. Finalization marks the contract as completed and deletes the booking's overrides in the same transaction, so the car reappears as free in searches. The contract then moves to the archive.
sql_check = "SELECT id FROM contract_extensions WHERE booked_id = %s AND extension_pdf IS NULL"
cursor.execute(sql_check, (booking_id,))
unsigned_extension = cursor.fetchone()
...
if unsigned_extension:
flash("An additional act exists without a signed PDF. Sign it before closing the contract.")
return redirect(url_for('booking_details', booking_id=booking_id))
...
sql = "UPDATE contracts_data SET completion_pv = %s, is_completed = %s WHERE booked_id = %s"
cursor.execute(sql, (binary_raport_to_insert, 1, booked_id))
# delete the override so the car is released in searches
cursor.execute("DELETE FROM booking_overrides WHERE external_booked_id = %s", (booked_id,))
db_conn.commit()
Reporting: contracts, clients and economic indicators
There is a dashboard (cars, contracts, unique customers) plus a family of fleet reports: per-car details, analytics with a date filter, performance, efficiency, profitability and seasonality. Profitability is computed per plate over a chosen interval, with a status filter (confirmed, triage or both). Revenue and ancillary fees are converted at each contract's own exchange rate. The report shows revenue per available vehicle-day, occupancy rate, average real price per day and ancillary revenue, with cars ranked against the best performer. Customers are counted as unique clients by name, phone and email.
for plate, info in fleet_records.items():
allocated_rev = info['allocated_revenue']
rented_days = info['rented_days_total']
rev_pav = allocated_rev / days_in_range # revenue per available vehicle-day
occupancy_rate = (rented_days / days_in_range) * 100
if occupancy_rate > 100: occupancy_rate = 100.0
avg_real_price = (allocated_rev / rented_days) if rented_days > 0 else 0.0
profitability_data.append({
'plate': plate,
'total_revenue': allocated_rev,
'rev_pav': rev_pav,
'occupancy_rate': occupancy_rate,
'avg_real_price': avg_real_price,
'ancillary_revenue': info['allocated_fees'],
'risk_revenue': info['allocated_risk']
})
profitability_data = sorted(profitability_data, key=lambda x: x['rev_pav'], reverse=True)
...
SELECT COUNT(*) FROM (
SELECT first_name, last_name, phone, email
FROM contracts_data
GROUP BY first_name, last_name, phone, email
) AS unique_clients_subquery
Quoting and commercial offers
The most business-oriented module. It has four steps. Pricing engine. It reproduces the public site's rules so prices quoted in the back office match what customers see: any started day counts as a day, tax is applied only when the site shows prices with tax, and out-of-hours pick-up or return adds a fee based on each location's opening hours for that weekday. It also handles two regimes, deposit or insurance, and optional extras. It is exposed as an API endpoint. Offers. The operator selects the available cars for a period and the app saves a commercial offer with a price per car, then generates a ready-to-send message. The text depends on the regime: standard deposit, or full CASCO with zero deposit. Sending. The message opens directly in WhatsApp through a wa.me link, pre-filled with the customer's number and the offer text. Conversion. An accepted offer becomes a real booking written into the WordPress database (customer, booking, options, invoice) in a single transaction: either everything is written or nothing, with the site's own booking-code format generated once the ID exists.
total_seconds = int((return_datetime - pickup_datetime).total_seconds())
if total_seconds <= 0:
total_seconds = 3600
days_render = int(math.ceil(total_seconds / 86400))
if days_render <= 0:
days_render = 1
tax_factor = (1 + tax_percentage / 100) if show_with_tax == 1 else 1.0
# after-hours check for the pick-up location on that weekday
p_open_raw = loc_p.get(f'open_time_{pickup_day_short}')
p_close_raw = loc_p.get(f'close_time_{pickup_day_short}')
# MySQL TIME columns arrive as timedelta -> normalize to HH:MM:SS strings
p_open = str(p_open_raw).zfill(8) if isinstance(p_open_raw, timedelta) else str(p_open_raw or '08:00:00')
p_close = str(p_close_raw).zfill(8) if isinstance(p_close_raw, timedelta) else str(p_close_raw or '22:00:00')
if pickup_time_str < p_open or pickup_time_str > p_close:
night_pickup_fee = float(loc_p['afterhours_pickup_fee'] or 0)
...
if is_guarantee_regime_final:
message_lines.append("✓ *Standard car deposit* - the model's deposit is held at pick-up")
message_lines.append("⚠️ *Contractual liability* - the customer is liable up to the deductible in case of damage")
else:
message_lines.append("✓ *Full CASCO package included* - no stress over damage or scratches")
message_lines.append("✓ *Zero deposit* - no deposit is held, no credit card required!")
...
generated_text_whatsapp = "\n".join(message_lines)
raw_phone = client_phone.strip().replace(" ", "").replace("+", "")
encoded_message = quote(generated_text_whatsapp)
whatsapp_direct_url = f"https://wa.me/{raw_phone}?text={encoded_message}"
return redirect(whatsapp_direct_url)
...
db_wp = pool_live.get_connection()
cursor_wp = db_wp.cursor(dictionary=True)
# disable autocommit: if a single table fails, everything is rolled back
db_wp.autocommit = False
cursor_wp.execute(insert_customer_sql, (first_name, last_name, phone, email, now_ts, now_ts))
generated_customer_id = cursor_wp.lastrowid
...
generated_booking_id = cursor_wp.lastrowid
# official code in the site's own format
official_code = generate_site_booking_code(generated_booking_id)
cursor_wp.execute(
"UPDATE wp_car_rental_bookings SET booking_code = %s WHERE booking_id = %s",
(official_code, generated_booking_id)
)
...
db_wp.commit()
...
except mysql.connector.Error as sql_error:
if db_wp:
db_wp.rollback() # cancel everything if any rule fails
...
def generate_site_booking_code(booking_id):
chars = string.ascii_uppercase + string.digits
random_suffix = "".join(secrets.choice(chars) for _ in range(5))
return f"R{booking_id}A{random_suffix}"
_if you want to look under the hood... the rabbit!
payload_size: 110 min read