285 lines
11 KiB
Python
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)
|