vbv/server/vbv_lernwelt/shop/invoice/abacus.py

285 lines
11 KiB
Python

import datetime
from io import BytesIO
from xml.dom import minidom
from xml.etree.ElementTree import Element, SubElement, tostring
import structlog
from django.db.models import F, Q
from django.utils import timezone
from vbv_lernwelt.shop.const import VV_PRODUCT_NUMBER
from vbv_lernwelt.shop.invoice.abacus_sftp_client import AbacusSftpClient
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState
logger = structlog.get_logger(__name__)
def filter_checkout_information_for_abacus_upload_qs():
return CheckoutInformation.objects.filter(
Q(abacus_ssh_upload_done=False)
| Q(updated_at__gt=F("invoice_transmitted_at") + datetime.timedelta(minutes=1)),
state=CheckoutState.PAID,
)
def abacus_ssh_upload(checkout_information: CheckoutInformation):
if checkout_information.state != CheckoutState.PAID:
# only upload invoice if checkout is paid
return True
try:
if (
# invoice not in abacus yet
not checkout_information.abacus_ssh_upload_done
# invoice updated more than 5 minutes ago
or (
checkout_information.abacus_ssh_upload_done
and checkout_information.invoice_transmitted_at is not None
and (
checkout_information.updated_at
- checkout_information.invoice_transmitted_at
)
> datetime.timedelta(minutes=1)
)
):
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 = timezone.now()
abacus_transmission_list = checkout_information.additional_json_data.get(
"abacus_transmission_list", []
)
abacus_transmission_list.append(
{
"invoice_filename": invoice_xml_filename,
"customer_filename": customer_xml_filename,
"transmitted_at": checkout_information.invoice_transmitted_at.isoformat(),
}
)
checkout_information.additional_json_data["abacus_transmission_list"] = (
abacus_transmission_list
)
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,
label="abacus_ssh_upload",
)
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.abacus_use_organisation_data()
else ""
),
street=(
checkout_information.organisation_street
if checkout_information.abacus_use_organisation_data()
else checkout_information.street
),
house_number=(
checkout_information.organisation_street_number
if checkout_information.abacus_use_organisation_data()
else checkout_information.street_number
),
zip_code=(
checkout_information.organisation_postal_code
if checkout_information.abacus_use_organisation_data()
else checkout_information.postal_code
),
city=(
checkout_information.organisation_city
if checkout_information.abacus_use_organisation_data()
else checkout_information.city
),
country=(
checkout_information.organisation_country_id
if checkout_information.abacus_use_organisation_data()
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 = VV_PRODUCT_NUMBER
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)