Compare commits

...

44 Commits

Author SHA1 Message Date
d40b1c5887 chore: update dependencies 2025-01-19 22:37:47 +01:00
79402510fd Update dependencies 2024-10-29 00:00:23 +01:00
c6a589b2fd fix: ignore oBDS containing Menge_Tumorkonferenz
This will replace previous used filter and only includes oBDS messages without
information about tumor board information.
2024-09-13 16:15:14 +02:00
851bdad9d0 chore: update copyright notice 2024-09-13 16:15:14 +02:00
0b6024e80b
Merge pull request #1 from pcvolkmer/issue_5
feat: ignore non oBDS 2.x messages
2024-08-30 17:42:59 +02:00
fb60cb5042 feat: ignore non oBDS 2.x messages 2024-07-24 13:55:20 +02:00
f48a96e64d refactor: Extract func to print list of missing messages 2024-07-03 19:43:38 +02:00
2ef4ec4c90 fix: use correct table alias 2024-07-02 09:17:08 +02:00
f7dd9c3b37 chore: use common query filter for export 2024-07-02 08:54:48 +02:00
cfd46c34af refactor: use references as argument 2024-06-25 15:49:20 +02:00
1c41b71ca2 docs: add information about reformatting XML files 2024-06-23 14:49:20 +02:00
925aa4e786 refactor: change parameter type to &str 2024-06-23 14:48:47 +02:00
54dd9163a9 refactor: use 'package' as primary argument name but keep 'export-package' 2024-06-18 09:22:32 +02:00
97004be3ba chore: bump version 2024-06-13 16:33:52 +02:00
7fc724db6a
Merge pull request #4 from pcvolkmer/issue_1
feat: show schema versions and change extern detection
2024-06-13 16:22:41 +02:00
46eb43aab9 feat: show schema versions and change extern detection
This adds a new optional argument to split conditions by used schema versions in
addition to ICD10 group.

Since a JOIN on table `lkr_meldung` might use newer, not yet exported information
the detection of external items will be done by using `Melder_ID`.
2024-06-13 16:16:50 +02:00
e1ca2d927a feat: also remove windows like line breaks 2024-06-13 09:25:46 +02:00
07478cf6a3 feat: convert self-closing tags when sanitizing xml 2024-06-13 09:24:16 +02:00
3520806e79 feat: sort items with different content by database id 2024-06-13 09:20:54 +02:00
aa1ec6c114 test: add test for Meldung#id() 2024-06-12 18:10:07 +02:00
a905a816f5 style: use brighter blue to make message more visible 2024-06-12 18:03:22 +02:00
8869090b08
Merge pull request #3 from pcvolkmer/issue_2
Vergleich der LKR-Meldungen in Datenbank mit LKR-Export-Protokolldatei
2024-06-12 17:49:59 +02:00
d318151055 docs: add section to README describing check-export command 2024-06-12 17:41:06 +02:00
fd45467513 feat: show items not equal within db and xml 2024-06-12 16:50:29 +02:00
58026f11d0 feat: extract ICD10 code 2024-06-12 16:48:58 +02:00
3e07408a7d feat: show entries with multiple meldung item in xml_daten 2024-06-12 15:51:23 +02:00
af4dec0279 feat: show entries missing in compared source 2024-06-12 13:42:02 +02:00
c5c0fcf6d4 feat: add function to resolve database id 2024-06-12 13:40:48 +02:00
ac2f73d0bb fix: ignore exports with typ -1 2024-06-11 20:26:40 +02:00
53cde34166 feat: use id as string 2024-06-11 20:19:41 +02:00
0dfc6a0083 feat: request data from db and compare with file 2024-06-11 19:43:13 +02:00
6617ce76a0 feat: parse LKR protocol file 2024-06-11 19:30:25 +02:00
66c02edccc build: create release drafts and upload assets 2024-06-07 11:33:21 +02:00
665e728040 build: use powershell to create zip file on windows 2024-06-07 11:32:30 +02:00
2b94618f9f test: add simple test to check icd10 code mapping 2024-06-07 09:54:46 +02:00
ceba8d28b9 build: add GitHub action to run tests 2024-06-07 09:34:55 +02:00
4c8c6295e0 chore: bump version 2024-05-23 08:29:49 +02:00
9f5188ce5d feat: use PathBuf as argument type 2024-05-23 08:27:24 +02:00
af44351f09 feat: enable messages with 'histologie_zytologie' using optional flag 2024-05-22 16:09:45 +02:00
02497cd007 refactor: replace deprecated method 2024-05-22 10:39:53 +02:00
9d7e511725 chore: bump version 2024-05-22 10:27:35 +02:00
e4c3e61f35 chore: update dependencies 2024-05-22 10:26:56 +02:00
38f9fe3628 chore: code cleanup 2024-05-22 10:26:35 +02:00
ab15e5c9ff feat: ignore messages containing 'histologie_zytologie' 2024-05-21 16:11:54 +02:00
17 changed files with 1054 additions and 79 deletions

46
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Create release and upload assets
on:
push:
tags:
- 'v*'
jobs:
linuxbuild:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Run tests
run: cargo test --verbose
- run: make linux-package
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
draft: 'true'
make_latest: 'true'
generate_release_notes: 'true'
files: |
*linux.tar.gz
windowsbuild:
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Run tests
run: cargo test --verbose
- run: make win-package
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
draft: 'true'
make_latest: 'true'
generate_release_notes: 'true'
files: |
*win64.zip

