85 lines
4.9 KiB
Python
85 lines
4.9 KiB
Python
"""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())
|