Initial version of the E-Invoice solution of JR IT Services
This commit is contained in:
137
jr_einvoice/init.py
Normal file
137
jr_einvoice/init.py
Normal file
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user