From 7074c367c32393f773e22bc97b63db9f35de20b9 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 1 Jul 2024 11:02:04 +0200 Subject: [PATCH 1/9] Fix course_sessoins list for person view --- .../dashboard/tests/test_views.py | 82 +++++++++++++++++++ server/vbv_lernwelt/dashboard/views.py | 9 +- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index c98d5ae1..277bbf5b 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -1,4 +1,6 @@ from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase from vbv_lernwelt.assignment.models import ( Assignment, @@ -9,7 +11,11 @@ from vbv_lernwelt.core.constants import ( TEST_COURSE_SESSION_BERN_ID, TEST_COURSE_SESSION_ZURICH_ID, TEST_STUDENT1_USER_ID, + TEST_STUDENT2_USER_ID, + TEST_STUDENT3_USER_ID, TEST_SUPERVISOR1_USER_ID, + TEST_TRAINER1_USER_ID, + TEST_TRAINER2_USER_ID, ) from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.models import User @@ -541,3 +547,79 @@ class ExportXlsTestCase(TestCase): [(TEST_COURSE_SESSION_ZURICH_ID, [circle_fahrzeug.id, circle_reisen.id])], allowed_circles, ) + + +class PersonsTestCase(APITestCase): + def setUp(self): + create_default_users() + create_test_course(include_uk=True, include_vv=False, with_sessions=True) + + self.student1 = User.objects.get(id=TEST_STUDENT1_USER_ID) + self.csu1_student1 = CourseSessionUser.objects.get( + user=self.student1, course_session__id=TEST_COURSE_SESSION_BERN_ID + ) + self.student2 = User.objects.get(id=TEST_STUDENT2_USER_ID) + self.csu1_student2 = CourseSessionUser.objects.get( + user=self.student2, course_session__id=TEST_COURSE_SESSION_ZURICH_ID + ) + + def test_get_course_sessions_with_roles_for_trainer(self): + trainer = User.objects.get(id=TEST_TRAINER1_USER_ID) + self.client.force_login(trainer) + + url = reverse( + "get_dashboard_persons", + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 4) + user_ids = [str(user["user_id"]) for user in response.data] + self.assertCountEqual( + user_ids, + [ + TEST_TRAINER1_USER_ID, + TEST_STUDENT1_USER_ID, + TEST_STUDENT2_USER_ID, + TEST_STUDENT3_USER_ID, + ], + ) + user1_index = user_ids.index(TEST_STUDENT1_USER_ID) + self.assertEqual( + response.data[user1_index]["course_sessions"][0]["id"], + str(CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID).id), + ) + + def test_get_course_sessions_with_roles_for_supervisor(self): + supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) + self.client.force_login(supervisor) + + url = reverse( + "get_dashboard_persons", + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 5) + user_ids = [str(user["user_id"]) for user in response.data] + self.assertCountEqual( + user_ids, + [ + TEST_TRAINER1_USER_ID, + TEST_STUDENT1_USER_ID, + TEST_STUDENT2_USER_ID, + TEST_STUDENT3_USER_ID, + TEST_TRAINER2_USER_ID, + ], + ) + user2_index = user_ids.index(TEST_STUDENT2_USER_ID) + user2_cs = response.data[user2_index]["course_sessions"] + self.assertEqual(len(user2_cs), 2) + + user2_cs_ids = [cs["id"] for cs in user2_cs] + self.assertCountEqual( + user2_cs_ids, + [str(TEST_COURSE_SESSION_ZURICH_ID), str(TEST_COURSE_SESSION_BERN_ID)], + ) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 33962032..1cdf533a 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -173,6 +173,7 @@ def _create_person_list_with_roles(user): "email": user_object.email, "avatar_url_small": user_object.avatar_url_small, "avatar_url": user_object.avatar_url, + "course_sessions": [], } course_sessions = get_course_sessions_with_roles_for_user(user) @@ -185,10 +186,12 @@ def _create_person_list_with_roles(user): ) my_role = user_role(cs.roles) for csu in course_session_users: - person_data = create_user_dict(csu.user) - person_data["course_sessions"] = [ + person_data = result_persons.get( + csu.user.id, create_user_dict(csu.user) + ) + person_data["course_sessions"].append( _create_course_session_dict(cs, my_role, csu.role) - ] + ) result_persons[csu.user.id] = person_data # add persons where request.user is mentor From aed574b13ca899aec40b6fe657f07ccf9b75dbcb Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 2 Jul 2024 07:25:22 +0200 Subject: [PATCH 2/9] Add 1 git-crypt collaborator New collaborators: C2175B8A Ramon Wenger --- ...72A3BBE62AD447B5A598326B07671799C2175B8A.gpg | Bin 725 -> 725 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.git-crypt/keys/default/0/72A3BBE62AD447B5A598326B07671799C2175B8A.gpg b/.git-crypt/keys/default/0/72A3BBE62AD447B5A598326B07671799C2175B8A.gpg index db281e8596e15f71b51ce55ce47511b64384505f..b30ef07b0c6955bd5cef8ce8b5dc848011299be9 100644 GIT binary patch literal 725 zcmV;`0xJE50t^Gn*6M@r4BHz45C3j3JFD^v9&Z6bxT3T$<;(7$Xr(1(3c|lL)~0FN zaEp$cy{73qra^*-_l{&WQh8=@%0CEBztwipBydd^2@Hy6C$V@j`*zbZF{B21Z}x+|3d{!C^jJF zn(v@X5;+dnWtH(CbY`xllTCo$YDg+r)hmsvEd}P1Y?oo%Vw%ds*qs0d;=xgap;s>% z&_DGDybJzc_Y@XP5wd^Y1_G^qD`ojR9`bIdOHk>iHP=ADl3yGLk#f@amD;dn#D22A zUG+RTFubuHf=y@pT`SOLSk&oF#SEEtJEQMNNNO3-@gVOh;*;O_`a2D*Ye|37 zbLqWv=jRa>ox~6(-H~;i=Ao>rV7MuF2&53w0@WDlA7c^e6DVQX#L{~rM%_|b+&X-^= H>aLB`3XEPr literal 725 zcmV;`0xJE50t^Gn*6M@r4BHz45C2I8y(M_uxTHXV4Bmn><#*M;Z+Dyn5^Vn!TiMxh zVyEuvd9hni-l`rd>zT-tGyfAZX^-Bv;UuZa;VOLpaYaSUouDBpq_M~+a6Wt3@QnXr zpc#$aJ8XiZ#D2BVbLZlRnssy7NXE`nqM{$(4Bfc8N5NxygO6Q)g?SHU(=x!3en#P# z_Wa(xT08~up!gLZa!!@AHNhQ=9tQ3hq0DT{Z5JJLIb;iBd-Js}(|c`ZX1WV5@m5ru z<*Mcc$4q!c$2+(yAynD4yPi#uKij?B?4L|AJ@v;5c7U0b-?}}c4$7>bxcgna-Ip0a z>8RZp6GB)M{2fK7GA}Bwyp%0vks*KTmjI+w!7JsCrDheb`3QNlLNn}TShC;}>Gn&? zEM~mw3SY7<)d&#n$lrA%v5~GgFr2EDYp~BM5}~4n^x%-K#3{U;XYY#Cw;|ENV{W}(z<}Qz^9wKf60Q3)b3Fq z$IJqwZ!c{}NRA&ZMFSDqDCj}p1U=4GDq3HTD7}lV)JL zKJ{~(>9?Wf=CmQ-V8`Y$wOl$exi==gz!EOK%vAhUL;6`0fJSDU_udMBO*X%%Aj HzzAGn9pZ1O From 4390dc0d472cad2d9b15b447752373ed9fdd5bd7 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 2 Jul 2024 09:11:46 +0200 Subject: [PATCH 3/9] Add 1 git-crypt collaborator [skip ci] New collaborators: 8DFE4008 Ramon Wenger --- .../70D75BE89768BB8DD5609A186CC392238DFE4008.gpg | Bin 0 -> 597 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .git-crypt/keys/default/0/70D75BE89768BB8DD5609A186CC392238DFE4008.gpg diff --git a/.git-crypt/keys/default/0/70D75BE89768BB8DD5609A186CC392238DFE4008.gpg b/.git-crypt/keys/default/0/70D75BE89768BB8DD5609A186CC392238DFE4008.gpg new file mode 100644 index 0000000000000000000000000000000000000000..7329d5db393778249e33db28dc2505fa9a71cf48 GIT binary patch literal 597 zcmV-b0;>Im0gMATvCa4N5Rnf73;rbZz#;oi(_nXC?y*uDzzM3qi#gG9;35Sa#$X%I z@@ex$AXjaTR1nd4BWU)>lxc~)tGh8Chc8;n=ob0*FYLw9 z!UY{)@6IzG&=fB4@%p-6Q$AKNL(W^Mstx{*O_O5(WFWU1*A1~SeOCms<$cT;hy;UH zXeL?(UVty?iD-NmQ4PTBMbKds&E4hlJzEcpM$eto7!@Y2Gw(lisw4T>t}P0Yi@lQH zxAX+PtottRADddFdjsDnIh+!>A+c-9nG4FB*rvz?)#a(Kf3H?e-cH(AoF7KloR;B@ zrZ?qH21NaL^g7`i*_`s6MxI({EYEA|M6pCs|USxFJ+sSPs z>ZZ>*o^PR2-~3ilq*sD}W~z~`DIp0na|oHMND%f$Ut?1`HcV+K=wzK7dWZFE$pjIS zkTwd^p<@<)B0&6|ScYW~!P1BT)OE+-xJb#)fuizD;l{O;WGA?39mf%9AQrx6`hnv* jh7@%9Uv9h?q~=siD;CS$Ea3%qD$@EHTUE)oq^9n6scR+k literal 0 HcmV?d00001 From 767f6c42f6cbfce4d502ef82ef1064b58018f22a Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 2 Jul 2024 14:07:43 +0200 Subject: [PATCH 4/9] Add export to feedback cockpit page --- client/src/pages/cockpit/FeedbackPage.vue | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/client/src/pages/cockpit/FeedbackPage.vue b/client/src/pages/cockpit/FeedbackPage.vue index 0267d5f4..8217045d 100644 --- a/client/src/pages/cockpit/FeedbackPage.vue +++ b/client/src/pages/cockpit/FeedbackPage.vue @@ -11,7 +11,18 @@
-

