import datetime from io import BytesIO from xml.dom import minidom from xml.etree.ElementTree import Element, SubElement, tostring import structlog from vbv_lernwelt.shop.invoice.abacus_sftp_client import AbacusSftpClient from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState logger = structlog.get_logger(__name__) def abacus_ssh_upload(checkout_information: CheckoutInformation): if checkout_information.state != CheckoutState.PAID: # only upload invoice if checkout is paid return True try: if not checkout_information.abacus_ssh_upload_done: # only upload data for not yet uploaded invoices invoice_xml_filename, invoice_xml_content = create_invoice_xml( checkout_information ) customer_xml_filename, customer_xml_content = create_customer_xml( checkout_information ) abacus_ssh_upload_invoice( customer_xml_filename, customer_xml_content, folder="debitor" ) abacus_ssh_upload_invoice( invoice_xml_filename, invoice_xml_content, folder="order" ) checkout_information.abacus_ssh_upload_done = True checkout_information.invoice_transmitted_at = datetime.datetime.now() checkout_information.save() return True except Exception as e: logger.warning( "Error uploading invoice to Abacus SFTP", checkout_information_id=checkout_information.id, exception=str(e), exc_info=True, ) return False def create_invoice_xml(checkout_information: CheckoutInformation): # set fill abacus numbers checkout_information = checkout_information.set_increment_abacus_order_id() customer = checkout_information.user.set_increment_abacus_debitor_number() invoice_xml_content = render_invoice_xml( abacus_debitor_number=customer.abacus_debitor_number, abacus_order_id=checkout_information.abacus_order_id, datatrans_transaction_id=checkout_information.transaction_id, order_date=checkout_information.created_at.date(), item_description=f"{checkout_information.product_name}, {checkout_information.created_at.date().isoformat()}, {checkout_information.user.last_name} {checkout_information.user.first_name}", ) # YYYYMMDDhhmmss filename_datetime = checkout_information.created_at.strftime("%Y%m%d%H%M%S") invoice_xml_filename = f"myVBV_orde_{filename_datetime}_{customer.abacus_debitor_number}_{checkout_information.abacus_order_id}.xml" return invoice_xml_filename, invoice_xml_content def create_customer_xml(checkout_information: CheckoutInformation): customer = checkout_information.user.set_increment_abacus_debitor_number() customer_xml_content = render_customer_xml( abacus_debitor_number=customer.abacus_debitor_number, last_name=checkout_information.last_name, first_name=checkout_information.first_name, company_name=( checkout_information.organisation_detail_name if checkout_information.invoice_address == "org" else "" ), street=( checkout_information.organisation_street if checkout_information.invoice_address == "org" else checkout_information.street ), house_number=( checkout_information.organisation_street_number if checkout_information.invoice_address == "org" else checkout_information.street_number ), zip_code=( checkout_information.organisation_postal_code if checkout_information.invoice_address == "org" else checkout_information.postal_code ), city=( checkout_information.organisation_city if checkout_information.invoice_address == "org" else checkout_information.city ), country=( checkout_information.organisation_country_id if checkout_information.invoice_address == "org" else checkout_information.country_id ), language=customer.language, email=customer.email, ) customer_xml_filename = f"myVBV_debi_{customer.abacus_debitor_number}.xml" return customer_xml_filename, customer_xml_content def render_invoice_xml( abacus_debitor_number: int, abacus_order_id: int, datatrans_transaction_id: str, order_date: datetime.date, item_description: str, ) -> str: container = Element("AbaConnectContainer") task = SubElement(container, "Task") parameter = SubElement(task, "Parameter") SubElement(parameter, "Application").text = "ORDE" SubElement(parameter, "Id").text = "Verkaufsauftrag" SubElement(parameter, "MapId").text = "AbaDefault" SubElement(parameter, "Version").text = "2022.00" transaction = SubElement(task, "Transaction") sales_order_header = SubElement(transaction, "SalesOrderHeader", mode="SAVE") sales_order_header_fields = SubElement( sales_order_header, "SalesOrderHeaderFields", mode="SAVE" ) # Skender: Kundennummer, erste Ziffer abhängig von der Plattform (4 = LMS, 6 = myVBV, 7 = EduManager), Plattform führt ein eigenständiges hochzählendes Mapping. SubElement(sales_order_header_fields, "CustomerNumber").text = str( abacus_debitor_number ) # Skender: ePayment: Ablaufnr. für ePayment Rechnung in Abacus SubElement(sales_order_header_fields, "ProcessFlowNumber").text = "30" # Skender: ePayment: Zahlungskondition für ePaymente in Abacus 9999 Tage Mahnungsfrist, da schon bezahlt SubElement(sales_order_header_fields, "PaymentCode").text = "9999" # Skender: Bestellzeitpunkt SubElement(sales_order_header_fields, "PurchaseOrderDate").text = ( order_date.isoformat() ) # Skender: ePayment: TRANSACTION-ID von Datatrans in Bestellreferenz SubElement(sales_order_header_fields, "ReferencePurchaseOrder").text = ( datatrans_transaction_id ) # Skender: ePayment: OrderID. max 10 Ziffern, erste Ziffer abhängig von der Plattform (4 = LMS, 6 = myVBV, 7 = EduManager) SubElement(sales_order_header_fields, "GroupingNumberAscii1").text = str( abacus_order_id ) item_element = SubElement(sales_order_header, "Item", mode="SAVE") item_fields = SubElement(item_element, "ItemFields", mode="SAVE") SubElement(item_fields, "DeliveryDate").text = order_date.isoformat() SubElement(item_fields, "ItemNumber").text = "1" SubElement(item_fields, "ProductNumber").text = "30202" SubElement(item_fields, "QuantityOrdered").text = "1" item_text = SubElement(item_element, "ItemText", mode="SAVE") item_text_fields = SubElement(item_text, "ItemTextFields", mode="SAVE") SubElement(item_text_fields, "Text").text = item_description return create_xml_string(container) def render_customer_xml( abacus_debitor_number: int, last_name: str, first_name: str, company_name: str, street: str, house_number: str, zip_code: str, city: str, country: str, language: str, email: str, ): container = Element("AbaConnectContainer") task = SubElement(container, "Task") parameter = SubElement(task, "Parameter") SubElement(parameter, "Application").text = "DEBI" SubElement(parameter, "ID").text = "Kunden" SubElement(parameter, "MapID").text = "AbaDefault" SubElement(parameter, "Version").text = "2022.00" transaction = SubElement(task, "Transaction") customer_element = SubElement(transaction, "Customer", mode="SAVE") SubElement(customer_element, "CustomerNumber").text = str(abacus_debitor_number) SubElement(customer_element, "DefaultCurrency").text = "CHF" SubElement(customer_element, "PaymentTermNumber").text = "1" SubElement(customer_element, "ReminderProcedure").text = "NORM" address_data = SubElement(customer_element, "AddressData", mode="SAVE") SubElement(address_data, "AddressNumber").text = str(abacus_debitor_number) SubElement(address_data, "Name").text = last_name[:100] SubElement(address_data, "FirstName").text = first_name[:50] if company_name: SubElement(address_data, "Text").text = company_name[:80] SubElement(address_data, "Street").text = street[:50] SubElement(address_data, "HouseNumber").text = house_number[:9] # only take the numbers from zip_code SubElement(address_data, "ZIP").text = "".join( filter(lambda ch: str.isdigit(ch), zip_code) )[:15] SubElement(address_data, "City").text = city[:50] SubElement(address_data, "Country").text = country[:4] SubElement(address_data, "Language").text = language[:6] SubElement(address_data, "Email").text = email[:65] return create_xml_string(container) def create_xml_string(container: Element, encoding: str = "utf-8") -> str: xml_bytes = tostring(container, encoding) xml_pretty_str = minidom.parseString(xml_bytes).toprettyxml( indent=" ", encoding=encoding ) return xml_pretty_str.decode(encoding) def abacus_ssh_upload_invoice( filename: str, content_xml: str, folder: str = "" ) -> None: invoice_io = BytesIO(content_xml.encode("utf-8")) with AbacusSftpClient() as sftp_client: path = filename if folder: path = f"{folder}/{filename}" sftp_client.putfo(invoice_io, path)