feat: BT-46 Kundennummer, BT-10 Leitweg-ID, BT-17 Vergabeangebot, Angebot->Freitext, format-abh. Pflichtfelder
- Kundennummer jetzt BT-46 (eigenes Feld), getrennt von Leitweg-ID (BT-10) - BT-10 Leitweg-ID: eigenes Feld, leer per Default; nur bei XRechnung Pflicht (BR-DE-15), bei ZUGFeRD optional (kein Blocker, kein XML-Eintrag wenn leer) - BT-12 nur noch Vertrag; Vergabeangebot/Ausschreibung als neues Feld BT-17 (UBL OriginatorDocumentReference, CII AdditionalReferencedDocument TypeCode 50) - 'Angebot <Nr> vom <Datum>' wird erkannt und in den Freitext (BT-22) uebernommen - BT-49 elektronische Adresse: bei ZUGFeRD optional (kein Blocker, in CII weggelassen wenn leer), bei XRechnung weiterhin Pflicht; Hinweis: E-Mail oder Netzwerkcode Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9a7dca1498
commit
5a9929aa9e
|
|
@ -17,7 +17,8 @@ class InvoiceMetadata(BaseModel):
|
||||||
invoice_number: str = Field(description="Die Rechnungsnummer nach 'Nummer:' (z.B. R26-219)")
|
invoice_number: str = Field(description="Die Rechnungsnummer nach 'Nummer:' (z.B. R26-219)")
|
||||||
invoice_date: str = Field(description="Das Rechnungsdatum nach 'Datum:' im Format YYYY-MM-DD")
|
invoice_date: str = Field(description="Das Rechnungsdatum nach 'Datum:' im Format YYYY-MM-DD")
|
||||||
delivery_date: str = Field(description="Das Lieferdatum nach 'Lieferdatum:' im Format YYYY-MM-DD")
|
delivery_date: str = Field(description="Das Lieferdatum nach 'Lieferdatum:' im Format YYYY-MM-DD")
|
||||||
customer_number: str = Field(default="", description="Kundennummer, wie sie beim Empfänger hinterlegt ist (z.B. der exakte Wert nach 'Kunden-Nr:'). Wird als Käuferreferenz (BT-10) verwendet. Falls im Text keine Kundennummer erkennbar ist: leer lassen.")
|
customer_number: str = Field(default="", description="Kundennummer (BT-46), unter der der Kunde beim Verkäufer geführt wird (z.B. der exakte Wert nach 'Kunden-Nr:'). Falls im Text keine Kundennummer erkennbar ist: leer lassen.")
|
||||||
|
leitweg_id: str = Field(default="", description="Leitweg-ID (BT-10) - NUR bei Rechnungen an Behörden (B2G), z.B. '04011000-1234-56'. Ist im Text keine Leitweg-ID erkennbar: leer lassen (NICHT die Kundennummer hier eintragen).")
|
||||||
buyer_reference: str = Field(default="", description="Bestellnummer des KÄUFERS für diesen Auftrag (BT-13): der exakte Wert nach 'Ihr Auftrag:' o.ä. (z.B. 05.06.2026-Herr.Knospe). NICHT die Kundennummer (die gehört in customer_number) und NICHT unsere eigene Auftragsbestätigungsnummer (die gehört in order_id). Wenn im Text keine solche Käufer-Bestellnummer erkennbar ist: leer lassen.")
|
buyer_reference: str = Field(default="", description="Bestellnummer des KÄUFERS für diesen Auftrag (BT-13): der exakte Wert nach 'Ihr Auftrag:' o.ä. (z.B. 05.06.2026-Herr.Knospe). NICHT die Kundennummer (die gehört in customer_number) und NICHT unsere eigene Auftragsbestätigungsnummer (die gehört in order_id). Wenn im Text keine solche Käufer-Bestellnummer erkennbar ist: leer lassen.")
|
||||||
recipient_name: str = Field(description="Vollständige Firma des Kunden/Empfängers (z.B. Stadtwerke Hennigsdorf GmbH)")
|
recipient_name: str = Field(description="Vollständige Firma des Kunden/Empfängers (z.B. Stadtwerke Hennigsdorf GmbH)")
|
||||||
recipient_street: str = Field(description="Straße und Hausnummer des Kunden")
|
recipient_street: str = Field(description="Straße und Hausnummer des Kunden")
|
||||||
|
|
@ -28,7 +29,8 @@ class InvoiceMetadata(BaseModel):
|
||||||
|
|
||||||
delivery_note_id: str = Field(default="", description="Gefundene Lieferscheinnummer (z.B. LS26-035) VOR Position 1. Sonst leer lassen.")
|
delivery_note_id: str = Field(default="", description="Gefundene Lieferscheinnummer (z.B. LS26-035) VOR Position 1. Sonst leer lassen.")
|
||||||
order_id: str = Field(default="", description="Gefundene EIGENE Auftragsbestätigungs-/Verkaufsauftragsnummer (BT-14, z.B. AB26-080) VOR Position 1. Das ist unsere eigene Nummer, NICHT die Bestellnummer des Käufers. Sonst leer lassen.")
|
order_id: str = Field(default="", description="Gefundene EIGENE Auftragsbestätigungs-/Verkaufsauftragsnummer (BT-14, z.B. AB26-080) VOR Position 1. Das ist unsere eigene Nummer, NICHT die Bestellnummer des Käufers. Sonst leer lassen.")
|
||||||
contract_id: str = Field(default="", description="Gefundene Nummer von Vergabeangebot, Vertrag oder Ausschreibung (z.B. AN26-130) VOR Position 1. Sonst leer lassen.")
|
contract_id: str = Field(default="", description="Vertragsnummer (BT-12) VOR Position 1 - NUR ein echter Vertrag. NICHT ein Vergabeangebot oder eine Ausschreibung (die gehören in tender_id). Sonst leer lassen.")
|
||||||
|
tender_id: str = Field(default="", description="Nummer eines Vergabeangebots oder einer Ausschreibung (BT-17, z.B. AN26-130) VOR Position 1. NICHT die Vertragsnummer (die gehört in contract_id). Sonst leer lassen.")
|
||||||
service_start_date: str = Field(default="", description="Startdatum eines explizit genannten Einsatz- oder Leistungszeitraums VOR Position 1 im Format YYYY-MM-DD. Sonst leer.")
|
service_start_date: str = Field(default="", description="Startdatum eines explizit genannten Einsatz- oder Leistungszeitraums VOR Position 1 im Format YYYY-MM-DD. Sonst leer.")
|
||||||
service_end_date: str = Field(default="", description="Enddatum eines explizit genannten Einsatz- oder Leistungszeitraums VOR Position 1 im Format YYYY-MM-DD. Sonst leer.")
|
service_end_date: str = Field(default="", description="Enddatum eines explizit genannten Einsatz- oder Leistungszeitraums VOR Position 1 im Format YYYY-MM-DD. Sonst leer.")
|
||||||
payment_term_days: str = Field(default="", description="Das Zahlungsziel in TAGEN als reine Zahl (z.B. '14' oder '30'). Suche im Text nach Formulierungen wie 'zahlbar innerhalb X Tagen', 'Zahlungsziel X Tage', 'X Tage netto'. Steht stattdessen ein konkretes Fälligkeitsdatum (z.B. 'zahlbar bis 31.07.2026'), berechne die Anzahl Tage zwischen Rechnungsdatum und diesem Datum und gib NUR diese Zahl zurück. Wenn im Text KEIN Zahlungsziel und KEIN Fälligkeitsdatum erkennbar ist: leer lassen (NICHT raten, NICHT 14 annehmen).")
|
payment_term_days: str = Field(default="", description="Das Zahlungsziel in TAGEN als reine Zahl (z.B. '14' oder '30'). Suche im Text nach Formulierungen wie 'zahlbar innerhalb X Tagen', 'Zahlungsziel X Tage', 'X Tage netto'. Steht stattdessen ein konkretes Fälligkeitsdatum (z.B. 'zahlbar bis 31.07.2026'), berechne die Anzahl Tage zwischen Rechnungsdatum und diesem Datum und gib NUR diese Zahl zurück. Wenn im Text KEIN Zahlungsziel und KEIN Fälligkeitsdatum erkennbar ist: leer lassen (NICHT raten, NICHT 14 annehmen).")
|
||||||
|
|
@ -144,6 +146,22 @@ def build_payment_terms_note(data):
|
||||||
return " ".join(parts)
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
ANGEBOT_PATTERN = re.compile(
|
||||||
|
r"Angebot\s+([A-Za-z0-9][A-Za-z0-9\-/.]*)\s+vom\s+(\d{1,2}[.\-/]\d{1,2}[.\-/]\d{2,4})",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_angebot_reference(text):
|
||||||
|
"""Findet einen Angebotsbezug der Form 'Angebot <Nummer> vom <Datum>' im Rechnungstext.
|
||||||
|
EN16931 kennt kein eigenes Feld fuer einen Angebotsbezug, daher wird der Fund als Freitext
|
||||||
|
(BT-22) uebernommen. Gibt den normalisierten Text zurueck oder "" wenn nichts passt."""
|
||||||
|
m = ANGEBOT_PATTERN.search(text)
|
||||||
|
if not m:
|
||||||
|
return ""
|
||||||
|
return f"Angebot {m.group(1)} vom {m.group(2)}"
|
||||||
|
|
||||||
|
|
||||||
def sanitize_filename_part(value):
|
def sanitize_filename_part(value):
|
||||||
"""Macht einen String dateisystem-sicher: entfernt in Dateinamen kritische Zeichen
|
"""Macht einen String dateisystem-sicher: entfernt in Dateinamen kritische Zeichen
|
||||||
(Pfadtrenner/Sonderzeichen) und fasst Whitespace zu einem Bindestrich zusammen."""
|
(Pfadtrenner/Sonderzeichen) und fasst Whitespace zu einem Bindestrich zusammen."""
|
||||||
|
|
@ -433,11 +451,11 @@ def generate_compliant_xrechnung(data):
|
||||||
ET.SubElement(root, f"{{{CBC}}}Note").text = str(data["invoice_note"]).strip()
|
ET.SubElement(root, f"{{{CBC}}}Note").text = str(data["invoice_note"]).strip()
|
||||||
|
|
||||||
ET.SubElement(root, f"{{{CBC}}}DocumentCurrencyCode").text = "EUR"
|
ET.SubElement(root, f"{{{CBC}}}DocumentCurrencyCode").text = "EUR"
|
||||||
# BT-10 Käuferreferenz: NUR eintragen, wenn tatsächlich ein Wert vorliegt. Für private/
|
# BT-10 Käuferreferenz = Leitweg-ID: NUR bei Behörden-Rechnungen (B2G) vorhanden. Fehlt sie,
|
||||||
# geschäftliche Empfänger (B2B) reicht die Kundennummer, die der Kunde bei euch hat; nur bei
|
# wird das Element weggelassen; bei XRechnung blockiert die UI den Export (BR-DE-15 Pflicht),
|
||||||
# Behörden (B2G) ist eine echte Leitweg-ID vorgesehen - die kommt dann vom Empfänger selbst.
|
# bei ZUGFeRD (B2B) bleibt es zulässigerweise leer.
|
||||||
if data.get("customer_number") and str(data["customer_number"]).strip() != "":
|
if data.get("leitweg_id") and str(data["leitweg_id"]).strip() != "":
|
||||||
ET.SubElement(root, f"{{{CBC}}}BuyerReference").text = str(data["customer_number"]).strip()
|
ET.SubElement(root, f"{{{CBC}}}BuyerReference").text = str(data["leitweg_id"]).strip()
|
||||||
|
|
||||||
if data.get("service_start_date") and data.get("service_end_date"):
|
if data.get("service_start_date") and data.get("service_end_date"):
|
||||||
period_elem = ET.SubElement(root, f"{{{CAC}}}InvoicePeriod")
|
period_elem = ET.SubElement(root, f"{{{CAC}}}InvoicePeriod")
|
||||||
|
|
@ -462,6 +480,12 @@ def generate_compliant_xrechnung(data):
|
||||||
despatch_ref = ET.SubElement(root, f"{{{CAC}}}DespatchDocumentReference")
|
despatch_ref = ET.SubElement(root, f"{{{CAC}}}DespatchDocumentReference")
|
||||||
ET.SubElement(despatch_ref, f"{{{CBC}}}ID").text = str(data["delivery_note_id"]).strip()
|
ET.SubElement(despatch_ref, f"{{{CBC}}}ID").text = str(data["delivery_note_id"]).strip()
|
||||||
|
|
||||||
|
# BT-17 Vergabeangebot/Ausschreibung. Muss laut UBL-Schema (cac:OriginatorDocumentReference)
|
||||||
|
# VOR cac:ContractDocumentReference (BT-12) stehen.
|
||||||
|
if data.get("tender_id") and str(data["tender_id"]).strip() != "":
|
||||||
|
originator_ref = ET.SubElement(root, f"{{{CAC}}}OriginatorDocumentReference")
|
||||||
|
ET.SubElement(originator_ref, f"{{{CBC}}}ID").text = str(data["tender_id"]).strip()
|
||||||
|
|
||||||
if data.get("contract_id") and str(data["contract_id"]).strip() != "":
|
if data.get("contract_id") and str(data["contract_id"]).strip() != "":
|
||||||
contract_ref = ET.SubElement(root, f"{{{CAC}}}ContractDocumentReference")
|
contract_ref = ET.SubElement(root, f"{{{CAC}}}ContractDocumentReference")
|
||||||
ET.SubElement(contract_ref, f"{{{CBC}}}ID").text = str(data["contract_id"]).strip()
|
ET.SubElement(contract_ref, f"{{{CBC}}}ID").text = str(data["contract_id"]).strip()
|
||||||
|
|
@ -501,6 +525,11 @@ def generate_compliant_xrechnung(data):
|
||||||
party_cust = ET.SubElement(customer, f"{{{CAC}}}Party")
|
party_cust = ET.SubElement(customer, f"{{{CAC}}}Party")
|
||||||
# BT-49-1 Schema der elektronischen Kaeufer-Adresse: dynamisch (EM/0204/0088), Default EM.
|
# BT-49-1 Schema der elektronischen Kaeufer-Adresse: dynamisch (EM/0204/0088), Default EM.
|
||||||
ET.SubElement(party_cust, f"{{{CBC}}}EndpointID", {"schemeID": str(data.get("recipient_endpoint_scheme", "EM"))}).text = str(data.get("recipient_email", "")).strip()
|
ET.SubElement(party_cust, f"{{{CBC}}}EndpointID", {"schemeID": str(data.get("recipient_endpoint_scheme", "EM"))}).text = str(data.get("recipient_email", "")).strip()
|
||||||
|
# BT-46 Kundennummer (Käufer-Kennung). Muss laut UBL-Schema nach EndpointID und vor
|
||||||
|
# PostalAddress stehen.
|
||||||
|
if data.get("customer_number") and str(data["customer_number"]).strip() != "":
|
||||||
|
party_ident_cust = ET.SubElement(party_cust, f"{{{CAC}}}PartyIdentification")
|
||||||
|
ET.SubElement(party_ident_cust, f"{{{CBC}}}ID").text = str(data["customer_number"]).strip()
|
||||||
addr_cust = ET.SubElement(party_cust, f"{{{CAC}}}PostalAddress")
|
addr_cust = ET.SubElement(party_cust, f"{{{CAC}}}PostalAddress")
|
||||||
ET.SubElement(addr_cust, f"{{{CBC}}}StreetName").text = str(data["recipient_street"])
|
ET.SubElement(addr_cust, f"{{{CBC}}}StreetName").text = str(data["recipient_street"])
|
||||||
ET.SubElement(addr_cust, f"{{{CBC}}}CityName").text = str(data["recipient_city"])
|
ET.SubElement(addr_cust, f"{{{CBC}}}CityName").text = str(data["recipient_city"])
|
||||||
|
|
@ -701,10 +730,10 @@ def generate_compliant_zugferd_cii(data):
|
||||||
ET.SubElement(mon_sum, f"{{{RAM}}}LineTotalAmount").text = f"{item['total_amount']:.2f}"
|
ET.SubElement(mon_sum, f"{{{RAM}}}LineTotalAmount").text = f"{item['total_amount']:.2f}"
|
||||||
|
|
||||||
header_agree = ET.SubElement(trade_trans, f"{{{RAM}}}ApplicableHeaderTradeAgreement")
|
header_agree = ET.SubElement(trade_trans, f"{{{RAM}}}ApplicableHeaderTradeAgreement")
|
||||||
# BT-10 Käuferreferenz: NUR eintragen, wenn tatsächlich ein Wert vorliegt (siehe UBL-Generator
|
# BT-10 Käuferreferenz = Leitweg-ID: nur bei Behörden-Rechnungen (B2G). Fehlt sie, wird das
|
||||||
# für die ausführliche Begründung).
|
# Element weggelassen (bei ZUGFeRD/B2B zulässig; bei XRechnung blockiert die UI).
|
||||||
if data.get("customer_number") and str(data["customer_number"]).strip() != "":
|
if data.get("leitweg_id") and str(data["leitweg_id"]).strip() != "":
|
||||||
ET.SubElement(header_agree, f"{{{RAM}}}BuyerReference").text = str(data["customer_number"]).strip()
|
ET.SubElement(header_agree, f"{{{RAM}}}BuyerReference").text = str(data["leitweg_id"]).strip()
|
||||||
|
|
||||||
supplier_party = ET.SubElement(header_agree, f"{{{RAM}}}SellerTradeParty")
|
supplier_party = ET.SubElement(header_agree, f"{{{RAM}}}SellerTradeParty")
|
||||||
ET.SubElement(supplier_party, f"{{{RAM}}}Name").text = VERKAEUFER["name"]
|
ET.SubElement(supplier_party, f"{{{RAM}}}Name").text = VERKAEUFER["name"]
|
||||||
|
|
@ -738,6 +767,9 @@ def generate_compliant_zugferd_cii(data):
|
||||||
ET.SubElement(supp_tax, f"{{{RAM}}}ID", {"schemeID": "VA"}).text = VERKAEUFER["vat_id"]
|
ET.SubElement(supp_tax, f"{{{RAM}}}ID", {"schemeID": "VA"}).text = VERKAEUFER["vat_id"]
|
||||||
|
|
||||||
customer_party = ET.SubElement(header_agree, f"{{{RAM}}}BuyerTradeParty")
|
customer_party = ET.SubElement(header_agree, f"{{{RAM}}}BuyerTradeParty")
|
||||||
|
# BT-46 Kundennummer (Käufer-Kennung). Muss laut CII-Schema als ram:ID VOR ram:Name stehen.
|
||||||
|
if data.get("customer_number") and str(data["customer_number"]).strip() != "":
|
||||||
|
ET.SubElement(customer_party, f"{{{RAM}}}ID").text = str(data["customer_number"]).strip()
|
||||||
ET.SubElement(customer_party, f"{{{RAM}}}Name").text = str(data["recipient_name"])
|
ET.SubElement(customer_party, f"{{{RAM}}}Name").text = str(data["recipient_name"])
|
||||||
|
|
||||||
cust_addr = ET.SubElement(customer_party, f"{{{RAM}}}PostalTradeAddress")
|
cust_addr = ET.SubElement(customer_party, f"{{{RAM}}}PostalTradeAddress")
|
||||||
|
|
@ -746,9 +778,10 @@ def generate_compliant_zugferd_cii(data):
|
||||||
ET.SubElement(cust_addr, f"{{{RAM}}}CityName").text = str(data["recipient_city"])
|
ET.SubElement(cust_addr, f"{{{RAM}}}CityName").text = str(data["recipient_city"])
|
||||||
ET.SubElement(cust_addr, f"{{{RAM}}}CountryID").text = "DE"
|
ET.SubElement(cust_addr, f"{{{RAM}}}CountryID").text = "DE"
|
||||||
|
|
||||||
# BT-49 Elektronische Adresse des Käufers (PEPPOL-EN16931-R010 Pflicht). Muss laut
|
# BT-49 Elektronische Adresse des Käufers. Muss laut XSD nach PostalTradeAddress stehen.
|
||||||
# XSD nach PostalTradeAddress stehen. KEIN erfundener Fallback - fehlt die Adresse, wird das
|
# Bei XRechnung Pflicht (UI blockiert leer), bei ZUGFeRD (B2B) optional - fehlt sie, wird der
|
||||||
# (leere) Pflichtfeld sichtbar und die UI blockiert den Export mit Warnung.
|
# Block ganz weggelassen statt als leeres Element geschrieben.
|
||||||
|
if str(data.get("recipient_email", "")).strip():
|
||||||
cust_uri = ET.SubElement(customer_party, f"{{{RAM}}}URIUniversalCommunication")
|
cust_uri = ET.SubElement(customer_party, f"{{{RAM}}}URIUniversalCommunication")
|
||||||
# BT-49-1 Schema der elektronischen Kaeufer-Adresse: dynamisch (EM/0204/0088), Default EM.
|
# BT-49-1 Schema der elektronischen Kaeufer-Adresse: dynamisch (EM/0204/0088), Default EM.
|
||||||
ET.SubElement(cust_uri, f"{{{RAM}}}URIID", {"schemeID": str(data.get("recipient_endpoint_scheme", "EM"))}).text = str(data.get("recipient_email", "")).strip()
|
ET.SubElement(cust_uri, f"{{{RAM}}}URIID", {"schemeID": str(data.get("recipient_endpoint_scheme", "EM"))}).text = str(data.get("recipient_email", "")).strip()
|
||||||
|
|
@ -769,6 +802,13 @@ def generate_compliant_zugferd_cii(data):
|
||||||
contract_ref = ET.SubElement(header_agree, f"{{{RAM}}}ContractReferencedDocument")
|
contract_ref = ET.SubElement(header_agree, f"{{{RAM}}}ContractReferencedDocument")
|
||||||
ET.SubElement(contract_ref, f"{{{RAM}}}IssuerAssignedID").text = str(data["contract_id"]).strip()
|
ET.SubElement(contract_ref, f"{{{RAM}}}IssuerAssignedID").text = str(data["contract_id"]).strip()
|
||||||
|
|
||||||
|
# BT-17 Vergabeangebot/Ausschreibung. In CII ein ram:AdditionalReferencedDocument mit
|
||||||
|
# ram:TypeCode "50"; muss laut XSD NACH ContractReferencedDocument stehen.
|
||||||
|
if data.get("tender_id") and str(data["tender_id"]).strip() != "":
|
||||||
|
tender_ref = ET.SubElement(header_agree, f"{{{RAM}}}AdditionalReferencedDocument")
|
||||||
|
ET.SubElement(tender_ref, f"{{{RAM}}}IssuerAssignedID").text = str(data["tender_id"]).strip()
|
||||||
|
ET.SubElement(tender_ref, f"{{{RAM}}}TypeCode").text = "50"
|
||||||
|
|
||||||
header_deliv = ET.SubElement(trade_trans, f"{{{RAM}}}ApplicableHeaderTradeDelivery")
|
header_deliv = ET.SubElement(trade_trans, f"{{{RAM}}}ApplicableHeaderTradeDelivery")
|
||||||
if data.get("delivery_date"):
|
if data.get("delivery_date"):
|
||||||
act_deliv = ET.SubElement(header_deliv, f"{{{RAM}}}ActualDeliverySupplyChainEvent")
|
act_deliv = ET.SubElement(header_deliv, f"{{{RAM}}}ActualDeliverySupplyChainEvent")
|
||||||
|
|
|
||||||
45
main.py
45
main.py
|
|
@ -28,6 +28,7 @@ from invoice_logic import (
|
||||||
extract_service_period,
|
extract_service_period,
|
||||||
detect_contract_type,
|
detect_contract_type,
|
||||||
extract_hardware_serial_numbers,
|
extract_hardware_serial_numbers,
|
||||||
|
extract_angebot_reference,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
@ -216,6 +217,14 @@ with col2:
|
||||||
final_data["contract_type"] = detect_contract_type(pdf_text)
|
final_data["contract_type"] = detect_contract_type(pdf_text)
|
||||||
final_data["hardware_serials"] = extract_hardware_serial_numbers(pdf_text)
|
final_data["hardware_serials"] = extract_hardware_serial_numbers(pdf_text)
|
||||||
|
|
||||||
|
# Angebotsbezug ("Angebot <Nr> vom <Datum>") hat kein eigenes EN16931-Feld -
|
||||||
|
# als Info fuer den Kunden in den Freitext (BT-22) uebernehmen, falls gefunden
|
||||||
|
# und noch nicht enthalten.
|
||||||
|
_angebot = extract_angebot_reference(pdf_text)
|
||||||
|
if _angebot and _angebot.lower() not in str(final_data.get("invoice_note", "")).lower():
|
||||||
|
_existing_note = str(final_data.get("invoice_note", "")).strip()
|
||||||
|
final_data["invoice_note"] = f"{_existing_note}\n{_angebot}".strip() if _existing_note else _angebot
|
||||||
|
|
||||||
st.session_state.invoice_counter += 1
|
st.session_state.invoice_counter += 1
|
||||||
st.session_state.extracted_data = final_data
|
st.session_state.extracted_data = final_data
|
||||||
|
|
||||||
|
|
@ -248,10 +257,15 @@ with col2:
|
||||||
inv_date = st.text_input("Rechnungsdatum (BT-2, YYYY-MM-DD) :red[*]", value=data.get("invoice_date", ""))
|
inv_date = st.text_input("Rechnungsdatum (BT-2, YYYY-MM-DD) :red[*]", value=data.get("invoice_date", ""))
|
||||||
inv_delivery = st.text_input("Lieferdatum (BT-72, YYYY-MM-DD)", value=data.get("delivery_date", ""))
|
inv_delivery = st.text_input("Lieferdatum (BT-72, YYYY-MM-DD)", value=data.get("delivery_date", ""))
|
||||||
cust_num = st.text_input(
|
cust_num = st.text_input(
|
||||||
"Kundennummer / Leitweg-ID (BT-10)",
|
"Kundennummer (BT-46)",
|
||||||
value=data.get("customer_number", ""),
|
value=data.get("customer_number", ""),
|
||||||
help="Eure Kundennummer beim Empfänger (Standardfall für B2B) oder eine Leitweg-ID, falls "
|
help="Nummer, unter der der Kunde bei euch geführt wird.",
|
||||||
"der Empfänger eine Behörde ist."
|
)
|
||||||
|
leitweg_id = st.text_input(
|
||||||
|
"Leitweg-ID (BT-10)",
|
||||||
|
value=data.get("leitweg_id", ""),
|
||||||
|
help="Nur für Behörden-Rechnungen (B2G). Bei XRechnung Pflicht (BR-DE-15) - dann die "
|
||||||
|
"Leitweg-ID der Behörde eintragen. Bei ZUGFeRD / B2B leer lassen.",
|
||||||
)
|
)
|
||||||
buyer_ref = st.text_input(
|
buyer_ref = st.text_input(
|
||||||
"Ihr Auftrag / Bestellnummer (BT-13)",
|
"Ihr Auftrag / Bestellnummer (BT-13)",
|
||||||
|
|
@ -261,13 +275,16 @@ with col2:
|
||||||
# UI-Eingabefelder für die Daten vor Position 1 (BT-22 etc.)
|
# UI-Eingabefelder für die Daten vor Position 1 (BT-22 etc.)
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("**Globale Referenzen & Vorlauftexte:**")
|
st.markdown("**Globale Referenzen & Vorlauftexte:**")
|
||||||
c_ref1, c_ref2, c_ref3 = st.columns(3)
|
c_ref1, c_ref2 = st.columns(2)
|
||||||
with c_ref1:
|
with c_ref1:
|
||||||
dn_id = st.text_input("Lieferscheinnummer (BT-16)", value=data.get("delivery_note_id", ""))
|
dn_id = st.text_input("Lieferscheinnummer (BT-16)", value=data.get("delivery_note_id", ""))
|
||||||
with c_ref2:
|
with c_ref2:
|
||||||
o_id = st.text_input("Auftragsbestätigung / Verkaufsauftrag (BT-14)", value=data.get("order_id", ""))
|
o_id = st.text_input("Auftragsbestätigung / Verkaufsauftrag (BT-14)", value=data.get("order_id", ""))
|
||||||
|
c_ref3, c_ref4 = st.columns(2)
|
||||||
with c_ref3:
|
with c_ref3:
|
||||||
c_id = st.text_input("Vertrag / Vergabeangebot (BT-12)", value=data.get("contract_id", ""))
|
c_id = st.text_input("Vertrag (BT-12)", value=data.get("contract_id", ""))
|
||||||
|
with c_ref4:
|
||||||
|
tender_id = st.text_input("Vergabeangebot / Ausschreibung (BT-17)", value=data.get("tender_id", ""))
|
||||||
|
|
||||||
c_p1, c_p2 = st.columns(2)
|
c_p1, c_p2 = st.columns(2)
|
||||||
with c_p1:
|
with c_p1:
|
||||||
|
|
@ -447,9 +464,11 @@ with col2:
|
||||||
"skonto_days": skonto_days,
|
"skonto_days": skonto_days,
|
||||||
"vorkasse": vorkasse,
|
"vorkasse": vorkasse,
|
||||||
"recipient_endpoint_scheme": r_endpoint_scheme,
|
"recipient_endpoint_scheme": r_endpoint_scheme,
|
||||||
|
"leitweg_id": leitweg_id,
|
||||||
"delivery_note_id": dn_id,
|
"delivery_note_id": dn_id,
|
||||||
"order_id": o_id,
|
"order_id": o_id,
|
||||||
"contract_id": c_id,
|
"contract_id": c_id,
|
||||||
|
"tender_id": tender_id,
|
||||||
"service_start_date": s_start,
|
"service_start_date": s_start,
|
||||||
"service_end_date": s_end,
|
"service_end_date": s_end,
|
||||||
"payment_term_days": pay_term,
|
"payment_term_days": pay_term,
|
||||||
|
|
@ -460,16 +479,24 @@ with col2:
|
||||||
# Sammelt blockierende Probleme (Export nicht möglich), damit fehlerhafte/fehlende
|
# Sammelt blockierende Probleme (Export nicht möglich), damit fehlerhafte/fehlende
|
||||||
# Pflichtangaben sichtbar werden, statt still mit Platzhaltern oder falschen Werten
|
# Pflichtangaben sichtbar werden, statt still mit Platzhaltern oder falschen Werten
|
||||||
# in die Rechnung zu wandern.
|
# in die Rechnung zu wandern.
|
||||||
|
# BT-10 (Leitweg-ID) und BT-49 (elektronische Adresse) sind NUR bei XRechnung Pflicht
|
||||||
|
# (Behörden/B2G). Bei ZUGFeRD (B2B) dürfen sie leer bleiben und blockieren den Export nicht.
|
||||||
|
is_xrechnung = "UBL XML" in export_format
|
||||||
|
|
||||||
blocker = []
|
blocker = []
|
||||||
if not str(inv_num).strip():
|
if not str(inv_num).strip():
|
||||||
blocker.append("Rechnungsnummer (BT-1) fehlt.")
|
blocker.append("Rechnungsnummer (BT-1) fehlt.")
|
||||||
if parse_to_strict_xml_date(inv_date) == "":
|
if parse_to_strict_xml_date(inv_date) == "":
|
||||||
blocker.append("Rechnungsdatum (BT-2) fehlt oder ist unlesbar (erwartet: YYYY-MM-DD). "
|
blocker.append("Rechnungsdatum (BT-2) fehlt oder ist unlesbar (erwartet: YYYY-MM-DD). "
|
||||||
"Bitte korrigieren - es wird KEIN Datum automatisch eingesetzt.")
|
"Bitte korrigieren - es wird KEIN Datum automatisch eingesetzt.")
|
||||||
if not str(r_email).strip():
|
if is_xrechnung and not str(leitweg_id).strip():
|
||||||
blocker.append("Elektronische Adresse des Kunden (BT-49) fehlt. Diese ist für "
|
blocker.append("Leitweg-ID / Käuferreferenz (BT-10) fehlt. Bei XRechnung ist sie Pflicht "
|
||||||
"XRechnung/ZUGFeRD ein Pflichtfeld (PEPPOL-EN16931-R010) und wird NICHT "
|
"(BR-DE-15) - für Behörden-Rechnungen bitte die Leitweg-ID eintragen. "
|
||||||
"mit einem Platzhalter befüllt. Bitte eintragen.")
|
"(Bei ZUGFeRD/B2B ist das Feld optional.)")
|
||||||
|
if is_xrechnung and not str(r_email).strip():
|
||||||
|
blocker.append("Elektronische Adresse des Kunden (BT-49) fehlt. Bei XRechnung ist sie "
|
||||||
|
"Pflicht (PEPPOL-EN16931-R010) - entweder E-Mail-Adresse oder Netzwerkcode "
|
||||||
|
"des Empfängers eintragen. (Bei ZUGFeRD/B2B ist das Feld optional.)")
|
||||||
if inv_delivery and parse_to_strict_xml_date(inv_delivery) == "":
|
if inv_delivery and parse_to_strict_xml_date(inv_delivery) == "":
|
||||||
blocker.append("Lieferdatum (BT-72) ist unlesbar (erwartet: YYYY-MM-DD). Bitte korrigieren oder leeren.")
|
blocker.append("Lieferdatum (BT-72) ist unlesbar (erwartet: YYYY-MM-DD). Bitte korrigieren oder leeren.")
|
||||||
if not edited_items:
|
if not edited_items:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue