""" D.intelligence Quotation Generator Agent #73 - dintel-quotation-mgr Generates branded Excel .xlsx quotation files using dintel-shared utilities. This is a stub script -- extend with full implementation as needed. Usage: python generate_quotation.py --client "고객사명" --modules A3,T6 --output ./output/ Dependencies: - dintel-shared (../../dintel-shared/) - openpyxl """ from __future__ import annotations import argparse import sys from dataclasses import dataclass, field from datetime import date, timedelta from pathlib import Path from typing import Optional # --------------------------------------------------------------------------- # Pricing data (mirrors shared/pricing-reference.md) # --------------------------------------------------------------------------- MODULE_PRICING: dict[str, dict] = { # Analysis (진단) "A1": {"name": "비즈니스·브랜드 진단", "duration": "2-3주", "min": 3_000_000, "max": 5_000_000, "phase": "Analysis"}, "A2": {"name": "고객·소비자 분석", "duration": "3-4주", "min": 4_000_000, "max": 7_000_000, "phase": "Analysis"}, "A3": {"name": "데이터 분석 (웹·앱)", "duration": "3-5주", "min": 4_000_000, "max": 8_000_000, "phase": "Analysis"}, "A4": {"name": "디지털 마케팅 진단", "duration": "2-4주", "min": 3_000_000, "max": 6_000_000, "phase": "Analysis"}, "A5": {"name": "퍼포먼스 마케팅 진단", "duration": "2-3주", "min": 3_000_000, "max": 5_000_000, "phase": "Analysis"}, "A6": {"name": "운영·관리 진단", "duration": "2-3주", "min": 2_000_000, "max": 4_000_000, "phase": "Analysis"}, # Treatment (처방) "T1": {"name": "브랜드 스토리텔링 & 가이드", "duration": "4-8주", "min": 5_000_000, "max": 12_000_000, "phase": "Treatment"}, "T2": {"name": "고객 접점 경험 최적화", "duration": "4-6주", "min": 4_000_000, "max": 8_000_000, "phase": "Treatment"}, "T3": {"name": "디지털 자산 통합관리", "duration": "4-8주", "min": 6_000_000, "max": 15_000_000, "phase": "Treatment"}, "T4": {"name": "콘텐츠 마케팅", "duration": "4-8주", "min": 4_000_000, "max": 10_000_000, "phase": "Treatment"}, "T5": {"name": "광고·전환 최적화", "duration": "3-6주", "min": 4_000_000, "max": 8_000_000, "phase": "Treatment"}, "T6": {"name": "Brand Visibility Treatment", "duration": "4-12주", "min": 5_000_000, "max": 15_000_000, "phase": "Treatment"}, "T7": {"name": "운영 시스템·자동화", "duration": "4-8주", "min": 4_000_000, "max": 10_000_000, "phase": "Treatment"}, # Growth (성장) "G1": {"name": "퍼포먼스 마케팅", "duration": "월간", "min": 2_000_000, "max": 5_000_000, "phase": "Growth", "monthly": True}, "G2": {"name": "콘텐츠 마케팅 대행", "duration": "월간", "min": 3_000_000, "max": 6_000_000, "phase": "Growth", "monthly": True}, "G3": {"name": "모니터링·이슈관리", "duration": "월간", "min": 2_000_000, "max": 4_000_000, "phase": "Growth", "monthly": True}, "G4": {"name": "연간 계약·운영", "duration": "12개월", "min": 0, "max": 0, "phase": "Growth", "monthly": True}, } COMPLEXITY_PERCENTILE = { "standard": 0.30, "complex": 0.60, "enterprise": 0.90, } DISCOUNT_POLICIES = { "multi_3plus": {"label": "3개 모듈 이상 동시 계약", "rate": 0.15}, "analysis_treatment": {"label": "Analysis → Treatment 연계", "rate": 0.20}, "full_cycle": {"label": "Full cycle (A→T→G)", "rate": 0.25}, "g4_annual": {"label": "G4 연간 계약", "rate": 0.20}, "renewal": {"label": "재계약 (기존 고객)", "rate": 0.10, "stackable": True}, } # --------------------------------------------------------------------------- # Data classes # --------------------------------------------------------------------------- @dataclass class LineItem: code: str name: str phase: str complexity: str = "standard" base_price: int = 0 months: int = 1 # for Growth modules subtotal: int = 0 @dataclass class QuotationDraft: ref: str = "" client_name: str = "" industry: str = "" date_created: date = field(default_factory=date.today) validity_days: int = 30 line_items: list[LineItem] = field(default_factory=list) subtotal: int = 0 discount_label: str = "" discount_rate: float = 0.0 discount_amount: int = 0 total_before_vat: int = 0 is_renewal: bool = False # --------------------------------------------------------------------------- # Pricing logic # --------------------------------------------------------------------------- def calculate_price(code: str, complexity: str = "standard") -> int: """Calculate module price based on complexity percentile within range.""" module = MODULE_PRICING.get(code) if not module: raise ValueError(f"Unknown module code: {code}") pct = COMPLEXITY_PERCENTILE.get(complexity, 0.30) price_range = module["max"] - module["min"] return int(module["min"] + price_range * pct) def determine_discount(line_items: list[LineItem], is_renewal: bool = False) -> tuple[str, float]: """Determine the highest applicable base discount, plus renewal if applicable.""" phases = {item.phase for item in line_items} num_modules = len(line_items) has_g4 = any(item.code == "G4" for item in line_items) # Determine base discount (highest wins) base_label = "" base_rate = 0.0 if phases >= {"Analysis", "Treatment", "Growth"}: base_label = DISCOUNT_POLICIES["full_cycle"]["label"] base_rate = DISCOUNT_POLICIES["full_cycle"]["rate"] elif "Analysis" in phases and "Treatment" in phases: base_label = DISCOUNT_POLICIES["analysis_treatment"]["label"] base_rate = DISCOUNT_POLICIES["analysis_treatment"]["rate"] elif has_g4: base_label = DISCOUNT_POLICIES["g4_annual"]["label"] base_rate = DISCOUNT_POLICIES["g4_annual"]["rate"] elif num_modules >= 3: base_label = DISCOUNT_POLICIES["multi_3plus"]["label"] base_rate = DISCOUNT_POLICIES["multi_3plus"]["rate"] # Stack renewal discount if is_renewal and base_rate > 0: base_label += " + 재계약" base_rate = min(base_rate + 0.10, 0.35) elif is_renewal: base_label = DISCOUNT_POLICIES["renewal"]["label"] base_rate = 0.10 return base_label, base_rate def build_quotation( client_name: str, modules: list[tuple[str, str]], # [(code, complexity), ...] industry: str = "", is_renewal: bool = False, growth_months: int = 3, ) -> QuotationDraft: """Build a complete quotation draft.""" today = date.today() ref = f"DI-Q-{today.strftime('%Y%m%d')}-001" draft = QuotationDraft( ref=ref, client_name=client_name, industry=industry, date_created=today, is_renewal=is_renewal, ) for code, complexity in modules: module = MODULE_PRICING[code] price = calculate_price(code, complexity) months = growth_months if module.get("monthly") else 1 item = LineItem( code=code, name=module["name"], phase=module["phase"], complexity=complexity, base_price=price, months=months, subtotal=price * months, ) draft.line_items.append(item) draft.subtotal = sum(item.subtotal for item in draft.line_items) draft.discount_label, draft.discount_rate = determine_discount(draft.line_items, is_renewal) draft.discount_amount = int(draft.subtotal * draft.discount_rate) draft.total_before_vat = draft.subtotal - draft.discount_amount return draft # --------------------------------------------------------------------------- # Excel generation (stub -- requires openpyxl and dintel-shared) # --------------------------------------------------------------------------- def generate_xlsx(draft: QuotationDraft, output_dir: Path) -> Path: """Generate branded .xlsx quotation file. TODO: Implement full branded workbook using dintel-shared/src/dintel/excel.py. This stub creates a basic workbook structure. """ try: from openpyxl import Workbook from openpyxl.styles import Alignment, Font, PatternFill except ImportError: print("ERROR: openpyxl is required. Install with: pip install openpyxl", file=sys.stderr) sys.exit(1) wb = Workbook() # -- Sheet 1: Cover -- ws_cover = wb.active ws_cover.title = "표지" ws_cover["B2"] = "D.intelligence :: SMART Marketing Clinic ::" ws_cover["B2"].font = Font(name="Pretendard", size=16, bold=True) ws_cover["B4"] = "견적서 (Quotation)" ws_cover["B4"].font = Font(name="Pretendard", size=24, bold=True) ws_cover["B6"] = f"고객사: {draft.client_name}" ws_cover["B7"] = f"업종: {draft.industry}" ws_cover["B8"] = f"견적번호: {draft.ref}" ws_cover["B9"] = f"작성일: {draft.date_created.isoformat()}" ws_cover["B10"] = f"유효기간: {(draft.date_created + timedelta(days=draft.validity_days)).isoformat()}" ws_cover["B12"] = "DRAFT -- 검토 대기" ws_cover["B12"].font = Font(color="FF0000", bold=True, size=14) # -- Sheet 2: Scope -- ws_scope = wb.create_sheet("서비스 범위") headers = ["모듈 코드", "모듈명", "Phase", "복잡도", "비고"] for col, header in enumerate(headers, 1): cell = ws_scope.cell(row=1, column=col, value=header) cell.font = Font(bold=True) for row, item in enumerate(draft.line_items, 2): ws_scope.cell(row=row, column=1, value=item.code) ws_scope.cell(row=row, column=2, value=item.name) ws_scope.cell(row=row, column=3, value=item.phase) ws_scope.cell(row=row, column=4, value=item.complexity) # -- Sheet 3: Timeline (placeholder) -- ws_timeline = wb.create_sheet("일정") ws_timeline["A1"] = "Phase" ws_timeline["B1"] = "Module" ws_timeline["C1"] = "Duration" ws_timeline["A1"].font = Font(bold=True) ws_timeline["B1"].font = Font(bold=True) ws_timeline["C1"].font = Font(bold=True) for row, item in enumerate(draft.line_items, 2): ws_timeline.cell(row=row, column=1, value=item.phase) ws_timeline.cell(row=row, column=2, value=f"{item.code} {item.name}") module = MODULE_PRICING[item.code] ws_timeline.cell(row=row, column=3, value=module["duration"]) # -- Sheet 4: Pricing -- ws_pricing = wb.create_sheet("견적 내역") draft_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") price_headers = ["모듈 코드", "모듈명", "단가 (원)", "수량/개월", "소계 (원)"] for col, header in enumerate(price_headers, 1): cell = ws_pricing.cell(row=1, column=col, value=header) cell.font = Font(bold=True) for row, item in enumerate(draft.line_items, 2): ws_pricing.cell(row=row, column=1, value=item.code) ws_pricing.cell(row=row, column=2, value=item.name) price_cell = ws_pricing.cell(row=row, column=3, value=item.base_price) price_cell.fill = draft_fill price_cell.number_format = "#,##0" ws_pricing.cell(row=row, column=4, value=item.months) subtotal_cell = ws_pricing.cell(row=row, column=5, value=item.subtotal) subtotal_cell.fill = draft_fill subtotal_cell.number_format = "#,##0" summary_row = len(draft.line_items) + 3 ws_pricing.cell(row=summary_row, column=4, value="소계").font = Font(bold=True) ws_pricing.cell(row=summary_row, column=5, value=draft.subtotal).number_format = "#,##0" if draft.discount_rate > 0: summary_row += 1 ws_pricing.cell(row=summary_row, column=3, value=draft.discount_label) ws_pricing.cell(row=summary_row, column=4, value=f"-{int(draft.discount_rate * 100)}%") disc_cell = ws_pricing.cell(row=summary_row, column=5, value=-draft.discount_amount) disc_cell.number_format = "#,##0" disc_cell.font = Font(color="FF0000") summary_row += 1 ws_pricing.cell(row=summary_row, column=4, value="합계 (VAT 별도)").font = Font(bold=True, size=12) total_cell = ws_pricing.cell(row=summary_row, column=5, value=draft.total_before_vat) total_cell.font = Font(bold=True, size=12) total_cell.number_format = "#,##0" total_cell.fill = draft_fill summary_row += 1 ws_pricing.cell(row=summary_row, column=5, value="Andrew 검토 필요").font = Font(color="FF0000", italic=True) # -- Sheet 5: Terms -- ws_terms = wb.create_sheet("계약 조건") terms = [ ("결제 조건", "착수금 50% / 완료 후 50%"), ("견적 유효기간", "발행일로부터 30일"), ("부가세", "별도 (10%)"), ("범위 변경", "서면 합의 후 별도 견적"), ("계약 해지", "착수 전 전액 환불 / 착수 후 진행분 정산"), ("", ""), ("D.intelligence", "SMART Marketing Clinic"), ("Website", "dintelligence.co.kr"), ("담당자", "Andrew Yim"), ] for row, (label, value) in enumerate(terms, 1): ws_terms.cell(row=row, column=1, value=label).font = Font(bold=True) ws_terms.cell(row=row, column=2, value=value) # Save output_dir.mkdir(parents=True, exist_ok=True) filename = f"{draft.ref}_{draft.client_name.replace(' ', '_')}_DRAFT.xlsx" filepath = output_dir / filename wb.save(filepath) return filepath # --------------------------------------------------------------------------- # CLI entry point # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser(description="D.intelligence Quotation Generator") parser.add_argument("--client", required=True, help="Client name (고객사명)") parser.add_argument("--industry", default="", help="Client industry (업종)") parser.add_argument("--modules", required=True, help="Comma-separated module codes (e.g., A3,T6,G2)") parser.add_argument("--complexity", default="standard", choices=["standard", "complex", "enterprise"], help="Default complexity tier for all modules") parser.add_argument("--renewal", action="store_true", help="Existing client (재계약)") parser.add_argument("--growth-months", type=int, default=3, help="Number of months for Growth modules") parser.add_argument("--output", default="./output", help="Output directory") args = parser.parse_args() module_list = [(code.strip(), args.complexity) for code in args.modules.split(",")] draft = build_quotation( client_name=args.client, modules=module_list, industry=args.industry, is_renewal=args.renewal, growth_months=args.growth_months, ) filepath = generate_xlsx(draft, Path(args.output)) print(f"Quotation draft generated: {filepath}") print(f" Reference: {draft.ref}") print(f" Client: {draft.client_name}") print(f" Modules: {', '.join(item.code for item in draft.line_items)}") print(f" Subtotal: {draft.subtotal:,}원") if draft.discount_rate > 0: print(f" Discount: {draft.discount_label} (-{int(draft.discount_rate * 100)}%, -{draft.discount_amount:,}원)") print(f" Total (VAT 별도): {draft.total_before_vat:,}원") print() print("STATUS: DRAFT -- Andrew 검토 대기") if __name__ == "__main__": main()