22
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: "Run Tests"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bzkf-rwdp-check" name = "bzkf-rwdp-check"
version = "0.3.1" version = "0.4.0"
edition = "2021" edition = "2021"
authors = ["Paul-Christian Volkmer <volkmer_p@ukw.de>"] authors = ["Paul-Christian Volkmer <volkmer_p@ukw.de>"]
description = "Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform." description = "Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform."
@ -11,11 +11,11 @@ clap = { version = "4.5", features = ["std", "help", "usage", "derive", "error-c
console = "0.15" console = "0.15"
csv = "1.3" csv = "1.3"
dialoguer = "0.11" dialoguer = "0.11"
itertools = "0.12" itertools = "0.14"
mysql = "25.0" mysql = "25.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
urlencoding = "2.1" urlencoding = "2.1"
regex = "1.10" regex = "1.11"
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"

View File

@ -16,7 +16,8 @@ win-package: win-binary-x86_64
cp target/x86_64-pc-windows-gnu/release/$(PROG_NAME).exe $(PROG_NAME)/ cp target/x86_64-pc-windows-gnu/release/$(PROG_NAME).exe $(PROG_NAME)/
cp README.md $(PROG_NAME)/ cp README.md $(PROG_NAME)/
cp LICENSE $(PROG_NAME)/ cp LICENSE $(PROG_NAME)/
zip $(PROG_NAME)-$(TAG)_win64.zip $(PROG_NAME)/* # first try (linux) zip command, then powershell sub command to create ZIP file
zip $(PROG_NAME)-$(TAG)_win64.zip $(PROG_NAME)/* || powershell Compress-ARCHIVE $(PROG_NAME) $(PROG_NAME)-$(TAG)_win64.zip
rm -rf $(PROG_NAME) || true rm -rf $(PROG_NAME) || true
.PHONY: linux-package .PHONY: linux-package

View File

@ -2,6 +2,9 @@
Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform. Anwendung zur Durchführung einer Plausibilitätsprüfung anhand der Daten für die BZKF Real World Data Platform.
**Hinweis:** Dies ist eine Weiterführung des ursprünglichen
Projekts [bzkf-rwdp-check](https://github.com/CCC-MF/bzkf-rwdp-check)
## Aufbau der ETL-Strecke an den Standorten ## Aufbau der ETL-Strecke an den Standorten
Die Daten werden aus der Onkostar-Datenbank ausgelesen und in Apache-Kafka eingespeist. Die Daten werden aus der Onkostar-Datenbank ausgelesen und in Apache-Kafka eingespeist.
@ -22,7 +25,8 @@ flowchart LR
Die Anwendung gibt für die möglichen Quellen der Kennzahlen die Anzahl der _Conditions_, gruppiert nach ICD-10 Gruppen, Die Anwendung gibt für die möglichen Quellen der Kennzahlen die Anzahl der _Conditions_, gruppiert nach ICD-10 Gruppen,
aus. aus.
Unterstützt wird eien OPAL-CSV-Datei (wie für BZKF vorgesehen) und eine Onkostar-Datenbank, basierend auf MariaDB oder MySQL. Unterstützt wird eien OPAL-CSV-Datei (wie für BZKF vorgesehen) und eine Onkostar-Datenbank, basierend auf MariaDB oder
MySQL.
![Ausgabe](docs/screenshot.png) ![Ausgabe](docs/screenshot.png)
@ -39,8 +43,8 @@ Die Anwendung gibt nun eine Liste der ICD-10-Gruppen mit Anzahl der _Conditions_
## Kennzahlen aus der Onkostar-Datenbank ## Kennzahlen aus der Onkostar-Datenbank
Die Anzahl der _Conditions_, gruppiert nach ICD-10-Gruppe, kann auch mit dem Befehl `database` aus der Onkostar-Datenbank Die Anzahl der _Conditions_, gruppiert nach ICD-10-Gruppe, kann auch mit dem Befehl `database` aus der
abgerufen werden. Onkostar-Datenbank abgerufen werden.
``` ```
bzkf-rwdp-check database --user me --year 2024 bzkf-rwdp-check database --user me --year 2024
@ -64,8 +68,15 @@ Der zusätzliche Parameter `--ignore-exports-since` ist optional.
Wird er angegeben, werden keine Einträge mit Exportdatum ab diesem Datum verwendet. Wird er angegeben, werden keine Einträge mit Exportdatum ab diesem Datum verwendet.
Dies eignet sich um nachträglich Zahlen zu einem bestimmten Datum zu ermitteln. Dies eignet sich um nachträglich Zahlen zu einem bestimmten Datum zu ermitteln.
Der Parameter `--include-extern` schließt Meldungen mit externer Diagnosestellung ein. Der optionale Parameter `--include-extern` schließt Meldungen mit externer Diagnosestellung ein.
Diese sind normalerweise nicht enthalten. Diese sind normalerweise nicht enthalten.
Die Entscheidung, ob eine Meldung intern oder extern gemeldet wird, wird anhand der `Melder_ID` getroffen.
Enthält diese die Zeichenkette `9999` wird von einer externen Meldung ausgegangen.
Der optionale Parameter `--include-histo-zyto` schließt Meldungen mit Meldeanlass `histologhie_zytologie` ein.
Diese sind normalerweise ebenfalls nicht enthalten.
Mit dem optionalen Parameter `--schema-versions` werden die Angaben zudem noch oBDS-Schema-Version getrennt ausgegeben.
## Export aus der Onkostar-Datenbank ## Export aus der Onkostar-Datenbank
@ -78,8 +89,9 @@ Die Anwendung ist in der Lage, mit dem Befehl `export` die Spalten
in eine CSV-Datei zum Abgleich mit der OPAL-CSV-Datei zu exportieren. in eine CSV-Datei zum Abgleich mit der OPAL-CSV-Datei zu exportieren.
Hierbei gelten die gleichen Datenbank-Parameter wie unter [Kennzahlen aus der Onkostar-Datenbank](#kennzahlen-aus-der-onkostar-datenbank), Hierbei gelten die gleichen Datenbank-Parameter wie
zusätzlich gibt es noch die folgenden Parameter: unter [Kennzahlen aus der Onkostar-Datenbank](#kennzahlen-aus-der-onkostar-datenbank), zusätzlich gibt es noch die
folgenden Parameter:
``` ```
Options: Options:
@ -92,5 +104,17 @@ Options:
Die Anwendung kann auch die Conditions in der CSV-Datei mit der Onkostar-Datenbank direkt vergleichen. Die Anwendung kann auch die Conditions in der CSV-Datei mit der Onkostar-Datenbank direkt vergleichen.
Hierzu kann der Befehl `compare` genutzt werden. Dieser verwendet alle Optionen für die Datenbank und die Option `--file` Hierzu kann der Befehl `compare` genutzt werden. Dieser verwendet alle Optionen für die Datenbank und die
für die CSV-Datei und gibt eine Übersicht auf der Konsole aus. Option `--file` für die CSV-Datei und gibt eine Übersicht auf der Konsole aus.
## Vergleich der XML-basierten LKR-Export-Protokolldatei mit der Datenbank
Mithilfe dieser Anwendung kann auch der aktuelle Inhalt der Datenbank gegen die LKR-Export-Protokolldatei für einen
Export verglichen werden.
Der Befehl `check-export` kann zusammen mit der Angabe der Protokolldatei (`--file`) und der Angabe des
Exports (`--package=...` bzw. `--export-package=...`) und den Optionen für den Datenbankzugriff ausgeführt werden.
Zur Überprüfung werden irrelevante Leerzeichen und Zeilenumbrüche entfernt und _Self-Closed-Tags_ ersetzt
(`<Meldeanlass />` => `<Meldeanlass></Meldeanlass>`),
da in Onkostar in der Datenbank und der LKR-Export-Protokolldatei verschiedene Formatierungen verwendet werden (können).

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use regex::Regex; use regex::Regex;
use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about)] #[command(author, version, about)]
@ -34,7 +35,7 @@ pub enum SubCommand {
#[command(about = "Ermittelt die Prüfwerte aus einem CSV-File für OPAL")] #[command(about = "Ermittelt die Prüfwerte aus einem CSV-File für OPAL")]
OpalFile { OpalFile {
#[arg(short, long, help = "CSV-File für Opal")] #[arg(short, long, help = "CSV-File für Opal")]
file: String, file: PathBuf,
}, },
#[command(about = "Ermittelt die Prüfwerte aus der Onkostar-Datenbank")] #[command(about = "Ermittelt die Prüfwerte aus der Onkostar-Datenbank")]
Database { Database {
@ -61,8 +62,17 @@ pub enum SubCommand {
year: String, year: String,
#[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")] #[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")]
ignore_exports_since: Option<String>, ignore_exports_since: Option<String>,
#[arg(long, help = "Ignoriere Meldungen, die nicht im oBDS 2.x Format sind")]
ignore_non_obds_2: bool,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")] #[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool, include_extern: bool,
#[arg(
long,
help = "Meldungen mit Meldeanlass 'histologie_zytologie' einschließen"
)]
include_histo_zyto: bool,
#[arg(long, help = "Meldungen mit oBDS-Schema-version anzeigen")]
schema_versions: bool,
}, },
#[command( #[command(
about = "Erstellt eine (reduzierte) CSV-Datei zum direkten Vergleich mit der OPAL-CSV-Datei" about = "Erstellt eine (reduzierte) CSV-Datei zum direkten Vergleich mit der OPAL-CSV-Datei"
@ -90,15 +100,22 @@ pub enum SubCommand {
#[arg(short = 'u', long, help = "Benutzername")] #[arg(short = 'u', long, help = "Benutzername")]
user: String, user: String,
#[arg(short = 'o', long, help = "Ausgabedatei")] #[arg(short = 'o', long, help = "Ausgabedatei")]
output: String, output: PathBuf,
#[arg(short = 'y', long, help = "Jahr der Diagnose")] #[arg(short = 'y', long, help = "Jahr der Diagnose")]
year: String, year: String,
#[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")] #[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")]
ignore_exports_since: Option<String>, ignore_exports_since: Option<String>,
#[arg(long, help = "Ignoriere Meldungen, die nicht im oBDS 2.x Format sind")]
ignore_non_obds_2: bool,
#[arg(long, help = "Export mit Trennzeichen ';' für Excel")] #[arg(long, help = "Export mit Trennzeichen ';' für Excel")]
xls_csv: bool, xls_csv: bool,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")] #[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool, include_extern: bool,
#[arg(
long,
help = "Meldungen mit Meldeanlass 'histologie_zytologie' einschließen"
)]
include_histo_zyto: bool,
}, },
#[command(about = "Abgleich zwischen CSV-Datei für OPAL und Onkostar-Datenbank")] #[command(about = "Abgleich zwischen CSV-Datei für OPAL und Onkostar-Datenbank")]
Compare { Compare {
@ -124,13 +141,51 @@ pub enum SubCommand {
#[arg(short = 'u', long, help = "Benutzername")] #[arg(short = 'u', long, help = "Benutzername")]
user: String, user: String,
#[arg(short, long, help = "CSV-File für Opal")] #[arg(short, long, help = "CSV-File für Opal")]
file: String, file: PathBuf,
#[arg(short = 'y', long, help = "Jahr der Diagnose")] #[arg(short = 'y', long, help = "Jahr der Diagnose")]
year: String, year: String,
#[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")] #[arg(long, value_parser = value_is_date, help = "Ignoriere LKR-Exporte seit Datum")]
ignore_exports_since: Option<String>, ignore_exports_since: Option<String>,
#[arg(long, help = "Ignoriere Meldungen, die nicht im oBDS 2.x Format sind")]
ignore_non_obds_2: bool,
#[arg(long, help = "Meldungen mit externer Diagnose einschließen")] #[arg(long, help = "Meldungen mit externer Diagnose einschließen")]
include_extern: bool, include_extern: bool,
#[arg(
long,
help = "Meldungen mit Meldeanlass 'histologie_zytologie' einschließen"
)]
include_histo_zyto: bool,
},
#[command(about = "Abgleich zwischen LKR-Export-Protokoll und Onkostar-Datenbank")]
CheckExport {
#[arg(short = 'D', long, help = "Datenbank-Name", default_value = "onkostar")]
database: String,
#[arg(
short = 'h',
long,
help = "Datenbank-Host",
default_value = "localhost"
)]
host: String,
#[arg(short = 'P', long, help = "Datenbank-Host", default_value = "3306")]
port: u16,
#[arg(
short = 'p',
long,
help = "Passwort. Wenn nicht angegeben, wird danach gefragt"
)]
password: Option<String>,
#[arg(short = 'u', long, help = "Benutzername")]
user: String,
#[arg(short, long, help = "LKR-Export-Protokoll-Datei")]
file: PathBuf,
#[arg(
long,
alias = "export-package",
help = "Exportpaketnummer",
default_value = "0"
)]
package: u16,
}, },
} }

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,6 +23,7 @@ use serde::{Deserialize, Serialize};
pub struct Icd10GroupSize { pub struct Icd10GroupSize {
pub name: String, pub name: String,
pub schema_version: Option<String>,
pub size: usize, pub size: usize,
} }
@ -63,11 +64,12 @@ impl Check {
icd10_code: Self::map_icd_code(&record.icd10_code), icd10_code: Self::map_icd_code(&record.icd10_code),
}) })
.sorted_by_key(|record| record.icd10_code.to_string()) .sorted_by_key(|record| record.icd10_code.to_string())
.group_by(|record| record.icd10_code.to_string()) .chunk_by(|record| record.icd10_code.to_string())
.into_iter() .into_iter()
.map(|(icd10, group)| (icd10, group.collect::<Vec<_>>())) .map(|(icd10, group)| (icd10, group.collect::<Vec<_>>()))
.map(|record| Icd10GroupSize { .map(|record| Icd10GroupSize {
name: record.0, name: record.0,
schema_version: None,
size: record.1.len(), size: record.1.len(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -116,3 +118,15 @@ impl Check {
.to_string() .to_string()
} }
} }
#[cfg(test)]
mod tests {
use crate::common::Check;
#[test]
fn should_map_icd10_code_as_expected() {
assert_eq!(Check::map_icd_code("D39.1"), "C56, D39.1");
assert_eq!(Check::map_icd_code("C00"), "C00-C14");
assert_eq!(Check::map_icd_code("F79.9"), "Other");
}
}

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -18,12 +18,29 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/ */
use mysql::prelude::Queryable;
use mysql::{params, Pool};
use std::time::Duration; use std::time::Duration;
use mysql::prelude::Queryable;
use mysql::{params, Pool};
use crate::common::{ExportData, Icd10GroupSize}; use crate::common::{ExportData, Icd10GroupSize};
use crate::resources::{EXPORT_QUERY, SQL_QUERY}; use crate::resources::{EXPORTED_TO_LKR, EXPORT_QUERY, SQL_QUERY, SQL_QUERY_WITH_SCHEMA_VERSION};
fn result_mapper() -> fn((String, String, usize)) -> Icd10GroupSize {
|(icd10_group, _, count)| Icd10GroupSize {
name: icd10_group,
schema_version: None,
size: count,
}
}
fn result_mapper_with_schema_version() -> fn((String, String, usize)) -> Icd10GroupSize {
|(icd10_group, schema_version, count)| Icd10GroupSize {
name: icd10_group,
schema_version: Some(schema_version),
size: count,
}
}
pub struct DatabaseSource(String); pub struct DatabaseSource(String);
@ -38,21 +55,35 @@ impl DatabaseSource {
&self, &self,
year: &str, year: &str,
ignore_exports_since: &str, ignore_exports_since: &str,
ignore_non_obds_2: bool,
include_extern: bool, include_extern: bool,
include_histo_zyto: bool,
schema_versions: bool,
) -> Result<Vec<Icd10GroupSize>, ()> { ) -> Result<Vec<Icd10GroupSize>, ()> {
let params = params! {
"year" => year,
"ignore_exports_since" => ignore_exports_since,
"ignore_non_obds_2" => if ignore_non_obds_2 { 1 } else { 0 },
"include_extern" => if include_extern { 1 } else { 0 },
"include_histo_zyto" => if include_histo_zyto { 1 } else { 0 }
};
match Pool::new(self.0.as_str()) { match Pool::new(self.0.as_str()) {
Ok(pool) => { Ok(pool) => {
if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) { if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) {
return match connection.exec_map( return match schema_versions {
SQL_QUERY, true => match connection.exec_map(
params! {"year" => year, "ignore_exports_since" => ignore_exports_since, "include_extern" => if include_extern { 1 } else { 0 } }, SQL_QUERY_WITH_SCHEMA_VERSION,
|(icd10_group, count)| Icd10GroupSize { params,
name: icd10_group, result_mapper_with_schema_version(),
size: count,
},
) { ) {
Ok(result) => Ok(result), Ok(result) => Ok(result),
Err(_) => Err(()), Err(_) => Err(()),
},
false => match connection.exec_map(SQL_QUERY, params, result_mapper()) {
Ok(result) => Ok(result),
Err(_) => Err(()),
},
}; };
} }
} }
@ -68,15 +99,23 @@ impl DatabaseSource {
&self, &self,
year: &str, year: &str,
ignore_exports_since: &str, ignore_exports_since: &str,
ignore_non_obds_2: bool,
use_pat_id: bool, use_pat_id: bool,
include_extern: bool, include_extern: bool,
include_histo_zyto: bool,
) -> Result<Vec<ExportData>, ()> { ) -> Result<Vec<ExportData>, ()> {
match Pool::new(self.0.as_str()) { match Pool::new(self.0.as_str()) {
Ok(pool) => { Ok(pool) => {
if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) { if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) {
return match connection.exec_map( return match connection.exec_map(
EXPORT_QUERY, EXPORT_QUERY,
params! {"year" => year, "ignore_exports_since" => ignore_exports_since, "include_extern" => if include_extern { 1 } else { 0 } }, params! {
"year" => year,
"ignore_exports_since" => ignore_exports_since,
"ignore_non_obds_2" => if ignore_non_obds_2 { 1 } else { 0 },
"include_extern" => if include_extern { 1 } else { 0 },
"include_histo_zyto" => if include_histo_zyto { 1 } else { 0 }
},
|(condition_id, icd_10_code, diagnosis_date, pat_id)| ExportData { |(condition_id, icd_10_code, diagnosis_date, pat_id)| ExportData {
condition_id, condition_id,
icd_10_code, icd_10_code,
@ -98,4 +137,30 @@ impl DatabaseSource {
Err(()) Err(())
} }
pub fn exported(&self, package: u16) -> Result<Vec<(String, String)>, ()> {
match Pool::new(self.0.as_str()) {
Ok(pool) => {
if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) {
return match connection.exec_map(
EXPORTED_TO_LKR,
params! {
"export_id" => package,
},
|(id, xml_data)| (id, xml_data),
) {
Ok(result) => Ok(result),
Err(_) => {
return Err(());
}
};
}
}
Err(_) => {
return Err(());
}
}
Err(())
}
} }

270
src/lkrexport.rs Normal file
View File

@ -0,0 +1,270 @@
/*
* This file is part of bzkf-rwdp-check
*
* Copyright (C) 2024 the original author or authors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
use std::fs;
use std::path::Path;
use std::str::FromStr;
use itertools::Itertools;
use regex::Regex;
pub struct LkrExportProtocolFile {
pub patients: Vec<Patient>,
}
impl LkrExportProtocolFile {
pub fn parse_file(path: &Path) -> Result<LkrExportProtocolFile, ()> {
let xml_file_content = fs::read_to_string(path).map_err(|_| ())?;
Self::parse(&xml_file_content)
}
pub fn parse(content: &str) -> Result<LkrExportProtocolFile, ()> {
let re = Regex::new(r"(?s)(?<patient><Patient>(.*?)</Patient>)").unwrap();
if re.is_match(content) {
let patients = re
.find_iter(content)
.map(|m| Patient {
raw_value: m.as_str().to_string(),
})
.collect_vec();
return Ok(LkrExportProtocolFile { patients });
}
Err(())
}
pub fn meldungen(&self) -> Vec<Meldung> {
self.patients
.iter()
.flat_map(|patient| patient.meldungen())
.collect_vec()
}
}
pub struct Patient {
pub raw_value: String,
}
impl Patient {
pub fn meldungen(&self) -> Vec<Meldung> {
let re = Regex::new(r"(?s)(?<meldung><Meldung(.*?)</Meldung>)").unwrap();
if re.is_match(&self.raw_value) {
return re
.find_iter(&self.raw_value)
.map(|m| Meldung {
raw_value: m.as_str().to_string(),
})
.collect_vec();
}
vec![]
}
}
pub struct Meldung {
pub raw_value: String,
}
impl FromStr for Meldung {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Meldung {
raw_value: s.to_string(),
})
}
}
#[allow(unused)]
impl Meldung {
pub fn id(&self) -> Option<String> {
let re = Regex::new(r#"Meldung_ID="(?<meldung_id>(.*?))""#).unwrap();
if re.is_match(&self.raw_value) {
let caps = re.captures(&self.raw_value).unwrap();
return Some(caps["meldung_id"].to_string());
}
None
}
pub fn icd10(&self) -> Option<String> {
let re = Regex::new(r"(?s)<Primaertumor_ICD_Code>(?<icd10>(.*?))</Primaertumor_ICD_Code>")
.unwrap();
if re.is_match(&self.raw_value) {
let caps = re.captures(&self.raw_value).unwrap();
return Some(caps["icd10"].to_string());
}
None
}
pub fn database_id(&self) -> Option<String> {
match self.id() {
Some(id) => to_database_id(&id),
_ => None,
}
}
pub fn sanitized_xml_string(&self) -> String {
let re = Regex::new(r"[\r|\n]+\s*").unwrap();
let content = re.replace_all(&self.raw_value, "").trim().to_string();
let re = Regex::new(r"<[^>]+/>").unwrap();
if re.is_match(&content) {
let mut c = content.to_string();
re.find_iter(&content)
.map(|m| m.as_str().to_string().replace('<', "").replace("/>", ""))
.for_each(|tag| {
c = c.replace(&format!("<{}/>", tag), &format!("<{}></{}>", tag, tag));
});
return c;
}
content
}
}
pub fn to_database_id(id: &str) -> Option<String> {
let re1 = Regex::new(r"^(?<id>[0-9A-F]+)").unwrap();
let re2 = Regex::new(r"(?<id>[0-9]+)$").unwrap();
if re1.is_match(id) {
match re1.find(id).map(|m| m.as_str().to_string()) {
Some(val) => match u64::from_str_radix(&val, 16) {
Ok(val) => Some(val.to_string()),
_ => None,
},
_ => None,
}
} else if re2.is_match(id) {
re2.find(id).map(|m| m.as_str().to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use crate::lkrexport::{LkrExportProtocolFile, Meldung};
#[test]
fn should_read_xml_file_content() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
assert_eq!(actual.unwrap().patients.len(), 2);
}
#[test]
fn should_get_meldungen() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(patients[0].meldungen().len(), 1);
assert_eq!(patients[1].meldungen().len(), 1);
}
#[test]
fn should_get_meldung_id() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(
patients[0].meldungen()[0].id(),
Some("TEST1727528".to_string())
);
assert_eq!(
patients[1].meldungen()[0].id(),
Some("001A5D50-TEST".to_string())
);
}
#[test]
fn should_get_meldung_database_id() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(
patients[0].meldungen()[0].database_id(),
Some("1727528".to_string())
);
assert_eq!(
patients[1].meldungen()[0].database_id(),
Some("1727824".to_string())
);
}
#[test]
fn should_get_meldung_icd10() {
let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml"));
assert!(actual.is_ok());
let patients = actual.unwrap().patients;
assert_eq!(
patients[0].meldungen()[0].icd10(),
Some("C17.1".to_string())
);
assert_eq!(
patients[1].meldungen()[0].icd10(),
Some("C17.2".to_string())
);
}
#[test]
fn should_get_meldung_with_trimmed_margin() {
let meldung = Meldung {
raw_value: " <Test>\n <Test2>TestInhalt 3</Test2>\n</Test>\n".into(),
};
assert_eq!(
meldung.sanitized_xml_string(),
"<Test><Test2>TestInhalt 3</Test2></Test>".to_string()
);
}
#[test]
fn should_get_meldung_without_self_closing_tags() {
let meldung = Meldung {
raw_value:
" <Test>\n <Test2/>\n <Content>Test</Content>\n <Test3/>\n <Test2/>\n</Test>\n"
.into(),
};
assert_eq!(
meldung.sanitized_xml_string(),
"<Test><Test2></Test2><Content>Test</Content><Test3></Test3><Test2></Test2></Test>"
.to_string()
);
}
}

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -18,8 +18,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/ */
use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::path::Path;
use clap::Parser; use clap::Parser;
use console::{style, Term}; use console::{style, Term};
@ -29,10 +29,12 @@ use itertools::Itertools;
use crate::cli::{Cli, SubCommand}; use crate::cli::{Cli, SubCommand};
use crate::common::{Check, DiffRecord, Icd10GroupSize}; use crate::common::{Check, DiffRecord, Icd10GroupSize};
use crate::database::DatabaseSource; use crate::database::DatabaseSource;
use crate::lkrexport::{to_database_id, LkrExportProtocolFile, Meldung};
mod cli; mod cli;
mod common; mod common;
mod database; mod database;
mod lkrexport;
mod opal; mod opal;
mod resources; mod resources;
@ -49,9 +51,9 @@ fn request_password_if_none(password: Option<String>) -> String {
} }
} }
fn sanitize_year(year: String) -> String { fn sanitize_year(year: &str) -> String {
if year.len() == 4 { if year.len() == 4 {
year year.to_string()
} else { } else {
format!("2{:0>3}", year) format!("2{:0>3}", year)
} }
@ -65,33 +67,48 @@ fn print_items(items: &[Icd10GroupSize]) {
.to_string(), .to_string(),
); );
items.iter().for_each(|item| { items.iter().for_each(|item| {
let _ = term.write_line(&format!("{:<20}={:>6}", item.name, item.size)); let _ = term.write_line(&format!(
"{:<20} {:<6} ={:>6}",
item.name,
item.schema_version.as_ref().unwrap_or(&String::new()),
item.size
));
}); });
let sum: usize = items let sum: usize = items
.iter() .iter()
.filter(|item| item.name != "Other") .filter(|item| item.name != "Other")
.map(|item| item.size) .map(|item| item.size)
.sum(); .sum();
let _ = term.write_line(&style("".repeat(27)).dim().to_string()); let _ = term.write_line(&style("".repeat(35)).dim().to_string());
let _ = term.write_line( let _ = term.write_line(
&style(format!("{:<20}={:>6}", "Summe (C**.*/D**.*)", sum)) &style(format!(
"{:<20} {:<6} ={:>6}",
"Summe (C**.*/D**.*)", "", sum
))
.dim() .dim()
.to_string(), .to_string(),
); );
let sum: usize = items.iter().map(|item| item.size).sum(); let sum: usize = items.iter().map(|item| item.size).sum();
let _ = term.write_line( let _ = term.write_line(
&style(format!("{:<20}={:>6}", "Gesamtsumme", sum)) &style(format!("{:<20} {:<6} ={:>6}", "Gesamtsumme", "", sum))
.dim() .dim()
.to_string(), .to_string(),
); );
let _ = term.write_line(&style("".repeat(27)).dim().to_string()); let _ = term.write_line(&style("".repeat(35)).dim().to_string());
} }
fn print_extern_notice(include_extern: bool) { fn print_extern_notice(include_extern: bool) {
let _ = Term::stdout().write_line(format!("{} Die Datenbankanfrage schließt Meldungen mit externer Diagnose {}.", style("Hinweis:").bold().underlined(), match include_extern { let _ = Term::stdout().write_line(
format!(
"{} Die Datenbankanfrage schließt Meldungen mit externer Diagnose {}.",
style("Hinweis:").bold().underlined(),
match include_extern {
true => style("ein").yellow(), true => style("ein").yellow(),
false => style("nicht ein (Standard)").green() false => style("nicht ein (Standard)").green(),
}).as_str()); }
)
.as_str(),
);
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
@ -99,8 +116,8 @@ fn main() -> Result<(), Box<dyn Error>> {
match Cli::parse().cmd { match Cli::parse().cmd {
SubCommand::OpalFile { file } => { SubCommand::OpalFile { file } => {
let items = opal::OpalCsvFile::check(Path::new(&file)) let items =
.map_err(|_e| "Kann Datei nicht lesen")?; opal::OpalCsvFile::check(file.as_path()).map_err(|_e| "Kann Datei nicht lesen")?;
print_items(&items); print_items(&items);
} }
@ -112,14 +129,18 @@ fn main() -> Result<(), Box<dyn Error>> {
user, user,
year, year,
ignore_exports_since, ignore_exports_since,
ignore_non_obds_2,
include_extern, include_extern,
include_histo_zyto,
schema_versions,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(year); let year = sanitize_year(&year);
let _ = term.write_line( let _ = term.write_line(
&style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) &style(format!("Warte auf Daten für das Diagnosejahr {}...", year))
.blue() .blue()
.bright()
.to_string(), .to_string(),
); );
@ -128,7 +149,10 @@ fn main() -> Result<(), Box<dyn Error>> {
.check( .check(
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
ignore_non_obds_2,
include_extern, include_extern,
include_histo_zyto,
schema_versions,
) )
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
@ -147,15 +171,18 @@ fn main() -> Result<(), Box<dyn Error>> {
output, output,
year, year,
ignore_exports_since, ignore_exports_since,
ignore_non_obds_2,
xls_csv, xls_csv,
include_extern, include_extern,
include_histo_zyto,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(year); let year = sanitize_year(&year);
let _ = term.write_line( let _ = term.write_line(
&style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) &style(format!("Warte auf Daten für das Diagnosejahr {}...", year))
.blue() .blue()
.bright()
.to_string(), .to_string(),
); );
@ -164,8 +191,10 @@ fn main() -> Result<(), Box<dyn Error>> {
.export( .export(
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
ignore_non_obds_2,
pat_id, pat_id,
include_extern, include_extern,
include_histo_zyto,
) )
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
@ -177,7 +206,7 @@ fn main() -> Result<(), Box<dyn Error>> {
writer_builder = writer_builder.delimiter(b';'); writer_builder = writer_builder.delimiter(b';');
} }
let mut writer = writer_builder let mut writer = writer_builder
.from_path(Path::new(&output)) .from_path(output.as_path())
.expect("writeable file"); .expect("writeable file");
items items
@ -189,7 +218,7 @@ fn main() -> Result<(), Box<dyn Error>> {
"{} Conditions für das Jahr {} in Datei '{}' exportiert", "{} Conditions für das Jahr {} in Datei '{}' exportiert",
items.len(), items.len(),
year, year,
output output.to_str().unwrap_or_default()
)) ))
.green() .green()
.to_string(), .to_string(),
@ -207,14 +236,17 @@ fn main() -> Result<(), Box<dyn Error>> {
file, file,
year, year,
ignore_exports_since, ignore_exports_since,
ignore_non_obds_2,
include_extern, include_extern,
include_histo_zyto,
} => { } => {
let password = request_password_if_none(password); let password = request_password_if_none(password);
let year = sanitize_year(year); let year = sanitize_year(&year);
let _ = term.write_line( let _ = term.write_line(
&style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) &style(format!("Warte auf Daten für das Diagnosejahr {}...", year))
.blue() .blue()
.bright()
.to_string(), .to_string(),
); );
@ -223,19 +255,21 @@ fn main() -> Result<(), Box<dyn Error>> {
.export( .export(
&year, &year,
&ignore_exports_since.unwrap_or("9999-12-31".into()), &ignore_exports_since.unwrap_or("9999-12-31".into()),
ignore_non_obds_2,
pat_id, pat_id,
include_extern, include_extern,
include_histo_zyto,
) )
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
let _ = term.clear_last_lines(1); let _ = term.clear_last_lines(1);
let csv_items = opal::OpalCsvFile::export(Path::new(&file)) let csv_items =
.map_err(|_e| "Kann Datei nicht lesen")?; opal::OpalCsvFile::export(file.as_path()).map_err(|_e| "Kann Datei nicht lesen")?;
let mut not_in_csv = db_items let mut not_in_csv = db_items
.iter() .iter()
.filter(|db_item| { .filter(|&db_item| {
!csv_items !csv_items
.iter() .iter()
.map(|csv_item| &csv_item.condition_id) .map(|csv_item| &csv_item.condition_id)
@ -250,7 +284,7 @@ fn main() -> Result<(), Box<dyn Error>> {
"{} Conditions aus der Datenbank für das Jahr {} - aber nicht in Datei '{}'", "{} Conditions aus der Datenbank für das Jahr {} - aber nicht in Datei '{}'",
not_in_csv.len(), not_in_csv.len(),
year, year,
file file.to_str().unwrap_or_default()
)) ))
.green() .green()
.to_string(), .to_string(),
@ -265,7 +299,7 @@ fn main() -> Result<(), Box<dyn Error>> {
not_in_csv not_in_csv
.iter() .iter()
.for_each(|item| match Check::is_relevant(&item.icd_10_code) { .for_each(|&item| match Check::is_relevant(&item.icd_10_code) {
true => { true => {
let _ = term.write_line(&format!( let _ = term.write_line(&format!(
"{:<64} {:<10} {:<5} {:<5} {}", "{:<64} {:<10} {:<5} {:<5} {}",
@ -296,7 +330,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut not_in_db = csv_items let mut not_in_db = csv_items
.iter() .iter()
.filter(|csv_item| { .filter(|&csv_item| {
!db_items !db_items
.iter() .iter()
.map(|db_item| &db_item.condition_id) .map(|db_item| &db_item.condition_id)
@ -308,7 +342,7 @@ fn main() -> Result<(), Box<dyn Error>> {
&style(format!( &style(format!(
"{} Conditions aus Datei '{}' - aber nicht in der Datenbank für das Jahr {}", "{} Conditions aus Datei '{}' - aber nicht in der Datenbank für das Jahr {}",
not_in_db.len(), not_in_db.len(),
file, file.to_str().unwrap_or_default(),
year year
)) ))
.green() .green()
@ -409,6 +443,185 @@ fn main() -> Result<(), Box<dyn Error>> {
)); ));
}); });
} }
SubCommand::CheckExport {
database,
host,
password,
port,
user,
file,
package,
} => {
let password = request_password_if_none(password);
let _ = term.write_line(
&style(format!(
"Warte auf Daten für den LKR-Export '{}'...",
package
))
.blue()
.bright()
.to_string(),
);
let db = DatabaseSource::new(&database, &host, &password, port, &user);
let db_entries = db
.exported(package)
.map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?;
let db_meldungen = db_entries
.iter()
.map(|entry| LkrExportProtocolFile::parse(&entry.1))
.filter(|entry| entry.is_ok())
.flat_map(|entry| entry.unwrap().meldungen())
.filter(|meldung| meldung.id().is_some())
.map(|meldung| (meldung.id().unwrap(), meldung))
.collect::<HashMap<_, _>>();
let xml_meldungen = LkrExportProtocolFile::parse_file(file.as_path())
.map_err(|_e| "Fehler bei Zugriff auf die Protokolldatei")?
.meldungen()
.into_iter()
.filter(|meldung| meldung.id().is_some())
.map(|meldung| (meldung.id().unwrap(), meldung))
.collect::<HashMap<_, _>>();
let missing_xml_ids = db_meldungen
.keys()
.filter(|&key| !xml_meldungen.contains_key(key))
.collect_vec();
let _ = term.clear_last_lines(1);
let _ = term.write_line(
&style(format!(
"{} Datenbankeinträge mit {} Meldungen abgerufen",
db_entries.len(),
db_meldungen.len()
))
.green()
.to_string(),
);
fn print_missing_ids(missing_ids: &[&String], term: &Term) {
missing_ids.iter().sorted().for_each(|&item| {
let _ = term.write_line(&format!(
"{} ({})",
item,
to_database_id(item).unwrap_or("?".into())
));
});
}
if db_meldungen.len() != xml_meldungen.len() {
let _ = term.write_line(
&style("\nNicht übereinstimmende Anzahl an Meldungen:")
.yellow()
.to_string(),
);
let _ = term.write_line(&format!(
"Datenbank: {:>10}\nProtokolldatei: {:>10}",
db_meldungen.len(),
xml_meldungen.len()
));
let missing_db_ids = xml_meldungen
.keys()
.filter(|&key| !db_meldungen.contains_key(key))
.collect_vec();
if !missing_db_ids.is_empty() {
let _ = term.write_line(
&style("\nIn der Datenbank fehlende Meldungen:")
.yellow()
.to_string(),
);
print_missing_ids(&missing_db_ids, &term);
}
if !missing_xml_ids.is_empty() {
let _ = term.write_line(
&style("\nIn der Protokolldatei fehlende Meldungen:")
.yellow()
.to_string(),
);
print_missing_ids(&missing_xml_ids, &term);
}
}
let multiple_meldung_entries = db_entries
.iter()
.map(|(lkr_meldung, meldung)| (lkr_meldung, LkrExportProtocolFile::parse(meldung)))
.filter_map(|(lkr_meldung, meldung)| {
if meldung.unwrap().meldungen().len() > 1 {
Some(lkr_meldung)
} else {
None
}
})
.sorted()
.collect_vec();
if !multiple_meldung_entries.is_empty() {
let _ = term.write_line(
&style("\nFolgende Einträge in `lkr_meldung_export` haben mehrere Meldungsinhalte in `xml_daten`:")
.yellow()
.to_string(),
);
multiple_meldung_entries.iter().for_each(|&item| {
let _ = term.write_line(&item.to_string());
});
}
let different_content = db_meldungen
.iter()
.filter(|(id, _)| !missing_xml_ids.contains(id))
.filter(|(id, meldung)| {
xml_meldungen
.get(&id.to_string())
.unwrap_or(&Meldung {
raw_value: String::new(),
})
.sanitized_xml_string()
!= meldung.sanitized_xml_string()
})
.map(|(_, meldung)| meldung.id().unwrap_or("?".into()))
.collect_vec();
if !different_content.is_empty() {
let _ = term.write_line(
&style(&format!(
"\nFolgende {} Meldungen unterscheiden sich in der Datenbank und der Protokolldatei:",
different_content.len()
))
.yellow()
.to_string(),
);
let _ = term.write_line(
"Dies kann auch aufgrund der verwendeten XML-Encodierung auftreten und bedeutet nicht immer eine inhaltliche Abweichung."
);
different_content
.iter()
.sorted_by(|&id1, &id2| {
to_database_id(id1)
.unwrap_or_default()
.cmp(&to_database_id(id2).unwrap_or_default())
})
.for_each(|id| {
let _ = term.write_line(&format!(
"{} ({})",
id,
to_database_id(id).unwrap_or("?".into())
));
});
}
}
} }
Ok(()) Ok(())

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -33,10 +33,13 @@ FROM (
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1) AS diagnosedatum, SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1) AS diagnosedatum,
SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
JOIN lkr_meldung lm ON (lm.id = lme.lkr_meldung AND lme.typ <> '-1' AND lm.extern <= :include_extern)
WHERE lme.xml_daten LIKE '%ICD_Version%' WHERE lme.xml_daten LIKE '%ICD_Version%'
AND lme.typ <> -1
AND lme.xml_daten NOT LIKE '%<Menge_Tumorkonferenz%'
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND (lme.xml_daten LIKE '%<cTNM%' OR lme.xml_daten LIKE '%<pTNM%' OR lme.xml_daten LIKE '%<Menge_Histologie>%' OR lme.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%') AND (lme.xml_daten NOT LIKE '%histologie_zytologie%' OR 1 = :include_histo_zyto)
AND (EXTRACTVALUE(lme.xml_daten, '//Meldende_Stelle') NOT LIKE '%9999%' OR 1 <= :include_extern)
AND (EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') LIKE '2.%' OR 1 = :ignore_non_obds_2)
) o1 ) o1
LEFT OUTER JOIN ( LEFT OUTER JOIN (

View File

@ -0,0 +1,26 @@
/*
* This file is part of bzkf-rwdp-check
*
* Copyright (C) 2024 the original author or authors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
SELECT
CONVERT(id,char) AS id,
xml_daten
FROM lkr_meldung_export
WHERE typ <> -1
AND (lkr_export = :export_id OR (0 = :export_id AND lkr_export IN (SELECT MAX(lkr_export) FROM lkr_meldung_export)));

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -20,4 +20,8 @@
pub const SQL_QUERY: &str = include_str!("query.sql"); pub const SQL_QUERY: &str = include_str!("query.sql");
pub const SQL_QUERY_WITH_SCHEMA_VERSION: &str = include_str!("query_with_schema_version.sql");
pub const EXPORT_QUERY: &str = include_str!("export.sql"); pub const EXPORT_QUERY: &str = include_str!("export.sql");
pub const EXPORTED_TO_LKR: &str = include_str!("exported-to-lkr.sql");

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of bzkf-rwdp-check * This file is part of bzkf-rwdp-check
* *
* Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * Copyright (C) 2024 the original author or authors.
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -107,32 +107,34 @@ SELECT CASE
ELSE 'Other' ELSE 'Other'
END AS ICD10_GROUP, END AS ICD10_GROUP,
'' AS schema_version,
COUNT(*) as COUNT COUNT(*) as COUNT
FROM ( FROM (
SELECT DISTINCT SELECT DISTINCT
lme.lkr_meldung,
EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid, EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid,
EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') AS schema_version,
lme.versionsnummer, lme.versionsnummer,
SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id, SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id,
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode, SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode,
SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
JOIN lkr_meldung lm ON (lm.id = lme.lkr_meldung AND lme.typ <> '-1' AND lm.extern <= :include_extern)
WHERE lme.xml_daten LIKE '%ICD_Version%' WHERE lme.xml_daten LIKE '%ICD_Version%'
AND lme.typ <> -1
AND lme.xml_daten NOT LIKE '%<Menge_Tumorkonferenz%'
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND (lme.xml_daten LIKE '%<cTNM%' OR lme.xml_daten LIKE '%<pTNM%' OR lme.xml_daten LIKE '%<Menge_Histologie>%' OR lme.xml_daten LIKE '%<Menge_Weitere_Klassifikation>%') AND (lme.xml_daten NOT LIKE '%histologie_zytologie%' OR 1 = :include_histo_zyto)
AND (EXTRACTVALUE(lme.xml_daten, '//Meldende_Stelle') NOT LIKE '%9999%' OR 1 <= :include_extern)
AND (EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') LIKE '2.%' OR 1 = :ignore_non_obds_2)
) o1 ) o1
LEFT OUTER JOIN ( LEFT OUTER JOIN (
SELECT DISTINCT SELECT DISTINCT
lme.lkr_meldung,
SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id, SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id,
CASE WHEN le.exportiert_am < :ignore_exports_since THEN MAX(versionsnummer) ELSE ~0 END AS max_version CASE WHEN STR_TO_DATE(EXTRACTVALUE(lme.xml_daten, '//Meldedatum'), '%d.%c.%Y') < :ignore_exports_since THEN MAX(versionsnummer) ELSE ~0 END AS max_version
FROM lkr_meldung_export lme FROM lkr_meldung_export lme
JOIN lkr_export le ON (lme.lkr_export = le.id)
WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
GROUP BY cond_id ORDER BY cond_id GROUP BY cond_id ORDER BY cond_id
) o2 ) o2
ON (o1.cond_id = o2.cond_id AND o1.versionsnummer < max_version) ON (o1.cond_id = o2.cond_id AND o1.versionsnummer < max_version)
WHERE diagnosejahr = :year AND o2.cond_id IS NULL WHERE diagnosejahr = :year AND o2.cond_id IS NULL

View File

@ -0,0 +1,141 @@
/*
* This file is part of bzkf-rwdp-check
*
* Copyright (C) 2024 the original author or authors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
SELECT CASE
WHEN condcodingcode LIKE 'C00%'
OR condcodingcode LIKE 'C01%'
OR condcodingcode LIKE 'C02%'
OR condcodingcode LIKE 'C03%'
OR condcodingcode LIKE 'C04%'
OR condcodingcode LIKE 'C05%'
OR condcodingcode LIKE 'C06%'
OR condcodingcode LIKE 'C07%'
OR condcodingcode LIKE 'C08%'
OR condcodingcode LIKE 'C09%'
OR condcodingcode LIKE 'C10%'
OR condcodingcode LIKE 'C11%'
OR condcodingcode LIKE 'C12%'
OR condcodingcode LIKE 'C13%'
OR condcodingcode LIKE 'C14%' THEN 'C00-C14'
WHEN condcodingcode LIKE 'C15%' THEN 'C15'
WHEN condcodingcode LIKE 'C16%' THEN 'C16'
WHEN condcodingcode LIKE 'C18%'
OR condcodingcode LIKE 'C19%'
OR condcodingcode LIKE 'C20%'
OR condcodingcode LIKE 'C21%' THEN 'C18-C21'
WHEN condcodingcode LIKE 'C22%' THEN 'C22'
WHEN condcodingcode LIKE 'C23%'
OR condcodingcode LIKE 'C24%' THEN 'C23-C24'
WHEN condcodingcode LIKE 'C25%' THEN 'C25'
WHEN condcodingcode LIKE 'C32%' THEN 'C32'
WHEN condcodingcode LIKE 'C33%'
OR condcodingcode LIKE 'C34%' THEN 'C33-C34'
WHEN condcodingcode LIKE 'C43%' THEN 'C43'
WHEN condcodingcode LIKE 'C50%'
OR condcodingcode LIKE 'D05%' THEN 'C50, D05'
WHEN condcodingcode LIKE 'C53%'
OR condcodingcode LIKE 'D06%' THEN 'C53, D06'
WHEN condcodingcode LIKE 'C54%'
OR condcodingcode LIKE 'C55%' THEN 'C54-C55'
WHEN condcodingcode LIKE 'C56%'
OR condcodingcode = 'D39.1' THEN 'C56, D39.1'
WHEN condcodingcode LIKE 'C61%' THEN 'C61'
WHEN condcodingcode LIKE 'C62%' THEN 'C62'
WHEN condcodingcode LIKE 'C64%' THEN 'C64'
WHEN condcodingcode LIKE 'C67%'
OR condcodingcode = 'D09.0'
OR condcodingcode = 'D41.4' THEN 'C67, D09.0, D41.4'
WHEN condcodingcode LIKE 'C70%'
OR condcodingcode LIKE 'C71%'
OR condcodingcode LIKE 'C72%' THEN 'C70-C72'
WHEN condcodingcode LIKE 'C73%' THEN 'C73'
WHEN condcodingcode LIKE 'C81%' THEN 'C81'
WHEN condcodingcode LIKE 'C82%'
OR condcodingcode LIKE 'C83%'
OR condcodingcode LIKE 'C84%'
OR condcodingcode LIKE 'C85%'
OR condcodingcode LIKE 'C86%'
OR condcodingcode LIKE 'C87%'
OR condcodingcode LIKE 'C88%'
OR condcodingcode LIKE 'C96%' THEN 'C82-C88, C96'
WHEN condcodingcode LIKE 'C90%' THEN 'C90'
WHEN condcodingcode LIKE 'C91%'
OR condcodingcode LIKE 'C92%'
OR condcodingcode LIKE 'C93%'
OR condcodingcode LIKE 'C94%'
OR condcodingcode LIKE 'C95%' THEN 'C91-C95'
ELSE 'Other'
END AS ICD10_GROUP,
schema_version,
COUNT(*) as COUNT
FROM (
SELECT DISTINCT
lme.lkr_meldung,
EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID') AS pid,
EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') AS schema_version,
lme.versionsnummer,
SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id,
SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Primaertumor_ICD_Code'), ' ', 1) AS condcodingcode,
SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) AS diagnosejahr
FROM lkr_meldung_export lme
WHERE lme.xml_daten LIKE '%ICD_Version%'
AND lme.typ <> -1
AND lme.xml_daten NOT LIKE '%<Menge_Tumorkonferenz%'
AND SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
AND (lme.xml_daten NOT LIKE '%histologie_zytologie%' OR 1 = :include_histo_zyto)
AND (EXTRACTVALUE(lme.xml_daten, '//Meldende_Stelle') NOT LIKE '%9999%' OR 1 <= :include_extern)
AND (EXTRACTVALUE(lme.xml_daten, '//ADT_GEKID/@Schema_Version') LIKE '2.%' OR 1 = :ignore_non_obds_2)
) o1
LEFT OUTER JOIN (
SELECT DISTINCT
lme.lkr_meldung,
SHA2(CONCAT('https://fhir.diz.uk-erlangen.de/identifiers/onkostar-xml-condition-id|', EXTRACTVALUE(lme.xml_daten, '//Patienten_Stammdaten/@Patient_ID'), 'condition', EXTRACTVALUE(lme.xml_daten, '//Diagnose/@Tumor_ID')), 256) AS cond_id,
CASE WHEN STR_TO_DATE(EXTRACTVALUE(lme.xml_daten, '//Meldedatum'), '%d.%c.%Y') < :ignore_exports_since THEN MAX(versionsnummer) ELSE ~0 END AS max_version
FROM lkr_meldung_export lme
WHERE SUBSTRING_INDEX(SUBSTRING_INDEX(EXTRACTVALUE(lme.xml_daten, '//Diagnosedatum'), ' ', 1), '.', -1) = :year
GROUP BY cond_id ORDER BY cond_id
) o2
ON (o1.cond_id = o2.cond_id AND o1.versionsnummer < max_version)
WHERE diagnosejahr = :year AND o2.cond_id IS NULL
GROUP BY ICD10_GROUP, schema_version;

89
testdaten/testdaten_1.xml Normal file
View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<ADT_GEKID xmlns="http://www.gekid.de/namespace" Schema_Version="2.2.3">
<Absender Absender_ID="TEST" Software_ID="ONKOSTAR" Installations_ID="2011">
<Absender_Bezeichnung>TEST</Absender_Bezeichnung>
<Absender_Anschrift>Musterstraße 1, 012345 Musterhausen</Absender_Anschrift>
</Absender>
<Menge_Patient>
<Patient>
<Patienten_Stammdaten Patient_ID="20001234">
<KrankenversichertenNr>E123456789</KrankenversichertenNr>
<KrankenkassenNr>123456789</KrankenkassenNr>
<Patienten_Nachname>Tester</Patienten_Nachname>
<Patienten_Titel></Patienten_Titel>
<Patienten_Vornamen>Patrick</Patienten_Vornamen>
<Patienten_Geburtsname>Tester</Patienten_Geburtsname>
<Patienten_Geschlecht>M</Patienten_Geschlecht>
<Patienten_Geburtsdatum>01.01.1980</Patienten_Geburtsdatum>
<Menge_Adresse>
<Adresse>
<Patienten_Strasse>Testweg</Patienten_Strasse>
<Patienten_Hausnummer>1</Patienten_Hausnummer>
<Patienten_Land>DE</Patienten_Land>
<Patienten_PLZ>01234</Patienten_PLZ>
<Patienten_Ort>Musterhausen</Patienten_Ort>
</Adresse>
</Menge_Adresse>
</Patienten_Stammdaten>
<Menge_Meldung>
<Meldung Meldung_ID="TEST1727528" Melder_ID="TEST">
<Meldedatum>11.06.2024</Meldedatum>
<Meldebegruendung>I</Meldebegruendung>
<Meldeanlass>statusaenderung</Meldeanlass>
<Tumorzuordnung Tumor_ID="1">
<Primaertumor_ICD_Code>C17.1</Primaertumor_ICD_Code>
<Primaertumor_ICD_Version>10 2015 GM</Primaertumor_ICD_Version>
<Diagnosedatum>10.06.2024</Diagnosedatum>
<Seitenlokalisation>T</Seitenlokalisation>
</Tumorzuordnung>
<Menge_Tumorkonferenz>
<Tumorkonferenz Tumorkonferenz_ID="1234567">
<Tumorkonferenz_Datum>11.06.2024</Tumorkonferenz_Datum>
<Tumorkonferenz_Typ>praeth</Tumorkonferenz_Typ>
</Tumorkonferenz>
</Menge_Tumorkonferenz>
</Meldung>
</Menge_Meldung>
</Patient>
<Patient>
<Patienten_Stammdaten Patient_ID="20004321">
<KrankenversichertenNr>E123456789</KrankenversichertenNr>
<KrankenkassenNr>123456789</KrankenkassenNr>
<Patienten_Nachname>Tester</Patienten_Nachname>
<Patienten_Titel></Patienten_Titel>
<Patienten_Vornamen>Patricia</Patienten_Vornamen>
<Patienten_Geburtsname>Tester</Patienten_Geburtsname>
<Patienten_Geschlecht>W</Patienten_Geschlecht>
<Patienten_Geburtsdatum>01.01.1980</Patienten_Geburtsdatum>
<Menge_Adresse>
<Adresse>
<Patienten_Strasse>Testweg</Patienten_Strasse>
<Patienten_Hausnummer>1</Patienten_Hausnummer>
<Patienten_Land>DE</Patienten_Land>
<Patienten_PLZ>01234</Patienten_PLZ>
<Patienten_Ort>Musterhausen</Patienten_Ort>
</Adresse>
</Menge_Adresse>
</Patienten_Stammdaten>
<Menge_Meldung>
<Meldung Meldung_ID="001A5D50-TEST" Melder_ID="TEST">
<Meldedatum>11.06.2024</Meldedatum>
<Meldebegruendung>I</Meldebegruendung>
<Meldeanlass>statusaenderung</Meldeanlass>
<Tumorzuordnung Tumor_ID="1">
<Primaertumor_ICD_Code>C17.2</Primaertumor_ICD_Code>
<Primaertumor_ICD_Version>10 2015 GM</Primaertumor_ICD_Version>
<Diagnosedatum>01.01.2024</Diagnosedatum>
<Seitenlokalisation>T</Seitenlokalisation>
</Tumorzuordnung>
<Menge_Tumorkonferenz>
<Tumorkonferenz Tumorkonferenz_ID="1234568">
<Tumorkonferenz_Datum>10.01.2024</Tumorkonferenz_Datum>
<Tumorkonferenz_Typ>praeth</Tumorkonferenz_Typ>
</Tumorkonferenz>
</Menge_Tumorkonferenz>
</Meldung>
</Menge_Meldung>
</Patient>
</Menge_Patient>
</ADT_GEKID>