feat: create validated ZUGFeRD invoices in one console command
This commit is contained in:
1 parent
e0c15fc7f2
commit
a643cf5227
30 files changed
+1509
-207
No files matched your search
@@ -0,0 +1,103 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user