feat: create validated ZUGFeRD invoices in one console command

This commit is contained in:
admin committed 2026-10-16 09:29:08 +02:00
1 parent e0c15fc7f2
commit a643cf5227
30 files changed
+1509 -207

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
"""Opt in: JR_EINVOICE_INTEGRATION=1 python -m unittest discover -s tests -v."""
import os
import io
import json
from unittest.mock import patch
import logging
import tempfile
import unittest
from pathlib import Path
from email import policy
from email.parser import BytesParser
import xml.etree.ElementTree as ET
import yaml
from jr_einvoice.pipeline import build_in_folder
from jr_einvoice.delivery import prepare_email
from jr_einvoice.mustang import validate, MustangError
from jr_einvoice.facturx import NS
ROOT = Path(__file__).resolve().parent.parent
JAR = Path(os.environ.get('JR_EINVOICE_MUSTANG_JAR', ROOT / 'tools/mustang/Mustang-CLI.jar'))
LOGGER = logging.getLogger('integration')
@unittest.skipUnless(os.environ.get('JR_EINVOICE_INTEGRATION') == '1', 'Opt in with JR_EINVOICE_INTEGRATION=1')
class MustangIntegrationTest(unittest.TestCase):
def test_build_email_and_reject_bad_xml_and_plain_pdf(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
data = yaml.safe_load((ROOT / 'examples/invoice.sample.yaml').read_text())
data['buyer']['email'] = 'rechnung@example.invalid'
data['items'][1]['tax_rate'] = 7
(root / 'invoice.yaml').write_text(yaml.safe_dump(data))
result = build_in_folder(root, 'out', ROOT / 'templates/invoice.html.j2', LOGGER, JAR, True, True)
self.assertIn('status="valid"', result.validate_report.read_text())
path = prepare_email(root, JAR, LOGGER)
message = BytesParser(policy=policy.default).parsebytes(path.read_bytes())
self.assertEqual(message['From'], data['seller']['email'])
self.assertEqual(message['To'], 'rechnung@example.invalid')
attachments = list(message.iter_attachments())
self.assertEqual(len(attachments), 1)
self.assertEqual(attachments[0].get_payload(decode=True), result.facturx_pdf.read_bytes())
self.assertTrue((root / 'out/manifest.json').is_file())
# A valid PDF/A without invoice XML must not be an accepted e-invoice.
with self.assertRaises(MustangError):
validate(JAR, result.invoice_pdf, root / 'plain-report.xml', LOGGER)
xml = ET.parse(result.facturx_xml)
xml.getroot().find('.//ram:GrandTotalAmount', NS).text = '0.01'
xml.write(root / 'broken.xml', encoding='utf-8', xml_declaration=True)
with self.assertRaises(MustangError):
validate(JAR, root / 'broken.xml', root / 'bad-report.xml', LOGGER)
self.assertTrue((root / 'bad-report.xml').is_file())
def test_one_call_new_existing_customer_and_rejected_invalid_unit(self):
from jr_einvoice.quick_cli import main
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
presets = root / 'presets'
presets.mkdir()
sample = yaml.safe_load((ROOT / 'examples/invoice.sample.yaml').read_text())
(presets / 'seller.default.yaml').write_text(yaml.safe_dump({
'seller': sample['seller'], 'payment': sample['payment']}))
base = ['--create', '--Customer', 'Beliebiger neuer / alter Kunde GmbH',
'--Entry', 'Geleistete Stunden Oktober 2026', '--Volume', '120',
'--Price', '85,00 €', '--Date', '2026-10-31', '--root', str(root / 'invoices'),
'--mustang-jar', str(JAR), '--json']
with patch.dict(os.environ, {'JR_EINVOICE_PRESETS': str(presets)}):
for expected, extra in [('JR-2026-0001', ['--Street', 'Testweg 1', '--PostalCode', '10115', '--City', 'Berlin']),
('JR-2026-0002', [])]:
with patch('sys.stdout', new_callable=io.StringIO) as output:
main(base + extra)
result = json.loads(output.getvalue())
self.assertEqual(result['invoice_number'], expected)
self.assertEqual(result['technical_validation'], 'valid')
self.assertTrue(Path(result['pdf']).is_file())
xml = ET.parse(Path(result['invoice_dir']) / 'out/factur-x.xml')
self.assertEqual(xml.getroot().findtext('.//ram:GrandTotalAmount', namespaces=NS), '12138.00')
with patch('sys.stdout', new_callable=io.StringIO) as output, patch('sys.stderr', new_callable=io.StringIO):
with self.assertRaises(SystemExit) as failure:
main(base + ['--Unit', 'ZZZ'])
self.assertEqual(failure.exception.code, 1)
self.assertNotIn('technical_validation', output.getvalue())
failed = next((root / 'invoices').glob('*JR-2026-0003*'))
self.assertFalse((failed / 'Rechnung_JR-2026-0003.pdf').exists())
self.assertTrue((failed / 'out/validation/validate_report.xml').exists())