{{ $t("feedback.feedbackPageTitle") }}

+
+

{{ $t("feedback.feedbackPageTitle") }}

+ +

{{ feedbackData.amount }} @@ -37,6 +48,9 @@ import type { FeedbackData, FeedbackType } from "@/types"; import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue"; import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue"; import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables"; +import { exportFeedback } from "@/services/dashboard"; +import { useUserStore } from "@/stores/user"; +import { openDataAsXls } from "@/utils/export"; const props = defineProps<{ courseSlug: string; @@ -46,9 +60,23 @@ const props = defineProps<{ log.debug("FeedbackPage created", props.circleId); const { loading } = useExpertCockpitPageData(props.courseSlug); const courseSession = useCurrentCourseSession(); +const userStore = useUserStore(); + const feedbackData = ref(undefined); const feedbackType = ref(undefined); +async function exportData() { + log.debug("FeedbackPage exportData"); + const data = await exportFeedback( + { + courseSessionIds: [Number(courseSession.value.id)], + circleIds: [Number(props.circleId)], + }, + userStore.language + ); + openDataAsXls(data.encoded_data, data.file_name); +} + onMounted(async () => { log.debug("FeedbackPage mounted"); feedbackData.value = await itGet( From c0a94ca98841fe78ac8429fb81ab796cf23ce290 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 2 Jul 2024 15:03:42 +0200 Subject: [PATCH 5/9] Add attendance export in cockpit --- client/src/pages/cockpit/FeedbackPage.vue | 1 - .../AttendanceCheckPage.vue | 39 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/client/src/pages/cockpit/FeedbackPage.vue b/client/src/pages/cockpit/FeedbackPage.vue index 8217045d..e48dc6d4 100644 --- a/client/src/pages/cockpit/FeedbackPage.vue +++ b/client/src/pages/cockpit/FeedbackPage.vue @@ -66,7 +66,6 @@ const feedbackData = ref(undefined); const feedbackType = ref(undefined); async function exportData() { - log.debug("FeedbackPage exportData"); const data = await exportFeedback( { courseSessionIds: [Number(courseSession.value.id)], diff --git a/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue b/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue index 502c67b4..27460184 100644 --- a/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue +++ b/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue @@ -2,7 +2,7 @@ import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue"; -import { useCourseSessionDetailQuery } from "@/composables"; +import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables"; import type { AttendanceUserStatus } from "@/gql/graphql"; import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations"; import type { DropdownSelectable } from "@/types"; @@ -13,10 +13,15 @@ import { computed, onMounted, reactive, watch } from "vue"; import { useTranslation } from "i18next-vue"; import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries"; import { graphqlClient } from "@/graphql/client"; +import { exportAttendance } from "@/services/dashboard"; +import { openDataAsXls } from "@/utils/export"; +import { useUserStore } from "@/stores/user"; const { t } = useTranslation(); const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION); const courseSessionDetailResult = useCourseSessionDetailQuery(); +const userStore = useUserStore(); +const courseSession = useCurrentCourseSession(); const attendanceCourses = computed(() => { return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? []; @@ -26,6 +31,13 @@ const courseSessionDetail = computed(() => { return courseSessionDetailResult.courseSessionDetail.value; }); +const attendanceCourseCircleId = computed(() => { + const selectedAttendandeCourse = attendanceCourses.value.find( + (course) => course.id === state.attendanceCourseSelected.id + ); + return selectedAttendandeCourse?.learning_content?.circle?.id; +}); + const presenceCoursesDropdownOptions = computed(() => { return attendanceCourses.value.map( (attendanceCourse) => @@ -114,6 +126,17 @@ function editAgain() { state.attendanceSaved = false; } +async function exportData() { + const data = await exportAttendance( + { + courseSessionIds: [Number(courseSession.value.id)], + circleIds: [Number(attendanceCourseCircleId.value)], + }, + userStore.language + ); + openDataAsXls(data.encoded_data, data.file_name); +} + onMounted(() => { log.debug("AttendanceCheckPage mounted"); loadAttendanceData(); @@ -141,8 +164,18 @@ watch( {{ $t("general.back") }} -

