Files
DutyLog/import_hours.py
T
2026-03-04 01:21:06 +03:00

252 lines
9.8 KiB
Python
Raw 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.
import argparse
import asyncio
import pandas as pd
import asyncpg
import re
import os
def clean_name(name: str) -> str:
if not isinstance(name, str):
return ""
name = re.sub(r'\s+[СC]$', '', name.strip())
name = re.sub(r'\s*\d+\s*$', '', name)
return name.strip()
def parse_excel_raw(file_path: str):
excel_file = pd.ExcelFile(file_path)
resident_hours = {}
room_hours = {}
for sheet_name in excel_file.sheet_names:
df = pd.read_excel(file_path, sheet_name=sheet_name)
# --- Парсинг жильцов ---
if 'ФИО' in df.columns and 'Часы' in df.columns:
for _, row in df.iterrows():
fio = clean_name(str(row['ФИО']))
if not fio or str(fio).lower() == 'nan':
continue
try:
hours_raw = str(row['Часы']).split('(')[0].strip()
amount = int(float(hours_raw))
except ValueError:
continue
if fio in resident_hours:
resident_hours[fio] += amount
else:
resident_hours[fio] = amount
# --- Парсинг комнат ---
elif df.iloc[0].astype(str).str.contains('Комната', case=False, na=False).any():
header_idx = df[df.apply(lambda r: r.astype(str).str.contains('Комната', case=False).any(), axis=1)].index[0]
df.columns = df.iloc[header_idx]
df = df.iloc[header_idx + 1:].reset_index(drop=True)
for _, row in df.iterrows():
room_str = str(row['Комната'])
if not room_str or room_str.lower() == 'nan':
continue
room_match = re.search(r'\b(\d{3,4})\b', room_str)
if not room_match:
continue
room_number = int(room_match.group(1))
try:
punishment_raw = str(row['Наказание']).split('(')[0].replace('часов', '').replace(',', ';').strip()
amounts = [int(x.strip()) for x in re.findall(r'\d+', punishment_raw)]
amount = sum(amounts) if amounts else 0
except (ValueError, AttributeError):
continue
if amount <= 0:
continue
if room_number in room_hours:
room_hours[room_number] += amount
else:
room_hours[room_number] = amount
return resident_hours, room_hours
async def run_parse(database_url: str, file_path: str):
raw_resident_hours, raw_room_hours = parse_excel_raw(file_path)
async with asyncpg.create_pool(database_url) as pool:
async with pool.acquire() as conn:
resident_rows = await conn.fetch("SELECT real_name FROM residents WHERE real_name IS NOT NULL;")
db_names = [row['real_name'] for row in resident_rows]
room_rows = await conn.fetch("SELECT number FROM rooms;")
db_rooms = {row['number'] for row in room_rows}
# --- Сопоставление жильцов ---
db_names_lower = {name.lower(): name for name in db_names}
matched_residents = {}
unmatched_residents = {}
for raw_fio, amount in raw_resident_hours.items():
fio_lower = raw_fio.lower()
matched_name = None
if fio_lower in db_names_lower:
matched_name = db_names_lower[fio_lower]
else:
parts = fio_lower.split()
if len(parts) >= 2:
search_key = f"{parts[0]} {parts[1]}"
if search_key in db_names_lower:
matched_name = db_names_lower[search_key]
if not matched_name:
for db_lower, db_orig in db_names_lower.items():
if fio_lower in db_lower or db_lower in fio_lower:
matched_name = db_orig
break
if matched_name:
matched_residents[matched_name] = matched_residents.get(matched_name, 0) + amount
else:
unmatched_residents[raw_fio] = unmatched_residents.get(raw_fio, 0) + amount
# --- Сопоставление комнат ---
matched_rooms = {}
unmatched_rooms = {}
for room_num, amount in raw_room_hours.items():
if room_num in db_rooms:
matched_rooms[room_num] = matched_rooms.get(room_num, 0) + amount
else:
unmatched_rooms[room_num] = unmatched_rooms.get(room_num, 0) + amount
# --- Запись в файлы ---
'''with open("parsed_residents.txt", "w", encoding="utf-8") as f:
for name, amount in sorted(matched_residents.items()):
f.write(f"{name} - {amount}\n")'''
with open("parsed_rooms.txt", "w", encoding="utf-8") as f:
for room, amount in sorted(matched_rooms.items()):
f.write(f"{room} - {amount}\n")
'''if unmatched_residents:
with open("unmatched_residents.txt", "w", encoding="utf-8") as f:
for name, amount in sorted(unmatched_residents.items()):
f.write(f"{name} - {amount}\n")'''
if unmatched_rooms:
with open("unmatched_rooms.txt", "w", encoding="utf-8") as f:
for room, amount in sorted(unmatched_rooms.items()):
f.write(f"{room} - {amount}\n")
async def run_load(database_url: str, admin_id: int):
if not os.path.exists("parsed_residents.txt") or not os.path.exists("parsed_rooms.txt"):
raise FileNotFoundError("Файлы parsed_residents.txt или parsed_rooms.txt не найдены. Сначала запустите parse.")
resident_transactions = []
with open("parsed_residents.txt", "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(" - ")
if len(parts) == 2:
resident_transactions.append((parts[0].strip(), int(parts[1].strip())))
room_transactions = []
with open("parsed_rooms.txt", "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(" - ")
if len(parts) == 2:
room_transactions.append((int(parts[0].strip()), int(parts[1].strip())))
async with asyncpg.create_pool(database_url) as pool:
async with pool.acquire() as conn:
# Маппинги
res_rows = await conn.fetch("SELECT id, real_name FROM residents WHERE real_name IS NOT NULL;")
name_to_id = {row['real_name']: row['id'] for row in res_rows}
rm_rows = await conn.fetch("SELECT id, number FROM rooms;")
num_to_id = {row['number']: row['id'] for row in rm_rows}
res_tx_params = []
res_update_params = []
for name, amount in resident_transactions:
res_id = name_to_id.get(name)
if res_id:
res_tx_params.append((res_id, "increase", amount, admin_id, "Инициализация существующих часов"))
res_update_params.append((amount, res_id))
rm_tx_params = []
rm_update_params = []
for room_num, amount in room_transactions:
rm_id = num_to_id.get(room_num)
if rm_id:
rm_tx_params.append((rm_id, "increase", amount, admin_id, "Инициализация существующих часов"))
rm_update_params.append((amount, rm_id))
async with conn.transaction():
# Транзакции резидентов
if res_tx_params:
await conn.executemany(
"""
INSERT INTO hours_transactions
(resident_id, transaction_type, amount, admin_id, remark, created_at)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP);
""", res_tx_params
)
# Обновление баланса резидентов
await conn.executemany(
"""
UPDATE residents
SET active_hours = active_hours + $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
""", res_update_params
)
# Транзакции комнат
if rm_tx_params:
await conn.executemany(
"""
INSERT INTO room_hours_transactions
(room_id, transaction_type, amount, admin_id, remark, created_at)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP);
""", rm_tx_params
)
# Обновление баланса комнат
await conn.executemany(
"""
UPDATE rooms
SET active_hours = active_hours + $1
WHERE id = $2;
""", rm_update_params
)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=["parse", "load"])
parser.add_argument("--db-url", required=True)
parser.add_argument("--file", required=False)
parser.add_argument("--admin-id", type=int, default=2047958833)
args = parser.parse_args()
if args.command == "parse":
if not args.file:
parser.error("Для команды parse требуется указать --file")
await run_parse(args.db_url, args.file)
elif args.command == "load":
await run_load(args.db_url, args.admin_id)
if __name__ == "__main__":
asyncio.run(main())