Update WEHAGO comparison data and tools
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\click_left_code_9225.py" %*
|
||||
@@ -0,0 +1,69 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "110"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
info = driver.execute_script(
|
||||
r"""
|
||||
const code = arguments[0];
|
||||
const out = [];
|
||||
for (const cell of document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td')) {
|
||||
const r = cell.getBoundingClientRect();
|
||||
const text = (cell.textContent || '').trim();
|
||||
if (text === code && r.left < 220 && r.top > 145 && r.width > 0 && r.height > 0) {
|
||||
out.push({left:r.left, top:r.top, width:r.width, height:r.height, text});
|
||||
}
|
||||
}
|
||||
return out.sort((a,b)=>a.top-b.top)[0] || null;
|
||||
""",
|
||||
target,
|
||||
)
|
||||
print("target", target, "info", json.dumps(info, ensure_ascii=False))
|
||||
if info:
|
||||
x = info["left"] + info["width"] / 2
|
||||
y = info["top"] + info["height"] / 2
|
||||
for typ in ["mouseMoved", "mousePressed", "mouseReleased"]:
|
||||
payload = {"type": typ, "x": x, "y": y, "button": "left", "clickCount": 1}
|
||||
if typ == "mouseMoved":
|
||||
payload["button"] = "none"
|
||||
driver.execute_cdp_cmd("Input.dispatchMouseEvent", payload)
|
||||
time.sleep(0.5)
|
||||
|
||||
visible = driver.execute_script(
|
||||
r"""
|
||||
const rows = [];
|
||||
const colorScore = (color) => {
|
||||
const m = String(color || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (!m) return 0;
|
||||
const r = Number(m[1]), g = Number(m[2]), b = Number(m[3]);
|
||||
return Math.max(0, b - r) + Math.max(0, g - r) + (b > 120 ? 20 : 0);
|
||||
};
|
||||
for (const cell of document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td')) {
|
||||
const r = cell.getBoundingClientRect();
|
||||
const code = (cell.textContent || '').trim();
|
||||
if (!/^\d{3,6}$/.test(code) || r.left > 220 || r.top < 145 || r.width <= 0 || r.height <= 0) continue;
|
||||
let node = cell, score = 0, cls = '';
|
||||
while (node && node !== document.body) {
|
||||
const style = window.getComputedStyle(node);
|
||||
score = Math.max(score, colorScore(style.backgroundColor));
|
||||
cls += ' ' + String(node.className || '');
|
||||
node = node.parentElement;
|
||||
}
|
||||
if (/selected|select|focus|current|active|checked/i.test(cls)) score += 80;
|
||||
rows.push({code, top:r.top, score, cls: cls.slice(0,120)});
|
||||
}
|
||||
rows.sort((a,b)=>(b.score-a.score)||(a.top-b.top));
|
||||
return rows.slice(0,8);
|
||||
"""
|
||||
)
|
||||
print(json.dumps(visible, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
set "PYTHONIOENCODING=utf-8"
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\click_left_search_button_9225.py"
|
||||
@@ -0,0 +1,27 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
for typ in ["mouseMoved", "mousePressed", "mouseReleased"]:
|
||||
driver.execute_cdp_cmd(
|
||||
"Input.dispatchMouseEvent",
|
||||
{"type": typ, "x": 130, "y": 188, "button": "left" if typ != "mouseMoved" else "none", "clickCount": 1},
|
||||
)
|
||||
time.sleep(0.8)
|
||||
items = driver.execute_script(
|
||||
r"""
|
||||
return Array.from(document.querySelectorAll('input,button,[role=button],a,div')).map(el=>{
|
||||
const r=el.getBoundingClientRect(); const st=getComputedStyle(el);
|
||||
return {tag:el.tagName,id:el.id||'',cls:String(el.className||'').slice(0,80),text:(el.textContent||'').trim().slice(0,80),value:el.value||'',placeholder:el.getAttribute('placeholder')||'',title:el.getAttribute('title')||'',left:Math.round(r.left),top:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height),display:st.display,vis:st.visibility};
|
||||
}).filter(x=>x.width>0&&x.height>0&&x.display!=='none'&&x.vis!=='hidden'&&x.top<400&&x.left<700).sort((a,b)=>(a.top-b.top)||(a.left-b.left)).slice(0,200);
|
||||
"""
|
||||
)
|
||||
print(json.dumps(items, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,437 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(BASE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from main import engine
|
||||
from wehago_compare import (
|
||||
_get_cached_status_rows_by_range,
|
||||
_get_cached_voucher_sections_by_range,
|
||||
discover_available_years,
|
||||
)
|
||||
|
||||
EXPORT_DIR = BASE_DIR / "static" / "exports"
|
||||
HEADER_FILL = PatternFill(fill_type="solid", fgColor="1F2937")
|
||||
HEADER_FONT = Font(color="FFFFFF", bold=True)
|
||||
SUMMARY_FILL = PatternFill(fill_type="solid", fgColor="E5E7EB")
|
||||
RECHECK_FILL = PatternFill(fill_type="solid", fgColor="FEF3C7")
|
||||
UNMATCHED_FILL = PatternFill(fill_type="solid", fgColor="FDE68A")
|
||||
ERP_UNMATCHED_FILL = PatternFill(fill_type="solid", fgColor="DBEAFE")
|
||||
|
||||
|
||||
def clean(value: Any) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
def amount(value: Any) -> float:
|
||||
text = clean(value).replace(",", "")
|
||||
if not text:
|
||||
return 0.0
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def autosize(ws) -> None:
|
||||
widths: dict[int, int] = {}
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
value = "" if cell.value is None else str(cell.value)
|
||||
widths[cell.column] = min(max(widths.get(cell.column, 0), len(value) + 2), 42)
|
||||
for col_idx, width in widths.items():
|
||||
ws.column_dimensions[get_column_letter(col_idx)].width = width
|
||||
|
||||
|
||||
def style_sheet(ws) -> None:
|
||||
ws.freeze_panes = "A2"
|
||||
ws.auto_filter.ref = ws.dimensions
|
||||
for cell in ws[1]:
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
for row in ws.iter_rows(min_row=2):
|
||||
status_value = clean(row[0].value)
|
||||
if status_value == "Recheck":
|
||||
fill = RECHECK_FILL
|
||||
elif status_value == "Unmatched":
|
||||
fill = UNMATCHED_FILL
|
||||
elif status_value == "ERP Unmatched":
|
||||
fill = ERP_UNMATCHED_FILL
|
||||
else:
|
||||
fill = None
|
||||
if fill:
|
||||
for cell in row:
|
||||
cell.fill = fill
|
||||
for cell in row:
|
||||
cell.alignment = Alignment(vertical="top", wrap_text=True)
|
||||
autosize(ws)
|
||||
|
||||
|
||||
def append_rows(ws, headers: list[str], rows: Iterable[list[Any]]) -> None:
|
||||
ws.append(headers)
|
||||
for row in rows:
|
||||
ws.append(row)
|
||||
style_sheet(ws)
|
||||
|
||||
|
||||
def append_rows_write_only(workbook: Workbook, title: str, headers: list[str], rows: Iterable[list[Any]]) -> None:
|
||||
ws = workbook.create_sheet(title)
|
||||
header_row: list[WriteOnlyCell] = []
|
||||
for value in headers:
|
||||
cell = WriteOnlyCell(ws, value=value)
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
header_row.append(cell)
|
||||
ws.append(header_row)
|
||||
for row in rows:
|
||||
ws.append(list(row))
|
||||
|
||||
|
||||
def flatten_groups(
|
||||
groups: list[dict[str, Any]],
|
||||
card_label: str,
|
||||
only_statuses: set[str] | None = None,
|
||||
) -> list[list[Any]]:
|
||||
rows: list[list[Any]] = []
|
||||
for group_index, group in enumerate(groups, start=1):
|
||||
summary = group.get("summary", {})
|
||||
link_count = len(summary.get("erp_group_keys") or summary.get("wehago_group_keys") or [])
|
||||
group_rows = group.get("rows", [])
|
||||
for row_index, row in enumerate(group_rows, start=1):
|
||||
status_label = clean(row.get("status_label"))
|
||||
if only_statuses and status_label not in only_statuses:
|
||||
continue
|
||||
rows.append(
|
||||
[
|
||||
card_label,
|
||||
group_index,
|
||||
row_index,
|
||||
status_label,
|
||||
summary.get("fiscal_year"),
|
||||
clean(summary.get("ledger_date")),
|
||||
clean(summary.get("proof_date")),
|
||||
clean(summary.get("voucher_no")),
|
||||
clean(summary.get("draft_no")),
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
clean(summary.get("ledger_vendors")),
|
||||
clean(summary.get("voucher_vendors")),
|
||||
clean(summary.get("review_reason")),
|
||||
len(group_rows),
|
||||
link_count,
|
||||
clean(row.get("ledger_date")),
|
||||
clean(row.get("proof_date")),
|
||||
clean(row.get("voucher_no")),
|
||||
clean(row.get("draft_no")),
|
||||
clean(row.get("ledger_account_name")),
|
||||
clean(row.get("voucher_account_name")),
|
||||
clean(row.get("ledger_vendor")),
|
||||
clean(row.get("voucher_vendor")),
|
||||
amount(row.get("ledger_debit")),
|
||||
amount(row.get("ledger_credit")),
|
||||
amount(row.get("voucher_debit")),
|
||||
amount(row.get("voucher_credit")),
|
||||
clean(row.get("ledger_desc")),
|
||||
clean(row.get("voucher_desc")),
|
||||
]
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def iter_card_issue_rows(groups: list[dict[str, Any]], card_kind: str) -> list[list[Any]]:
|
||||
issues: list[list[Any]] = []
|
||||
for group in groups:
|
||||
summary = group.get("summary", {})
|
||||
parent_year = summary.get("fiscal_year")
|
||||
parent_date = clean(summary.get("ledger_date") or summary.get("proof_date"))
|
||||
parent_voucher_no = clean(summary.get("voucher_no"))
|
||||
parent_draft_no = clean(summary.get("draft_no"))
|
||||
for row in group.get("rows", []):
|
||||
status_label = clean(row.get("status_label"))
|
||||
if card_kind == "wehago" and status_label not in {"ERP Unmatched", "Recheck"}:
|
||||
continue
|
||||
if card_kind == "voucher" and status_label not in {"Unmatched", "Recheck"}:
|
||||
continue
|
||||
issues.append(
|
||||
[
|
||||
status_label,
|
||||
parent_year,
|
||||
parent_date,
|
||||
parent_voucher_no,
|
||||
parent_draft_no,
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
clean(summary.get("ledger_vendors")),
|
||||
clean(summary.get("voucher_vendors")),
|
||||
clean(summary.get("review_reason")),
|
||||
clean(row.get("ledger_date")),
|
||||
clean(row.get("proof_date")),
|
||||
clean(row.get("voucher_no")),
|
||||
clean(row.get("draft_no")),
|
||||
clean(row.get("ledger_account_name")),
|
||||
clean(row.get("voucher_account_name")),
|
||||
clean(row.get("ledger_vendor")),
|
||||
clean(row.get("voucher_vendor")),
|
||||
amount(row.get("ledger_debit")),
|
||||
amount(row.get("ledger_credit")),
|
||||
amount(row.get("voucher_debit")),
|
||||
amount(row.get("voucher_credit")),
|
||||
clean(row.get("ledger_desc")),
|
||||
clean(row.get("voucher_desc")),
|
||||
]
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def build_relation_rows(
|
||||
wehago_groups: list[dict[str, Any]],
|
||||
voucher_groups: list[dict[str, Any]],
|
||||
) -> list[list[Any]]:
|
||||
rows: list[list[Any]] = []
|
||||
for group in wehago_groups:
|
||||
summary = group.get("summary", {})
|
||||
erp_keys = summary.get("erp_group_keys") or set()
|
||||
if len(erp_keys) <= 1:
|
||||
continue
|
||||
rows.append(
|
||||
[
|
||||
"WEHAGO 1 : ERP N",
|
||||
summary.get("fiscal_year"),
|
||||
clean(summary.get("ledger_date")),
|
||||
clean(summary.get("proof_date")),
|
||||
clean(summary.get("voucher_no")),
|
||||
clean(summary.get("draft_no")),
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
len(erp_keys),
|
||||
len(group.get("rows", [])),
|
||||
clean(summary.get("review_reason")),
|
||||
" | ".join(sorted(clean(key) for key in erp_keys if clean(key))),
|
||||
]
|
||||
)
|
||||
for group in voucher_groups:
|
||||
summary = group.get("summary", {})
|
||||
wehago_keys = summary.get("wehago_group_keys") or set()
|
||||
if len(wehago_keys) <= 1:
|
||||
continue
|
||||
rows.append(
|
||||
[
|
||||
"ERP 1 : WEHAGO N",
|
||||
summary.get("fiscal_year"),
|
||||
clean(summary.get("ledger_date")),
|
||||
clean(summary.get("proof_date")),
|
||||
clean(summary.get("voucher_no")),
|
||||
clean(summary.get("draft_no")),
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
len(wehago_keys),
|
||||
len(group.get("rows", [])),
|
||||
clean(summary.get("review_reason")),
|
||||
" | ".join(sorted(clean(key) for key in wehago_keys if clean(key))),
|
||||
]
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def export_voucher_card_diff(start_year: int, end_year: int) -> Path:
|
||||
EXPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
workbook = Workbook(write_only=True)
|
||||
|
||||
detail_headers = [
|
||||
"카드",
|
||||
"그룹순번",
|
||||
"행순번",
|
||||
"행상태",
|
||||
"연도",
|
||||
"부모 WEHAGO 일자",
|
||||
"부모 ERP 일자",
|
||||
"부모 전표번호",
|
||||
"부모 가전표번호",
|
||||
"부모 WEHAGO 계정",
|
||||
"부모 ERP 계정",
|
||||
"부모 WEHAGO 거래처",
|
||||
"부모 ERP 거래처",
|
||||
"부모 검증근거",
|
||||
"부모 그룹 행수",
|
||||
"연결된 반대 그룹 수",
|
||||
"행 WEHAGO 일자",
|
||||
"행 ERP 일자",
|
||||
"행 전표번호",
|
||||
"행 가전표번호",
|
||||
"행 WEHAGO 계정",
|
||||
"행 ERP 계정",
|
||||
"행 WEHAGO 거래처",
|
||||
"행 ERP 거래처",
|
||||
"행 WEHAGO 차변",
|
||||
"행 WEHAGO 대변",
|
||||
"행 ERP 차변",
|
||||
"행 ERP 대변",
|
||||
"행 WEHAGO 적요",
|
||||
"행 ERP 적요",
|
||||
]
|
||||
|
||||
all_wehago_match_rows: list[list[Any]] = []
|
||||
all_wehago_unmatched_rows: list[list[Any]] = []
|
||||
all_voucher_match_rows: list[list[Any]] = []
|
||||
all_voucher_unmatched_rows: list[list[Any]] = []
|
||||
diff_summary_rows: list[list[Any]] = []
|
||||
relation_rows_all: list[list[Any]] = []
|
||||
|
||||
for year in range(start_year, end_year + 1):
|
||||
rows_by_status = _get_cached_status_rows_by_range(engine, year, year)
|
||||
sections = _get_cached_voucher_sections_by_range(engine, year, year, rows_by_status)
|
||||
wehago_groups = sections.get("voucher_matched", [])
|
||||
voucher_groups = sections.get("erp_voucher_matched", [])
|
||||
wehago_unmatched_groups = sections.get("voucher_unmatched", [])
|
||||
wehago_issue_rows = iter_card_issue_rows(wehago_groups, "wehago")
|
||||
voucher_issue_rows = iter_card_issue_rows(voucher_groups, "voucher")
|
||||
relation_rows = build_relation_rows(wehago_groups, voucher_groups)
|
||||
wehago_multi = sum(1 for row in relation_rows if row[0] == "WEHAGO 1 : ERP N")
|
||||
voucher_multi = sum(1 for row in relation_rows if row[0] == "ERP 1 : WEHAGO N")
|
||||
all_wehago_match_rows.extend(flatten_groups(wehago_groups, "WEHAGO"))
|
||||
all_wehago_unmatched_rows.extend(flatten_groups(wehago_unmatched_groups, "WEHAGO Unmatched"))
|
||||
all_voucher_match_rows.extend(flatten_groups(voucher_groups, "Voucher"))
|
||||
all_voucher_unmatched_rows.extend(flatten_groups(voucher_groups, "Voucher", {"Unmatched", "Recheck"}))
|
||||
relation_rows_all.extend(relation_rows)
|
||||
diff_summary_rows.append(
|
||||
[
|
||||
"요약",
|
||||
year,
|
||||
len(wehago_groups),
|
||||
len(wehago_unmatched_groups),
|
||||
len(voucher_groups),
|
||||
len(voucher_issue_rows),
|
||||
len(wehago_groups) - len(voucher_groups),
|
||||
wehago_multi,
|
||||
voucher_multi,
|
||||
len(wehago_issue_rows),
|
||||
len(voucher_issue_rows),
|
||||
"WEHAGO 카드는 WEHAGO 전표 기준 그룹 수, Voucher 카드는 ERP 전표 기준 그룹 수입니다.",
|
||||
]
|
||||
)
|
||||
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"1_WEHAGO_Match",
|
||||
detail_headers,
|
||||
all_wehago_match_rows or [["WEHAGO", "", "", "", "", "", "", "", "", "", "", "", "", "데이터가 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"2_WEHAGO_Unmatched",
|
||||
detail_headers,
|
||||
all_wehago_unmatched_rows or [["WEHAGO Unmatched", "", "", "", "", "", "", "", "", "", "", "", "", "데이터가 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"3_Voucher_Match",
|
||||
detail_headers,
|
||||
all_voucher_match_rows or [["Voucher", "", "", "", "", "", "", "", "", "", "", "", "", "데이터가 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"4_Voucher_Unmatched",
|
||||
detail_headers,
|
||||
all_voucher_unmatched_rows or [["Voucher", "", "", "", "", "", "", "", "", "", "", "", "", "Voucher 카드 안에서 Unmatched/Recheck로 남은 행이 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
|
||||
diff_headers = [
|
||||
"구분",
|
||||
"연도",
|
||||
"WEHAGO 매치 그룹 수",
|
||||
"WEHAGO Unmatched 그룹 수",
|
||||
"Voucher 매치 그룹 수",
|
||||
"Voucher unmatched 행 수",
|
||||
"WEHAGO-Voucher 그룹 수 차이",
|
||||
"WEHAGO 1:ERP N 건수",
|
||||
"ERP 1:WEHAGO N 건수",
|
||||
"WEHAGO 카드 내 ERP Unmatched/Recheck 행 수",
|
||||
"Voucher 카드 내 Unmatched/Recheck 행 수",
|
||||
"설명",
|
||||
]
|
||||
diff_rows: list[list[Any]] = []
|
||||
diff_rows.append(diff_headers)
|
||||
diff_rows.extend(diff_summary_rows)
|
||||
if diff_summary_rows:
|
||||
total_wehago = sum(int(row[2]) for row in diff_summary_rows)
|
||||
total_wehago_unmatched = sum(int(row[3]) for row in diff_summary_rows)
|
||||
total_voucher = sum(int(row[4]) for row in diff_summary_rows)
|
||||
total_voucher_unmatched = sum(int(row[5]) for row in diff_summary_rows)
|
||||
total_diff = sum(int(row[6]) for row in diff_summary_rows)
|
||||
total_wehago_multi = sum(int(row[7]) for row in diff_summary_rows)
|
||||
total_voucher_multi = sum(int(row[8]) for row in diff_summary_rows)
|
||||
total_wehago_issues = sum(int(row[9]) for row in diff_summary_rows)
|
||||
total_voucher_issues = sum(int(row[10]) for row in diff_summary_rows)
|
||||
diff_rows.append(
|
||||
[
|
||||
"전체",
|
||||
f"{start_year}~{end_year}",
|
||||
total_wehago,
|
||||
total_wehago_unmatched,
|
||||
total_voucher,
|
||||
total_voucher_unmatched,
|
||||
total_diff,
|
||||
total_wehago_multi,
|
||||
total_voucher_multi,
|
||||
total_wehago_issues,
|
||||
total_voucher_issues,
|
||||
"그룹 수 차이는 한쪽 전표 1건이 반대쪽 여러 전표와 연결되는 경우에서 주로 발생합니다.",
|
||||
]
|
||||
)
|
||||
diff_rows.append([])
|
||||
diff_rows.append(["관계 유형", "연도", "WEHAGO 일자", "ERP 일자", "전표번호", "가전표번호", "WEHAGO 계정", "ERP 계정", "연결된 반대 그룹 수", "그룹 행 수", "검증 근거", "연결된 그룹 키"])
|
||||
diff_rows.extend(relation_rows_all or [["", "", "", "", "", "", "", "", "", "", "수 차이를 만든 다대일/일대다 사례가 없습니다.", ""]])
|
||||
diff_ws = workbook.create_sheet("5_Count_Diff")
|
||||
for index, row in enumerate(diff_rows, start=1):
|
||||
if index in {1, len(diff_summary_rows) + 3}:
|
||||
cells: list[WriteOnlyCell] = []
|
||||
for value in row:
|
||||
cell = WriteOnlyCell(diff_ws, value=value)
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
cells.append(cell)
|
||||
diff_ws.append(cells)
|
||||
else:
|
||||
diff_ws.append(row)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = EXPORT_DIR / f"voucher_card_diff_{start_year}_{end_year}_{timestamp}.xlsx"
|
||||
workbook.save(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
years = discover_available_years()
|
||||
default_start = min(years) if years else datetime.now().year
|
||||
default_end = max(years) if years else default_start
|
||||
parser = argparse.ArgumentParser(description="WEHAGO/Voucher 카드 차이 분석 엑셀 생성")
|
||||
parser.add_argument("--start-year", type=int, default=default_start)
|
||||
parser.add_argument("--end-year", type=int, default=default_end)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
start_year = min(args.start_year, args.end_year)
|
||||
end_year = max(args.start_year, args.end_year)
|
||||
output_path = export_voucher_card_diff(start_year, end_year)
|
||||
print(output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,316 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from collections import Counter, defaultdict, deque
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(BASE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from main import engine
|
||||
from wehago_compare import _get_cached_status_rows_by_range, _get_cached_voucher_sections_by_range
|
||||
|
||||
EXPORT_DIR = BASE_DIR / "static" / "exports"
|
||||
HEADER_FILL = PatternFill(fill_type="solid", fgColor="111827")
|
||||
HEADER_FONT = Font(color="FFFFFF", bold=True)
|
||||
POS_FILL = PatternFill(fill_type="solid", fgColor="DCFCE7")
|
||||
NEG_FILL = PatternFill(fill_type="solid", fgColor="DBEAFE")
|
||||
|
||||
|
||||
def clean(value: Any) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
def amount(value: Any) -> float:
|
||||
text = clean(value).replace(",", "")
|
||||
if not text:
|
||||
return 0.0
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def write_row(ws, values: list[Any], fill: PatternFill | None = None, header: bool = False) -> None:
|
||||
row = []
|
||||
for value in values:
|
||||
cell = WriteOnlyCell(ws, value=value)
|
||||
cell.alignment = Alignment(vertical="top", wrap_text=True)
|
||||
if header:
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
elif fill:
|
||||
cell.fill = fill
|
||||
row.append(cell)
|
||||
ws.append(row)
|
||||
|
||||
|
||||
def summarize_group(summary: dict[str, Any], side: str) -> dict[str, Any]:
|
||||
if side == "w":
|
||||
return {
|
||||
"date": clean(summary.get("ledger_date")),
|
||||
"voucher_no": clean(summary.get("voucher_no")),
|
||||
"draft_no": clean(summary.get("draft_no")),
|
||||
"accounts": clean(summary.get("ledger_accounts")),
|
||||
"vendors": clean(summary.get("ledger_vendors")),
|
||||
"debit": amount(summary.get("ledger_debit")),
|
||||
"credit": amount(summary.get("ledger_credit")),
|
||||
"links": len(summary.get("erp_group_keys") or []),
|
||||
"reason": clean(summary.get("review_reason")),
|
||||
}
|
||||
return {
|
||||
"date": clean(summary.get("proof_date")) or clean(summary.get("ledger_date")),
|
||||
"voucher_no": clean(summary.get("voucher_no")),
|
||||
"draft_no": clean(summary.get("draft_no")),
|
||||
"accounts": clean(summary.get("voucher_accounts")),
|
||||
"vendors": clean(summary.get("voucher_vendors")),
|
||||
"debit": amount(summary.get("voucher_debit")),
|
||||
"credit": amount(summary.get("voucher_credit")),
|
||||
"links": len(summary.get("wehago_group_keys") or []),
|
||||
"reason": clean(summary.get("review_reason")),
|
||||
}
|
||||
|
||||
|
||||
def build_gap_report(year: int) -> Path:
|
||||
rows = _get_cached_status_rows_by_range(engine, year, year)
|
||||
sections = _get_cached_voucher_sections_by_range(engine, year, year, rows)
|
||||
wehago_groups = sections["voucher_matched"]
|
||||
voucher_groups = sections["erp_voucher_matched"]
|
||||
|
||||
wehago_by_key: dict[Any, dict[str, Any]] = {}
|
||||
voucher_by_key: dict[Any, dict[str, Any]] = {}
|
||||
adj: dict[tuple[str, Any], set[tuple[str, Any]]] = defaultdict(set)
|
||||
|
||||
for group in wehago_groups:
|
||||
summary = group["summary"]
|
||||
w_key = summary.get("wehago_group_key")
|
||||
wehago_by_key[w_key] = group
|
||||
for e_key in summary.get("erp_group_keys") or set():
|
||||
adj[("w", w_key)].add(("e", e_key))
|
||||
adj[("e", e_key)].add(("w", w_key))
|
||||
for group in voucher_groups:
|
||||
summary = group["summary"]
|
||||
e_key = summary.get("erp_group_key")
|
||||
voucher_by_key[e_key] = group
|
||||
for w_key in summary.get("wehago_group_keys") or set():
|
||||
adj[("e", e_key)].add(("w", w_key))
|
||||
adj[("w", w_key)].add(("e", e_key))
|
||||
|
||||
components: list[dict[str, Any]] = []
|
||||
visited: set[tuple[str, Any]] = set()
|
||||
for node in list(adj):
|
||||
if node in visited:
|
||||
continue
|
||||
q = deque([node])
|
||||
visited.add(node)
|
||||
w_keys: list[Any] = []
|
||||
e_keys: list[Any] = []
|
||||
while q:
|
||||
side, key = q.popleft()
|
||||
if side == "w":
|
||||
w_keys.append(key)
|
||||
else:
|
||||
e_keys.append(key)
|
||||
for nxt in adj[node if False else (side, key)]:
|
||||
if nxt not in visited:
|
||||
visited.add(nxt)
|
||||
q.append(nxt)
|
||||
components.append({"w_keys": w_keys, "e_keys": e_keys})
|
||||
|
||||
shape_counter = Counter((len(item["w_keys"]), len(item["e_keys"])) for item in components)
|
||||
gap_components = [item for item in components if len(item["w_keys"]) != len(item["e_keys"])]
|
||||
gap_components.sort(key=lambda item: (abs(len(item["w_keys"]) - len(item["e_keys"])) * -1, len(item["w_keys"]) * -1, len(item["e_keys"]) * -1))
|
||||
|
||||
EXPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
wb = Workbook(write_only=True)
|
||||
|
||||
ws_summary = wb.create_sheet("summary")
|
||||
write_row(ws_summary, ["항목", "값", "설명"], header=True)
|
||||
write_row(ws_summary, ["연도", year, "분석 기준 연도"])
|
||||
write_row(ws_summary, ["WEHAGO 매치 그룹 수", len(wehago_groups), "WEHAGO 카드에 표시되는 매치 전표 그룹 수"])
|
||||
write_row(ws_summary, ["Voucher 매치 그룹 수", len(voucher_groups), "Voucher 카드에 표시되는 매치 전표 그룹 수"])
|
||||
write_row(ws_summary, ["그룹 수 차이", len(wehago_groups) - len(voucher_groups), "WEHAGO 그룹 수 - Voucher 그룹 수"])
|
||||
write_row(ws_summary, ["전체 연결 성분 수", len(components), "WEHAGO/ERP 매치 관계를 그래프로 묶은 연결 단위 수"])
|
||||
write_row(ws_summary, ["1:1 성분 수", shape_counter.get((1, 1), 0), "양쪽 그룹 수가 같은 일반적인 경우"])
|
||||
write_row(ws_summary, ["차이 발생 성분 수", len(gap_components), "WEHAGO 그룹 수와 Voucher 그룹 수가 다른 연결 성분 수"])
|
||||
write_row(ws_summary, ["WEHAGO 쪽 초과 합계", sum(max(len(item['w_keys']) - len(item['e_keys']), 0) for item in gap_components), "WEHAGO 그룹 수가 더 많은 만큼의 합계"])
|
||||
write_row(ws_summary, ["Voucher 쪽 초과 합계", sum(max(len(item['e_keys']) - len(item['w_keys']), 0) for item in gap_components), "Voucher 그룹 수가 더 많은 만큼의 합계"])
|
||||
write_row(ws_summary, [])
|
||||
write_row(ws_summary, ["형태", "건수", "설명"], header=True)
|
||||
for (w_count, e_count), count in shape_counter.most_common(20):
|
||||
desc = "정상 1:1" if (w_count, e_count) == (1, 1) else ("WEHAGO가 더 많이 쪼개짐" if w_count > e_count else "Voucher가 더 많이 쪼개짐")
|
||||
write_row(ws_summary, [f"{w_count}:{e_count}", count, desc])
|
||||
|
||||
ws_gap = wb.create_sheet("gap_components")
|
||||
write_row(
|
||||
ws_gap,
|
||||
[
|
||||
"component_id",
|
||||
"형태",
|
||||
"WEHAGO 그룹 수",
|
||||
"Voucher 그룹 수",
|
||||
"차이(WEHAGO-Voucher)",
|
||||
"WEHAGO 대표 전표",
|
||||
"Voucher 대표 전표",
|
||||
"WEHAGO 전표 목록",
|
||||
"Voucher 전표 목록",
|
||||
"WEHAGO 계정 요약",
|
||||
"Voucher 계정 요약",
|
||||
],
|
||||
header=True,
|
||||
)
|
||||
for idx, item in enumerate(gap_components, start=1):
|
||||
w_summaries = [summarize_group(wehago_by_key[key]["summary"], "w") for key in item["w_keys"] if key in wehago_by_key]
|
||||
e_summaries = [summarize_group(voucher_by_key[key]["summary"], "e") for key in item["e_keys"] if key in voucher_by_key]
|
||||
fill = POS_FILL if len(item["w_keys"]) > len(item["e_keys"]) else NEG_FILL
|
||||
write_row(
|
||||
ws_gap,
|
||||
[
|
||||
idx,
|
||||
f"{len(item['w_keys'])}:{len(item['e_keys'])}",
|
||||
len(item["w_keys"]),
|
||||
len(item["e_keys"]),
|
||||
len(item["w_keys"]) - len(item["e_keys"]),
|
||||
" | ".join(f"{row['date']} {row['voucher_no']}" for row in w_summaries[:3]),
|
||||
" | ".join(f"{row['date']} {row['voucher_no']}/{row['draft_no']}" for row in e_summaries[:3]),
|
||||
"\n".join(f"{row['date']} / {row['voucher_no']} / {row['accounts']}" for row in w_summaries),
|
||||
"\n".join(f"{row['date']} / {row['voucher_no']} / {row['draft_no']} / {row['accounts']}" for row in e_summaries),
|
||||
"\n".join(row["accounts"] for row in w_summaries),
|
||||
"\n".join(row["accounts"] for row in e_summaries),
|
||||
],
|
||||
fill=fill,
|
||||
)
|
||||
|
||||
ws_wehago = wb.create_sheet("wehago_extra_groups")
|
||||
write_row(
|
||||
ws_wehago,
|
||||
[
|
||||
"component_id",
|
||||
"형태",
|
||||
"WEHAGO 일자",
|
||||
"WEHAGO 전표번호",
|
||||
"WEHAGO 계정",
|
||||
"WEHAGO 거래처",
|
||||
"WEHAGO 차변",
|
||||
"WEHAGO 대변",
|
||||
"연결 ERP 그룹 수",
|
||||
"연결 ERP 전표 목록",
|
||||
"검증 근거",
|
||||
],
|
||||
header=True,
|
||||
)
|
||||
for idx, item in enumerate(gap_components, start=1):
|
||||
if len(item["w_keys"]) <= len(item["e_keys"]):
|
||||
continue
|
||||
linked_erp = [summarize_group(voucher_by_key[key]["summary"], "e") for key in item["e_keys"] if key in voucher_by_key]
|
||||
linked_erp_text = "\n".join(f"{row['date']} / {row['voucher_no']} / {row['draft_no']}" for row in linked_erp)
|
||||
for w_key in item["w_keys"]:
|
||||
group = wehago_by_key.get(w_key)
|
||||
if not group:
|
||||
continue
|
||||
s = summarize_group(group["summary"], "w")
|
||||
write_row(
|
||||
ws_wehago,
|
||||
[
|
||||
idx,
|
||||
f"{len(item['w_keys'])}:{len(item['e_keys'])}",
|
||||
s["date"],
|
||||
s["voucher_no"],
|
||||
s["accounts"],
|
||||
s["vendors"],
|
||||
s["debit"],
|
||||
s["credit"],
|
||||
s["links"],
|
||||
linked_erp_text,
|
||||
s["reason"],
|
||||
],
|
||||
fill=POS_FILL,
|
||||
)
|
||||
|
||||
ws_voucher = wb.create_sheet("voucher_extra_groups")
|
||||
write_row(
|
||||
ws_voucher,
|
||||
[
|
||||
"component_id",
|
||||
"형태",
|
||||
"ERP 일자",
|
||||
"ERP 전표번호",
|
||||
"ERP 가전표번호",
|
||||
"ERP 계정",
|
||||
"ERP 거래처",
|
||||
"ERP 차변",
|
||||
"ERP 대변",
|
||||
"연결 WEHAGO 그룹 수",
|
||||
"연결 WEHAGO 전표 목록",
|
||||
"검증 근거",
|
||||
],
|
||||
header=True,
|
||||
)
|
||||
for idx, item in enumerate(gap_components, start=1):
|
||||
if len(item["e_keys"]) <= len(item["w_keys"]):
|
||||
continue
|
||||
linked_wehago = [summarize_group(wehago_by_key[key]["summary"], "w") for key in item["w_keys"] if key in wehago_by_key]
|
||||
linked_wehago_text = "\n".join(f"{row['date']} / {row['voucher_no']}" for row in linked_wehago)
|
||||
for e_key in item["e_keys"]:
|
||||
group = voucher_by_key.get(e_key)
|
||||
if not group:
|
||||
continue
|
||||
s = summarize_group(group["summary"], "e")
|
||||
write_row(
|
||||
ws_voucher,
|
||||
[
|
||||
idx,
|
||||
f"{len(item['w_keys'])}:{len(item['e_keys'])}",
|
||||
s["date"],
|
||||
s["voucher_no"],
|
||||
s["draft_no"],
|
||||
s["accounts"],
|
||||
s["vendors"],
|
||||
s["debit"],
|
||||
s["credit"],
|
||||
s["links"],
|
||||
linked_wehago_text,
|
||||
s["reason"],
|
||||
],
|
||||
fill=NEG_FILL,
|
||||
)
|
||||
|
||||
ws_examples = wb.create_sheet("top_patterns")
|
||||
write_row(ws_examples, ["형태", "건수", "설명"], header=True)
|
||||
for (w_count, e_count), count in shape_counter.most_common(50):
|
||||
if (w_count, e_count) == (1, 1):
|
||||
continue
|
||||
write_row(
|
||||
ws_examples,
|
||||
[
|
||||
f"{w_count}:{e_count}",
|
||||
count,
|
||||
"WEHAGO 전표 그룹 수가 더 많음" if w_count > e_count else "Voucher 전표 그룹 수가 더 많음",
|
||||
],
|
||||
fill=POS_FILL if w_count > e_count else NEG_FILL,
|
||||
)
|
||||
|
||||
output = EXPORT_DIR / f"voucher_count_gap_{year}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
wb.save(output)
|
||||
return output
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="WEHAGO/Voucher 매치 그룹 수 차이 분석 엑셀 생성")
|
||||
parser.add_argument("--year", type=int, default=2025)
|
||||
args = parser.parse_args()
|
||||
output = build_gap_report(args.year)
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,281 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from openpyxl import load_workbook
|
||||
from sqlalchemy import bindparam, create_engine, text
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(BASE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from wehago_compare import (
|
||||
WEHAGO_SOURCE_ROOT,
|
||||
_fallback_metric_counts_from_db,
|
||||
compute_file_hash,
|
||||
detect_file_kind,
|
||||
import_ledger_rows,
|
||||
import_voucher_rows,
|
||||
infer_year_hint,
|
||||
init_wehago_compare_db,
|
||||
rebuild_comparison_results,
|
||||
upsert_source_file,
|
||||
)
|
||||
|
||||
|
||||
DB_PATH = BASE_DIR / "data.db"
|
||||
ENGINE = create_engine(f"sqlite:///{DB_PATH}", connect_args={"check_same_thread": False})
|
||||
|
||||
LEDGER_FILES = {
|
||||
2022: WEHAGO_SOURCE_ROOT / "data_download/2022/ledger_2022_20220101_20221231_260507.xlsx",
|
||||
2023: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260506_2023_compare/ledger_2023_20230101_20231231_260506.xlsx",
|
||||
2024: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260506_2024_compare/ledger_2024_20240101_20241231_260504.xlsx",
|
||||
2025: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260423/ledger_2025_20250101_20251231_260423.xlsx",
|
||||
}
|
||||
VOUCHER_FILES = [
|
||||
{
|
||||
"path": WEHAGO_SOURCE_ROOT / "voucher_sort_20220101_20221231_260507.xlsx",
|
||||
"expected_year": 2022,
|
||||
"year_hint": 2022,
|
||||
"allowed_years": {2022},
|
||||
},
|
||||
{
|
||||
"path": WEHAGO_SOURCE_ROOT / "voucher_sort_20230101_20251231_260424.xlsx",
|
||||
"expected_year": 2023,
|
||||
"year_hint": None,
|
||||
"allowed_years": {2023, 2024},
|
||||
},
|
||||
{
|
||||
"path": WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260423/voucher_2025_20250101_20251231_260422.xlsx",
|
||||
"expected_year": 2025,
|
||||
"year_hint": 2025,
|
||||
"allowed_years": {2025},
|
||||
},
|
||||
]
|
||||
_DEFAULT_YEAR_HINT = object()
|
||||
|
||||
|
||||
def _load_sheet(path: Path):
|
||||
workbook = load_workbook(path, read_only=True, data_only=True)
|
||||
return workbook, workbook.worksheets[0]
|
||||
|
||||
|
||||
def _delete_year_data(conn, *, ledger_years: set[int], voucher_years: set[int]) -> dict[str, int]:
|
||||
ledger_deleted = conn.execute(
|
||||
text("DELETE FROM wehago_ledger_rows WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)),
|
||||
{"years": sorted(ledger_years)},
|
||||
).rowcount
|
||||
voucher_deleted = conn.execute(
|
||||
text("DELETE FROM wehago_voucher_rows WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)),
|
||||
{"years": sorted(voucher_years)},
|
||||
).rowcount
|
||||
|
||||
unused_source_ids = {
|
||||
int(row[0])
|
||||
for row in conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT sf.id
|
||||
FROM wehago_source_files sf
|
||||
LEFT JOIN wehago_ledger_rows l ON l.source_file_id = sf.id
|
||||
LEFT JOIN wehago_voucher_rows v ON v.source_file_id = sf.id
|
||||
GROUP BY sf.id
|
||||
HAVING COUNT(l.id) = 0 AND COUNT(v.id) = 0
|
||||
"""
|
||||
)
|
||||
)
|
||||
}
|
||||
if unused_source_ids:
|
||||
conn.execute(
|
||||
text("DELETE FROM wehago_source_files WHERE id IN :ids").bindparams(bindparam("ids", expanding=True)),
|
||||
{"ids": sorted(unused_source_ids)},
|
||||
)
|
||||
conn.execute(
|
||||
text("DELETE FROM wehago_comparison_results WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)),
|
||||
{"years": sorted(ledger_years | voucher_years)},
|
||||
)
|
||||
conn.execute(text("DELETE FROM wehago_metric_count_cache"))
|
||||
conn.execute(text("DELETE FROM wehago_result_row_cache"))
|
||||
conn.execute(text("DELETE FROM wehago_pair_recommend_cache"))
|
||||
conn.execute(text("DELETE FROM wehago_background_jobs WHERE job_type = 'pair_recommend_precompute'"))
|
||||
return {"ledger_deleted": int(ledger_deleted or 0), "voucher_deleted": int(voucher_deleted or 0), "source_deleted": len(unused_source_ids)}
|
||||
|
||||
|
||||
def _import_one(
|
||||
conn,
|
||||
path: Path,
|
||||
expected_kind: str,
|
||||
expected_year: int,
|
||||
*,
|
||||
allowed_years: set[int] | None = None,
|
||||
year_hint_override: int | None | object = _DEFAULT_YEAR_HINT,
|
||||
) -> dict[str, object]:
|
||||
file_kind, header, sheet_name = detect_file_kind(path)
|
||||
if file_kind != expected_kind:
|
||||
raise ValueError(f"{path} 형식이 {expected_kind}가 아닙니다: {file_kind}")
|
||||
|
||||
workbook, sheet = _load_sheet(path)
|
||||
try:
|
||||
sample_rows = list(sheet.iter_rows(min_row=2, max_row=51, values_only=True))
|
||||
if year_hint_override is _DEFAULT_YEAR_HINT:
|
||||
year_hint = infer_year_hint(path, file_kind, sample_rows) or expected_year
|
||||
else:
|
||||
year_hint = year_hint_override
|
||||
source_id, _changed = upsert_source_file(conn, path, file_kind, year_hint, sheet_name, header)
|
||||
if file_kind == "ledger":
|
||||
conn.execute(text("DELETE FROM wehago_ledger_rows WHERE source_file_id = :source_id"), {"source_id": source_id})
|
||||
inserted = import_ledger_rows(conn, source_id, sheet_name, sheet.iter_rows(min_row=2, values_only=True), year_hint)
|
||||
else:
|
||||
conn.execute(text("DELETE FROM wehago_voucher_rows WHERE source_file_id = :source_id"), {"source_id": source_id})
|
||||
inserted = import_voucher_rows(conn, source_id, sheet_name, sheet.iter_rows(min_row=2, values_only=True), year_hint)
|
||||
allowed = sorted(allowed_years or {expected_year})
|
||||
if allowed:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
DELETE FROM wehago_ledger_rows
|
||||
WHERE source_file_id = :source_id
|
||||
AND fiscal_year NOT IN :allowed_years
|
||||
"""
|
||||
).bindparams(bindparam("allowed_years", expanding=True)),
|
||||
{"source_id": source_id, "allowed_years": allowed},
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
DELETE FROM wehago_voucher_rows
|
||||
WHERE source_file_id = :source_id
|
||||
AND fiscal_year NOT IN :allowed_years
|
||||
"""
|
||||
).bindparams(bindparam("allowed_years", expanding=True)),
|
||||
{"source_id": source_id, "allowed_years": allowed},
|
||||
)
|
||||
inserted = int(
|
||||
conn.execute(
|
||||
text(f"SELECT COUNT(*) FROM wehago_{file_kind}_rows WHERE source_file_id = :source_id"),
|
||||
{"source_id": source_id},
|
||||
).scalar_one()
|
||||
or 0
|
||||
)
|
||||
conn.execute(
|
||||
text("UPDATE wehago_source_files SET row_count = :row_count, imported_at = CURRENT_TIMESTAMP WHERE id = :source_id"),
|
||||
{"row_count": inserted, "source_id": source_id},
|
||||
)
|
||||
return {
|
||||
"path": str(path),
|
||||
"kind": file_kind,
|
||||
"year_hint": year_hint,
|
||||
"allowed_years": allowed,
|
||||
"source_id": source_id,
|
||||
"inserted": inserted,
|
||||
"file_hash": compute_file_hash(path),
|
||||
}
|
||||
finally:
|
||||
workbook.close()
|
||||
|
||||
|
||||
def _fetch_counts(conn) -> dict[str, object]:
|
||||
counts: dict[str, object] = {}
|
||||
for table in ("wehago_ledger_rows", "wehago_voucher_rows", "wehago_comparison_results"):
|
||||
rows = conn.execute(
|
||||
text(f"SELECT fiscal_year, COUNT(*) AS row_count FROM {table} GROUP BY fiscal_year ORDER BY fiscal_year")
|
||||
).mappings()
|
||||
counts[table] = {str(row["fiscal_year"]): int(row["row_count"]) for row in rows}
|
||||
return counts
|
||||
|
||||
|
||||
def _fetch_quality(conn) -> dict[str, object]:
|
||||
total_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM wehago_ledger_rows
|
||||
WHERE REPLACE(COALESCE(description, ''), ' ', '') IN ('[월계]', '[누계]', '월계', '누계')
|
||||
OR REPLACE(COALESCE(ledger_date, ''), ' ', '') IN ('[월계]', '[누계]', '월계', '누계')
|
||||
"""
|
||||
)
|
||||
).scalar_one()
|
||||
blank_account_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM wehago_ledger_rows
|
||||
WHERE fiscal_year IN (2022, 2023, 2024)
|
||||
AND (COALESCE(account_code, '') = '' OR COALESCE(account_name, '') = '')
|
||||
"""
|
||||
)
|
||||
).scalar_one()
|
||||
account_names = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT fiscal_year, account_code, COUNT(DISTINCT account_name) AS name_count,
|
||||
GROUP_CONCAT(DISTINCT account_name) AS names
|
||||
FROM wehago_ledger_rows
|
||||
WHERE fiscal_year IN (2022, 2023, 2024)
|
||||
AND COALESCE(account_code, '') <> ''
|
||||
GROUP BY fiscal_year, account_code
|
||||
HAVING COUNT(DISTINCT account_name) > 1
|
||||
ORDER BY fiscal_year, account_code
|
||||
LIMIT 20
|
||||
"""
|
||||
)
|
||||
).mappings()
|
||||
voucher_counts_by_source = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT sf.file_name, v.fiscal_year, COUNT(*) AS row_count
|
||||
FROM wehago_voucher_rows v
|
||||
JOIN wehago_source_files sf ON sf.id = v.source_file_id
|
||||
WHERE v.fiscal_year IN (2022, 2023, 2024, 2025)
|
||||
GROUP BY sf.file_name, v.fiscal_year
|
||||
ORDER BY v.fiscal_year, sf.file_name
|
||||
"""
|
||||
)
|
||||
).mappings()
|
||||
return {
|
||||
"monthly_cumulative_rows": int(total_rows or 0),
|
||||
"blank_account_rows_2022_2024": int(blank_account_rows or 0),
|
||||
"multi_name_accounts": [dict(row) for row in account_names],
|
||||
"voucher_counts_by_source": [dict(row) for row in voucher_counts_by_source],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
voucher_paths = [item["path"] for item in VOUCHER_FILES]
|
||||
missing = [path for path in [*LEDGER_FILES.values(), *voucher_paths] if not path.exists()]
|
||||
if missing:
|
||||
raise FileNotFoundError("\\n".join(str(path) for path in missing))
|
||||
|
||||
init_wehago_compare_db(ENGINE)
|
||||
summary: dict[str, object] = {"deleted": {}, "imported": []}
|
||||
with ENGINE.begin() as conn:
|
||||
summary["deleted"] = _delete_year_data(conn, ledger_years={2022, 2023, 2024, 2025}, voucher_years={2022, 2023, 2024, 2025})
|
||||
for year, path in LEDGER_FILES.items():
|
||||
summary["imported"].append(_import_one(conn, path, "ledger", year, allowed_years={year}))
|
||||
for item in VOUCHER_FILES:
|
||||
summary["imported"].append(
|
||||
_import_one(
|
||||
conn,
|
||||
item["path"],
|
||||
"voucher",
|
||||
int(item["expected_year"]),
|
||||
allowed_years=set(item["allowed_years"]),
|
||||
year_hint_override=item["year_hint"],
|
||||
)
|
||||
)
|
||||
rebuild_comparison_results(conn)
|
||||
summary["dashboard_counts"] = {
|
||||
str(year): _fallback_metric_counts_from_db(conn, year, year)
|
||||
for year in range(2022, 2026)
|
||||
}
|
||||
summary["counts"] = _fetch_counts(conn)
|
||||
summary["quality"] = _fetch_quality(conn)
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\find_realgrid_registry.py"
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_realgrid_views_9225.py"
|
||||
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const ids = Array.from(document.querySelectorAll('.rg-body')).map((el) => ({
|
||||
view: String(el.$_view || ''),
|
||||
text: (el.textContent || '').slice(0, 200),
|
||||
rect: (() => { const r = el.getBoundingClientRect(); return {left:r.left, top:r.top, width:r.width, height:r.height}; })()
|
||||
}));
|
||||
const summarize = (name, value) => {
|
||||
const out = {name, type: typeof value, text: String(value).slice(0, 300)};
|
||||
if (value && typeof value === 'object') {
|
||||
let keys = [];
|
||||
try { keys = Object.getOwnPropertyNames(value).concat(Object.keys(value)); } catch(e) {}
|
||||
out.keys = Array.from(new Set(keys)).slice(0, 250);
|
||||
out.samples = out.keys.slice(0, 80).map((key) => {
|
||||
try {
|
||||
const v = value[key];
|
||||
return {key, type: typeof v, text: String(v).slice(0, 160)};
|
||||
} catch (e) {
|
||||
return {key, error: String(e)};
|
||||
}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const out = {ids, globals: []};
|
||||
const names = ['Grids', 'RealGridJS', 'RealGrid', 'realgrid'];
|
||||
for (const name of names) out.globals.push(summarize(name, window[name]));
|
||||
for (const key of Object.keys(window)) {
|
||||
if (/grid|real|view|provider/i.test(key)) {
|
||||
try { out.globals.push(summarize('window.' + key, window[key])); } catch(e) {}
|
||||
}
|
||||
if (out.globals.length > 80) break;
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:100000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_rg_body_props_9225.py"
|
||||
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const out = [];
|
||||
for (const body of document.querySelectorAll('.rg-body')) {
|
||||
const item = {view: String(body.$_view || ''), props: []};
|
||||
const keys = [];
|
||||
let node = body;
|
||||
while (node) {
|
||||
try { keys.push(...Object.getOwnPropertyNames(node)); } catch(e) {}
|
||||
node = Object.getPrototypeOf(node);
|
||||
if (!node || node === Object.prototype) break;
|
||||
}
|
||||
for (const key of Array.from(new Set(keys)).slice(0, 500)) {
|
||||
if (!/^\$_|grid|view|data|provider|handler|container|model|_/.test(key)) continue;
|
||||
try {
|
||||
const v = body[key];
|
||||
const rec = {key, type: typeof v, text: String(v).slice(0, 200)};
|
||||
if (v && typeof v === 'object') {
|
||||
let sub = [];
|
||||
try { sub = Object.getOwnPropertyNames(v).concat(Object.keys(v)); } catch(e) {}
|
||||
rec.keys = Array.from(new Set(sub)).slice(0, 80);
|
||||
rec.funcs = rec.keys.filter(k => { try { return typeof v[k] === 'function'; } catch(e) { return false; } }).slice(0, 40);
|
||||
}
|
||||
item.props.push(rec);
|
||||
} catch(e) {}
|
||||
}
|
||||
out.push(item);
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:80000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_wehago_grid_9225.py"
|
||||
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const g = window.Grids && window.Grids.realgrid;
|
||||
const safe = (fn) => {
|
||||
try { return fn(); } catch (e) { return {error: String(e)}; }
|
||||
};
|
||||
const out = {};
|
||||
out.current = safe(() => g && g.getCurrent && g.getCurrent());
|
||||
out.fullItemCount = safe(() => g && g.fullItemCount && g.fullItemCount());
|
||||
out.columns = safe(() => g && g.getColumns && g.getColumns().map(c => ({
|
||||
name: c.name || c.fieldName || c.field,
|
||||
fieldName: c.fieldName,
|
||||
header: c.header && (c.header.text || c.header)
|
||||
})).slice(0, 80));
|
||||
out.rows = [];
|
||||
if (g) {
|
||||
const n = Math.min(30, Number(out.fullItemCount) || 30);
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
out.rows.push({
|
||||
item: i,
|
||||
dataRow: safe(() => g.getDataRow(i)),
|
||||
values: safe(() => g.getValues(i)),
|
||||
display: safe(() => g.getDisplayValuesOfRow ? g.getDisplayValuesOfRow(i) : g.getDisplayValues(i))
|
||||
});
|
||||
}
|
||||
}
|
||||
out.visibleText = Array.from(document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td'))
|
||||
.map(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return {text: (el.textContent || '').trim(), left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height)};
|
||||
})
|
||||
.filter(x => x.text && x.width > 0 && x.height > 0 && x.top > 120)
|
||||
.slice(0, 120);
|
||||
return out;
|
||||
"""
|
||||
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:60000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_wehago_inputs_9225.py"
|
||||
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
script = r"""
|
||||
const items = [];
|
||||
for (const el of document.querySelectorAll('input, textarea, select, button, [role=button], a')) {
|
||||
const r = el.getBoundingClientRect();
|
||||
const style = getComputedStyle(el);
|
||||
if (r.width <= 0 || r.height <= 0 || style.display === 'none' || style.visibility === 'hidden') continue;
|
||||
items.push({
|
||||
tag: el.tagName,
|
||||
type: el.getAttribute('type'),
|
||||
text: (el.textContent || '').trim(),
|
||||
value: el.value || '',
|
||||
placeholder: el.getAttribute('placeholder') || '',
|
||||
title: el.getAttribute('title') || '',
|
||||
aria: el.getAttribute('aria-label') || '',
|
||||
name: el.getAttribute('name') || '',
|
||||
id: el.id || '',
|
||||
cls: String(el.className || '').slice(0, 120),
|
||||
left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height)
|
||||
});
|
||||
}
|
||||
return items.sort((a,b)=>(a.top-b.top)||(a.left-b.left));
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:50000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\mark_wehago_2022_no_data.py"
|
||||
@@ -0,0 +1,33 @@
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
base = Path(r"\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022")
|
||||
target = max(base.glob("ledger_*.xlsx"), key=lambda item: item.stat().st_mtime)
|
||||
no_data = {"377", "400", "636", "643", "805", "819", "820", "824", "843", "932", "936", "950", "951"}
|
||||
|
||||
workbook = load_workbook(target)
|
||||
status_ws = workbook["계정별상태"]
|
||||
error_ws = workbook["오류_누락"]
|
||||
|
||||
for row in status_ws.iter_rows(min_row=2):
|
||||
code = str(row[0].value)
|
||||
if code in no_data:
|
||||
row[2].value = "데이터없음"
|
||||
row[5].value = "2022년 WEHAGO 계정별원장 화면에서 거래 데이터가 없어 다운로드 파일을 생성하지 않았습니다."
|
||||
|
||||
for row_index in range(2, error_ws.max_row + 1):
|
||||
for col_index in range(1, error_ws.max_column + 1):
|
||||
error_ws.cell(row_index, col_index).value = None
|
||||
|
||||
write_row = 2
|
||||
for row in status_ws.iter_rows(min_row=2, values_only=True):
|
||||
code, name, status, file_name, _row_count, message = row[:6]
|
||||
if status != "정상":
|
||||
for col_index, value in enumerate([code, name, status, file_name, message], start=1):
|
||||
error_ws.cell(write_row, col_index).value = value
|
||||
write_row += 1
|
||||
|
||||
workbook.save(target)
|
||||
print(target)
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\mark_wehago_no_data.py" %*
|
||||
@@ -0,0 +1,42 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
YEAR_NO_DATA = {
|
||||
"2022": {"377", "400", "636", "643", "805", "819", "820", "824", "843", "932", "936", "950", "951"},
|
||||
"2023": {"263", "417", "636", "805", "819", "843", "932", "933", "936", "950", "951"},
|
||||
"2024": {"116", "263", "417", "805", "950", "951"},
|
||||
}
|
||||
|
||||
|
||||
year = sys.argv[1]
|
||||
base = Path(r"\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download") / year
|
||||
target = max(base.glob("ledger_*.xlsx"), key=lambda item: item.stat().st_mtime)
|
||||
no_data = YEAR_NO_DATA[year]
|
||||
|
||||
workbook = load_workbook(target)
|
||||
status_ws = workbook["계정별상태"]
|
||||
error_ws = workbook["오류_누락"]
|
||||
|
||||
for row in status_ws.iter_rows(min_row=2):
|
||||
code = str(row[0].value)
|
||||
if code in no_data:
|
||||
row[2].value = "데이터없음"
|
||||
row[5].value = f"{year}년 WEHAGO 계정별원장 화면에서 거래 데이터가 없어 다운로드 파일을 생성하지 않았습니다."
|
||||
|
||||
for row_index in range(2, error_ws.max_row + 1):
|
||||
for col_index in range(1, error_ws.max_column + 1):
|
||||
error_ws.cell(row_index, col_index).value = None
|
||||
|
||||
write_row = 2
|
||||
for row in status_ws.iter_rows(min_row=2, values_only=True):
|
||||
code, name, status, file_name, _row_count, message = row[:6]
|
||||
if status != "정상":
|
||||
for col_index, value in enumerate([code, name, status, file_name, message], start=1):
|
||||
error_ws.cell(write_row, col_index).value = value
|
||||
write_row += 1
|
||||
|
||||
workbook.save(target)
|
||||
print(target)
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --merge-only
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2023"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --merge-only
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2024"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --merge-only
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\probe_realgrid_active_9225.py"
|
||||
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const body = Array.from(document.querySelectorAll('.rg-body')).find(el => String(el.$_view) === '75');
|
||||
if (body) {
|
||||
const r = body.getBoundingClientRect();
|
||||
const x = r.left + 80, y = r.top + 85;
|
||||
for (const type of ['mouseover','mousemove','mousedown','mouseup','click']) {
|
||||
body.dispatchEvent(new MouseEvent(type,{bubbles:true,cancelable:true,view:window,clientX:x,clientY:y,button:0}));
|
||||
}
|
||||
}
|
||||
const summarize = (obj) => {
|
||||
if (!obj) return null;
|
||||
let keys = [];
|
||||
try { keys = Object.getOwnPropertyNames(obj).concat(Object.keys(obj)); } catch(e) {}
|
||||
const funcs = Array.from(new Set(keys)).filter(k => {
|
||||
try { return typeof obj[k] === 'function'; } catch(e) { return false; }
|
||||
}).slice(0, 160);
|
||||
const props = {};
|
||||
for (const k of keys.slice(0, 100)) {
|
||||
try {
|
||||
const v = obj[k];
|
||||
if (['string','number','boolean'].includes(typeof v)) props[k]=v;
|
||||
} catch(e) {}
|
||||
}
|
||||
return {text:String(obj).slice(0,200), keys:Array.from(new Set(keys)).slice(0,200), funcs, props};
|
||||
};
|
||||
const active = Grids.getActiveGrid && Grids.getActiveGrid();
|
||||
const handler = active && active.getHandler && active.getHandler();
|
||||
const out = {
|
||||
active: summarize(active),
|
||||
handler: summarize(handler),
|
||||
current: null,
|
||||
itemCount: null,
|
||||
columns: null,
|
||||
rows: []
|
||||
};
|
||||
const targets = [active, handler].filter(Boolean);
|
||||
for (const g of targets) {
|
||||
if (!out.current) {
|
||||
try { if (g.getCurrent) out.current = g.getCurrent(); } catch(e) { out.current = {error:String(e)}; }
|
||||
}
|
||||
if (!out.itemCount) {
|
||||
try { if (g.getItemCount) out.itemCount = g.getItemCount(); } catch(e) {}
|
||||
try { if (!out.itemCount && g.fullItemCount) out.itemCount = g.fullItemCount(); } catch(e) {}
|
||||
}
|
||||
if (!out.columns) {
|
||||
try { if (g.getColumns) out.columns = g.getColumns().map(c => ({name:c.name, fieldName:c.fieldName, text:c.header && c.header.text})); } catch(e) {}
|
||||
}
|
||||
try {
|
||||
if (g.getValues) {
|
||||
for (let i=0; i<Math.min(10, out.itemCount || 10); i++) out.rows.push({i, values:g.getValues(i)});
|
||||
break;
|
||||
}
|
||||
} catch(e) { out.rowsError = String(e); }
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:80000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\probe_realgrid_view_methods_9225.py"
|
||||
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const rg = Grids && Grids.realgrid;
|
||||
const ids = [75, '75', 327, '327'];
|
||||
const methods = ['getCurrent','fullItemCount','getColumns','getValues','getDataRow','getDisplayValues','getDisplayValuesOfRow','setCurrent','setSelectionItem','getSelectionItem','getSelectionItems'];
|
||||
const out = {};
|
||||
for (const m of methods) {
|
||||
out[m] = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
if (!rg || typeof rg[m] !== 'function') { out[m].push({id, missing:true}); continue; }
|
||||
let value;
|
||||
if (m === 'getValues' || m === 'getDataRow' || m === 'getDisplayValues' || m === 'getDisplayValuesOfRow') value = rg[m](id, 0);
|
||||
else if (m === 'setCurrent') value = rg[m](id, {itemIndex: 5, column: 'Code'});
|
||||
else if (m === 'setSelectionItem') value = rg[m](id, 5);
|
||||
else value = rg[m](id);
|
||||
out[m].push({id, ok:true, value});
|
||||
} catch(e) {
|
||||
out[m].push({id, error:String(e).slice(0,300)});
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:80000])
|
||||
driver.quit()
|
||||
+11
-2
@@ -5,6 +5,7 @@ cd "$(dirname "$0")/.."
|
||||
|
||||
PORT="${1:-${INTRANET_PORT:-8010}}"
|
||||
HEALTH_URL="http://127.0.0.1:${PORT}/health"
|
||||
COMPARE_HEALTH_URL="http://127.0.0.1:${PORT}/health/wehago-compare"
|
||||
|
||||
if [ ! -x ".venv/bin/python" ]; then
|
||||
echo "가상환경이 없습니다. 먼저 아래를 실행하세요."
|
||||
@@ -15,8 +16,16 @@ if [ ! -x ".venv/bin/python" ]; then
|
||||
fi
|
||||
|
||||
if curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then
|
||||
echo "이미 실행 중입니다: $HEALTH_URL"
|
||||
exit 0
|
||||
if curl -fsS --max-time 4 "$COMPARE_HEALTH_URL" >/dev/null 2>&1; then
|
||||
echo "이미 실행 중입니다: $HEALTH_URL"
|
||||
exit 0
|
||||
fi
|
||||
echo "서버 포트는 열려 있지만 전표비교 readiness 가 실패했습니다. 재시작합니다."
|
||||
EXISTING_PID="$(ss -ltnp "( sport = :${PORT} )" | awk 'NR>1 {print $NF}' | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | head -n 1)"
|
||||
if [ -n "${EXISTING_PID}" ]; then
|
||||
kill "${EXISTING_PID}" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if ss -ltn "( sport = :${PORT} )" | tail -n +2 | grep -q .; then
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL="
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2022.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2022.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=27&yminsa=2026&searchData=2022010120221231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2022.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2022.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=27&yminsa=2026&searchData=2022010120221231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2023.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2023.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2023"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=28&yminsa=2026&searchData=2023010120231231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2024.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2024.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2024"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=29&yminsa=2026&searchData=2024010120241231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\test_left_click_modes_9225.py" %*
|
||||
@@ -0,0 +1,55 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "110"
|
||||
mode = sys.argv[2] if len(sys.argv) > 2 else "name-double"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
result = driver.execute_script(
|
||||
r"""
|
||||
const code = arguments[0], mode = arguments[1];
|
||||
const cells = Array.from(document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td'));
|
||||
const codeCell = cells.find(cell => {
|
||||
const r = cell.getBoundingClientRect();
|
||||
return (cell.textContent || '').trim() === code && r.left < 220 && r.top > 145 && r.width > 0 && r.height > 0;
|
||||
});
|
||||
if (!codeCell) return {error:'code not visible'};
|
||||
const cr = codeCell.getBoundingClientRect();
|
||||
const nameCell = cells.find(cell => {
|
||||
const r = cell.getBoundingClientRect();
|
||||
return Math.abs(r.top - cr.top) < 3 && r.left > 180 && r.left < 360 && r.width > 0 && r.height > 0;
|
||||
});
|
||||
const targetEl = mode.includes('name') && nameCell ? nameCell : codeCell;
|
||||
const r = targetEl.getBoundingClientRect();
|
||||
const x = r.left + (mode.includes('left') ? 8 : r.width / 2);
|
||||
const y = r.top + r.height / 2;
|
||||
const send = (type, detail=1) => {
|
||||
targetEl.dispatchEvent(new MouseEvent(type, {bubbles:true,cancelable:true,view:window,clientX:x,clientY:y,button:0,detail}));
|
||||
};
|
||||
const seq = ['mouseover','mousemove','mousedown','mouseup','click'];
|
||||
for (const type of seq) send(type, 1);
|
||||
if (mode.includes('double')) {
|
||||
for (const type of ['mousedown','mouseup','click','dblclick']) send(type, 2);
|
||||
}
|
||||
return {clicked: code, mode, x, y, codeTop: cr.top, nameText: nameCell && nameCell.textContent};
|
||||
""",
|
||||
target,
|
||||
mode,
|
||||
)
|
||||
time.sleep(1.0)
|
||||
right = driver.execute_script(
|
||||
r"""
|
||||
const body = Array.from(document.querySelectorAll('.rg-body')).find(el => String(el.$_view) === '327');
|
||||
return body ? (body.textContent || '').slice(0, 500) : '';
|
||||
"""
|
||||
)
|
||||
print(json.dumps({"result": result, "right": right}, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\test_left_list_search_9225.py" %*
|
||||
@@ -0,0 +1,75 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
|
||||
code = sys.argv[1] if len(sys.argv) > 1 else "263"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
def visible_controls():
|
||||
return driver.execute_script(
|
||||
r"""
|
||||
const items=[];
|
||||
for (const el of document.querySelectorAll('input,button,[role=button],a')) {
|
||||
const r=el.getBoundingClientRect();
|
||||
const st=getComputedStyle(el);
|
||||
if (r.width<=0 || r.height<=0 || st.display==='none' || st.visibility==='hidden') continue;
|
||||
items.push({tag:el.tagName,id:el.id||'',cls:String(el.className||'').slice(0,80),text:(el.textContent||'').trim(),value:el.value||'',placeholder:el.getAttribute('placeholder')||'',title:el.getAttribute('title')||'',left:Math.round(r.left),top:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height)});
|
||||
}
|
||||
return items.sort((a,b)=>(a.top-b.top)||(a.left-b.left));
|
||||
"""
|
||||
)
|
||||
|
||||
before = visible_controls()
|
||||
button = None
|
||||
for el in driver.find_elements(By.CSS_SELECTOR, "button, [role=button], a"):
|
||||
if not el.is_displayed():
|
||||
continue
|
||||
r = driver.execute_script("const r=arguments[0].getBoundingClientRect(); return {left:r.left,top:r.top,width:r.width,height:r.height};", el)
|
||||
text = (el.text or "").strip()
|
||||
title = el.get_attribute("title") or ""
|
||||
cls = el.get_attribute("class") or ""
|
||||
if r["left"] < 180 and 150 <= r["top"] <= 210 and (text or title or "LSbutton" in cls):
|
||||
button = el
|
||||
break
|
||||
if button:
|
||||
button.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
after_click = visible_controls()
|
||||
inputs = driver.find_elements(By.CSS_SELECTOR, "input")
|
||||
typed = []
|
||||
for inp in inputs:
|
||||
if not inp.is_displayed() or not inp.is_enabled():
|
||||
continue
|
||||
r = driver.execute_script("const r=arguments[0].getBoundingClientRect(); return {left:r.left,top:r.top,width:r.width,height:r.height};", inp)
|
||||
if 120 <= r["top"] <= 260 and r["left"] < 500:
|
||||
try:
|
||||
inp.click()
|
||||
inp.send_keys(Keys.CONTROL, "a")
|
||||
inp.send_keys(code)
|
||||
inp.send_keys(Keys.ENTER)
|
||||
typed.append(r)
|
||||
time.sleep(0.8)
|
||||
break
|
||||
except Exception as exc:
|
||||
typed.append({"error": str(exc), **r})
|
||||
|
||||
visible = driver.execute_script(
|
||||
r"""
|
||||
return Array.from(document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td')).map(cell=>{
|
||||
const r=cell.getBoundingClientRect();
|
||||
return {text:(cell.textContent||'').trim(),left:Math.round(r.left),top:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height)};
|
||||
}).filter(x=>x.text && x.left<500 && x.top>140 && x.width>0 && x.height>0).slice(0,80);
|
||||
"""
|
||||
)
|
||||
print(json.dumps({"clickedButton": bool(button), "typed": typed, "before": before[:40], "afterClick": after_click[:80], "visible": visible}, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\test_selenium_left_click_9225.py" %*
|
||||
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "110"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
clicked = None
|
||||
for el in driver.find_elements(By.XPATH, f"//*[normalize-space(.)='{target}']"):
|
||||
if not el.is_displayed():
|
||||
continue
|
||||
r = driver.execute_script("const r=arguments[0].getBoundingClientRect(); return {left:r.left, top:r.top, width:r.width, height:r.height};", el)
|
||||
if r["left"] < 220 and r["top"] > 145 and r["width"] > 0 and r["height"] > 0:
|
||||
clicked = r
|
||||
try:
|
||||
el.click()
|
||||
except Exception:
|
||||
ActionChains(driver).move_to_element(el).click(el).perform()
|
||||
break
|
||||
|
||||
time.sleep(1.0)
|
||||
right = driver.execute_script("const b=Array.from(document.querySelectorAll('.rg-body')).find(el=>String(el.$_view)==='327'); return b ? (b.textContent||'').slice(0,500) : '';")
|
||||
print(json.dumps({"clicked": clicked, "right": right}, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
Executable
+3024
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user