{{ $t("Anwesenheit Präsenzkurse") }}
- +
+

{{ $t("Anwesenheit Präsenzkurse") }}

+ +
Date: Wed, 3 Jul 2024 17:07:28 +0200 Subject: [PATCH 6/9] Increase number of gunicorn workers to 8 [skip-ci] --- compose/django/supervisord.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/django/supervisord.conf b/compose/django/supervisord.conf index 2e668f56..08490e64 100644 --- a/compose/django/supervisord.conf +++ b/compose/django/supervisord.conf @@ -8,7 +8,7 @@ stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 [program:gunicorn] -command=newrelic-admin run-program gunicorn config.asgi --bind 0.0.0.0:7555 --chdir=/app -k uvicorn.workers.UvicornWorker +command=newrelic-admin run-program gunicorn config.asgi --bind 0.0.0.0:7555 --chdir=/app -k uvicorn.workers.UvicornWorker --workers 8 --worker-connections 1000 --backlog 2048 --timeout 60 --keep-alive 3 redirect_stderr=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 From ae3ecec979f37087fbd2e4b17563f73f89308812 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 8 Jul 2024 08:44:05 +0200 Subject: [PATCH 7/9] Use slug not title --- server/vbv_lernwelt/importer/services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 1db6fea8..0121df22 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -874,7 +874,7 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de" # circle expert handling circle_data = parse_circle_group_string(data["Circles"]) for circle_key in circle_data: - circle_name = LP_DATA[circle_key][language]["title"] + circle_slug = LP_DATA[circle_key][language]["slug"] # print(circle_name, groups) import_id = f"{data['Generation'].strip()} {group}" @@ -882,7 +882,7 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de" import_id=import_id, group=group ).first() circle = Circle.objects.filter( - slug=f"{course.slug}-lp-circle-{circle_name.lower()}" + slug=f"{course.slug}-lp-circle-{circle_slug}" ).first() if course_session and circle: From bdb671c6e97f738cbdd74341cdfb7b33d7264b9e Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 8 Jul 2024 13:16:22 +0200 Subject: [PATCH 8/9] Use user language if there's a mismatch --- server/vbv_lernwelt/importer/services.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 0121df22..d100dff5 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -837,6 +837,10 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de" user.language = data["Sprache"] user.save() + # As the is never set this is the only way to determine the correct course + if user.language != language: + language = user.language + group = data["Klasse"].strip() # general expert handling From 0eeae993cb2e92606917ea1068645f0ac102fcac Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 9 Jul 2024 13:21:08 +0200 Subject: [PATCH 9/9] VBV-703: abacus filename: timestamp comes first --- .../abacus_sftp/test_abacus_sftp.py | 2 +- server/vbv_lernwelt/shop/invoice/abacus.py | 25 +++++++++++-------- .../shop/tests/test_abacus_invoice.py | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/server/integration_tests/abacus_sftp/test_abacus_sftp.py b/server/integration_tests/abacus_sftp/test_abacus_sftp.py index dc747504..483f7102 100644 --- a/server/integration_tests/abacus_sftp/test_abacus_sftp.py +++ b/server/integration_tests/abacus_sftp/test_abacus_sftp.py @@ -102,7 +102,7 @@ def test_upload_abacus_xml(setup_abacus_env): assert "andreas.feuz@eiger-versicherungen.ch" in debi_content order_filepath = os.path.join( - tmppath, "order/myVBV_orde_60000012_20240215083312_6000000124.xml" + tmppath, "order/myVBV_orde_20240215083312_60000012_6000000124.xml" ) assert os.path.exists(order_filepath) with open(order_filepath) as order_file: diff --git a/server/vbv_lernwelt/shop/invoice/abacus.py b/server/vbv_lernwelt/shop/invoice/abacus.py index 201345d3..ffd9ee33 100644 --- a/server/vbv_lernwelt/shop/invoice/abacus.py +++ b/server/vbv_lernwelt/shop/invoice/abacus.py @@ -63,7 +63,7 @@ def create_invoice_xml(checkout_information: CheckoutInformation): # YYYYMMDDhhmmss filename_datetime = checkout_information.created_at.strftime("%Y%m%d%H%M%S") - invoice_xml_filename = f"myVBV_orde_{customer.abacus_debitor_number}_{filename_datetime}_{checkout_information.abacus_order_id}.xml" + 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 @@ -207,17 +207,20 @@ def render_customer_xml( address_data = SubElement(customer_element, "AddressData", mode="SAVE") SubElement(address_data, "AddressNumber").text = str(abacus_debitor_number) - SubElement(address_data, "Name").text = last_name - SubElement(address_data, "FirstName").text = first_name + 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 - SubElement(address_data, "Street").text = street - SubElement(address_data, "HouseNumber").text = house_number - SubElement(address_data, "ZIP").text = zip_code - SubElement(address_data, "City").text = city - SubElement(address_data, "Country").text = country - SubElement(address_data, "Language").text = language - SubElement(address_data, "Email").text = email + 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) diff --git a/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py b/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py index f9e5e171..e7171ba0 100644 --- a/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py +++ b/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py @@ -43,7 +43,7 @@ class AbacusInvoiceTestCase(TestCase): ) self.assertEqual( - invoice_xml_filename, "myVBV_orde_60000012_20240215083312_6000000124.xml" + invoice_xml_filename, "myVBV_orde_20240215083312_60000012_6000000124.xml" ) print(invoice_xml_content)