Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- core/config.py +52 -0
- generators/korea_rental_gen.py +171 -0
- mappers/mapping_utils.py +99 -0
- processors/rental_processor.py +270 -0
- utils/file_handler.py +115 -0
- utils/reporter.py +91 -0
core/config.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
설정 파일 및 경로 관리 모듈
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
# 기본 파일 경로 설정
|
| 8 |
+
INPUT_DIR = os.path.join(os.getcwd(), 'input')
|
| 9 |
+
OUTPUT_DIR = os.path.join(os.getcwd(), 'output')
|
| 10 |
+
MAPPING_DIR = os.path.join(os.getcwd(), 'mapping')
|
| 11 |
+
TEMPLATE_DIR = os.path.join(os.getcwd(), 'templates')
|
| 12 |
+
|
| 13 |
+
# 디렉토리가 없으면 생성
|
| 14 |
+
for directory in [INPUT_DIR, OUTPUT_DIR, MAPPING_DIR, TEMPLATE_DIR]:
|
| 15 |
+
if not os.path.exists(directory):
|
| 16 |
+
os.makedirs(directory)
|
| 17 |
+
|
| 18 |
+
# 현재 날짜 정보
|
| 19 |
+
CURRENT_DATE = datetime.now().strftime("%Y%m%d")
|
| 20 |
+
CURRENT_MONTH = datetime.now().strftime("%m") # 현재 월 (01-12)
|
| 21 |
+
|
| 22 |
+
# 렌탈사 설정 (추후 다른 렌탈사가 추가될 수 있음)
|
| 23 |
+
RENTAL_COMPANIES = {
|
| 24 |
+
'한국렌탈': {
|
| 25 |
+
'input_file': os.path.join(INPUT_DIR, '한국렌탈_렌탈료.csv'),
|
| 26 |
+
'mapping_file': os.path.join(MAPPING_DIR, 'team_name_mapping.json'),
|
| 27 |
+
'erp_form_file': os.path.join(TEMPLATE_DIR, 'erp_form.csv'),
|
| 28 |
+
'output_csv': os.path.join(OUTPUT_DIR, f'자동전표_한국렌탈_{CURRENT_DATE}.csv'),
|
| 29 |
+
'output_excel': os.path.join(OUTPUT_DIR, f'자동전표_한국렌탈_{CURRENT_DATE}.xls'),
|
| 30 |
+
'partner_code': '101388', # 거래처 코드 (한국렌탈: 101388)
|
| 31 |
+
'cost_center': '5020', # 코스트센터(운영2)
|
| 32 |
+
'expense_acct': '53000', # 기본 비용 계정
|
| 33 |
+
'payable_acct': '25300', # 미지급금 계정
|
| 34 |
+
'cd_company': '1200', # 회사 코드
|
| 35 |
+
'cd_pc': '1200', # 회계단위
|
| 36 |
+
'cd_wdept': '1010', # 작성부서
|
| 37 |
+
'amount_field': f'{CURRENT_MONTH}월렌탈료', # 금액 필드명 (현재 월 기준)
|
| 38 |
+
'team_fields': [f'{CURRENT_MONTH}월 변경PJT', f'{int(CURRENT_MONTH)-1}월 PJT'], # 팀 정보 필드명 (우선순위 순)
|
| 39 |
+
'note_prefix': '한국렌탈㈜_PC 렌탈료', # 적요 접두어
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# 기본 설정값
|
| 44 |
+
DEFAULT_ENCODING = 'utf-8'
|
| 45 |
+
CSV_OUTPUT_ENCODING = 'utf-8-sig' # Excel에서 한글이 깨지지 않도록 BOM 포함
|
| 46 |
+
|
| 47 |
+
# ERP 관련 설정
|
| 48 |
+
ERP_DATA_ROW_START = 4 # 데이터 시작 행 (5행)
|
| 49 |
+
ERP_DOCUMENT_TYPE = '11' # 전표유형 (11: 일반)
|
| 50 |
+
ERP_APPROVAL_STATUS = '1' # 승인여부 (1: 미결/임시)
|
| 51 |
+
ERP_PROCESS_STATUS = 'N' # 전표처리결과 (N: 미처리/임시)
|
| 52 |
+
ERP_DOCUMENT_GUBUN = '3' # 전표구분 (3: 대체전표)
|
generators/korea_rental_gen.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ERP 데이터 생성 모듈
|
| 3 |
+
"""
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from typing import Dict, List, Any, Tuple
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import config as cfg
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def generate_erp_data(df_filtered: pd.DataFrame, company_config: Dict[str, Any]) -> pd.DataFrame:
|
| 11 |
+
"""
|
| 12 |
+
ERP 업로드용 데이터프레임 생성
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
df_filtered: 필터링된 데이터프레임
|
| 16 |
+
company_config: 렌탈사 설정 정보
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
ERP 업로드용 데이터프레임
|
| 20 |
+
"""
|
| 21 |
+
print("ERP 업로드용 데이터프레임 생성 중...")
|
| 22 |
+
current_date = datetime.now().strftime("%Y%m%d")
|
| 23 |
+
document_number = f"FI{current_date[-8:]}{company_config.get('id_write', '00000')[-3:]}"
|
| 24 |
+
|
| 25 |
+
# 1. 차변 데이터 생성 (각 팀별 계정별 비용)
|
| 26 |
+
debit_data = {
|
| 27 |
+
"ROW_ID": [document_number] * len(df_filtered),
|
| 28 |
+
"ROW_NO": [str(i) for i in range(1, len(df_filtered)+1)],
|
| 29 |
+
"NO_TAX": ["*"] * len(df_filtered),
|
| 30 |
+
"CD_PC": [company_config['cd_pc']] * len(df_filtered),
|
| 31 |
+
"CD_WDEPT": [company_config['cd_wdept']] * len(df_filtered),
|
| 32 |
+
"NO_DOCU": [document_number] * len(df_filtered),
|
| 33 |
+
"NO_DOLINE": [str(i) for i in range(1, len(df_filtered)+1)],
|
| 34 |
+
"CD_COMPANY": [company_config['cd_company']] * len(df_filtered),
|
| 35 |
+
"ID_WRITE": [company_config['id_write']] * len(df_filtered),
|
| 36 |
+
"CD_DOCU": [cfg.ERP_DOCUMENT_TYPE] * len(df_filtered),
|
| 37 |
+
"DT_ACCT": [current_date] * len(df_filtered),
|
| 38 |
+
"ST_DOCU": [cfg.ERP_APPROVAL_STATUS] * len(df_filtered),
|
| 39 |
+
"TP_DRCR": ["1"] * len(df_filtered), # 차대구분 (1: 차변)
|
| 40 |
+
"CD_ACCT": df_filtered["CD_ACCT"].tolist(), # 각 팀별 계정 코드
|
| 41 |
+
"AMT": df_filtered["금액"].apply(lambda x: str(int(x)) if pd.notnull(x) else "0").tolist(),
|
| 42 |
+
"CD_PARTNER": [company_config['partner_code']] * len(df_filtered),
|
| 43 |
+
"NM_NOTE": df_filtered["적요"].tolist(),
|
| 44 |
+
"TP_DOCU": [cfg.ERP_PROCESS_STATUS] * len(df_filtered),
|
| 45 |
+
"NO_ACCT": ["0"] * len(df_filtered),
|
| 46 |
+
"TP_GUBUN": [cfg.ERP_DOCUMENT_GUBUN] * len(df_filtered),
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# 2. 대변 데이터 생성 (미지급금으로 합계 금액)
|
| 50 |
+
total_amount = df_filtered["금액"].sum()
|
| 51 |
+
total_amount_str = str(total_amount)
|
| 52 |
+
|
| 53 |
+
# 대변 데이터
|
| 54 |
+
credit_data = {
|
| 55 |
+
"ROW_ID": [document_number],
|
| 56 |
+
"ROW_NO": [str(len(df_filtered) + 1)], # 마지막 번호 다음
|
| 57 |
+
"NO_TAX": ["*"],
|
| 58 |
+
"CD_PC": [company_config['cd_pc']],
|
| 59 |
+
"CD_WDEPT": [company_config['cd_wdept']],
|
| 60 |
+
"NO_DOCU": [document_number],
|
| 61 |
+
"NO_DOLINE": [str(len(df_filtered) + 1)], # 마지막 라인 다음
|
| 62 |
+
"CD_COMPANY": [company_config['cd_company']],
|
| 63 |
+
"ID_WRITE": [company_config['id_write']],
|
| 64 |
+
"CD_DOCU": [cfg.ERP_DOCUMENT_TYPE],
|
| 65 |
+
"DT_ACCT": [current_date],
|
| 66 |
+
"ST_DOCU": [cfg.ERP_APPROVAL_STATUS],
|
| 67 |
+
"TP_DRCR": ["2"], # 차대구분 (2: 대변)
|
| 68 |
+
"CD_ACCT": [company_config['payable_acct']], # 미지급금 계정코드
|
| 69 |
+
"AMT": [total_amount_str], # 전체 금액의 합계
|
| 70 |
+
"CD_PARTNER": [company_config['partner_code']],
|
| 71 |
+
"NM_NOTE": [f"{company_config['note_prefix']} 미지급금"], # 적요
|
| 72 |
+
"TP_DOCU": [cfg.ERP_PROCESS_STATUS],
|
| 73 |
+
"NO_ACCT": ["0"],
|
| 74 |
+
"TP_GUBUN": [cfg.ERP_DOCUMENT_GUBUN],
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# 3. 차변과 대변 데이터프레임 생성
|
| 78 |
+
debit_df = pd.DataFrame(debit_data)
|
| 79 |
+
credit_df = pd.DataFrame(credit_data)
|
| 80 |
+
|
| 81 |
+
# 4. 두 데이터프레임 합치기
|
| 82 |
+
erp_df = pd.concat([debit_df, credit_df], ignore_index=True)
|
| 83 |
+
|
| 84 |
+
# 금액 필드 확인
|
| 85 |
+
print("\nAMT 필드 확인:")
|
| 86 |
+
print("차변 금액 합계:", df_filtered["금액"].sum())
|
| 87 |
+
print("대변 금액:", total_amount)
|
| 88 |
+
print("차변 건수:", len(debit_df))
|
| 89 |
+
print("대변 건수:", len(credit_df))
|
| 90 |
+
|
| 91 |
+
return erp_df
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def prepare_erp_columns(erp_df: pd.DataFrame) -> pd.DataFrame:
|
| 95 |
+
"""
|
| 96 |
+
ERP 표준 컬럼 구조로 데이터프레임 준비
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
erp_df: ERP 데이터프레임
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
표준 컬럼 구조를 가진 ERP 데이터프레임
|
| 103 |
+
"""
|
| 104 |
+
# 필드 순서 지정 - ERP 양식에 맞게 정확한 순서로 컬럼 정렬
|
| 105 |
+
erp_columns = [
|
| 106 |
+
"ROW_ID", "ROW_NO", "NO_TAX", "CD_PC", "CD_WDEPT", "NO_DOCU", "NO_DOLINE",
|
| 107 |
+
"CD_COMPANY", "ID_WRITE", "CD_DOCU", "DT_ACCT", "ST_DOCU", "TP_DRCR",
|
| 108 |
+
"CD_ACCT", "AMT", "CD_PARTNER", "DT_START", "DT_END", "AM_TAXSTD",
|
| 109 |
+
"AM_ADDTAX", "TP_TAX", "NO_COMPANY", "NM_NOTE", "CD_BIZAREA", "CD_DEPT",
|
| 110 |
+
"CD_CC", "CD_PJT", "CD_FUND", "CD_BUDGET", "NO_CASH", "ST_MUTUAL",
|
| 111 |
+
"CD_CARD", "NO_DEPOSIT", "CD_BANK", "UCD_MNG1", "UCD_MNG2", "UCD_MNG3",
|
| 112 |
+
"UCD_MNG4", "UCD_MNG5", "CD_EMPLOY", "CD_MNG", "NO_BDOCU", "NO_BDOLINE",
|
| 113 |
+
"TP_DOCU", "NO_ACCT", "TP_TRADE", "NO_CHECK3", "NO_CHECK4", "CD_EXCH",
|
| 114 |
+
"RT_EXCH", "CD_TRADE", "AM_EX", "TP_EXPORT", "NO_TO", "DT_SHIPPING",
|
| 115 |
+
"TP_GUBUN", "NO_INVOICE", "NO_ITEM", "MD_TAX1", "NM_ITEM1", "NM_SIZE1",
|
| 116 |
+
"QT_TAX1", "AM_PRC1", "AM_SUPPLY1", "AM_TAX1", "NM_NOTE1", "CD_BIZPLAN",
|
| 117 |
+
"CD_BGACCT", "CD_MNGD1", "NM_MNGD1", "CD_MNGD2", "NM_MNGD2", "CD_MNGD3",
|
| 118 |
+
"NM_MNGD3", "CD_MNGD4", "NM_MNGD4", "CD_MNGD5", "NM_MNGD5", "CD_MNGD6",
|
| 119 |
+
"NM_MNGD6", "CD_MNGD7", "NM_MNGD7", "CD_MNGD8", "NM_MNGD8", "YN_ISS",
|
| 120 |
+
"FINAL_STATUS", "NO_BILL", "NM_BIGO", "TP_BILL", "TP_RECORD", "TP_ETCACCT",
|
| 121 |
+
"ST_GWARE", "SELL_DAM_NM", "SELL_DAM_EMAIL", "SELL_DAM_MOBIL", "SELL_DAM_TEL",
|
| 122 |
+
"NM_PUMM", "JEONJASEND15_YN", "DT_WRITE", "ST_TAX", "MD_TAX2", "NM_ITEM2",
|
| 123 |
+
"NM_SIZE2", "QT_TAX2", "AM_PRC2", "AM_SUPPLY2", "AM_TAX2", "NM_NOTE2",
|
| 124 |
+
"MD_TAX3", "NM_ITEM3", "NM_SIZE3", "QT_TAX3", "AM_PRC3", "AM_SUPPLY3",
|
| 125 |
+
"AM_TAX3", "NM_NOTE3", "MD_TAX4", "NM_ITEM4", "NM_SIZE4", "QT_TAX4",
|
| 126 |
+
"AM_PRC4", "AM_SUPPLY4", "AM_TAX4", "NM_NOTE4", "NM_PTR", "EX_HP",
|
| 127 |
+
"EX_EMIL", "NO_BIZTAX", "NO_ASSET", "TP_EVIDENCE", "NO_CAR", "NO_CARBODY",
|
| 128 |
+
"CD_BIZCAR", "NM_PARTNER", "YN_IMPORT", "YN_FIXASSET"
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
# 나머지 열 추가 (빈 문자열로)
|
| 132 |
+
for col in erp_columns:
|
| 133 |
+
if col not in erp_df.columns:
|
| 134 |
+
erp_df[col] = [""] * len(erp_df)
|
| 135 |
+
|
| 136 |
+
return erp_df[erp_columns]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def set_management_items(erp_df: pd.DataFrame, df_filtered: pd.DataFrame, company_config: Dict[str, Any]) -> pd.DataFrame:
|
| 140 |
+
"""
|
| 141 |
+
관리항목 설정
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
erp_df: ERP 데이터프레임
|
| 145 |
+
df_filtered: 필터링된 데이터프레임
|
| 146 |
+
company_config: 렌탈사 설정 정보
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
관리항목이 설정된 ERP 데이터프레임
|
| 150 |
+
"""
|
| 151 |
+
# 차변 행 설정
|
| 152 |
+
debit_rows = erp_df["TP_DRCR"] == "1"
|
| 153 |
+
erp_df.loc[debit_rows, "CD_CC"] = company_config['cost_center'] # 코스트센터
|
| 154 |
+
|
| 155 |
+
# 부서코드 설정 (관리항목2) - 필수 항목으로 보임
|
| 156 |
+
if 'cd_wdept' in company_config:
|
| 157 |
+
erp_df.loc[debit_rows, "CD_DEPT"] = company_config['cd_wdept'] # 부서코드
|
| 158 |
+
|
| 159 |
+
# CD_PJT를 정수형으로 확실하게 설정
|
| 160 |
+
pjt_codes = df_filtered["CD_PJT"].astype(int).tolist()
|
| 161 |
+
erp_df.loc[debit_rows, "CD_PJT"] = pjt_codes # 프로젝트 코드
|
| 162 |
+
|
| 163 |
+
# 대변 행 설정
|
| 164 |
+
credit_rows = erp_df["TP_DRCR"] == "2"
|
| 165 |
+
erp_df.loc[credit_rows, "CD_CC"] = company_config['cost_center'] # 코스트센터
|
| 166 |
+
|
| 167 |
+
# 대변에도 부서코드 설정 필요
|
| 168 |
+
if 'cd_wdept' in company_config:
|
| 169 |
+
erp_df.loc[credit_rows, "CD_DEPT"] = company_config['cd_wdept'] # 부서코드
|
| 170 |
+
|
| 171 |
+
return erp_df
|
mappers/mapping_utils.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
팀명 매핑 관련 유틸리티 모듈
|
| 3 |
+
"""
|
| 4 |
+
import json
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from typing import Dict, List, Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_mapping_file(mapping_file: str) -> Dict[str, Dict[str, str]]:
|
| 10 |
+
"""
|
| 11 |
+
매핑 파일을 로드하여 딕셔너리 형태로 반환
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
mapping_file: 매핑 파일 경로
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
매핑 딕셔너리: {팀명: {present: 현재팀명, CD_ACCT: 계정코드, CD_PJT: 프로젝트코드}}
|
| 18 |
+
"""
|
| 19 |
+
try:
|
| 20 |
+
with open(mapping_file, 'r', encoding='utf-8') as f:
|
| 21 |
+
mapping_list = json.load(f)
|
| 22 |
+
|
| 23 |
+
# 매핑 딕셔너리 생성
|
| 24 |
+
mapping_dict = {}
|
| 25 |
+
for item in mapping_list:
|
| 26 |
+
mapping_dict[item['past']] = {
|
| 27 |
+
'present': item['present'],
|
| 28 |
+
'CD_ACCT': item['CD_ACCT'],
|
| 29 |
+
'CD_PJT': item['CD_PJT']
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
print(f"매핑 정보 로드 완료: {len(mapping_dict)}개 항목")
|
| 33 |
+
return mapping_dict
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"매핑 파일 로드 중 오류 발생: {e}")
|
| 37 |
+
return {}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def apply_mapping(team_name: str, mapping_dict: Dict[str, Dict[str, str]]) -> Dict[str, str]:
|
| 41 |
+
"""
|
| 42 |
+
팀명에 매핑 정보 적용
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
team_name: 원본 팀명
|
| 46 |
+
mapping_dict: 매핑 딕셔너리
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
매핑된 정보: {present: 현재팀명, CD_ACCT: 계정코드, CD_PJT: 프로젝트코드}
|
| 50 |
+
"""
|
| 51 |
+
if pd.isna(team_name) or team_name == "":
|
| 52 |
+
return {"present": "", "CD_ACCT": "", "CD_PJT": ""}
|
| 53 |
+
|
| 54 |
+
if team_name in mapping_dict:
|
| 55 |
+
return mapping_dict[team_name]
|
| 56 |
+
|
| 57 |
+
# 없는 경우 빈 값 반환
|
| 58 |
+
return {"present": team_name, "CD_ACCT": "", "CD_PJT": ""}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def get_unmapped_teams(df: pd.DataFrame) -> List[str]:
|
| 62 |
+
"""
|
| 63 |
+
매핑되지 않은 팀명 목록 추출
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
df: 데이터프레임
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
매핑되지 않은 팀명 목록
|
| 70 |
+
"""
|
| 71 |
+
unmapped_df = df[(df["CD_ACCT"] == "") | (df["CD_PJT"] == "")]
|
| 72 |
+
return unmapped_df["원본팀명"].unique().tolist()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_mapping_summary(df_filtered: pd.DataFrame, mapping_dict: Dict[str, Dict[str, str]]) -> Dict[str, Any]:
|
| 76 |
+
"""
|
| 77 |
+
매핑 결과 요약 정보 생성
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
df_filtered: 필터링된 데이터프레임
|
| 81 |
+
mapping_dict: 매핑 딕셔너리
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
매핑 요약 정보: {mapped_teams: 매핑된 팀명 목록, unmapped_teams: 매핑되지 않은 팀명 목록}
|
| 85 |
+
"""
|
| 86 |
+
mapped_teams = []
|
| 87 |
+
for team in df_filtered["원본팀명"].unique():
|
| 88 |
+
mapped_info = mapping_dict.get(team, {})
|
| 89 |
+
mapped_teams.append({
|
| 90 |
+
'original': team,
|
| 91 |
+
'mapped': mapped_info.get('present', team),
|
| 92 |
+
'acct': mapped_info.get('CD_ACCT', ''),
|
| 93 |
+
'pjt': mapped_info.get('CD_PJT', '')
|
| 94 |
+
})
|
| 95 |
+
|
| 96 |
+
return {
|
| 97 |
+
'mapped_teams': mapped_teams,
|
| 98 |
+
'mapped_count': len(mapped_teams)
|
| 99 |
+
}
|
processors/rental_processor.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
데이터 전처리 및 가공 모듈
|
| 3 |
+
"""
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from typing import Dict, List, Any, Tuple
|
| 6 |
+
import mapping_utils
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_and_preprocess_data(input_file: str, config: Dict[str, Any], mapping_dict: Dict[str, Dict[str, str]]) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
| 10 |
+
"""
|
| 11 |
+
데이터 로드 및 전처리
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
input_file: 입력 파일 경로
|
| 15 |
+
config: 렌탈사 설정 정보
|
| 16 |
+
mapping_dict: 매핑 딕셔너리
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
전처리된 데이터프레임, 필터링된 데이터프레임
|
| 20 |
+
"""
|
| 21 |
+
# CSV 파일 로드 - 다양한 인코딩 시도
|
| 22 |
+
print(f"'{input_file}' 파일 로딩 중...")
|
| 23 |
+
try:
|
| 24 |
+
rental_df = pd.read_csv(input_file, encoding='utf-8')
|
| 25 |
+
except UnicodeDecodeError:
|
| 26 |
+
try:
|
| 27 |
+
# UTF-8 실패 시 CP949 시도
|
| 28 |
+
rental_df = pd.read_csv(input_file, encoding='cp949')
|
| 29 |
+
print("CP949 인코딩으로 파일 로드 성공")
|
| 30 |
+
except UnicodeDecodeError:
|
| 31 |
+
try:
|
| 32 |
+
# EUC-KR 시도
|
| 33 |
+
rental_df = pd.read_csv(input_file, encoding='euc-kr')
|
| 34 |
+
print("EUC-KR 인코딩으로 파일 로드 성공")
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"파일 로드 실패: {e}")
|
| 37 |
+
raise
|
| 38 |
+
|
| 39 |
+
print(f"로딩 완료: {len(rental_df)}개 행 발견")
|
| 40 |
+
|
| 41 |
+
# 컬럼명 양쪽 공백 제거 (더 엄격한 처리)
|
| 42 |
+
original_columns = rental_df.columns.tolist()
|
| 43 |
+
print("원본 컬럼명:")
|
| 44 |
+
for col in original_columns:
|
| 45 |
+
print(f"- '{col}'")
|
| 46 |
+
|
| 47 |
+
# 컬럼명에서 공백 제거 및 처리
|
| 48 |
+
rental_df.columns = [col.strip() for col in rental_df.columns]
|
| 49 |
+
|
| 50 |
+
# 처리된 컬럼명 출력
|
| 51 |
+
processed_columns = rental_df.columns.tolist()
|
| 52 |
+
print("처리 후 컬럼명:")
|
| 53 |
+
for i, col in enumerate(processed_columns):
|
| 54 |
+
orig = original_columns[i] if i < len(original_columns) else "?"
|
| 55 |
+
print(f"- '{orig}' -> '{col}'")
|
| 56 |
+
|
| 57 |
+
# 컬럼명 중복 체크 및 처리
|
| 58 |
+
if len(set(rental_df.columns)) != len(rental_df.columns):
|
| 59 |
+
print("경고: 공백 제거 후 중복된 컬럼명이 있습니다.")
|
| 60 |
+
duplicate_count = {}
|
| 61 |
+
new_columns = []
|
| 62 |
+
|
| 63 |
+
for col in rental_df.columns:
|
| 64 |
+
if col in duplicate_count:
|
| 65 |
+
duplicate_count[col] += 1
|
| 66 |
+
new_col = f"{col}_{duplicate_count[col]}"
|
| 67 |
+
new_columns.append(new_col)
|
| 68 |
+
print(f" 중복 컬럼 처리: '{col}' -> '{new_col}'")
|
| 69 |
+
else:
|
| 70 |
+
duplicate_count[col] = 0
|
| 71 |
+
new_columns.append(col)
|
| 72 |
+
|
| 73 |
+
rental_df.columns = new_columns
|
| 74 |
+
|
| 75 |
+
# 필요한 필드 확인 및 조정
|
| 76 |
+
# 필요한 컬럼이 있는지 확인
|
| 77 |
+
column_exists = {}
|
| 78 |
+
required_columns = ["모델명", "영업분류", "관리부서", "거래처명", "관리지점"]
|
| 79 |
+
|
| 80 |
+
for col in required_columns:
|
| 81 |
+
if col in rental_df.columns:
|
| 82 |
+
column_exists[col] = True
|
| 83 |
+
else:
|
| 84 |
+
column_exists[col] = False
|
| 85 |
+
print(f"경고: '{col}' 컬럼이 파일에 없습니다.")
|
| 86 |
+
|
| 87 |
+
# 금액 필드 찾기 - 월별 자동 인식 패턴
|
| 88 |
+
amount_field = None
|
| 89 |
+
|
| 90 |
+
# 1. 먼저 config에 설정된 필드 시도 (앞뒤 공백 제거 후 비교)
|
| 91 |
+
clean_amount_field = config['amount_field'].strip()
|
| 92 |
+
for col in rental_df.columns:
|
| 93 |
+
if col.strip() == clean_amount_field:
|
| 94 |
+
amount_field = col
|
| 95 |
+
print(f"금액 필드로 '{amount_field}'를 설정값에서 찾았습니다.")
|
| 96 |
+
break
|
| 97 |
+
|
| 98 |
+
if not amount_field:
|
| 99 |
+
# 2. 'N월렌탈료' 패턴 찾기 - 공백 고려
|
| 100 |
+
import re
|
| 101 |
+
month_pattern = re.compile(r'^\s*(?:[0-9]{1,2})월렌탈료\s*$')
|
| 102 |
+
|
| 103 |
+
for col in rental_df.columns:
|
| 104 |
+
if month_pattern.match(col):
|
| 105 |
+
amount_field = col
|
| 106 |
+
print(f"금액 필드로 '{amount_field}'를 자동 인식했습니다.")
|
| 107 |
+
break
|
| 108 |
+
|
| 109 |
+
# 3. 렌탈료 포함 필드 찾기
|
| 110 |
+
if not amount_field:
|
| 111 |
+
for col in rental_df.columns:
|
| 112 |
+
if '렌탈료' in col:
|
| 113 |
+
amount_field = col
|
| 114 |
+
print(f"금액 필드로 '{amount_field}'를 사용합니다.")
|
| 115 |
+
break
|
| 116 |
+
|
| 117 |
+
if not amount_field:
|
| 118 |
+
# 4. 컬럼명에 '원'이나 '₩' 또는 '₩'가 포함된 것을 amount_field로 사용
|
| 119 |
+
for col in rental_df.columns:
|
| 120 |
+
if '원' in col or '₩' in col or '₩' in col:
|
| 121 |
+
amount_field = col
|
| 122 |
+
print(f"금액 필드로 '{amount_field}'를 사용합니다.")
|
| 123 |
+
break
|
| 124 |
+
|
| 125 |
+
# 금액 필드를 찾을 수 없으면 오류 발생
|
| 126 |
+
if not amount_field:
|
| 127 |
+
raise ValueError("금액 필드를 찾을 수 없습니다. 파일 형식을 확인해주세요.")
|
| 128 |
+
|
| 129 |
+
# 금액 필드 확인 출력
|
| 130 |
+
print(f"사용할 금액 필드: '{amount_field}'")
|
| 131 |
+
print(f"금액 필드 샘플 값: {rental_df[amount_field].head().tolist()}")
|
| 132 |
+
|
| 133 |
+
# 팀 필드 찾기 - 월별 자동 인식 패턴
|
| 134 |
+
team_fields = []
|
| 135 |
+
|
| 136 |
+
# 1. 먼저 config에 설정된 필드 시도
|
| 137 |
+
configured_team_fields = config.get('team_fields', [])
|
| 138 |
+
if isinstance(configured_team_fields, str):
|
| 139 |
+
configured_team_fields = [configured_team_fields]
|
| 140 |
+
|
| 141 |
+
for field in configured_team_fields:
|
| 142 |
+
clean_field = field.strip()
|
| 143 |
+
for col in rental_df.columns:
|
| 144 |
+
if col.strip() == clean_field:
|
| 145 |
+
team_fields.append(col)
|
| 146 |
+
print(f"팀 필드로 '{col}'를 설정값에서 찾았습니다.")
|
| 147 |
+
break
|
| 148 |
+
|
| 149 |
+
if not team_fields:
|
| 150 |
+
# 2. '[0-9]월 변경PJT' 패턴만 찾기 - 공백 허용
|
| 151 |
+
import re
|
| 152 |
+
# 공백 허용하고 '변경PJT'만 찾는 패턴
|
| 153 |
+
month_pjt_pattern = re.compile(r'^\s*(?:[0-9]{1,2})월\s*변경PJT\s*$')
|
| 154 |
+
|
| 155 |
+
for col in rental_df.columns:
|
| 156 |
+
if month_pjt_pattern.match(col):
|
| 157 |
+
team_fields.append(col)
|
| 158 |
+
print(f"팀 필드로 '{col}'를 자동 인식했습니다 (변경PJT 패턴).")
|
| 159 |
+
|
| 160 |
+
# 팀 필드를 찾을 수 없음 - 오류 발생
|
| 161 |
+
if not team_fields:
|
| 162 |
+
raise ValueError("팀 정보 필드를 찾을 수 없습니다. 파일 형식을 확인해주세요.")
|
| 163 |
+
|
| 164 |
+
# 사용 가능한 컬럼만 선택
|
| 165 |
+
available_columns = []
|
| 166 |
+
for col in required_columns:
|
| 167 |
+
if column_exists.get(col, False):
|
| 168 |
+
available_columns.append(col)
|
| 169 |
+
|
| 170 |
+
if amount_field:
|
| 171 |
+
available_columns.append(amount_field)
|
| 172 |
+
|
| 173 |
+
available_columns.extend(team_fields)
|
| 174 |
+
|
| 175 |
+
# 중복 제거
|
| 176 |
+
available_columns = list(dict.fromkeys(available_columns))
|
| 177 |
+
|
| 178 |
+
print(f"사용할 컬럼: {available_columns}")
|
| 179 |
+
|
| 180 |
+
# 필요한 필드만 선택 (존재하는 컬럼만)
|
| 181 |
+
df = rental_df[available_columns].copy()
|
| 182 |
+
|
| 183 |
+
# 금액 필드 처리 - 간단한 방법으로 숫자만 추출
|
| 184 |
+
print(f"금액 필드 '{amount_field}' 데이터 처리 중...")
|
| 185 |
+
|
| 186 |
+
# 숫자로 변환 가능한 값만 유효한 것으로 간주 (한 줄로 처리)
|
| 187 |
+
valid_amount_mask = pd.to_numeric(df[amount_field], errors='coerce').notna()
|
| 188 |
+
|
| 189 |
+
# 유효하지 않은 행 수 출력
|
| 190 |
+
invalid_rows = (~valid_amount_mask).sum()
|
| 191 |
+
if invalid_rows > 0:
|
| 192 |
+
print(f"금액이 없거나 숫자가 아닌 행(반납 항목) {invalid_rows}개를 제외합니다.")
|
| 193 |
+
|
| 194 |
+
# 유효한 행만 선택
|
| 195 |
+
df = df[valid_amount_mask].copy()
|
| 196 |
+
|
| 197 |
+
# 금액 변환 - 단순화된 방법
|
| 198 |
+
df["금액"] = pd.to_numeric(df[amount_field], errors='coerce')
|
| 199 |
+
df["금액"] = df["금액"].astype(int)
|
| 200 |
+
print(f"금액 변환 성공: 샘플 값 = {df['금액'].head().tolist()}")
|
| 201 |
+
|
| 202 |
+
# 팀명 처리 (우선순위에 따라)
|
| 203 |
+
if team_fields:
|
| 204 |
+
df["원본팀명"] = df[team_fields[0]].copy()
|
| 205 |
+
for field in team_fields[1:]:
|
| 206 |
+
df["원본팀명"] = df["원본팀명"].combine_first(df[field])
|
| 207 |
+
|
| 208 |
+
# 매핑 적용
|
| 209 |
+
df["매핑정보"] = df["원본팀명"].apply(lambda x: mapping_utils.apply_mapping(x, mapping_dict))
|
| 210 |
+
|
| 211 |
+
# 매핑 정보에서 필드 추출
|
| 212 |
+
df["팀명"] = df["매핑정보"].apply(lambda x: x["present"])
|
| 213 |
+
df["CD_ACCT"] = df["매핑정보"].apply(lambda x: x["CD_ACCT"])
|
| 214 |
+
|
| 215 |
+
# CD_PJT를 정수형으로 변환하는 부분
|
| 216 |
+
df["CD_PJT"] = df["매핑정보"].apply(lambda x: x["CD_PJT"])
|
| 217 |
+
# 문자열이나 NaN 값 처리 후 정수형으로 변환
|
| 218 |
+
df["CD_PJT"] = pd.to_numeric(df["CD_PJT"], errors='coerce').fillna(1000).astype(int)
|
| 219 |
+
|
| 220 |
+
# 적요 생성
|
| 221 |
+
df["적요"] = f"{config['note_prefix']}(" + df["팀명"] + ")"
|
| 222 |
+
|
| 223 |
+
# MNG 코드 설정
|
| 224 |
+
df["CD_MNG1"] = config['cost_center'] # 코스트센터
|
| 225 |
+
df["CD_MNG3"] = config['partner_code'] # 거래처 코드
|
| 226 |
+
|
| 227 |
+
# 매핑된 항목만 선택 (CD_ACCT와 CD_PJT가 있는 항목만)
|
| 228 |
+
df_filtered = df[(df["CD_ACCT"] != "") & (df["CD_PJT"] != "")].copy()
|
| 229 |
+
|
| 230 |
+
# 매핑되지 않은 팀명 정보 출력
|
| 231 |
+
if len(df_filtered) < len(df):
|
| 232 |
+
unmapped_teams = df[~df.index.isin(df_filtered.index)]["원본팀명"].unique()
|
| 233 |
+
print(f"매핑되지 않은 팀명 {len(unmapped_teams)}개:")
|
| 234 |
+
for team in unmapped_teams:
|
| 235 |
+
print(f"- '{team}'")
|
| 236 |
+
|
| 237 |
+
# 매핑되지 않은 항목이 있으면 경고 (전체 다 매핑 안 되는 경우만 오류)
|
| 238 |
+
if len(df_filtered) == 0:
|
| 239 |
+
raise ValueError("모든 팀명이 매핑되지 않았습니다. 매핑 파일을 확인해주세요.")
|
| 240 |
+
|
| 241 |
+
print(f"매핑된 항목: {len(df_filtered)}개 / 전체 {len(df)}개")
|
| 242 |
+
|
| 243 |
+
return df, df_filtered
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def summarize_data(df_filtered: pd.DataFrame, mapping_dict: Dict[str, Dict[str, str]]) -> Dict[str, Any]:
|
| 247 |
+
"""
|
| 248 |
+
데이터 요약 정보 생성
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
df_filtered: 필터링된 데이터프레임
|
| 252 |
+
mapping_dict: 매핑 딕셔너리
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
데이터 요약 정보
|
| 256 |
+
"""
|
| 257 |
+
total_amount = df_filtered["금액"].sum()
|
| 258 |
+
|
| 259 |
+
# 매핑 결과 요약
|
| 260 |
+
mapping_summary = mapping_utils.get_mapping_summary(df_filtered, mapping_dict)
|
| 261 |
+
|
| 262 |
+
# 계정 사용 현황
|
| 263 |
+
account_counts = df_filtered['CD_ACCT'].value_counts().to_dict()
|
| 264 |
+
|
| 265 |
+
return {
|
| 266 |
+
'total_count': len(df_filtered),
|
| 267 |
+
'total_amount': total_amount,
|
| 268 |
+
'account_counts': account_counts,
|
| 269 |
+
'mapping_summary': mapping_summary
|
| 270 |
+
}
|
utils/file_handler.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
파일 입출력 관련 모듈
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
import config as cfg
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def load_erp_form_template(erp_form_file: str) -> pd.DataFrame:
|
| 11 |
+
"""
|
| 12 |
+
ERP 양식 파일 로드
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
erp_form_file: ERP 양식 파일 경로
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
ERP 양식 데이터프레임
|
| 19 |
+
"""
|
| 20 |
+
try:
|
| 21 |
+
erp_form = pd.read_csv(erp_form_file, encoding=cfg.DEFAULT_ENCODING)
|
| 22 |
+
print(f"ERP 양식 파일 '{erp_form_file}'을 성공적으로 로드했습니다.")
|
| 23 |
+
return erp_form
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(f"ERP 양식 파일 로드 실패: {e}")
|
| 26 |
+
print("기본 양식 없이 진행합니다.")
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def save_to_files(result_df: pd.DataFrame, output_csv: str, output_excel: str, erp_data_count: int) -> None:
|
| 31 |
+
"""
|
| 32 |
+
결과를 CSV 및 Excel 파일로 저장
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
result_df: 결과 데이터프레임
|
| 36 |
+
output_csv: CSV 출력 파일 경로
|
| 37 |
+
output_excel: Excel 출력 파일 경로
|
| 38 |
+
erp_data_count: ERP 데이터 행 수
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
None
|
| 42 |
+
"""
|
| 43 |
+
# CSV 파일 저장
|
| 44 |
+
print(f"'{output_csv}'로 CSV 저장 중...")
|
| 45 |
+
try:
|
| 46 |
+
result_df.to_csv(output_csv, index=False, encoding=cfg.CSV_OUTPUT_ENCODING)
|
| 47 |
+
print(f"처리 완료: {erp_data_count}개 행이 '{output_csv}'에 저장됨 ({cfg.CSV_OUTPUT_ENCODING} 인코딩)")
|
| 48 |
+
print(f"데이터는 {cfg.ERP_DATA_ROW_START}행부터 시작합니다.")
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"CSV 파일 저장 중 오류 발생: {e}")
|
| 51 |
+
|
| 52 |
+
# Excel 파일 저장 (.xls 형식으로 변경)
|
| 53 |
+
xls_output_excel = output_excel.replace('.xlsx', '.xls')
|
| 54 |
+
print(f"\n'{xls_output_excel}'로 엑셀 파일 저장 중...")
|
| 55 |
+
try:
|
| 56 |
+
# xlwt 엔진을 사용하여 Excel 97-2003 형식(.xls)으로 저장
|
| 57 |
+
result_df.to_excel(xls_output_excel, index=False, engine='xlwt')
|
| 58 |
+
print(f"처리 완료: {erp_data_count}개 행이 '{xls_output_excel}'에 저장됨")
|
| 59 |
+
print(f"엑셀 파일이 성공적으로 생성되었습니다: {os.path.abspath(xls_output_excel)}")
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"엑셀 파일 저장 중 오류 발생: {e}")
|
| 62 |
+
print("CSV 파일은 정상적으로 저장되었습니다.")
|
| 63 |
+
print("CSV 파일을 열 때는 Excel의 '데이터' 탭에서 '텍스트/CSV에서' 기능을 사용하시기 바랍니다.")
|
| 64 |
+
print(f"오류 내용: {e}")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def prepare_file_with_template(erp_df: pd.DataFrame, erp_form: pd.DataFrame) -> pd.DataFrame:
|
| 68 |
+
"""
|
| 69 |
+
ERP 양식을 적용하여 파일 준비
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
erp_df: ERP 데이터프레임
|
| 73 |
+
erp_form: ERP 양식 데이터프레임
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
결과 데이터프레임
|
| 77 |
+
"""
|
| 78 |
+
if erp_form is not None:
|
| 79 |
+
# 양식 파일의 컬럼 순서 사용
|
| 80 |
+
form_columns = erp_form.columns.tolist()
|
| 81 |
+
|
| 82 |
+
# 결과 데이터프레임을 양식 컬럼 순서에 맞게 재정렬
|
| 83 |
+
for col in form_columns:
|
| 84 |
+
if col not in erp_df.columns:
|
| 85 |
+
erp_df[col] = ""
|
| 86 |
+
|
| 87 |
+
erp_df = erp_df[form_columns]
|
| 88 |
+
|
| 89 |
+
# erp_form 복사 (양식 파일의 처음 4행만 사용)
|
| 90 |
+
result_df = erp_form.copy()
|
| 91 |
+
|
| 92 |
+
# 양식 파일이 4행보다 많으면 4행만 유지
|
| 93 |
+
if len(result_df) > cfg.ERP_DATA_ROW_START - 1:
|
| 94 |
+
result_df = result_df.iloc[:(cfg.ERP_DATA_ROW_START - 1)]
|
| 95 |
+
|
| 96 |
+
# 빈 행 추가 (필요한 경우)
|
| 97 |
+
current_rows = len(result_df)
|
| 98 |
+
target_rows = cfg.ERP_DATA_ROW_START - 1 # 시작행 - 1 (인덱스는 0부터 시작하므로)
|
| 99 |
+
|
| 100 |
+
# 현재 행 수가 타겟 행 수보다 적으면 빈 행 추가
|
| 101 |
+
if current_rows < target_rows:
|
| 102 |
+
empty_rows_needed = target_rows - current_rows
|
| 103 |
+
empty_df = pd.DataFrame([[""] * len(form_columns) for _ in range(empty_rows_needed)], columns=form_columns)
|
| 104 |
+
result_df = pd.concat([result_df, empty_df], ignore_index=True)
|
| 105 |
+
|
| 106 |
+
# 처리된 데이터 추가 (ERP_DATA_ROW_START행부터 시작)
|
| 107 |
+
result_df = pd.concat([result_df, erp_df], ignore_index=True)
|
| 108 |
+
return result_df
|
| 109 |
+
else:
|
| 110 |
+
# 양식 파일이 없는 경우 빈 데이터프레임 생성 후 데이터 추가
|
| 111 |
+
# 필요한 빈 행 생성 (ERP_DATA_ROW_START-1개의 빈 행)
|
| 112 |
+
empty_rows = cfg.ERP_DATA_ROW_START - 1
|
| 113 |
+
empty_df = pd.DataFrame([[""] * len(erp_df.columns) for _ in range(empty_rows)], columns=erp_df.columns)
|
| 114 |
+
result_df = pd.concat([empty_df, erp_df], ignore_index=True)
|
| 115 |
+
return result_df
|
utils/reporter.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
처리 결과 보고 모듈
|
| 3 |
+
"""
|
| 4 |
+
from typing import Dict, Any
|
| 5 |
+
import pandas as pd
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def print_data_summary(summary: Dict[str, Any], company_config: Dict[str, Any]) -> None:
|
| 9 |
+
"""
|
| 10 |
+
데이터 처리 결과 요약 출력
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
summary: 데이터 요약 정보
|
| 14 |
+
company_config: 렌탈사 설정 정보
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
None
|
| 18 |
+
"""
|
| 19 |
+
total_count = summary['total_count']
|
| 20 |
+
total_amount = summary['total_amount']
|
| 21 |
+
account_counts = summary['account_counts']
|
| 22 |
+
mapping_summary = summary['mapping_summary']
|
| 23 |
+
|
| 24 |
+
print(f"\n총 처리 건수: {total_count + 1}건 (차변 {total_count}건, 대변 1건)")
|
| 25 |
+
print(f"총 금액: {total_amount:,.0f}원")
|
| 26 |
+
print(f"차변 계정: {len(account_counts)}개 계정 사용")
|
| 27 |
+
print(f"대변 계정: {company_config['payable_acct']} (미지급금) 1개 계정 사용")
|
| 28 |
+
|
| 29 |
+
# 관리항목 설정 내용 출력
|
| 30 |
+
print("\n관리항목 설정 정보:")
|
| 31 |
+
print(f"- CD_CC (코스트센터): {company_config['cost_center']} (고정)")
|
| 32 |
+
print(f"- CD_PARTNER (거래처코드): {company_config['partner_code']} (고정)")
|
| 33 |
+
print(f"- CD_PJT (프로젝트코드): 각 팀별 매핑된 코드 사용")
|
| 34 |
+
|
| 35 |
+
# 매핑 정보 요약
|
| 36 |
+
print("\n매핑 성공 팀명:")
|
| 37 |
+
mapped_teams = mapping_summary['mapped_teams']
|
| 38 |
+
for idx, team in enumerate(mapped_teams[:10]):
|
| 39 |
+
if len(mapped_teams) > 10 and idx == 9:
|
| 40 |
+
print(f"- {team['original']} ... 외 {len(mapped_teams)-10}개")
|
| 41 |
+
else:
|
| 42 |
+
print(f"- {team['original']} -> {team['mapped']} (ACCT: {team['acct']}, PJT: {team['pjt']})")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def generate_report_file(summary: Dict[str, Any], erp_df: pd.DataFrame, report_file: str) -> None:
|
| 46 |
+
"""
|
| 47 |
+
처리 결과 보고서 파일 생성
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
summary: 데이터 요약 정보
|
| 51 |
+
erp_df: ERP 데이터프레임
|
| 52 |
+
report_file: 보고서 파일 경로
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
None
|
| 56 |
+
"""
|
| 57 |
+
with open(report_file, 'w', encoding='utf-8') as f:
|
| 58 |
+
f.write("# ERP 전표 생성 결과 보고서\n\n")
|
| 59 |
+
|
| 60 |
+
# 기본 정보
|
| 61 |
+
f.write("## 1. 기본 정보\n")
|
| 62 |
+
f.write(f"- 생성 일시: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 63 |
+
f.write(f"- 처리 건수: 차변 {summary['total_count']}건, 대변 1건\n")
|
| 64 |
+
f.write(f"- 총 금액: {summary['total_amount']:,.0f}원\n\n")
|
| 65 |
+
|
| 66 |
+
# 계정 분포
|
| 67 |
+
f.write("## 2. 계정 코드별 사용 현황\n")
|
| 68 |
+
for acct, count in summary['account_counts'].items():
|
| 69 |
+
f.write(f"- {acct}: {count}건\n")
|
| 70 |
+
f.write("\n")
|
| 71 |
+
|
| 72 |
+
# 팀별 분포
|
| 73 |
+
f.write("## 3. 팀별 매핑 정보\n")
|
| 74 |
+
for team in summary['mapping_summary']['mapped_teams']:
|
| 75 |
+
f.write(f"- {team['original']} -> {team['mapped']} (계정: {team['acct']}, 프로젝트: {team['pjt']})\n")
|
| 76 |
+
|
| 77 |
+
f.write("\n## 4. 전표 주요 정보\n")
|
| 78 |
+
# 차변 행 정보
|
| 79 |
+
debit_rows = erp_df[erp_df["TP_DRCR"] == "1"]
|
| 80 |
+
f.write(f"- 차변 건수: {len(debit_rows)}건\n")
|
| 81 |
+
f.write(f"- 차변 계정: {len(debit_rows['CD_ACCT'].unique())}개 계정 사용\n")
|
| 82 |
+
|
| 83 |
+
# 대변 행 정보
|
| 84 |
+
credit_rows = erp_df[erp_df["TP_DRCR"] == "2"]
|
| 85 |
+
f.write(f"- 대변 건수: {len(credit_rows)}건\n")
|
| 86 |
+
f.write(f"- 대변 계정: {credit_rows['CD_ACCT'].iloc[0]} (미지급금)\n")
|
| 87 |
+
|
| 88 |
+
# 전표 번호 정보
|
| 89 |
+
f.write(f"- 전표 번호: {erp_df['NO_DOCU'].iloc[0]}\n")
|
| 90 |
+
|
| 91 |
+
print(f"\n처리 결과 보고서가 '{report_file}'에 저장되었습니다.")
|