# -*- coding: utf-8 -*- import os.path import sys from decimal import Decimal # Search for included submodule python-drafthorse atoms = os.path.abspath(os.path.dirname(__file__)).split('/') dir = '' while atoms: candidate = os.path.join('/'.join(atoms), 'external/python-drafthorse') if os.path.exists(candidate): dir = candidate break atoms = atoms[:-1] sys.path.insert(0, dir) from drafthorse.models.document import Document from drafthorse.models.accounting import ApplicableTradeTax from drafthorse.models.tradelines import LineItem from .InvoiceObjects import InvoiceTable, InvoiceText, RECHNUNG, GUTSCHRIFT, KORREKTUR, \ VAT_REGULAR, VAT_KLEINUNTERNEHMER, VAT_INNERGEM, PAYMENT_UEBERWEISUNG, PAYMENT_LASTSCHRIFT, UNITS from drafthorse.models.party import TaxRegistration, URIUniversalCommunication from drafthorse.models.payment import PaymentTerms from drafthorse.models.note import IncludedNote from drafthorse.pdf import attach_xml def InvoiceToXML(invoice): doc = Document() doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017" if invoice.leitweg_id: doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0" # Standardwert für XRechnung 3.0.1 doc.context.business_parameter.id = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0" doc.header.id = invoice.id # Typecodes: # 380: Handelsrechnungen # 381: Gutschrift # 384: Korrekturrechnung # 389: Eigenrechnung (vom Käufer im Namen des Lieferanten erstellt). # 261: Selbstverfasste Gutschrift. # 386: Vorauszahlungsrechnung # 326: Teilrechnung # 751: Rechnungsinformation - KEINE RECHNUNG if invoice.type == RECHNUNG: doc.header.type_code = "380" elif invoice.type == GUTSCHRIFT: doc.header.type_code = "381" elif invoice.type == KORREKTUR: doc.header.type_code = "384" else: raise TypeError("Unbekannter Rechnungstyp, kann kein XML erstellen") doc.header.issue_date_time = invoice.date # Seller-Address if invoice.seller['trade_name']: doc.trade.agreement.seller.legal_organization.trade_name = invoice.seller['trade_name'] doc.trade.agreement.seller.name = invoice.seller['name'] doc.trade.agreement.seller.address.country_id = invoice.seller['address']['country_id'] doc.trade.agreement.seller.address.postcode = invoice.seller['address']['postcode'] doc.trade.agreement.seller.address.city_name = invoice.seller['address']['city_name'] doc.trade.agreement.seller.address.line_one = invoice.seller['address']['line1'] doc.trade.agreement.seller.address.line_two = invoice.seller['address']['line2'] doc.trade.agreement.seller.address.line_three = invoice.seller['address']['line3'] # Für XRECHNUNG muss das Contact-Array mindestens eine Angabe enthalten if invoice.seller['contactPerson']: doc.trade.agreement.seller.contact.person_name = invoice.seller['contactPerson'] if invoice.seller['phone']: doc.trade.agreement.seller.contact.telephone.number = invoice.seller['phone'] if invoice.seller_vat_id: tax_reg = TaxRegistration() tax_reg.id = ('VA', invoice.seller_vat_id) doc.trade.agreement.seller.tax_registrations.add(tax_reg) if invoice.seller['email']: email = URIUniversalCommunication() email.uri_ID = ('EM', invoice.seller['email']) doc.trade.agreement.seller.electronic_address.add(email) doc.trade.agreement.seller.contact.email.address = invoice.seller['email'] # Buyer-Address doc.trade.agreement.buyer.name = invoice.customer['name'] doc.trade.agreement.buyer.address.country_id = invoice.customer['address']['country_id'] doc.trade.agreement.buyer.address.postcode = invoice.customer['address']['postcode'] doc.trade.agreement.buyer.address.city_name = invoice.customer['address']['city_name'] doc.trade.agreement.buyer.address.line_one = invoice.customer['address']['line1'] doc.trade.agreement.buyer.address.line_two = invoice.customer['address']['line2'] doc.trade.agreement.buyer.address.line_three = invoice.customer['address']['line3'] # E-Mail-Adresse des Kunden auf jeden Fall in den Kontakt if invoice.buyer['email']: doc.trade.agreement.buyer.contact.email.address = invoice.buyer['email'] if invoice.buyer['phone']: doc.trade.agreement.buyer.contact.telephone.number = invoice.buyer['phone'] if invoice.buyer_vat_id: tax_reg = TaxRegistration() tax_reg.id = ('VA', invoice.buyer_vat_id) doc.trade.agreement.buyer.tax_registrations.add(tax_reg) if invoice.leitweg_id: leitwegid = URIUniversalCommunication() leitwegid.uri_ID = ('0204', invoice.leitweg_id) doc.trade.agreement.buyer.electronic_address.add(leitwegid) # Bei ZUGFeRD 2.1 darf das Feld nur einmal vorkommen. In XRechnung 3.0 darf es mehrmals drin sein. # drafthorse kann aktuell noch kein XRechnung 3.0 erzeugen, daher hier als "elif". elif invoice.buyer['email']: email = URIUniversalCommunication() email.uri_ID = ('EM', invoice.buyer['email']) doc.trade.agreement.buyer.electronic_address.add(email) if invoice.buyer_reference: doc.trade.agreement.buyer_reference = invoice.buyer_reference elif invoice.leitweg_id: # "Leitweg-ID" in XRechnung # Wenn wir hier sind, ist die aber bereits in der electronic_address eingetragen, # daher hier nur noch wiederholen, wenn wir das Feld nicht für was anderes brauchen (Kunden-Referenz). doc.trade.agreement.buyer_reference = invoice.leitweg_id if invoice.order_number: doc.trade.agreement.buyer_order.issuer_assigned_id = invoice.order_number if invoice.contract_number: doc.trade.agreement.contract.issuer_assigned_id = invoice.contract_number # Wenn alle Leistungsdaten aller Posten exakt ein fixes Datum sind, sollte statt dem Datumbereich der # einzelnen Posten doc.trade.delivery.event.occurence verwendet werden. Dazu müssen vorab alle Posten # aller Tabellen geprüft werden only_one_date = True deliverydate = None try: for part in invoice.parts: if not isinstance(part, InvoiceTable): continue for el in part.entries: end = None start = None if 'period_end' in el and el['period_end']: end = el['period_end'] if 'period_start' in el and el['period_start']: start = el['period_start'] if start and end and start != end: deliverydate = None only_one_date = False raise Exception('loop abort') if start and not deliverydate: deliverydate = el['period_start'] continue if start != deliverydate: deliverydate = None only_one_date = False raise Exception('loop abort') except Exception as e: pass if only_one_date and deliverydate: doc.trade.delivery.event.occurrence = deliverydate # Line Items summe_netto = 0.0 summe_brutto = 0.0 summe_bezahlt = 0.0 summe_ust = 0.0 line_id_count = 0 textparts = [] for part in invoice.parts: if isinstance(part, InvoiceText): textparts += part.paragraphs if isinstance(part, InvoiceTable): last_title = None for el in part.entries: if el['type'] == 'title': # Diese Information ist im XML nicht auf diese Weise darstellbar last_title = el['title'] continue line_id_count += 1 li = LineItem() li.document.line_id = f"{line_id_count}" if last_title: title = IncludedNote() title.content.add(last_title) li.document.notes.add(title) li.product.name = el['subject'] if 'desc' in el and el['desc'] != '': li.product.description = el['desc'] if not only_one_date: if 'period_start' in el and el['period_start']: li.settlement.period.start = el['period_start'] if 'period_end' in el and el['period_end']: li.settlement.period.end = el['period_end'] unit = 'C62' if el['unit']: for key, value in UNITS.items(): if el['unit'] in value: unit = key li.delivery.billed_quantity = (Decimal(el['count']), unit) # C62 = ohne Einheit # H87 = Stück # MON = Month, SEC = second, MIN = minute, HUR = hour, DAY = day, WEE = week, ANN = year # LTR = Liter, MTQ = m³ # GRM = gram, KGM = Kilogram, TNE = Tonne, DTN = decitonne, # MTR = Meter, MMT = Millimeter, KMT = Kilometer # MTK = m² # KWT = kW, MAW = MW, KWH = kWh, MWH = MWh li.settlement.trade_tax.type_code = "VAT" if invoice.vat_type == VAT_REGULAR: li.settlement.trade_tax.category_code = "S" elif invoice.vat_type == VAT_KLEINUNTERNEHMER: li.settlement.trade_tax.category_code = "E" elif invoice.vat_type == VAT_INNERGEM: li.settlement.trade_tax.category_code = "K" # FIXME: Typ bei uns nur global gesetzt, nicht pro Artikel # S = Standard VAT rate # Z = Zero rated goods # E = VAT exempt # AE = Reverse charge # K = Intra-Community supply (specific reverse charge) # G = Exempt VAT for Export outside EU # O = Outside VAT scope li.settlement.trade_tax.rate_applicable_percent = Decimal(f"{el['vat'] * 100:.1f}") nettopreis = el['price'] if part.vatType == 'gross': nettopreis = el['price'] / (1 + el['vat']) li.agreement.net.amount = Decimal(f"{nettopreis:.3f}") nettosumme = el['total'] if part.vatType == 'gross': nettosumme = el['total'] / (1 + el['vat']) li.settlement.monetary_summation.total_amount = Decimal(f"{nettosumme:.3f}") summe_netto += nettosumme if part.vatType == 'net': summe_brutto += el['total'] * (1 + el['vat']) else: summe_brutto += el['total'] doc.trade.items.add(li) for pay in part.payments: summe_bezahlt += pay['amount'] for vat, vatdata in part.vat.items(): trade_tax = ApplicableTradeTax() # Steuerbetrag dieses Steuersatzes amount = vatdata[0] * vat if part.vatType == 'gross': amount = (vatdata[0] / (vat + 1)) * vat trade_tax.calculated_amount = Decimal(f"{amount:.2f}") # Nettosumme dieses Steuersatzes amount = vatdata[0] if part.vatType == 'gross': amount = amount / (1 + vat) trade_tax.basis_amount = Decimal(f"{amount:.2f}") trade_tax.type_code = "VAT" if invoice.vat_type == VAT_REGULAR: trade_tax.category_code = "S" elif invoice.vat_type == VAT_KLEINUNTERNEHMER: trade_tax.category_code = "E" trade_tax.exemption_reason = 'Als Kleinunternehmer wird gemäß §19 UStG keine USt in Rechnung gestellt.' elif invoice.vat_type == VAT_INNERGEM: trade_tax.category_code = "K" trade_tax.rate_applicable_percent = Decimal(f"{vat * 100:.1f}") if part.vatType == 'gross': summe_ust += (vatdata[0] / (vat + 1)) * vat else: summe_ust += vatdata[0] * vat doc.trade.settlement.trade_tax.add(trade_tax) for paragraph in textparts: note = IncludedNote() note.content.add(paragraph) doc.header.notes.add(note) rest = summe_brutto - summe_bezahlt if invoice.creditor_reference_id: # Gläubiger-ID für SEPA doc.trade.settlement.creditor_reference_id = invoice.creditor_reference_id doc.trade.settlement.payment_reference = invoice.id doc.trade.settlement.currency_code = 'EUR' if invoice.payment_type: doc.trade.settlement.payment_means.type_code = invoice.payment_type if invoice.seller_bank_data['iban'] and invoice.payment_type == PAYMENT_UEBERWEISUNG: doc.trade.settlement.payment_means.payee_account.account_name = \ invoice.seller_bank_data['kontoinhaber'] or invoice.seller['trade_name'] or invoice.seller['name'] doc.trade.settlement.payment_means.payee_account.iban = invoice.seller_bank_data['iban'] if invoice.seller_bank_data['bic']: doc.trade.settlement.payment_means.payee_institution.bic = invoice.seller_bank_data['bic'] if invoice.buyer_bank_data['iban'] and invoice.payment_type == PAYMENT_LASTSCHRIFT: # Kunden-Bankverbindung bei Lastschrift doc.trade.settlement.payment_means.payer_account.iban = invoice.buyer_bank_data['iban'] terms = PaymentTerms() if invoice.due_date and invoice.payment_type == PAYMENT_UEBERWEISUNG: terms.description = f"Bitte begleichen Sie den Betrag bis zum {invoice.due_date.strftime('%d.%m.%Y')} ohne Abzüge." terms.due = invoice.due_date if invoice.type in [GUTSCHRIFT, KORREKTUR]: terms.description = f"Wir überweisen den Betrag auf Ihr Konto." elif invoice.debit: if invoice.debit_mandate_id: # Mandatsreferenz für Lastschrift terms.debit_mandate_id = invoice.debit_mandate_id terms.description = 'Wir buchen von Ihrem Konto ab.' doc.trade.settlement.terms.add(terms) doc.trade.settlement.monetary_summation.line_total = Decimal(f"{summe_netto:.2f}") doc.trade.settlement.monetary_summation.charge_total = Decimal("0.00") doc.trade.settlement.monetary_summation.allowance_total = Decimal("0.00") doc.trade.settlement.monetary_summation.tax_basis_total = Decimal(f"{summe_netto:.2f}") doc.trade.settlement.monetary_summation.tax_total = (Decimal(f"{summe_ust:.2f}"), "EUR") doc.trade.settlement.monetary_summation.prepaid_total = Decimal(f"{summe_bezahlt:.2f}") doc.trade.settlement.monetary_summation.grand_total = Decimal(f"{summe_brutto:.2f}") doc.trade.settlement.monetary_summation.due_amount = Decimal(f"{rest:.2f}") # Generate XML file xml = doc.serialize(schema="FACTUR-X_EXTENDED") return xml