mirror of
https://github.com/koloideal/DutyLog.git
synced 2026-06-10 10:25:29 +03:00
252 lines
9.8 KiB
Python
252 lines
9.8 KiB
Python
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())
|