1
2 """GNUmed German KVK/eGK objects.
3
4 These objects handle German patient cards (KVK and eGK).
5
6 KVK: http://www.kbv.de/ita/register_G.html
7 eGK: http://www.gematik.de/upload/gematik_Qop_eGK_Spezifikation_Teil1_V1_1_0_Kommentare_4_1652.pdf
8 """
9
10 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
11 __license__ = "GPL v2 or later"
12
13 import sys
14 import os
15 import os.path
16 import fileinput
17 import io
18 import time
19 import glob
20 import datetime as pyDT
21 import re as regex
22
23 import logging
24
25
26
27 if __name__ == '__main__':
28 sys.path.insert(0, '../../')
29 from Gnumed.business import gmPerson
30 from Gnumed.pycommon import gmExceptions, gmDateTime, gmTools, gmPG2
31
32
33 _log = logging.getLogger('gm.kvk')
34
35 true_egk_fields = [
36 'insurance_company',
37 'insurance_number',
38 'insuree_number',
39 'insuree_status',
40 'insuree_status_detail',
41 'insuree_status_comment',
42 'title',
43 'firstnames',
44 'lastnames',
45 'dob',
46 'street',
47 'zip',
48 'urb',
49 'valid_since',
50 ]
51
52
53 true_kvk_fields = [
54 'insurance_company',
55 'insurance_number',
56 'insurance_number_vknr',
57 'insuree_number',
58 'insuree_status',
59 'insuree_status_detail',
60 'insuree_status_comment',
61 'title',
62 'firstnames',
63 'name_affix',
64 'lastnames',
65 'dob',
66 'street',
67 'urb_region_code',
68 'zip',
69 'urb',
70 'valid_until'
71 ]
72
73
74 map_kvkd_tags2dto = {
75 'Version': 'libchipcard_version',
76 'Datum': 'last_read_date',
77 'Zeit': 'last_read_time',
78 'Lesertyp': 'reader_type',
79 'Kartentyp': 'card_type',
80 'KK-Name': 'insurance_company',
81 'KK-Nummer': 'insurance_number',
82 'KVK-Nummer': 'insurance_number_vknr',
83 'VKNR': 'insurance_number_vknr',
84 'V-Nummer': 'insuree_number',
85 'V-Status': 'insuree_status',
86 'V-Statusergaenzung': 'insuree_status_detail',
87 'V-Status-Erlaeuterung': 'insuree_status_comment',
88 'Titel': 'title',
89 'Vorname': 'firstnames',
90 'Namenszusatz': 'name_affix',
91 'Familienname': 'lastnames',
92 'Geburtsdatum': 'dob',
93 'Strasse': 'street',
94 'Laendercode': 'urb_region_code',
95 'PLZ': 'zip',
96 'Ort': 'urb',
97 'gueltig-seit': 'valid_since',
98 'gueltig-bis': 'valid_until',
99 'Pruefsumme-gueltig': 'crc_valid',
100 'Kommentar': 'comment'
101 }
102
103
104 map_CCRdr_gender2gm = {
105 'M': 'm',
106 'W': 'f',
107 'U': None,
108 'D': 'h'
109 }
110
111
112 map_CCRdr_region_code2country = {
113 'D': 'DE'
114 }
115
116
117 EXTERNAL_ID_ISSUER_TEMPLATE = '%s (%s)'
118 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER = 'Versichertennummer'
119 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER_EGK = 'Versichertennummer (eGK)'
120
121
123
124 - def __init__(self, filename=None, strict=True):
136
137
138
139
140
141
142
143
145 old_idents = gmPerson.cDTO_person.get_candidate_identities(self, can_create = can_create)
146
147
148 if not self.card_is_rejected:
149 cmd = """
150 SELECT pk_identity FROM dem.v_external_ids4identity WHERE
151 value = %(val)s AND
152 name = %(name)s AND
153 issuer = %(kk)s
154 """
155 args = {
156 'val': self.insuree_number,
157 'name': '%s (%s)' % (
158 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
159 self.raw_data['Karte']
160 ),
161 'kk': EXTERNAL_ID_ISSUER_TEMPLATE % (self.raw_data['KostentraegerName'], self.raw_data['Kostentraegerkennung'])
162 }
163 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = None)
164
165
166 name_candidate_ids = [ o.ID for o in old_idents ]
167 for r in rows:
168 if r[0] not in name_candidate_ids:
169 old_idents.append(gmPerson.cPerson(aPK_obj = r[0]))
170
171 return old_idents
172
173
175
176
177
178
179
180 pass
181
182
184 if not self.card_is_rejected:
185 args = {
186 'pat': identity.ID,
187 'dob': self.preformatted_dob,
188 'valid_until': self.valid_until,
189 'data': self.raw_data
190 }
191 cmd = """
192 INSERT INTO de_de.insurance_card (
193 fk_identity,
194 formatted_dob,
195 valid_until,
196 raw_data
197 ) VALUES (
198 %(pat)s,
199 %(dob)s,
200 %(valid_until)s,
201 %(data)s
202 )"""
203 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
204
205
206
207
209
210 _log.debug('loading eGK/KVK/PKVK data from [%s]', self.filename)
211 vk_file = io.open(self.filename, mode = 'rt', encoding = 'utf8')
212 self.raw_data = json.load(vk_file)
213 vk_file.close()
214
215 if self.raw_data['Fehlerbericht']['FehlerNr'] != '0x9000':
216 _log.error('error [%s] reading VK: %s', self.raw_data['Fehlerbericht']['FehlerNr'], self.raw_data['Fehlerbericht']['Fehlermeldung'])
217 raise ValueError('error [%s] reading VK: %s' % (self.raw_data['Fehlerbericht']['FehlerNr'], self.raw_data['Fehlerbericht']['Fehlermeldung']))
218
219
220 if self.raw_data['Ablehnen'] == 'ja':
221 self.card_is_rejected = True
222 _log.info('eGK may contain insurance information but KBV says it must be rejected because it is of generation 0')
223
224
225
226 tmp = time.strptime(self.raw_data['VersicherungsschutzBeginn'], self.date_format)
227 self.valid_since = pyDT.date(tmp.tm_year, tmp.tm_mon, tmp.tm_mday)
228
229 tmp = time.strptime(self.raw_data['VersicherungsschutzEnde'], self.date_format)
230 self.valid_until = pyDT.date(tmp.tm_year, tmp.tm_mon, tmp.tm_mday)
231 if self.valid_until < pyDT.date.today():
232 self.card_is_expired = True
233
234
235 src_attrs = []
236 if self.card_is_expired:
237 src_attrs.append(_('expired'))
238 if self.card_is_rejected:
239 _log.info('eGK contains insurance information but KBV says it must be rejected because it is of generation 0')
240 src_attrs.append(_('rejected'))
241 src_attrs.append('CCReader')
242 self.source = '%s (%s)' % (
243 self.raw_data['Karte'],
244 ', '.join(src_attrs)
245 )
246
247
248 self.firstnames = self.raw_data['Vorname']
249 self.lastnames = self.raw_data['Nachname']
250 self.gender = map_CCRdr_gender2gm[self.raw_data['Geschlecht']]
251
252
253 title_parts = []
254 for part in ['Titel', 'Namenszusatz', 'Vorsatzwort']:
255 tmp = self.raw_data[part].strip()
256 if tmp == '':
257 continue
258 title_parts.append(tmp)
259 if len(title_parts) > 0:
260 self.title = ' '.join(title_parts)
261
262
263 dob_str = self.raw_data['Geburtsdatum']
264 year_str = dob_str[:4]
265 month_str = dob_str[4:6]
266 day_str = dob_str[6:8]
267 self.preformatted_dob = '%s.%s.%s' % (day_str, month_str, year_str)
268 if year_str == '0000':
269 self.dob = None
270 else:
271 if day_str == '00':
272 self.dob_is_estimated = True
273 day_str = '01'
274 if month_str == '00':
275 self.dob_is_estimated = True
276 month_str = '01'
277 dob_str = year_str + month_str + day_str
278 tmp = time.strptime(dob_str, self.date_format)
279 self.dob = pyDT.datetime(tmp.tm_year, tmp.tm_mon, tmp.tm_mday, 11, 11, 11, 111, tzinfo = gmDateTime.gmCurrentLocalTimezone)
280
281
282
283 try:
284 adr = self.raw_data['StrassenAdresse']
285 try:
286 self.remember_address (
287 adr_type = 'eGK (Wohnadresse)',
288 number = adr['Hausnummer'],
289 subunit = adr['Anschriftenzusatz'],
290 street = adr['Strasse'],
291 urb = adr['Ort'],
292 region_code = '',
293 zip = adr['Postleitzahl'],
294 country_code = map_CCRdr_region_code2country[adr['Wohnsitzlaendercode']]
295 )
296 except ValueError:
297 _log.exception('invalid street address on card')
298 except KeyError:
299 _log.error('unknown country code [%s] on card in street address', adr['Wohnsitzlaendercode'])
300 except KeyError:
301 _log.warning('no street address on card')
302
303 try:
304 adr = self.raw_data['PostfachAdresse']
305 try:
306 self.remember_address (
307 adr_type = 'eGK (Postfach)',
308 number = adr['Postfach'],
309
310 street = _('PO Box'),
311 urb = adr['PostfachOrt'],
312 region_code = '',
313 zip = adr['PostfachPLZ'],
314 country_code = map_CCRdr_region_code2country[adr['PostfachWohnsitzlaendercode']]
315 )
316 except ValueError:
317 _log.exception('invalid PO Box address on card')
318 except KeyError:
319 _log.error('unknown country code [%s] on card in PO Box address', adr['Wohnsitzlaendercode'])
320 except KeyError:
321 _log.warning('no PO Box address on card')
322
323 if not (self.card_is_expired or self.card_is_rejected):
324 self.insuree_number = None
325 try:
326 self.insuree_number = self.raw_data['Versicherten_ID']
327 except KeyError:
328 pass
329 try:
330 self.insuree_number = self.raw_data['Versicherten_ID_KVK']
331 except KeyError:
332 pass
333 try:
334 self.insuree_number = self.raw_data['Versicherten_ID_PKV']
335 except KeyError:
336 pass
337 if self.insuree_number is not None:
338 try:
339 self.remember_external_id (
340 name = '%s (%s)' % (
341 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
342 self.raw_data['Karte']
343 ),
344 value = self.insuree_number,
345 issuer = EXTERNAL_ID_ISSUER_TEMPLATE % (self.raw_data['KostentraegerName'], self.raw_data['Kostentraegerkennung']),
346 comment = 'Nummer (eGK) des Versicherten bei der Krankenkasse, gültig: %s - %s' % (
347 gmDateTime.pydt_strftime(self.valid_since, '%Y %b %d'),
348 gmDateTime.pydt_strftime(self.valid_until, '%Y %b %d')
349 )
350 )
351 except KeyError:
352 _log.exception('no insurance information on eGK')
353
354
356
357 kvkd_card_id_string = 'Elektronische Gesundheitskarte'
358
359 - def __init__(self, filename=None, strict=True):
360 self.card_type = 'eGK'
361 self.dob_format = '%d%m%Y'
362 self.valid_since_format = '%d%m%Y'
363 self.last_read_time_format = '%H:%M:%S'
364 self.last_read_date_format = '%d.%m.%Y'
365 self.filename = filename
366
367 self.__parse_egk_file(strict = strict)
368
369
370
371
372
373
374
376 old_idents = gmPerson.cDTO_person.get_candidate_identities(self, can_create = can_create)
377
378 cmd = """
379 select pk_identity from dem.v_external_ids4identity where
380 value = %(val)s and
381 name = %(name)s and
382 issuer = %(kk)s
383 """
384 args = {
385 'val': self.insuree_number,
386 'name': EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
387 'kk': EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number)
388 }
389 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
390
391
392 new_idents = []
393 for r in rows:
394 for oid in old_idents:
395 if r[0] == oid.ID:
396 break
397 new_idents.append(gmPerson.cPerson(aPK_obj = r['pk_identity']))
398
399 old_idents.extend(new_idents)
400
401 return old_idents
402
404
405
406
407 identity.add_external_id (
408 type_name = EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER_EGK,
409 value = self.insuree_number,
410 issuer = EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number),
411 comment = 'Nummer (eGK) des Versicherten bei der Krankenkasse'
412 )
413
414 street = self.street
415 number = regex.findall(' \d+.*', street)
416 if len(number) == 0:
417 number = None
418 else:
419 street = street.replace(number[0], '')
420 number = number[0].strip()
421 identity.link_address (
422 number = number,
423 street = street,
424 postcode = self.zip,
425 urb = self.urb,
426 region_code = '',
427 country_code = 'DE'
428 )
429
430
437
438
439
441
442 _log.debug('parsing eGK data in [%s]', self.filename)
443
444 egk_file = io.open(self.filename, mode = 'rt', encoding = 'utf8')
445
446 card_type_seen = False
447 for line in egk_file:
448 line = line.replace('\n', '').replace('\r', '')
449 tag, content = line.split(':', 1)
450 content = content.strip()
451
452 if tag == 'Kartentyp':
453 card_type_seen = True
454 if content != cDTO_eGK.kvkd_card_id_string:
455 _log.error('parsing wrong card type')
456 _log.error('found : %s', content)
457 _log.error('expected: %s', cDTO_KVK.kvkd_card_id_string)
458 if strict:
459 raise ValueError('wrong card type: %s, expected %s', content, cDTO_KVK.kvkd_card_id_string)
460 else:
461 _log.debug('trying to parse anyway')
462
463 if tag == 'Geburtsdatum':
464 tmp = time.strptime(content, self.dob_format)
465 content = pyDT.datetime(tmp.tm_year, tmp.tm_mon, tmp.tm_mday, tzinfo = gmDateTime.gmCurrentLocalTimezone)
466
467 try:
468 setattr(self, map_kvkd_tags2dto[tag], content)
469 except KeyError:
470 _log.exception('unknown KVKd eGK file key [%s]' % tag)
471
472
473 ts = time.strptime (
474 '%s20%s' % (self.valid_since[:4], self.valid_since[4:]),
475 self.valid_since_format
476 )
477
478
479 ts = time.strptime (
480 '%s %s' % (self.last_read_date, self.last_read_time),
481 '%s %s' % (self.last_read_date_format, self.last_read_time_format)
482 )
483 self.last_read_timestamp = pyDT.datetime(ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, tzinfo = gmDateTime.gmCurrentLocalTimezone)
484
485
486 self.gender = gmTools.coalesce(gmPerson.map_firstnames2gender(firstnames=self.firstnames), 'f')
487
488 if not card_type_seen:
489 _log.warning('no line with card type found, unable to verify')
490
491
493
494 kvkd_card_id_string = 'Krankenversichertenkarte'
495
496 - def __init__(self, filename=None, strict=True):
497 self.card_type = 'KVK'
498 self.dob_format = '%d%m%Y'
499 self.valid_until_format = '%d%m%Y'
500 self.last_read_time_format = '%H:%M:%S'
501 self.last_read_date_format = '%d.%m.%Y'
502 self.filename = filename
503
504 self.__parse_kvk_file(strict = strict)
505
506
507
508
509
510
511
513 old_idents = gmPerson.cDTO_person.get_candidate_identities(self, can_create = can_create)
514
515 cmd = """
516 select pk_identity from dem.v_external_ids4identity where
517 value = %(val)s and
518 name = %(name)s and
519 issuer = %(kk)s
520 """
521 args = {
522 'val': self.insuree_number,
523 'name': EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
524 'kk': EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number)
525 }
526 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
527
528
529 new_idents = []
530 for r in rows:
531 for oid in old_idents:
532 if r[0] == oid.ID:
533 break
534 new_idents.append(gmPerson.cPerson(aPK_obj = r['pk_identity']))
535
536 old_idents.extend(new_idents)
537
538 return old_idents
539
541
542 identity.add_external_id (
543 type_name = EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
544 value = self.insuree_number,
545 issuer = EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number),
546 comment = 'Nummer des Versicherten bei der Krankenkasse'
547 )
548
549 street = self.street
550 number = regex.findall(' \d+.*', street)
551 if len(number) == 0:
552 number = None
553 else:
554 street = street.replace(number[0], '')
555 number = number[0].strip()
556 identity.link_address (
557 number = number,
558 street = street,
559 postcode = self.zip,
560 urb = self.urb,
561 region_code = '',
562 country_code = 'DE'
563 )
564
565
572
573
574
576
577 _log.debug('parsing KVK data in [%s]', self.filename)
578
579 kvk_file = io.open(self.filename, mode = 'rt', encoding = 'utf8')
580
581 card_type_seen = False
582 for line in kvk_file:
583 line = line.replace('\n', '').replace('\r', '')
584 tag, content = line.split(':', 1)
585 content = content.strip()
586
587 if tag == 'Kartentyp':
588 card_type_seen = True
589 if content != cDTO_KVK.kvkd_card_id_string:
590 _log.error('parsing wrong card type')
591 _log.error('found : %s', content)
592 _log.error('expected: %s', cDTO_KVK.kvkd_card_id_string)
593 if strict:
594 raise ValueError('wrong card type: %s, expected %s', content, cDTO_KVK.kvkd_card_id_string)
595 else:
596 _log.debug('trying to parse anyway')
597
598 if tag == 'Geburtsdatum':
599 tmp = time.strptime(content, self.dob_format)
600 content = pyDT.datetime(tmp.tm_year, tmp.tm_mon, tmp.tm_mday, tzinfo = gmDateTime.gmCurrentLocalTimezone)
601
602 try:
603 setattr(self, map_kvkd_tags2dto[tag], content)
604 except KeyError:
605 _log.exception('unknown KVKd kvk file key [%s]' % tag)
606
607
608 ts = time.strptime (
609 '28%s20%s' % (self.valid_until[:2], self.valid_until[2:]),
610 self.valid_until_format
611 )
612
613
614 ts = time.strptime (
615 '%s %s' % (self.last_read_date, self.last_read_time),
616 '%s %s' % (self.last_read_date_format, self.last_read_time_format)
617 )
618 self.last_read_timestamp = pyDT.datetime(ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, tzinfo = gmDateTime.gmCurrentLocalTimezone)
619
620
621 self.gender = gmTools.coalesce(gmPerson.map_firstnames2gender(firstnames=self.firstnames), 'f')
622
623 if not card_type_seen:
624 _log.warning('no line with card type found, unable to verify')
625
627
628 data_file = io.open(card_file, mode = 'rt', encoding = 'utf8')
629
630 for line in kvk_file:
631 line = line.replace('\n', '').replace('\r', '')
632 tag, content = line.split(':', 1)
633 content = content.strip()
634
635 if tag == 'Kartentyp':
636 pass
637
639
640 kvk_files = glob.glob(os.path.join(spool_dir, 'KVK-*.dat'))
641 dtos = []
642 for kvk_file in kvk_files:
643 try:
644 dto = cDTO_KVK(filename = kvk_file)
645 except:
646 _log.exception('probably not a KVKd KVK file: [%s]' % kvk_file)
647 continue
648 dtos.append(dto)
649
650 return dtos
651
653
654 egk_files = glob.glob(os.path.join(spool_dir, 'eGK-*.dat'))
655 dtos = []
656 for egk_file in egk_files:
657 try:
658 dto = cDTO_eGK(filename = egk_file)
659 except:
660 _log.exception('probably not a KVKd eGK file: [%s]' % egk_file)
661 continue
662 dtos.append(dto)
663
664 return dtos
665
667
668 ccrdr_files = glob.glob(os.path.join(spool_dir, 'CCReader-*.dat'))
669 dtos = []
670 for ccrdr_file in ccrdr_files:
671 try:
672 dto = cDTO_CCRdr(filename = ccrdr_file)
673 except:
674 _log.exception('probably not a CCReader file: [%s]' % ccrdr_file)
675 continue
676 dtos.append(dto)
677
678 return dtos
679
687
688
689
690
691 if __name__ == "__main__":
692
693 from Gnumed.pycommon import gmI18N
694
695 gmI18N.activate_locale()
696 gmDateTime.init()
697
702
704
705 kvkd_file = sys.argv[2]
706 print("reading eGK data from KVKd file", kvkd_file)
707 dto = cDTO_eGK(filename = kvkd_file, strict = False)
708 print(dto)
709 for attr in true_egk_fields:
710 print(getattr(dto, attr))
711
713
714 kvkd_file = sys.argv[2]
715 print("reading KVK data from KVKd file", kvkd_file)
716 dto = cDTO_KVK(filename = kvkd_file, strict = False)
717 print(dto)
718 for attr in true_kvk_fields:
719 print(getattr(dto, attr))
720
725
726 if (len(sys.argv)) > 1 and (sys.argv[1] == 'test'):
727 if len(sys.argv) < 3:
728 print("give name of KVKd file as first argument")
729 sys.exit(-1)
730 test_vks()
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756