138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
from __future__ import annotations
|
|
from pathlib import Path
|
|
import shutil
|
|
from datetime import date, timedelta
|
|
|
|
from .presets import load_yaml, deep_merge, load_customer_preset
|
|
from .invoice_number import suggest_next_invoice_number
|
|
from .folder_naming import default_invoice_folder_name
|
|
|
|
import yaml
|
|
|
|
def _dump_yaml(data: dict, path: Path) -> None:
|
|
path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
|
|
|
|
def init_invoice_folder(
|
|
target_dir: Path,
|
|
repo_root: Path,
|
|
logger,
|
|
force: bool = False,
|
|
customer: str | None = None,
|
|
invoice_number: str | None = None,
|
|
due_days: int = 14,
|
|
invoices_root: Path | None = None,
|
|
project: str | None = None,
|
|
) -> Path:
|
|
target_dir = target_dir.resolve()
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create standard subfolders
|
|
(target_dir / "assets").mkdir(parents=True, exist_ok=True)
|
|
for p in [target_dir / "out", target_dir / "out" / "logs", target_dir / "out" / "validation"]:
|
|
p.mkdir(parents=True, exist_ok=True)
|
|
(p / ".gitkeep").write_text("", encoding="utf-8")
|
|
|
|
# Copy default logo if not present (or force)
|
|
src_logo = repo_root / "assets" / "logo.svg"
|
|
dst_logo = target_dir / "assets" / "logo.svg"
|
|
if src_logo.exists() and (force or not dst_logo.exists()):
|
|
shutil.copy2(src_logo, dst_logo)
|
|
logger.info("Copied default logo to %s", dst_logo)
|
|
|
|
# Base invoice template
|
|
base_tpl = load_yaml(repo_root / "examples" / "invoice.init.yaml")
|
|
|
|
# Apply seller defaults
|
|
seller_defaults = load_yaml(repo_root / "presets" / "seller.default.yaml")
|
|
merged = deep_merge(base_tpl, seller_defaults)
|
|
|
|
# Apply customer preset
|
|
if customer:
|
|
preset = load_customer_preset(repo_root, customer)
|
|
if preset:
|
|
merged = deep_merge(merged, preset)
|
|
logger.info("Applied customer preset: %s", customer)
|
|
else:
|
|
logger.warning("Customer preset not found for: %s (continuing with generic buyer)", customer)
|
|
|
|
# Smart defaults for meta/payment
|
|
today = date.today()
|
|
merged.setdefault("meta", {})
|
|
merged["meta"]["invoice_date"] = str(today)
|
|
merged["meta"]["service_date"] = str(today)
|
|
|
|
# invoice number
|
|
if not invoice_number and invoices_root:
|
|
try:
|
|
invoice_number = suggest_next_invoice_number(invoices_root)
|
|
logger.info("Suggested next invoice number: %s (root=%s)", invoice_number, invoices_root)
|
|
except Exception as e:
|
|
logger.warning("Failed to suggest invoice number from %s: %s", invoices_root, e)
|
|
|
|
if invoice_number:
|
|
merged["meta"]["invoice_number"] = invoice_number
|
|
|
|
# project -> default first item name
|
|
if project:
|
|
merged.setdefault("items", [])
|
|
if len(merged["items"]) == 0:
|
|
merged["items"] = [{
|
|
"name": project,
|
|
"description": "Leistung/Beschreibung",
|
|
"quantity": 1,
|
|
"unit": "C62",
|
|
"unit_price_net": 100.00,
|
|
"tax_rate": 19.0
|
|
}]
|
|
else:
|
|
if merged["items"][0].get("name") in ("Leistung", "Service", None, ""):
|
|
merged["items"][0]["name"] = project
|
|
logger.info("Applied project default: %s", project)
|
|
|
|
merged.setdefault("payment", {})
|
|
merged["payment"]["due_date"] = str(today + timedelta(days=due_days))
|
|
inv_no_for_ref = merged["meta"].get("invoice_number", "")
|
|
merged["payment"]["reference"] = f"Rechnung {inv_no_for_ref}".strip()
|
|
|
|
dst_yaml = target_dir / "invoice.yaml"
|
|
if dst_yaml.exists() and not force:
|
|
raise FileExistsError(f"{dst_yaml} already exists (use --force to overwrite)")
|
|
_dump_yaml(merged, dst_yaml)
|
|
|
|
logger.info("Initialized invoice folder: %s", target_dir)
|
|
return target_dir
|
|
|
|
def init_invoice_under_root(
|
|
invoices_root: Path,
|
|
repo_root: Path,
|
|
logger,
|
|
customer: str | None,
|
|
project: str | None,
|
|
invoice_number: str | None,
|
|
due_days: int,
|
|
force: bool,
|
|
) -> Path:
|
|
invoices_root = invoices_root.expanduser().resolve()
|
|
invoices_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
# suggest number if missing
|
|
if not invoice_number:
|
|
invoice_number = suggest_next_invoice_number(invoices_root)
|
|
logger.info("Auto invoice number: %s", invoice_number)
|
|
|
|
folder_name = default_invoice_folder_name(invoice_number, customer)
|
|
target_dir = invoices_root / folder_name
|
|
logger.info("Auto folder name: %s", target_dir)
|
|
|
|
return init_invoice_folder(
|
|
target_dir=target_dir,
|
|
repo_root=repo_root,
|
|
logger=logger,
|
|
force=force,
|
|
customer=customer,
|
|
invoice_number=invoice_number,
|
|
due_days=due_days,
|
|
invoices_root=invoices_root,
|
|
project=project,
|
|
)
|