Merged develop into feature/VBV-692-keycloak
This commit is contained in:
commit
9692b441b2
Binary file not shown.
Binary file not shown.
|
|
@ -11,7 +11,18 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
<main v-if="feedbackData">
|
<main v-if="feedbackData">
|
||||||
<h1 class="mb-2">{{ $t("feedback.feedbackPageTitle") }}</h1>
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="mb-2">{{ $t("feedback.feedbackPageTitle") }}</h2>
|
||||||
|
<button
|
||||||
|
v-if="feedbackType == 'uk'"
|
||||||
|
class="flex"
|
||||||
|
data-cy="export-button"
|
||||||
|
@click="exportData"
|
||||||
|
>
|
||||||
|
<it-icon-export></it-icon-export>
|
||||||
|
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p class="mb-10">
|
<p class="mb-10">
|
||||||
<span class="font-bold" data-cy="feedback-data-amount">
|
<span class="font-bold" data-cy="feedback-data-amount">
|
||||||
{{ feedbackData.amount }}
|
{{ feedbackData.amount }}
|
||||||
|
|
@ -37,6 +48,9 @@ import type { FeedbackData, FeedbackType } from "@/types";
|
||||||
import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue";
|
import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue";
|
||||||
import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue";
|
import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue";
|
||||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
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<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -46,9 +60,22 @@ const props = defineProps<{
|
||||||
log.debug("FeedbackPage created", props.circleId);
|
log.debug("FeedbackPage created", props.circleId);
|
||||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const feedbackData = ref<FeedbackData | undefined>(undefined);
|
const feedbackData = ref<FeedbackData | undefined>(undefined);
|
||||||
const feedbackType = ref<FeedbackType | undefined>(undefined);
|
const feedbackType = ref<FeedbackType | undefined>(undefined);
|
||||||
|
|
||||||
|
async function 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 () => {
|
onMounted(async () => {
|
||||||
log.debug("FeedbackPage mounted");
|
log.debug("FeedbackPage mounted");
|
||||||
feedbackData.value = await itGet(
|
feedbackData.value = await itGet(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||||
import { useCourseSessionDetailQuery } from "@/composables";
|
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||||
import type { AttendanceUserStatus } from "@/gql/graphql";
|
import type { AttendanceUserStatus } from "@/gql/graphql";
|
||||||
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
|
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
|
||||||
import type { DropdownSelectable } from "@/types";
|
import type { DropdownSelectable } from "@/types";
|
||||||
|
|
@ -13,10 +13,15 @@ import { computed, onMounted, reactive, watch } from "vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
|
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
|
||||||
import { graphqlClient } from "@/graphql/client";
|
import { graphqlClient } from "@/graphql/client";
|
||||||
|
import { exportAttendance } from "@/services/dashboard";
|
||||||
|
import { openDataAsXls } from "@/utils/export";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
|
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
|
||||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
|
||||||
const attendanceCourses = computed(() => {
|
const attendanceCourses = computed(() => {
|
||||||
return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? [];
|
return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? [];
|
||||||
|
|
@ -26,6 +31,13 @@ const courseSessionDetail = computed(() => {
|
||||||
return courseSessionDetailResult.courseSessionDetail.value;
|
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(() => {
|
const presenceCoursesDropdownOptions = computed(() => {
|
||||||
return attendanceCourses.value.map(
|
return attendanceCourses.value.map(
|
||||||
(attendanceCourse) =>
|
(attendanceCourse) =>
|
||||||
|
|
@ -114,6 +126,17 @@ function editAgain() {
|
||||||
state.attendanceSaved = false;
|
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(() => {
|
onMounted(() => {
|
||||||
log.debug("AttendanceCheckPage mounted");
|
log.debug("AttendanceCheckPage mounted");
|
||||||
loadAttendanceData();
|
loadAttendanceData();
|
||||||
|
|
@ -141,8 +164,18 @@ watch(
|
||||||
<span>{{ $t("general.back") }}</span>
|
<span>{{ $t("general.back") }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="pb-4 text-xl font-bold">{{ $t("Anwesenheit Präsenzkurse") }}</div>
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="pb-4 text-xl font-bold">{{ $t("Anwesenheit Präsenzkurse") }}</h3>
|
||||||
|
<button
|
||||||
|
v-if="state.attendanceSaved"
|
||||||
|
class="flex"
|
||||||
|
data-cy="export-button"
|
||||||
|
@click="exportData"
|
||||||
|
>
|
||||||
|
<it-icon-export></it-icon-export>
|
||||||
|
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<section v-if="attendanceCourses.length && state.attendanceCourseSelected">
|
<section v-if="attendanceCourses.length && state.attendanceCourseSelected">
|
||||||
<div class="flex flex-row justify-between bg-white p-6">
|
<div class="flex flex-row justify-between bg-white p-6">
|
||||||
<ItDropdownSelect
|
<ItDropdownSelect
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ stdout_logfile=/dev/stdout
|
||||||
stdout_logfile_maxbytes=0
|
stdout_logfile_maxbytes=0
|
||||||
|
|
||||||
[program:gunicorn]
|
[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
|
redirect_stderr=true
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
stdout_logfile_maxbytes=0
|
stdout_logfile_maxbytes=0
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ def test_upload_abacus_xml(setup_abacus_env):
|
||||||
assert "<Email>andreas.feuz@eiger-versicherungen.ch</Email>" in debi_content
|
assert "<Email>andreas.feuz@eiger-versicherungen.ch</Email>" in debi_content
|
||||||
|
|
||||||
order_filepath = os.path.join(
|
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)
|
assert os.path.exists(order_filepath)
|
||||||
with open(order_filepath) as order_file:
|
with open(order_filepath) as order_file:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import (
|
from vbv_lernwelt.assignment.models import (
|
||||||
Assignment,
|
Assignment,
|
||||||
|
|
@ -9,7 +11,11 @@ from vbv_lernwelt.core.constants import (
|
||||||
TEST_COURSE_SESSION_BERN_ID,
|
TEST_COURSE_SESSION_BERN_ID,
|
||||||
TEST_COURSE_SESSION_ZURICH_ID,
|
TEST_COURSE_SESSION_ZURICH_ID,
|
||||||
TEST_STUDENT1_USER_ID,
|
TEST_STUDENT1_USER_ID,
|
||||||
|
TEST_STUDENT2_USER_ID,
|
||||||
|
TEST_STUDENT3_USER_ID,
|
||||||
TEST_SUPERVISOR1_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.create_default_users import create_default_users
|
||||||
from vbv_lernwelt.core.models import User
|
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])],
|
[(TEST_COURSE_SESSION_ZURICH_ID, [circle_fahrzeug.id, circle_reisen.id])],
|
||||||
allowed_circles,
|
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)],
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,7 @@ def _create_person_list_with_roles(user):
|
||||||
"email": user_object.email,
|
"email": user_object.email,
|
||||||
"avatar_url_small": user_object.avatar_url_small,
|
"avatar_url_small": user_object.avatar_url_small,
|
||||||
"avatar_url": user_object.avatar_url,
|
"avatar_url": user_object.avatar_url,
|
||||||
|
"course_sessions": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
course_sessions = get_course_sessions_with_roles_for_user(user)
|
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)
|
my_role = user_role(cs.roles)
|
||||||
for csu in course_session_users:
|
for csu in course_session_users:
|
||||||
person_data = create_user_dict(csu.user)
|
person_data = result_persons.get(
|
||||||
person_data["course_sessions"] = [
|
csu.user.id, create_user_dict(csu.user)
|
||||||
|
)
|
||||||
|
person_data["course_sessions"].append(
|
||||||
_create_course_session_dict(cs, my_role, csu.role)
|
_create_course_session_dict(cs, my_role, csu.role)
|
||||||
]
|
)
|
||||||
result_persons[csu.user.id] = person_data
|
result_persons[csu.user.id] = person_data
|
||||||
|
|
||||||
# add persons where request.user is mentor
|
# add persons where request.user is mentor
|
||||||
|
|
|
||||||
|
|
@ -847,6 +847,10 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"
|
||||||
init_notification_settings(user)
|
init_notification_settings(user)
|
||||||
user.save()
|
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()
|
group = data["Klasse"].strip()
|
||||||
|
|
||||||
# general expert handling
|
# general expert handling
|
||||||
|
|
@ -884,7 +888,7 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"
|
||||||
# circle expert handling
|
# circle expert handling
|
||||||
circle_data = parse_circle_group_string(data["Circles"])
|
circle_data = parse_circle_group_string(data["Circles"])
|
||||||
for circle_key in circle_data:
|
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)
|
# print(circle_name, groups)
|
||||||
import_id = f"{data['Generation'].strip()} {group}"
|
import_id = f"{data['Generation'].strip()} {group}"
|
||||||
|
|
@ -892,7 +896,7 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"
|
||||||
import_id=import_id, group=group
|
import_id=import_id, group=group
|
||||||
).first()
|
).first()
|
||||||
circle = Circle.objects.filter(
|
circle = Circle.objects.filter(
|
||||||
slug=f"{course.slug}-lp-circle-{circle_name.lower()}"
|
slug=f"{course.slug}-lp-circle-{circle_slug}"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if course_session and circle:
|
if course_session and circle:
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ def create_invoice_xml(checkout_information: CheckoutInformation):
|
||||||
|
|
||||||
# YYYYMMDDhhmmss
|
# YYYYMMDDhhmmss
|
||||||
filename_datetime = checkout_information.created_at.strftime("%Y%m%d%H%M%S")
|
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
|
return invoice_xml_filename, invoice_xml_content
|
||||||
|
|
||||||
|
|
@ -207,17 +207,20 @@ def render_customer_xml(
|
||||||
|
|
||||||
address_data = SubElement(customer_element, "AddressData", mode="SAVE")
|
address_data = SubElement(customer_element, "AddressData", mode="SAVE")
|
||||||
SubElement(address_data, "AddressNumber").text = str(abacus_debitor_number)
|
SubElement(address_data, "AddressNumber").text = str(abacus_debitor_number)
|
||||||
SubElement(address_data, "Name").text = last_name
|
SubElement(address_data, "Name").text = last_name[:100]
|
||||||
SubElement(address_data, "FirstName").text = first_name
|
SubElement(address_data, "FirstName").text = first_name[:50]
|
||||||
if company_name:
|
if company_name:
|
||||||
SubElement(address_data, "Text").text = company_name
|
SubElement(address_data, "Text").text = company_name[:80]
|
||||||
SubElement(address_data, "Street").text = street
|
SubElement(address_data, "Street").text = street[:50]
|
||||||
SubElement(address_data, "HouseNumber").text = house_number
|
SubElement(address_data, "HouseNumber").text = house_number[:9]
|
||||||
SubElement(address_data, "ZIP").text = zip_code
|
# only take the numbers from zip_code
|
||||||
SubElement(address_data, "City").text = city
|
SubElement(address_data, "ZIP").text = "".join(
|
||||||
SubElement(address_data, "Country").text = country
|
filter(lambda ch: str.isdigit(ch), zip_code)
|
||||||
SubElement(address_data, "Language").text = language
|
)[:15]
|
||||||
SubElement(address_data, "Email").text = email
|
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)
|
return create_xml_string(container)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class AbacusInvoiceTestCase(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
invoice_xml_filename, "myVBV_orde_60000012_20240215083312_6000000124.xml"
|
invoice_xml_filename, "myVBV_orde_20240215083312_60000012_6000000124.xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(invoice_xml_content)
|
print(invoice_xml_content)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue