feat: add D.intelligence Agent Corps (9 skills + shared infra)
Add 9 agent skills (#70-#77, #88) for D.intelligence business operations: brand guardian, brand editor, doc secretary, quotation manager, service architect, marketing manager, back office manager, account manager, and skill update meta-agent. Includes shared Python package (dintel), reference docs, document/quotation templates, service module CSVs, cross-device installer, and comprehensive user guide. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user