hariqueen commited on
Commit
a47e303
·
verified ·
1 Parent(s): 1950ec9

Upload 6 files

Browse files
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}'에 저장되었습니다.")