"""Friendly CLI: einvoice --create --Customer ... --Entry ... --Volume ... --Price ...""" import argparse from datetime import date import json from pathlib import Path import sys from .create import create_invoice, decimal_input, find_customer, save_customer from .logging_utils import setup_logging from .paths import private_root, presets_root, resource_root def parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(description='Mit einem Aufruf eine validierte ZUGFeRD-Rechnung als PDF erzeugen.', allow_abbrev=False) mode = p.add_mutually_exclusive_group(required=True) mode.add_argument('--create', action='store_true', help='Rechnung anlegen, PDF erzeugen und validieren') mode.add_argument('--customers', action='store_true', help='Gespeicherte Kunden anzeigen') p.add_argument('--Customer', '--customer', dest='customer', help='Gespeicherter Kundenname oder neuer Kunde') p.add_argument('--Entry', '--entry', dest='entry', help='Leistungsbeschreibung') p.add_argument('--Volume', '--volume', dest='volume', type=decimal_input, help='Menge, z. B. 120 oder 72,5') p.add_argument('--Price', '--price', dest='price', type=decimal_input, help='Nettopreis je Einheit, z. B. "85,00 €"') p.add_argument('--Unit', '--unit', dest='unit', default='HUR', help='Einheit: HUR=Stunden (Standard), C62=Stück') p.add_argument('--Tax', '--tax', dest='tax', type=decimal_input, default=decimal_input('19'), help='USt in Prozent, Standard 19') p.add_argument('--Number', '--number', dest='number', help='Freie Nummer; Standard automatisch JR-JJJJ-NNNN') p.add_argument('--Date', '--date', dest='issued', type=date.fromisoformat, help='Rechnungsdatum YYYY-MM-DD, Standard heute') p.add_argument('--Month', '--month', dest='month', help='Leistungsmonat YYYY-MM; auch aus deutschem Monatsnamen in Entry erkannt') p.add_argument('--ServiceDate', '--service-date', dest='service_date', type=date.fromisoformat, help='Einzelnes Leistungsdatum YYYY-MM-DD') p.add_argument('--DueDays', '--due-days', dest='due_days', type=int, default=30, help='Zahlungsziel in Tagen, Standard 30') p.add_argument('--BuyerReference', '--buyer-reference', dest='buyer_reference', help='Kundenreferenz') p.add_argument('--OrderReference', '--order-reference', dest='order_reference', help='Bestellnummer') p.add_argument('--Street', '--street', dest='street', help='Straße/Hausnummer bei neuem Kunden') p.add_argument('--PostalCode', '--postal-code', dest='postal_code', help='Postleitzahl bei neuem Kunden') p.add_argument('--City', '--city', dest='city', help='Ort bei neuem Kunden') p.add_argument('--Country', '--country', dest='country', default='DE', help='Ländercode, Standard DE') p.add_argument('--Email', '--email', dest='email', help='Rechnungsadresse bei neuem Kunden (optional)') p.add_argument('--VatId', '--vat-id', dest='vat_id', help='USt-ID bei neuem Kunden (optional)') p.add_argument('--root', type=Path, default=private_root() / 'invoices', help='Stammordner für Rechnungen') p.add_argument('--mustang-jar', type=Path, help='Abweichender Mustang-CLI-Pfad') p.add_argument('--json', action='store_true', help='Ergebnisse als JSON; keine interaktiven Fragen') p.add_argument('--non-interactive', action='store_true', help='Fehlende Daten als Fehler melden statt fragen') return p def main(argv=None): p = parser() args = p.parse_args(argv) presets = presets_root() try: if args.customers: for path in sorted((presets / 'customers').glob('*.yaml')): from .presets import load_yaml print(f'{path.stem}: {load_yaml(path).get("buyer", {}).get("name", "?")}') return missing = [flag for flag in ('customer', 'entry', 'volume', 'price') if getattr(args, flag) is None] if missing: p.error('Für --create fehlen: ' + ', '.join('--' + name.title() for name in missing)) if args.volume <= 0 or args.price < 0 or not (0 < args.tax <= 100) or args.due_days < 0: p.error('Menge muss positiv sein; Preis/Zahlungsziel nicht negativ; Steuer > 0 und <= 100') if find_customer(presets, args.customer) is None: fields = {'street': args.street, 'postal_code': args.postal_code, 'city': args.city} labels = {'street': 'Straße und Hausnummer', 'postal_code': 'Postleitzahl', 'city': 'Ort'} absent = [name for name, value in fields.items() if not value] if absent and (args.non_interactive or args.json or not sys.stdin.isatty()): raise ValueError('Neuer Kunde: bitte ' + ', '.join('--' + {'street':'Street','postal_code':'PostalCode','city':'City'}[name] for name in absent) + ' angeben') for name in absent: fields[name] = input(f'Neuer Kunde {args.customer} – {labels[name]}: ').strip() save_customer(presets, args.customer, {'name': args.customer, 'email': args.email, 'vat_id': args.vat_id, 'address': {**fields, 'country': args.country.upper()}}) if not args.json: print(f'Kunde gespeichert: {args.customer}', flush=True) else: if any([args.street, args.postal_code, args.city, args.email, args.vat_id]) or args.country != 'DE': raise ValueError('Kunde bereits gespeichert. Adressoptionen gelten nur bei Neuanlage; Stammdaten separat bearbeiten') from .cli import _default_mustang_jar result = create_invoice(root=args.root, resources=resource_root(), presets=presets, customer=args.customer, entry=args.entry, volume=args.volume, price=args.price, jar=args.mustang_jar or _default_mustang_jar(), logger=setup_logging('WARNING', None), number=args.number, issued=args.issued, month=args.month, service_date=args.service_date, unit=args.unit, tax=args.tax, due_days=args.due_days, buyer_reference=args.buyer_reference, order_reference=args.order_reference, progress=(lambda message: None) if args.json else lambda message: print(message, flush=True)) if args.json: print(json.dumps({'invoice_number': result.invoice.meta.invoice_number, 'invoice_dir': str(result.build.invoice_dir), 'pdf': str(result.attachment), 'validation_report': str(result.build.validate_report), 'log': str(result.build.out_dir / 'logs/run.log'), 'technical_validation': 'valid'}, ensure_ascii=False)) else: inv = result.invoice print(f'Leistung: {inv.meta.service_period_start or inv.meta.service_date}' + (f' bis {inv.meta.service_period_end}' if inv.meta.service_period_end else '')) print(f'PDF zum Anhängen an deine E-Mail:\n{result.attachment}') print(f'Prüfbericht: {result.build.validate_report}') print('Vor Versand Leistungsdaten, Betrag und Empfänger prüfen.') except Exception as exc: print(f'FEHLER: {exc}', file=sys.stderr) raise SystemExit(1) from exc if __name__ == '__main__': main()