"""Fast monthly service billing from existing customer presets.""" import calendar import tempfile from .invoice_number import invoice_root_lock import re from datetime import date, timedelta from decimal import Decimal from pathlib import Path from .init import init_invoice_folder from .presets import load_yaml from .models import Invoice from .init import _dump_yaml from .folder_naming import default_invoice_folder_name MONTHS = ('Januar Februar März April Mai Juni Juli August September Oktober November Dezember').split() def _monthly_invoice(root: Path, repo_root: Path, logger, customer: str, month: str, hours: Decimal, number: str, issued: date | None = None, rate: Decimal | None = None, due_days: int = 30) -> Path: if not re.fullmatch(r'\d{4}-\d{2}', month): raise ValueError('Month must be YYYY-MM') year, m = map(int, month.split('-')) start, end = date(year, m, 1), date(year, m, calendar.monthrange(year, m)[1]) if not hours.is_finite() or hours <= 0 or due_days < 0: raise ValueError('Hours must be positive and due_days nonnegative') target = root.expanduser().resolve() / default_invoice_folder_name(number, customer) # Explicit number required: existing PDF invoices may live outside this root. if target.exists(): raise FileExistsError(f'Invoice folder exists: {target}') for existing in root.expanduser().glob('*/invoice.yaml'): if str(load_yaml(existing).get('meta', {}).get('invoice_number', '')) == number: raise ValueError(f'Invoice number already used: {number}') with tempfile.TemporaryDirectory(prefix='.draft-', dir=root.expanduser().resolve()) as staging: work = Path(staging) / 'invoice' init_invoice_folder(work, repo_root, logger, customer=customer, invoice_number=number, due_days=due_days) path = work / 'invoice.yaml' data = load_yaml(path) if len(data['items']) != 1 or data['items'][0]['unit'] != 'HUR': raise ValueError('Monthly billing requires a preset with one HUR line') issued = issued or date.today() data['meta'].update(invoice_date=str(issued), service_date=str(end), service_period_start=str(start), service_period_end=str(end)) data['payment']['due_date'] = str(issued + timedelta(days=due_days)) item = data['items'][0] item['quantity'] = str(hours) item['name'] = f'Geleistete Stunden {MONTHS[m-1]} {year}' if rate is not None: item['unit_price_net'] = str(rate) Invoice.model_validate(data) _dump_yaml(data, path) work.rename(target) return target def monthly_invoice(root: Path, *args, **kwargs) -> Path: with invoice_root_lock(root): return _monthly_invoice(root, *args, **kwargs)