- automation/page.tsx: 부동산 크롤러·회계 자동화 프리미엄 섹션 UI 추가 - accounting_automation_v1.0.py: 사업장 회계 장부 자동화 프로그램 생성 (5개 업종·19개 지출 항목·손익계산서·분기요약·부가세 자료 등 5시트 Excel 보고서) - prompt/page.tsx: 이미지 생성 프롬프트 패키지(45,000원) + 자소서·이력서 첨삭 프롬프트(35,000원) 상품 추가 (다크 그라디언트 카드, 실제 프롬프트 미리보기, ContactModal 연결) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1244 lines
50 KiB
Python
1244 lines
50 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
쟁승메이드 사업장 회계 장부 자동화 프로그램 v1.0
|
|
문의: bgg8988@gmail.com | jaengseung-made.vercel.app
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
# 필수 패키지 설치 확인
|
|
try:
|
|
import pandas as pd
|
|
import openpyxl
|
|
from openpyxl.styles import (
|
|
Font, PatternFill, Alignment, Border, Side, numbers
|
|
)
|
|
from openpyxl.utils import get_column_letter
|
|
from openpyxl.styles.numbers import FORMAT_NUMBER_COMMA_SEPARATED1
|
|
except ImportError:
|
|
print("필수 패키지가 설치되어 있지 않습니다.")
|
|
print("다음 명령어를 실행하세요: pip install pandas openpyxl")
|
|
sys.exit(1)
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 상수 정의
|
|
# ─────────────────────────────────────────────
|
|
|
|
BRAND_NAME = "쟁승메이드"
|
|
BRAND_EMAIL = "bgg8988@gmail.com"
|
|
BRAND_URL = "jaengseung-made.vercel.app"
|
|
VERSION = "v1.0"
|
|
|
|
# 업종 정의
|
|
INDUSTRIES = {
|
|
"1": "쇼핑몰/이커머스",
|
|
"2": "음식점/카페",
|
|
"3": "제조업",
|
|
"4": "서비스업/프리랜서",
|
|
"5": "기타",
|
|
}
|
|
|
|
# 업종별 수입 카테고리
|
|
INCOME_CATEGORIES = {
|
|
"쇼핑몰/이커머스": [
|
|
("product_sales", "제품판매"),
|
|
("shipping_income", "배송비수입"),
|
|
("refund_deduct", "반품환불차감 (마이너스 입력)"),
|
|
],
|
|
"음식점/카페": [
|
|
("hall_sales", "홀매출"),
|
|
("baemin_sales", "배달의민족 매출"),
|
|
("coupang_sales", "쿠팡이츠 매출"),
|
|
("yogiyo_sales", "요기요 매출"),
|
|
("takeout_sales", "포장매출"),
|
|
],
|
|
"제조업": [
|
|
("product_sales", "제품판매"),
|
|
("b2b_sales", "B2B 납품"),
|
|
("inventory_change", "원자재 재고변동"),
|
|
],
|
|
"서비스업/프리랜서": [
|
|
("project_income", "프로젝트 수입"),
|
|
("monthly_income", "월정액 수입"),
|
|
("consulting_income","컨설팅 수입"),
|
|
],
|
|
"기타": [
|
|
("sales_1", "매출1"),
|
|
("sales_2", "매출2"),
|
|
("other_income", "기타 수입"),
|
|
],
|
|
}
|
|
|
|
# 공통 지출 카테고리
|
|
EXPENSE_CATEGORIES = [
|
|
# 매출원가
|
|
("cogs_goods", "상품/재료비", "매출원가", "변동"),
|
|
("cogs_outsource", "외주비", "매출원가", "변동"),
|
|
("cogs_packaging", "포장비", "매출원가", "변동"),
|
|
# 인건비
|
|
("labor_salary", "급여", "인건비", "고정"),
|
|
("labor_insurance", "4대보험", "인건비", "고정"),
|
|
("labor_severance", "퇴직금적립", "인건비", "고정"),
|
|
# 임대료
|
|
("rent", "임대료/관리비", "임대료", "고정"),
|
|
# 공과금
|
|
("util_electricity", "전기요금", "공과금", "고정"),
|
|
("util_water", "수도요금", "공과금", "고정"),
|
|
("util_gas", "가스요금", "공과금", "고정"),
|
|
("util_telecom", "통신비", "공과금", "고정"),
|
|
# 마케팅
|
|
("mkt_ads", "광고비", "마케팅", "변동"),
|
|
("mkt_platform", "플랫폼수수료", "마케팅", "변동"),
|
|
# 세금
|
|
("tax_vat", "부가세", "세금", "변동"),
|
|
("tax_income", "종합소득세", "세금", "변동"),
|
|
("tax_local", "지방소득세", "세금", "변동"),
|
|
# 기타
|
|
("etc_supplies", "소모품비", "기타", "변동"),
|
|
("etc_depreciation", "감가상각비", "기타", "고정"),
|
|
("etc_insurance", "보험료", "기타", "고정"),
|
|
]
|
|
|
|
MONTHS_KR = [
|
|
"1월","2월","3월","4월","5월","6월",
|
|
"7월","8월","9월","10월","11월","12월",
|
|
]
|
|
|
|
QUARTERS_KR = ["1분기 (1~3월)", "2분기 (4~6월)", "3분기 (7~9월)", "4분기 (10~12월)"]
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 유틸리티 함수
|
|
# ─────────────────────────────────────────────
|
|
|
|
def print_banner():
|
|
"""프로그램 시작 배너 출력"""
|
|
print("\n" + "=" * 60)
|
|
print(f" {BRAND_NAME} 사업장 회계 장부 자동화 프로그램 {VERSION}")
|
|
print(f" 문의: {BRAND_EMAIL}")
|
|
print(f" 웹사이트: {BRAND_URL}")
|
|
print("=" * 60 + "\n")
|
|
|
|
|
|
def print_separator(title: str = ""):
|
|
"""구분선 출력"""
|
|
if title:
|
|
print(f"\n{'─' * 20} {title} {'─' * 20}")
|
|
else:
|
|
print("─" * 60)
|
|
|
|
|
|
def input_int(prompt: str, default: int = 0) -> int:
|
|
"""정수 입력 (기본값 지원)"""
|
|
while True:
|
|
raw = input(prompt).strip()
|
|
if raw == "" and default is not None:
|
|
return default
|
|
try:
|
|
return int(raw.replace(",", ""))
|
|
except ValueError:
|
|
print(" [오류] 숫자만 입력하세요. (천단위 콤마 허용)")
|
|
|
|
|
|
def input_float(prompt: str) -> float:
|
|
"""실수 입력"""
|
|
while True:
|
|
raw = input(prompt).strip()
|
|
try:
|
|
return float(raw.replace(",", ""))
|
|
except ValueError:
|
|
print(" [오류] 숫자만 입력하세요.")
|
|
|
|
|
|
def fmt_krw(value: float) -> str:
|
|
"""원화 포맷 (천단위 콤마)"""
|
|
if value < 0:
|
|
return f"-{abs(value):,.0f}원"
|
|
return f"{value:,.0f}원"
|
|
|
|
|
|
def fmt_pct(value: float) -> str:
|
|
"""퍼센트 포맷"""
|
|
return f"{value:.1f}%"
|
|
|
|
|
|
def select_industry() -> str:
|
|
"""업종 선택 메뉴"""
|
|
print_separator("업종 선택")
|
|
for key, name in INDUSTRIES.items():
|
|
print(f" [{key}] {name}")
|
|
while True:
|
|
choice = input("\n업종을 선택하세요 (1~5): ").strip()
|
|
if choice in INDUSTRIES:
|
|
selected = INDUSTRIES[choice]
|
|
print(f" → '{selected}' 선택됨\n")
|
|
return selected
|
|
print(" [오류] 1~5 사이의 번호를 입력하세요.")
|
|
|
|
|
|
def select_year() -> int:
|
|
"""분석 연도 선택"""
|
|
current_year = datetime.now().year
|
|
year = input_int(f"분석 연도를 입력하세요 (기본값: {current_year}): ", default=current_year)
|
|
return year
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 데이터 수집
|
|
# ─────────────────────────────────────────────
|
|
|
|
def collect_data_manual(industry: str, year: int) -> dict:
|
|
"""직접 입력 방식으로 월별 데이터 수집"""
|
|
income_cats = INCOME_CATEGORIES[industry]
|
|
data = {
|
|
"industry": industry,
|
|
"year": year,
|
|
"months": {},
|
|
}
|
|
|
|
print_separator(f"{year}년 월별 데이터 입력")
|
|
print(" ※ 금액은 원(₩) 단위로 입력하세요. (천단위 콤마 허용)")
|
|
print(" ※ 입력을 건너뛰려면 그냥 Enter를 누르세요 (0원 처리).\n")
|
|
|
|
for month_idx in range(1, 13):
|
|
month_label = MONTHS_KR[month_idx - 1]
|
|
print(f"\n ── {year}년 {month_label} ──")
|
|
|
|
month_data = {"income": {}, "expense": {}}
|
|
|
|
# 수입 입력
|
|
print(" [수입]")
|
|
for cat_key, cat_name in income_cats:
|
|
val = input_int(f" {cat_name}: ", default=0)
|
|
month_data["income"][cat_key] = val
|
|
|
|
# 지출 입력
|
|
print(" [지출]")
|
|
for cat_key, cat_name, cat_group, cost_type in EXPENSE_CATEGORIES:
|
|
val = input_int(f" {cat_name} ({cat_group}): ", default=0)
|
|
month_data["expense"][cat_key] = val
|
|
|
|
data["months"][month_idx] = month_data
|
|
|
|
return data
|
|
|
|
|
|
def collect_data_excel(industry: str, year: int) -> Optional[dict]:
|
|
"""Excel 파일에서 데이터 가져오기"""
|
|
print_separator("Excel 파일에서 가져오기")
|
|
print(" ※ 먼저 메인 메뉴에서 '입력양식 생성'을 선택하여 양식을 만드세요.")
|
|
file_path = input(" 가져올 Excel 파일 경로를 입력하세요: ").strip().strip('"')
|
|
|
|
if not os.path.exists(file_path):
|
|
print(f" [오류] 파일을 찾을 수 없습니다: {file_path}")
|
|
return None
|
|
|
|
try:
|
|
print(" Excel 파일을 읽는 중...")
|
|
xl = pd.ExcelFile(file_path)
|
|
data = {
|
|
"industry": industry,
|
|
"year": year,
|
|
"months": {},
|
|
}
|
|
income_cats = INCOME_CATEGORIES[industry]
|
|
|
|
for month_idx in range(1, 13):
|
|
sheet_name = f"{month_idx}월"
|
|
if sheet_name not in xl.sheet_names:
|
|
# 해당 월 시트가 없으면 0으로 초기화
|
|
month_data = {
|
|
"income": {k: 0 for k, _ in income_cats},
|
|
"expense": {k: 0 for k, _, __, ___ in EXPENSE_CATEGORIES},
|
|
}
|
|
data["months"][month_idx] = month_data
|
|
continue
|
|
|
|
df = pd.read_excel(file_path, sheet_name=sheet_name, header=0)
|
|
# 양식: A열=항목키, B열=항목명, C열=금액
|
|
df.columns = ["key", "name", "amount"]
|
|
df["amount"] = pd.to_numeric(df["amount"].fillna(0), errors="coerce").fillna(0)
|
|
amount_map = dict(zip(df["key"].astype(str), df["amount"]))
|
|
|
|
month_data = {
|
|
"income": {k: int(amount_map.get(k, 0)) for k, _ in income_cats},
|
|
"expense": {k: int(amount_map.get(k, 0)) for k, _, __, ___ in EXPENSE_CATEGORIES},
|
|
}
|
|
data["months"][month_idx] = month_data
|
|
|
|
print(f" [완료] {file_path} 파일에서 데이터를 가져왔습니다.")
|
|
return data
|
|
|
|
except Exception as e:
|
|
print(f" [오류] Excel 파일 읽기 실패: {e}")
|
|
return None
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 재무 계산
|
|
# ─────────────────────────────────────────────
|
|
|
|
def calc_month_financials(month_data: dict) -> dict:
|
|
"""월별 재무 지표 계산"""
|
|
income = month_data["income"]
|
|
expense = month_data["expense"]
|
|
|
|
# 총 매출 (반품은 마이너스로 입력됨)
|
|
total_revenue = sum(income.values())
|
|
|
|
# 매출원가
|
|
cogs = (
|
|
expense.get("cogs_goods", 0) +
|
|
expense.get("cogs_outsource", 0) +
|
|
expense.get("cogs_packaging", 0)
|
|
)
|
|
|
|
# 매출총이익
|
|
gross_profit = total_revenue - cogs
|
|
|
|
# 판매관리비 (인건비 + 임대료 + 공과금 + 마케팅 + 기타)
|
|
sg_and_a = (
|
|
expense.get("labor_salary", 0) +
|
|
expense.get("labor_insurance", 0) +
|
|
expense.get("labor_severance", 0) +
|
|
expense.get("rent", 0) +
|
|
expense.get("util_electricity", 0) +
|
|
expense.get("util_water", 0) +
|
|
expense.get("util_gas", 0) +
|
|
expense.get("util_telecom", 0) +
|
|
expense.get("mkt_ads", 0) +
|
|
expense.get("mkt_platform", 0) +
|
|
expense.get("etc_supplies", 0) +
|
|
expense.get("etc_depreciation", 0) +
|
|
expense.get("etc_insurance", 0)
|
|
)
|
|
|
|
# 영업이익
|
|
operating_profit = gross_profit - sg_and_a
|
|
|
|
# 세금
|
|
taxes = (
|
|
expense.get("tax_vat", 0) +
|
|
expense.get("tax_income", 0) +
|
|
expense.get("tax_local", 0)
|
|
)
|
|
|
|
# 순이익
|
|
net_profit = operating_profit - taxes
|
|
|
|
# 총 지출
|
|
total_expense = sum(expense.values())
|
|
|
|
# 고정비 / 변동비 분리
|
|
fixed_cost = sum(
|
|
expense.get(k, 0)
|
|
for k, _, __, cost_type in EXPENSE_CATEGORIES
|
|
if cost_type == "고정"
|
|
)
|
|
variable_cost = sum(
|
|
expense.get(k, 0)
|
|
for k, _, __, cost_type in EXPENSE_CATEGORIES
|
|
if cost_type == "변동"
|
|
)
|
|
|
|
# 비율 계산 (0 나누기 방지)
|
|
def safe_pct(numerator, denominator):
|
|
if denominator == 0:
|
|
return 0.0
|
|
return round(numerator / denominator * 100, 2)
|
|
|
|
gross_margin = safe_pct(gross_profit, total_revenue)
|
|
operating_margin = safe_pct(operating_profit, total_revenue)
|
|
net_margin = safe_pct(net_profit, total_revenue)
|
|
labor_ratio = safe_pct(
|
|
expense.get("labor_salary", 0) + expense.get("labor_insurance", 0) + expense.get("labor_severance", 0),
|
|
total_revenue
|
|
)
|
|
mkt_ratio = safe_pct(
|
|
expense.get("mkt_ads", 0) + expense.get("mkt_platform", 0),
|
|
total_revenue
|
|
)
|
|
|
|
# 손익분기점 (변동비율 기반)
|
|
variable_ratio = safe_pct(variable_cost, total_revenue) / 100
|
|
if variable_ratio < 1:
|
|
bep = round(fixed_cost / (1 - variable_ratio))
|
|
else:
|
|
bep = 0 # 계산 불가
|
|
|
|
return {
|
|
"total_revenue": total_revenue,
|
|
"cogs": cogs,
|
|
"gross_profit": gross_profit,
|
|
"sg_and_a": sg_and_a,
|
|
"operating_profit": operating_profit,
|
|
"taxes": taxes,
|
|
"net_profit": net_profit,
|
|
"total_expense": total_expense,
|
|
"fixed_cost": fixed_cost,
|
|
"variable_cost": variable_cost,
|
|
"gross_margin": gross_margin,
|
|
"operating_margin": operating_margin,
|
|
"net_margin": net_margin,
|
|
"labor_ratio": labor_ratio,
|
|
"mkt_ratio": mkt_ratio,
|
|
"bep": bep,
|
|
}
|
|
|
|
|
|
def calc_all_financials(data: dict) -> dict:
|
|
"""전체 월별 재무 계산 후 분기/연간 집계"""
|
|
results = {}
|
|
for month_idx, month_data in data["months"].items():
|
|
results[month_idx] = calc_month_financials(month_data)
|
|
|
|
# 분기별 합산
|
|
quarters = {}
|
|
for q in range(1, 5):
|
|
month_range = range((q - 1) * 3 + 1, q * 3 + 1)
|
|
q_data = {
|
|
key: sum(results[m][key] for m in month_range if m in results)
|
|
for key in results[1].keys()
|
|
if key not in ("gross_margin", "operating_margin", "net_margin", "labor_ratio", "mkt_ratio", "bep")
|
|
}
|
|
# 분기 비율 재계산
|
|
rev = q_data["total_revenue"]
|
|
q_data["gross_margin"] = round(q_data["gross_profit"] / rev * 100, 2) if rev else 0
|
|
q_data["operating_margin"] = round(q_data["operating_profit"] / rev * 100, 2) if rev else 0
|
|
q_data["net_margin"] = round(q_data["net_profit"] / rev * 100, 2) if rev else 0
|
|
q_data["labor_ratio"] = 0
|
|
q_data["mkt_ratio"] = 0
|
|
q_data["bep"] = 0
|
|
quarters[q] = q_data
|
|
|
|
# 연간 합산
|
|
annual = {
|
|
key: sum(results[m][key] for m in results)
|
|
for key in results[1].keys()
|
|
if key not in ("gross_margin", "operating_margin", "net_margin", "labor_ratio", "mkt_ratio", "bep")
|
|
}
|
|
rev = annual["total_revenue"]
|
|
annual["gross_margin"] = round(annual["gross_profit"] / rev * 100, 2) if rev else 0
|
|
annual["operating_margin"] = round(annual["operating_profit"] / rev * 100, 2) if rev else 0
|
|
annual["net_margin"] = round(annual["net_profit"] / rev * 100, 2) if rev else 0
|
|
annual["labor_ratio"] = 0
|
|
annual["mkt_ratio"] = 0
|
|
annual["bep"] = 0
|
|
|
|
return {"monthly": results, "quarterly": quarters, "annual": annual}
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 회계 전문가 조언 엔진
|
|
# ─────────────────────────────────────────────
|
|
|
|
def generate_advice(data: dict, financials: dict) -> list:
|
|
"""규칙 기반 회계 조언 생성"""
|
|
advice_list = []
|
|
annual = financials["annual"]
|
|
year = data["year"]
|
|
|
|
# 1. 마진율 경보
|
|
if annual["gross_margin"] < 20:
|
|
advice_list.append({
|
|
"category": "수익성 경고",
|
|
"priority": "긴급",
|
|
"title": f"매출총이익률 {fmt_pct(annual['gross_margin'])} — 위험 수준",
|
|
"detail": (
|
|
"매출총이익률이 20% 미만입니다. 원가율이 너무 높아 사업 지속성이 위협받을 수 있습니다.\n"
|
|
"• 원재료/상품 공급처 재협상 또는 대체 공급처 탐색\n"
|
|
"• 저마진 제품/서비스 라인 구조조정 검토\n"
|
|
"• 판매가격 인상 가능 여부 시장 조사 필요"
|
|
),
|
|
})
|
|
elif annual["gross_margin"] < 35:
|
|
advice_list.append({
|
|
"category": "수익성 주의",
|
|
"priority": "주의",
|
|
"title": f"매출총이익률 {fmt_pct(annual['gross_margin'])} — 개선 필요",
|
|
"detail": (
|
|
"매출총이익률이 35% 미만으로 업종 평균 대비 낮은 수준입니다.\n"
|
|
"• 고부가가치 제품/서비스 비중 확대 전략 수립\n"
|
|
"• 원가 절감 가능 항목 면밀히 검토"
|
|
),
|
|
})
|
|
|
|
# 2. 인건비 비율
|
|
labor_total = (
|
|
sum(
|
|
data["months"][m]["expense"].get("labor_salary", 0) +
|
|
data["months"][m]["expense"].get("labor_insurance", 0) +
|
|
data["months"][m]["expense"].get("labor_severance", 0)
|
|
for m in data["months"]
|
|
)
|
|
)
|
|
labor_ratio = labor_total / annual["total_revenue"] * 100 if annual["total_revenue"] else 0
|
|
if labor_ratio > 30:
|
|
advice_list.append({
|
|
"category": "비용 구조",
|
|
"priority": "주의",
|
|
"title": f"인건비 비율 {fmt_pct(labor_ratio)} — 업무 자동화 검토 필요",
|
|
"detail": (
|
|
"인건비가 매출의 30%를 초과합니다. 생산성 향상 방안을 모색하세요.\n"
|
|
"• RPA, 엑셀 자동화 등 업무 자동화 도입으로 인건비 절감\n"
|
|
"• 아웃소싱·프리랜서 활용으로 고정 인건비를 변동비화\n"
|
|
"• 직원별 생산성 KPI 측정 및 성과급 체계 도입"
|
|
),
|
|
})
|
|
|
|
# 3. 마케팅비 비율
|
|
mkt_total = sum(
|
|
data["months"][m]["expense"].get("mkt_ads", 0) +
|
|
data["months"][m]["expense"].get("mkt_platform", 0)
|
|
for m in data["months"]
|
|
)
|
|
mkt_ratio = mkt_total / annual["total_revenue"] * 100 if annual["total_revenue"] else 0
|
|
if mkt_ratio > 10:
|
|
advice_list.append({
|
|
"category": "마케팅 효율",
|
|
"priority": "주의",
|
|
"title": f"마케팅비 비율 {fmt_pct(mkt_ratio)} — ROI 분석 필요",
|
|
"detail": (
|
|
"마케팅비가 매출의 10%를 초과합니다. 채널별 ROI를 정밀 분석하세요.\n"
|
|
"• 채널별 전환율·CAC(고객획득비용) 측정 체계 구축\n"
|
|
"• 효과 낮은 광고 채널 예산 축소, 고ROI 채널에 집중 투자\n"
|
|
"• 리타겟팅·CRM을 통한 재구매율 향상으로 CAC 절감"
|
|
),
|
|
})
|
|
|
|
# 4. 순이익 적자 판단
|
|
loss_months = [m for m, f in financials["monthly"].items() if f["net_profit"] < 0]
|
|
if len(loss_months) >= 6:
|
|
advice_list.append({
|
|
"category": "경영 위기",
|
|
"priority": "긴급",
|
|
"title": f"연간 {len(loss_months)}개월 순손실 — 고정비 구조 즉시 재검토",
|
|
"detail": (
|
|
f"총 {len(loss_months)}개월 순손실이 발생했습니다. 사업 구조 전면 재검토가 필요합니다.\n"
|
|
"• 임대료 협상 또는 이전을 통한 고정비 절감\n"
|
|
"• 손실 유발 서비스/제품 라인 즉시 중단 또는 개편\n"
|
|
"• 단기 자금 확보: 소상공인 정책자금, 신용보증기금 활용 검토"
|
|
),
|
|
})
|
|
elif annual["net_profit"] < 0:
|
|
advice_list.append({
|
|
"category": "수익성 경고",
|
|
"priority": "긴급",
|
|
"title": f"연간 순손실 {fmt_krw(annual['net_profit'])} — 고정비 구조 재검토",
|
|
"detail": (
|
|
"연간 순손실이 발생했습니다. 고정비 절감이 최우선 과제입니다.\n"
|
|
"• 임대료·인건비 등 고정비 재협상\n"
|
|
"• 불필요한 구독 서비스, 보험료 등 점검"
|
|
),
|
|
})
|
|
|
|
# 5. 부가세 신고 안내
|
|
advice_list.append({
|
|
"category": "세무 일정",
|
|
"priority": "정보",
|
|
"title": "부가세 신고 시기 안내",
|
|
"detail": (
|
|
f"■ {year}년 부가세 신고 일정\n"
|
|
f" • 1기 확정신고: {year}년 7월 1일 ~ 7월 25일 (1~6월 매출분)\n"
|
|
f" • 2기 확정신고: {year + 1}년 1월 1일 ~ 1월 25일 (7~12월 매출분)\n\n"
|
|
"■ 준비 서류\n"
|
|
" • 세금계산서 합계표 (매입/매출)\n"
|
|
" • 신용카드 매출전표 합계표\n"
|
|
" • 현금영수증 발행·수취 내역\n\n"
|
|
" ※ 홈택스(hometax.go.kr)에서 전자신고 가능"
|
|
),
|
|
})
|
|
|
|
# 6. 세금 납부 시뮬레이션 (종합소득세)
|
|
taxable_income = annual["net_profit"]
|
|
if taxable_income > 0:
|
|
# 2024년 기준 종합소득세 세율 구간
|
|
tax_brackets = [
|
|
(14_000_000, 0.06, 0),
|
|
(50_000_000, 0.15, 1_260_000),
|
|
(88_000_000, 0.24, 5_760_000),
|
|
(150_000_000, 0.35, 15_440_000),
|
|
(300_000_000, 0.38, 19_940_000),
|
|
(500_000_000, 0.40, 25_940_000),
|
|
(1_000_000_000, 0.42, 35_940_000),
|
|
(float("inf"), 0.45, 65_940_000),
|
|
]
|
|
estimated_tax = 0
|
|
for limit, rate, deduction in tax_brackets:
|
|
if taxable_income <= limit:
|
|
estimated_tax = int(taxable_income * rate - deduction)
|
|
break
|
|
local_tax = int(estimated_tax * 0.1)
|
|
advice_list.append({
|
|
"category": "세금 시뮬레이션",
|
|
"priority": "정보",
|
|
"title": f"종합소득세 예상 납부액: {fmt_krw(estimated_tax + local_tax)}",
|
|
"detail": (
|
|
f"■ {year}년 종합소득세 시뮬레이션 (과세표준: {fmt_krw(taxable_income)})\n"
|
|
f" • 종합소득세: {fmt_krw(estimated_tax)}\n"
|
|
f" • 지방소득세 (10%): {fmt_krw(local_tax)}\n"
|
|
f" • 합계 예상 납부액: {fmt_krw(estimated_tax + local_tax)}\n\n"
|
|
" ※ 각종 공제항목에 따라 실제 납부액은 달라집니다.\n"
|
|
" ※ 세무사 상담을 통해 절세 전략을 수립하세요."
|
|
),
|
|
})
|
|
|
|
return advice_list
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 콘솔 요약 출력
|
|
# ─────────────────────────────────────────────
|
|
|
|
def print_summary(data: dict, financials: dict, advice_list: list):
|
|
"""콘솔에 연간 요약 출력"""
|
|
annual = financials["annual"]
|
|
year = data["year"]
|
|
|
|
print_separator(f"{year}년 연간 재무 요약")
|
|
print(f" 업종 : {data['industry']}")
|
|
print(f" 총 매출 : {fmt_krw(annual['total_revenue'])}")
|
|
print(f" 총 지출 : {fmt_krw(annual['total_expense'])}")
|
|
print(f" 매출총이익 : {fmt_krw(annual['gross_profit'])} ({fmt_pct(annual['gross_margin'])})")
|
|
print(f" 영업이익 : {fmt_krw(annual['operating_profit'])} ({fmt_pct(annual['operating_margin'])})")
|
|
print(f" 순이익 : {fmt_krw(annual['net_profit'])} ({fmt_pct(annual['net_margin'])})")
|
|
|
|
print_separator("분기별 순이익")
|
|
for q in range(1, 5):
|
|
q_data = financials["quarterly"][q]
|
|
profit = q_data["net_profit"]
|
|
sign = "▲" if profit >= 0 else "▼"
|
|
print(f" {QUARTERS_KR[q-1]}: {sign} {fmt_krw(profit)}")
|
|
|
|
print_separator("회계사 조언 요약")
|
|
for i, adv in enumerate(advice_list, 1):
|
|
print(f" [{adv['priority']}] {i}. {adv['title']}")
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Excel 스타일 헬퍼
|
|
# ─────────────────────────────────────────────
|
|
|
|
HEADER_FILL = PatternFill(start_color="1D4ED8", end_color="1D4ED8", fill_type="solid")
|
|
ALT_FILL = PatternFill(start_color="EFF6FF", end_color="EFF6FF", fill_type="solid")
|
|
PROFIT_FONT = Font(color="1D4ED8", bold=True)
|
|
LOSS_FONT = Font(color="DC2626", bold=True)
|
|
HEADER_FONT = Font(color="FFFFFF", bold=True, size=11)
|
|
BORDER_SIDE = Side(style="thin", color="CBD5E1")
|
|
THIN_BORDER = Border(
|
|
left=BORDER_SIDE, right=BORDER_SIDE,
|
|
top=BORDER_SIDE, bottom=BORDER_SIDE,
|
|
)
|
|
NUM_FMT = '#,##0"원"'
|
|
PCT_FMT = '0.0"%"'
|
|
|
|
|
|
def style_header_row(ws, row_num: int, num_cols: int):
|
|
"""헤더 행 스타일 적용"""
|
|
for col in range(1, num_cols + 1):
|
|
cell = ws.cell(row=row_num, column=col)
|
|
cell.fill = HEADER_FILL
|
|
cell.font = HEADER_FONT
|
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
cell.border = THIN_BORDER
|
|
|
|
|
|
def style_data_cell(cell, value, is_alt_row: bool = False, is_amount: bool = True, is_pct: bool = False):
|
|
"""데이터 셀 스타일 적용"""
|
|
cell.value = value
|
|
cell.border = THIN_BORDER
|
|
cell.alignment = Alignment(horizontal="right" if (is_amount or is_pct) else "left", vertical="center")
|
|
if is_alt_row:
|
|
cell.fill = ALT_FILL
|
|
if is_amount and isinstance(value, (int, float)):
|
|
cell.number_format = NUM_FMT
|
|
if value < 0:
|
|
cell.font = LOSS_FONT
|
|
elif value > 0:
|
|
cell.font = PROFIT_FONT
|
|
elif is_pct and isinstance(value, (int, float)):
|
|
cell.number_format = PCT_FMT
|
|
|
|
|
|
def auto_col_width(ws, min_width: int = 10):
|
|
"""열 너비 자동 조정"""
|
|
for col in ws.columns:
|
|
max_len = min_width
|
|
col_letter = get_column_letter(col[0].column)
|
|
for cell in col:
|
|
try:
|
|
cell_len = len(str(cell.value)) if cell.value else 0
|
|
if cell_len > max_len:
|
|
max_len = cell_len
|
|
except Exception:
|
|
pass
|
|
ws.column_dimensions[col_letter].width = min(max_len + 4, 30)
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Excel 보고서 생성
|
|
# ─────────────────────────────────────────────
|
|
|
|
def create_report_excel(data: dict, financials: dict, advice_list: list, output_path: str):
|
|
"""Excel 보고서 생성 (5개 시트)"""
|
|
wb = openpyxl.Workbook()
|
|
wb.remove(wb.active) # 기본 시트 제거
|
|
|
|
_sheet1_monthly_pl(wb, data, financials)
|
|
_sheet2_quarterly(wb, data, financials)
|
|
_sheet3_cost_structure(wb, data, financials)
|
|
_sheet4_advice(wb, data, advice_list)
|
|
_sheet5_vat(wb, data, financials)
|
|
|
|
wb.save(output_path)
|
|
print(f"\n [완료] 보고서가 저장되었습니다: {output_path}")
|
|
|
|
|
|
def _sheet1_monthly_pl(wb, data: dict, financials: dict):
|
|
"""시트1: 월별 손익계산서"""
|
|
ws = wb.create_sheet("월별 손익계산서")
|
|
year = data["year"]
|
|
|
|
# 타이틀
|
|
ws.merge_cells("A1:N1")
|
|
title_cell = ws["A1"]
|
|
title_cell.value = f"{year}년 월별 손익계산서 — {data['industry']}"
|
|
title_cell.font = Font(bold=True, size=14)
|
|
title_cell.alignment = Alignment(horizontal="center")
|
|
|
|
# 헤더
|
|
headers = ["항목"] + [f"{m}월" for m in range(1, 13)] + ["연간 합계"]
|
|
for col_idx, h in enumerate(headers, 1):
|
|
ws.cell(row=2, column=col_idx, value=h)
|
|
style_header_row(ws, 2, len(headers))
|
|
|
|
# 항목 정의
|
|
rows = [
|
|
("총 매출", "total_revenue", True, False),
|
|
("매출원가", "cogs", True, False),
|
|
("매출총이익", "gross_profit", True, False),
|
|
(" 매출총이익률", "gross_margin", False, True),
|
|
("판매관리비", "sg_and_a", True, False),
|
|
("영업이익", "operating_profit", True, False),
|
|
(" 영업이익률", "operating_margin", False, True),
|
|
("세금", "taxes", True, False),
|
|
("순이익", "net_profit", True, False),
|
|
(" 순이익률", "net_margin", False, True),
|
|
("고정비", "fixed_cost", True, False),
|
|
("변동비", "variable_cost", True, False),
|
|
("손익분기점", "bep", True, False),
|
|
]
|
|
|
|
annual = financials["annual"]
|
|
for row_offset, (label, key, is_amount, is_pct) in enumerate(rows):
|
|
row_num = row_offset + 3
|
|
is_alt = row_offset % 2 == 0
|
|
|
|
# 항목명
|
|
name_cell = ws.cell(row=row_num, column=1, value=label)
|
|
name_cell.border = THIN_BORDER
|
|
name_cell.alignment = Alignment(horizontal="left")
|
|
if is_alt:
|
|
name_cell.fill = ALT_FILL
|
|
if key in ("gross_profit", "operating_profit", "net_profit"):
|
|
name_cell.font = Font(bold=True)
|
|
|
|
# 월별 값
|
|
for m in range(1, 13):
|
|
val = financials["monthly"][m][key]
|
|
cell = ws.cell(row=row_num, column=m + 1)
|
|
style_data_cell(cell, val, is_alt, is_amount, is_pct)
|
|
|
|
# 연간 합계
|
|
ann_val = annual[key]
|
|
ann_cell = ws.cell(row=row_num, column=14)
|
|
style_data_cell(ann_cell, ann_val, is_alt, is_amount, is_pct)
|
|
if key in ("gross_profit", "operating_profit", "net_profit"):
|
|
ann_cell.font = Font(
|
|
color="1D4ED8" if ann_val >= 0 else "DC2626",
|
|
bold=True, size=11
|
|
)
|
|
|
|
# 틀 고정 (헤더 + 항목명)
|
|
ws.freeze_panes = "B3"
|
|
auto_col_width(ws, min_width=8)
|
|
ws.column_dimensions["A"].width = 16
|
|
|
|
|
|
def _sheet2_quarterly(wb, data: dict, financials: dict):
|
|
"""시트2: 분기별 요약"""
|
|
ws = wb.create_sheet("분기별 요약")
|
|
year = data["year"]
|
|
|
|
ws.merge_cells("A1:F1")
|
|
ws["A1"].value = f"{year}년 분기별 재무 요약"
|
|
ws["A1"].font = Font(bold=True, size=14)
|
|
ws["A1"].alignment = Alignment(horizontal="center")
|
|
|
|
headers = ["항목", "1분기 (1~3월)", "2분기 (4~6월)", "3분기 (7~9월)", "4분기 (10~12월)", "연간 합계"]
|
|
for col_idx, h in enumerate(headers, 1):
|
|
ws.cell(row=2, column=col_idx, value=h)
|
|
style_header_row(ws, 2, 6)
|
|
|
|
keys = [
|
|
("총 매출", "total_revenue", True, False),
|
|
("매출원가", "cogs", True, False),
|
|
("매출총이익", "gross_profit", True, False),
|
|
(" 이익률", "gross_margin", False, True),
|
|
("영업이익", "operating_profit", True, False),
|
|
(" 이익률", "operating_margin", False, True),
|
|
("순이익", "net_profit", True, False),
|
|
(" 이익률", "net_margin", False, True),
|
|
]
|
|
annual = financials["annual"]
|
|
for row_offset, (label, key, is_amount, is_pct) in enumerate(keys):
|
|
row_num = row_offset + 3
|
|
is_alt = row_offset % 2 == 0
|
|
|
|
name_cell = ws.cell(row=row_num, column=1, value=label)
|
|
name_cell.border = THIN_BORDER
|
|
name_cell.alignment = Alignment(horizontal="left")
|
|
if is_alt:
|
|
name_cell.fill = ALT_FILL
|
|
|
|
for q in range(1, 5):
|
|
val = financials["quarterly"][q][key]
|
|
cell = ws.cell(row=row_num, column=q + 1)
|
|
style_data_cell(cell, val, is_alt, is_amount, is_pct)
|
|
|
|
ann_cell = ws.cell(row=row_num, column=6)
|
|
style_data_cell(ann_cell, annual[key], is_alt, is_amount, is_pct)
|
|
|
|
ws.freeze_panes = "B3"
|
|
auto_col_width(ws, min_width=12)
|
|
ws.column_dimensions["A"].width = 14
|
|
|
|
|
|
def _sheet3_cost_structure(wb, data: dict, financials: dict):
|
|
"""시트3: 비용 구조 분석"""
|
|
ws = wb.create_sheet("비용 구조 분석")
|
|
year = data["year"]
|
|
annual_revenue = financials["annual"]["total_revenue"]
|
|
|
|
ws.merge_cells("A1:E1")
|
|
ws["A1"].value = f"{year}년 비용 구조 분석 — 항목별 금액 및 매출 비율"
|
|
ws["A1"].font = Font(bold=True, size=14)
|
|
ws["A1"].alignment = Alignment(horizontal="center")
|
|
|
|
headers = ["비용 항목", "분류", "성격", "연간 금액", "매출 비율"]
|
|
for col_idx, h in enumerate(headers, 1):
|
|
ws.cell(row=2, column=col_idx, value=h)
|
|
style_header_row(ws, 2, 5)
|
|
|
|
# 항목별 연간 합산
|
|
annual_expenses = {}
|
|
for m, month_data in data["months"].items():
|
|
for k, v in month_data["expense"].items():
|
|
annual_expenses[k] = annual_expenses.get(k, 0) + v
|
|
|
|
for row_offset, (cat_key, cat_name, cat_group, cost_type) in enumerate(EXPENSE_CATEGORIES):
|
|
row_num = row_offset + 3
|
|
is_alt = row_offset % 2 == 0
|
|
amount = annual_expenses.get(cat_key, 0)
|
|
ratio = (amount / annual_revenue * 100) if annual_revenue else 0
|
|
|
|
for col_idx, val in enumerate([cat_name, cat_group, cost_type, amount, ratio], 1):
|
|
cell = ws.cell(row=row_num, column=col_idx)
|
|
is_amount = col_idx == 4
|
|
is_pct = col_idx == 5
|
|
style_data_cell(cell, val, is_alt, is_amount, is_pct)
|
|
if col_idx <= 3:
|
|
cell.alignment = Alignment(horizontal="left")
|
|
|
|
# 합계 행
|
|
total_row = len(EXPENSE_CATEGORIES) + 3
|
|
total_expense = sum(annual_expenses.values())
|
|
total_ratio = (total_expense / annual_revenue * 100) if annual_revenue else 0
|
|
|
|
ws.cell(row=total_row, column=1, value="합 계").font = Font(bold=True)
|
|
total_amt_cell = ws.cell(row=total_row, column=4, value=total_expense)
|
|
total_amt_cell.number_format = NUM_FMT
|
|
total_amt_cell.font = Font(bold=True)
|
|
total_pct_cell = ws.cell(row=total_row, column=5, value=total_ratio)
|
|
total_pct_cell.number_format = PCT_FMT
|
|
total_pct_cell.font = Font(bold=True)
|
|
|
|
for col in range(1, 6):
|
|
ws.cell(row=total_row, column=col).border = THIN_BORDER
|
|
ws.cell(row=total_row, column=col).fill = PatternFill(start_color="DBEAFE", end_color="DBEAFE", fill_type="solid")
|
|
|
|
auto_col_width(ws, min_width=10)
|
|
ws.column_dimensions["A"].width = 16
|
|
ws.freeze_panes = "A3"
|
|
|
|
|
|
def _sheet4_advice(wb, data: dict, advice_list: list):
|
|
"""시트4: 회계사 조언 리포트"""
|
|
ws = wb.create_sheet("회계사 조언 리포트")
|
|
year = data["year"]
|
|
|
|
ws.merge_cells("A1:D1")
|
|
ws["A1"].value = f"{year}년 회계 전문가 조언 리포트 — {data['industry']}"
|
|
ws["A1"].font = Font(bold=True, size=14)
|
|
ws["A1"].alignment = Alignment(horizontal="center")
|
|
|
|
ws.merge_cells("A2:D2")
|
|
ws["A2"].value = f"생성일시: {datetime.now().strftime('%Y-%m-%d %H:%M')} | {BRAND_NAME} ({BRAND_URL})"
|
|
ws["A2"].font = Font(size=10, color="64748B")
|
|
ws["A2"].alignment = Alignment(horizontal="center")
|
|
|
|
headers = ["No.", "분류", "우선순위", "조언 제목 및 상세 내용"]
|
|
for col_idx, h in enumerate(headers, 1):
|
|
ws.cell(row=3, column=col_idx, value=h)
|
|
style_header_row(ws, 3, 4)
|
|
|
|
priority_colors = {
|
|
"긴급": "FEE2E2",
|
|
"주의": "FEF3C7",
|
|
"정보": "EFF6FF",
|
|
}
|
|
priority_font_colors = {
|
|
"긴급": "DC2626",
|
|
"주의": "D97706",
|
|
"정보": "1D4ED8",
|
|
}
|
|
|
|
current_row = 4
|
|
for i, adv in enumerate(advice_list, 1):
|
|
# 상세 내용은 줄 수에 따라 행 병합
|
|
detail_lines = adv["detail"].split("\n")
|
|
row_height = max(len(detail_lines) * 15, 40)
|
|
|
|
for col_idx in range(1, 5):
|
|
cell = ws.cell(row=current_row, column=col_idx)
|
|
cell.border = THIN_BORDER
|
|
fill_color = priority_colors.get(adv["priority"], "FFFFFF")
|
|
cell.fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid")
|
|
|
|
ws.cell(row=current_row, column=1, value=i).alignment = Alignment(horizontal="center", vertical="top")
|
|
ws.cell(row=current_row, column=2, value=adv["category"]).alignment = Alignment(horizontal="center", vertical="top")
|
|
|
|
pri_cell = ws.cell(row=current_row, column=3, value=adv["priority"])
|
|
pri_cell.alignment = Alignment(horizontal="center", vertical="top")
|
|
pri_cell.font = Font(
|
|
color=priority_font_colors.get(adv["priority"], "000000"),
|
|
bold=True
|
|
)
|
|
|
|
content = f"■ {adv['title']}\n\n{adv['detail']}"
|
|
content_cell = ws.cell(row=current_row, column=4, value=content)
|
|
content_cell.alignment = Alignment(
|
|
horizontal="left", vertical="top", wrap_text=True
|
|
)
|
|
|
|
ws.row_dimensions[current_row].height = row_height
|
|
current_row += 1
|
|
|
|
ws.column_dimensions["A"].width = 5
|
|
ws.column_dimensions["B"].width = 14
|
|
ws.column_dimensions["C"].width = 10
|
|
ws.column_dimensions["D"].width = 70
|
|
ws.freeze_panes = "A4"
|
|
|
|
|
|
def _sheet5_vat(wb, data: dict, financials: dict):
|
|
"""시트5: 부가세 신고 준비 자료"""
|
|
ws = wb.create_sheet("부가세 신고 준비")
|
|
year = data["year"]
|
|
|
|
ws.merge_cells("A1:F1")
|
|
ws["A1"].value = f"{year}년 부가세 신고 준비 자료"
|
|
ws["A1"].font = Font(bold=True, size=14)
|
|
ws["A1"].alignment = Alignment(horizontal="center")
|
|
|
|
# 1기 (1~6월) / 2기 (7~12월) 분리
|
|
periods = {
|
|
f"1기 (1~6월) — {year}년 7월 신고": range(1, 7),
|
|
f"2기 (7~12월) — {year + 1}년 1월 신고": range(7, 13),
|
|
}
|
|
|
|
income_cats = INCOME_CATEGORIES[data["industry"]]
|
|
current_row = 3
|
|
|
|
for period_label, month_range in periods.items():
|
|
# 기간 헤더
|
|
ws.merge_cells(f"A{current_row}:F{current_row}")
|
|
period_cell = ws.cell(row=current_row, column=1, value=period_label)
|
|
period_cell.fill = PatternFill(start_color="1E40AF", end_color="1E40AF", fill_type="solid")
|
|
period_cell.font = Font(color="FFFFFF", bold=True, size=12)
|
|
period_cell.alignment = Alignment(horizontal="center")
|
|
current_row += 1
|
|
|
|
# 소계 헤더
|
|
headers = ["구분", "항목", "과세표준(공급가액)", "세율", "부가세액", "비고"]
|
|
for col_idx, h in enumerate(headers, 1):
|
|
ws.cell(row=current_row, column=col_idx, value=h)
|
|
style_header_row(ws, current_row, 6)
|
|
current_row += 1
|
|
|
|
# 매출 내역
|
|
period_revenue = 0
|
|
for cat_key, cat_name in income_cats:
|
|
period_amount = sum(
|
|
data["months"][m]["income"].get(cat_key, 0)
|
|
for m in month_range if m in data["months"]
|
|
)
|
|
supply_value = int(period_amount / 1.1) # 공급가액 (부가세 포함 금액 기준)
|
|
vat = period_amount - supply_value
|
|
period_revenue += period_amount
|
|
|
|
is_alt = (current_row % 2 == 0)
|
|
for col_idx in range(1, 7):
|
|
cell = ws.cell(row=current_row, column=col_idx)
|
|
cell.border = THIN_BORDER
|
|
if is_alt:
|
|
cell.fill = ALT_FILL
|
|
|
|
ws.cell(row=current_row, column=1, value="매출").alignment = Alignment(horizontal="center")
|
|
ws.cell(row=current_row, column=2, value=cat_name)
|
|
supply_cell = ws.cell(row=current_row, column=3, value=supply_value)
|
|
supply_cell.number_format = NUM_FMT
|
|
ws.cell(row=current_row, column=4, value=0.10).number_format = PCT_FMT
|
|
vat_cell = ws.cell(row=current_row, column=5, value=vat)
|
|
vat_cell.number_format = NUM_FMT
|
|
ws.cell(row=current_row, column=6, value="과세")
|
|
current_row += 1
|
|
|
|
# 매입 세금계산서 관련 비용
|
|
vatable_expenses = [
|
|
("cogs_goods", "상품/재료비"),
|
|
("cogs_outsource","외주비"),
|
|
("mkt_ads", "광고비"),
|
|
("etc_supplies", "소모품비"),
|
|
]
|
|
for exp_key, exp_name in vatable_expenses:
|
|
period_amount = sum(
|
|
data["months"][m]["expense"].get(exp_key, 0)
|
|
for m in month_range if m in data["months"]
|
|
)
|
|
if period_amount == 0:
|
|
continue
|
|
supply_value = int(period_amount / 1.1)
|
|
vat = period_amount - supply_value
|
|
|
|
is_alt = (current_row % 2 == 0)
|
|
for col_idx in range(1, 7):
|
|
cell = ws.cell(row=current_row, column=col_idx)
|
|
cell.border = THIN_BORDER
|
|
if is_alt:
|
|
cell.fill = ALT_FILL
|
|
|
|
ws.cell(row=current_row, column=1, value="매입").alignment = Alignment(horizontal="center")
|
|
ws.cell(row=current_row, column=2, value=exp_name)
|
|
supply_cell = ws.cell(row=current_row, column=3, value=supply_value)
|
|
supply_cell.number_format = NUM_FMT
|
|
ws.cell(row=current_row, column=4, value=0.10).number_format = PCT_FMT
|
|
vat_cell = ws.cell(row=current_row, column=5, value=vat)
|
|
vat_cell.number_format = NUM_FMT
|
|
ws.cell(row=current_row, column=6, value="매입공제")
|
|
current_row += 1
|
|
|
|
current_row += 2 # 간격
|
|
|
|
auto_col_width(ws, min_width=10)
|
|
ws.column_dimensions["A"].width = 8
|
|
ws.column_dimensions["B"].width = 16
|
|
ws.column_dimensions["F"].width = 12
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 입력 양식 Excel 생성
|
|
# ─────────────────────────────────────────────
|
|
|
|
def create_input_template(industry: str, year: int, output_path: str):
|
|
"""업종별 입력 양식 Excel 생성"""
|
|
wb = openpyxl.Workbook()
|
|
wb.remove(wb.active)
|
|
income_cats = INCOME_CATEGORIES[industry]
|
|
|
|
for month_idx in range(1, 13):
|
|
sheet_name = f"{month_idx}월"
|
|
ws = wb.create_sheet(sheet_name)
|
|
|
|
# 타이틀
|
|
ws.merge_cells("A1:C1")
|
|
ws["A1"].value = f"{year}년 {MONTHS_KR[month_idx - 1]} 입력 양식 — {industry}"
|
|
ws["A1"].font = Font(bold=True, size=12)
|
|
ws["A1"].alignment = Alignment(horizontal="center")
|
|
|
|
# 컬럼 헤더
|
|
for col_idx, h in enumerate(["항목 키", "항목명", "금액 (원)"], 1):
|
|
ws.cell(row=2, column=col_idx, value=h)
|
|
style_header_row(ws, 2, 3)
|
|
|
|
# 수입 섹션
|
|
ws.cell(row=3, column=1, value="──── 수입 ────")
|
|
ws.cell(row=3, column=1).font = Font(bold=True, color="1D4ED8")
|
|
ws.merge_cells(f"A3:C3")
|
|
ws["A3"].fill = PatternFill(start_color="DBEAFE", end_color="DBEAFE", fill_type="solid")
|
|
ws["A3"].alignment = Alignment(horizontal="center")
|
|
|
|
row = 4
|
|
for cat_key, cat_name in income_cats:
|
|
ws.cell(row=row, column=1, value=cat_key)
|
|
ws.cell(row=row, column=2, value=cat_name)
|
|
amount_cell = ws.cell(row=row, column=3, value=0)
|
|
amount_cell.number_format = NUM_FMT
|
|
for col in range(1, 4):
|
|
ws.cell(row=row, column=col).border = THIN_BORDER
|
|
row += 1
|
|
|
|
# 지출 섹션
|
|
ws.cell(row=row, column=1, value="──── 지출 ────")
|
|
ws.merge_cells(f"A{row}:C{row}")
|
|
ws[f"A{row}"].font = Font(bold=True, color="DC2626")
|
|
ws[f"A{row}"].fill = PatternFill(start_color="FEE2E2", end_color="FEE2E2", fill_type="solid")
|
|
ws[f"A{row}"].alignment = Alignment(horizontal="center")
|
|
row += 1
|
|
|
|
for cat_key, cat_name, cat_group, cost_type in EXPENSE_CATEGORIES:
|
|
ws.cell(row=row, column=1, value=cat_key)
|
|
ws.cell(row=row, column=2, value=f"[{cat_group}] {cat_name} ({cost_type})")
|
|
amount_cell = ws.cell(row=row, column=3, value=0)
|
|
amount_cell.number_format = NUM_FMT
|
|
for col in range(1, 4):
|
|
ws.cell(row=row, column=col).border = THIN_BORDER
|
|
row += 1
|
|
|
|
ws.column_dimensions["A"].width = 20
|
|
ws.column_dimensions["B"].width = 30
|
|
ws.column_dimensions["C"].width = 18
|
|
ws.freeze_panes = "A3"
|
|
|
|
wb.save(output_path)
|
|
print(f" [완료] 입력 양식이 생성되었습니다: {output_path}")
|
|
print(" ※ C열 (금액) 셀에 숫자를 입력하고 저장하세요.")
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 메인 실행 흐름
|
|
# ─────────────────────────────────────────────
|
|
|
|
def main():
|
|
print_banner()
|
|
|
|
# 작업 디렉토리 설정
|
|
work_dir = os.getcwd()
|
|
print(f" 작업 디렉토리: {work_dir}\n")
|
|
|
|
# ── 메인 메뉴 ──
|
|
while True:
|
|
print_separator("메인 메뉴")
|
|
print(" [1] 새 회계 분석 시작")
|
|
print(" [2] 입력 양식(Excel) 생성")
|
|
print(" [0] 종료")
|
|
choice = input("\n선택하세요: ").strip()
|
|
|
|
if choice == "0":
|
|
print("\n 프로그램을 종료합니다. 감사합니다!\n")
|
|
break
|
|
|
|
elif choice == "2":
|
|
# 입력 양식 생성
|
|
industry = select_industry()
|
|
year = select_year()
|
|
template_path = os.path.join(
|
|
work_dir,
|
|
f"입력양식_{industry}_{year}.xlsx"
|
|
)
|
|
create_input_template(industry, year, template_path)
|
|
|
|
elif choice == "1":
|
|
# 분석 시작
|
|
industry = select_industry()
|
|
year = select_year()
|
|
|
|
# 데이터 입력 방식 선택
|
|
print_separator("데이터 입력 방식")
|
|
print(" [A] 직접 입력 (프로그램에서 월별 금액 입력)")
|
|
print(" [B] Excel 파일에서 가져오기")
|
|
input_method = input("\n선택하세요 (A/B): ").strip().upper()
|
|
|
|
data = None
|
|
if input_method == "B":
|
|
data = collect_data_excel(industry, year)
|
|
if data is None: # A 선택 or B 실패 시 직접 입력
|
|
if input_method == "B":
|
|
print(" → 직접 입력 방식으로 전환합니다.")
|
|
data = collect_data_manual(industry, year)
|
|
|
|
# 재무 계산
|
|
print("\n 재무 지표를 계산하는 중...")
|
|
financials = calc_all_financials(data)
|
|
|
|
# 조언 생성
|
|
advice_list = generate_advice(data, financials)
|
|
|
|
# 콘솔 요약 출력
|
|
print_summary(data, financials, advice_list)
|
|
|
|
# Excel 보고서 출력
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
|
report_path = os.path.join(
|
|
work_dir,
|
|
f"회계보고서_{industry}_{year}_{timestamp}.xlsx"
|
|
)
|
|
|
|
print_separator("Excel 보고서 생성")
|
|
print(f" 보고서 저장 경로: {report_path}")
|
|
create_report_excel(data, financials, advice_list, report_path)
|
|
|
|
# 완료 메시지
|
|
print_separator("완료")
|
|
print(f" 보고서가 성공적으로 생성되었습니다!")
|
|
print(f" 파일: {report_path}")
|
|
print("\n 포함된 시트:")
|
|
print(" • 시트1: 월별 손익계산서 (12개월)")
|
|
print(" • 시트2: 분기별 요약")
|
|
print(" • 시트3: 비용 구조 분석")
|
|
print(" • 시트4: 회계사 조언 리포트")
|
|
print(" • 시트5: 부가세 신고 준비 자료")
|
|
|
|
# 데이터 저장 여부 (JSON 백업)
|
|
save_json = input("\n 입력 데이터를 JSON으로 백업하시겠습니까? (y/N): ").strip().lower()
|
|
if save_json == "y":
|
|
json_path = report_path.replace(".xlsx", "_data.json")
|
|
with open(json_path, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
print(f" [완료] 데이터 백업: {json_path}")
|
|
|
|
else:
|
|
print(" [오류] 0, 1, 2 중 하나를 입력하세요.")
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 진입점
|
|
# ─────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
print("\n\n [중단] 사용자가 프로그램을 종료했습니다.")
|
|
print(" 저장된 데이터는 유지됩니다.\n")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
print(f"\n [오류] 예기치 않은 오류가 발생했습니다: {e}")
|
|
print(" 문의: bgg8988@gmail.com\n")
|
|
sys.exit(1)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 쟁승메이드 (JaengseungMade) — 프리미엄 개발 서비스
|
|
# ▸ 웹사이트 : jaengseung-made.vercel.app
|
|
# ▸ 이메일 : bgg8988@gmail.com
|
|
# ▸ 연락처 : 010-3907-1392
|
|
# 7년차 대기업 백엔드 개발자가 만드는 실전 자동화 솔루션
|
|
# ═══════════════════════════════════════════════════════════════
|