Files

58 lines
2.8 KiB
Python

"""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)