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 }
109
110
111 map_CCRdr_region_code2country = {
112 'D': 'DE'
113 }
114
115
116 EXTERNAL_ID_ISSUER_TEMPLATE = '%s (%s)'
117 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER = 'Versichertennummer'
118 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER_EGK = 'Versichertennummer (eGK)'
119
120
122
123 - def __init__(self, filename=None, strict=True):
135
136
137
138
139
140
141
142
144 old_idents = gmPerson.cDTO_person.get_candidate_identities(self, can_create = can_create)
145
146
147 if not self.card_is_rejected:
148 cmd = """
149 SELECT pk_identity FROM dem.v_external_ids4identity WHERE
150 value = %(val)s AND
151 name = %(name)s AND
152 issuer = %(kk)s
153 """
154 args = {
155 'val': self.insuree_number,
156 'name': '%s (%s)' % (
157 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
158 self.raw_data['Karte']
159 ),
160 'kk': EXTERNAL_ID_ISSUER_TEMPLATE % (self.raw_data['KostentraegerName'], self.raw_data['Kostentraegerkennung'])
161 }
162 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = None)
163
164
165 name_candidate_ids = [ o.ID for o in old_idents ]
166 for r in rows:
167 if r[0] not in name_candidate_ids:
168 old_idents.append(gmPerson.cPerson(aPK_obj = r[0]))
169
170 return old_idents
171
172
174
175
176
177
178
179 pass
180
181
183 if not self.card_is_rejected:
184 args = {
185 'pat': identity.ID,
186 'dob': self.preformatted_dob,
187 'valid_until': self.valid_until,
188 'data': self.raw_data
189 }
190 cmd = """
191 INSERT INTO de_de.insurance_card (
192 fk_identity,
193 formatted_dob,
194 valid_until,
195 raw_data
196 ) VALUES (
197 %(pat)s,
198 %(dob)s,
199 %(valid_until)s,
200 %(data)s
201 )"""
202 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
203
204
205
206
208
209 _log.debug('loading eGK/KVK/PKVK data from [%s]', self.filename)
210 vk_file = io.open(self.filename, mode = 'rt', encoding = 'utf8')
211 self.raw_data = json.load(vk_file)
212 vk_file.close()
213
214 if self.raw_data['Fehlerbericht']['FehlerNr'] != '0x9000':
215 _log.error('error [%s] reading VK: %s', self.raw_data['Fehlerbericht']['FehlerNr'], self.raw_data['Fehlerbericht']['Fehlermeldung'])
216 raise ValueError('error [%s] reading VK: %s' % (self.raw_data['Fehlerbericht']['FehlerNr'], self.raw_data['Fehlerbericht']['Fehlermeldung']))
217
218
219 if self.raw_data['Ablehnen'] == 'ja':
220 self.card_is_rejected = True
221 _log.info('eGK may contain insurance information but KBV says it must be rejected because it is of generation 0')
222
223
224
225 tmp = time.strptime(self.raw_data['VersicherungsschutzBeginn'], self.date_format)
226 self.valid_since = pyDT.date(tmp.tm_year, tmp.tm_mon, tmp.tm_mday)
227
228 tmp = time.strptime(self.raw_data['VersicherungsschutzEnde'], self.date_format)
229 self.valid_until = pyDT.date(tmp.tm_year, tmp.tm_mon, tmp.tm_mday)
230 if self.valid_until < pyDT.date.today():
231 self.card_is_expired = True
232
233
234 src_attrs = []
235 if self.card_is_expired:
236 src_attrs.append(_('expired'))
237 if self.card_is_rejected:
238 _log.info('eGK contains insurance information but KBV says it must be rejected because it is of generation 0')
239 src_attrs.append(_('rejected'))
240 src_attrs.append('CCReader')
241 self.source = '%s (%s)' % (
242 self.raw_data['Karte'],
243 ', '.join(src_attrs)
244 )
245
246
247 self.firstnames = self.raw_data['Vorname']
248 self.lastnames = self.raw_data['Nachname']
249 self.gender = map_CCRdr_gender2gm[self.raw_data['Geschlecht']]
250
251
252 title_parts = []
253 for part in ['Titel', 'Namenszusatz', 'Vorsatzwort']:
254 tmp = self.raw_data[part].strip()
255 if tmp == '':
256 continue
257 title_parts.append(tmp)
258 if len(title_parts) > 0:
259 self.title = ' '.join(title_parts)
260
261
262 dob_str = self.raw_data['Geburtsdatum']
263 year_str = dob_str[:4]
264 month_str = dob_str[4:6]
265 day_str = dob_str[6:8]
266 self.preformatted_dob = '%s.%s.%s' % (day_str, month_str, year_str)
267 if year_str == '0000':
268 self.dob = None
269 else:
270 if day_str == '00':
271 self.dob_is_estimated = True
272 day_str = '01'
273 if month_str == '00':
274 self.dob_is_estimated = True
275 month_str = '01'
276 dob_str = year_str + month_str + day_str
277 tmp = time.strptime(dob_str, self.date_format)
278 self.dob = pyDT.datetime(tmp.tm_year, tmp.tm_mon, tmp.tm_mday, 11, 11, 11, 111, tzinfo = gmDateTime.gmCurrentLocalTimezone)
279
280
281
282 try:
283 adr = self.raw_data['StrassenAdresse']
284 try:
285 self.remember_address (
286 adr_type = 'eGK (Wohnadresse)',
287 number = adr['Hausnummer'],
288 subunit = adr['Anschriftenzusatz'],
289 street = adr['Strasse'],
290 urb = adr['Ort'],
291 region_code = '',
292 zip = adr['Postleitzahl'],
293 country_code = map_CCRdr_region_code2country[adr['Wohnsitzlaendercode']]
294 )
295 except ValueError:
296 _log.exception('invalid street address on card')
297 except KeyError:
298 _log.error('unknown country code [%s] on card in street address', adr['Wohnsitzlaendercode'])
299 except KeyError:
300 _log.warning('no street address on card')
301
302 try:
303 adr = self.raw_data['PostfachAdresse']
304 try:
305 self.remember_address (
306 adr_type = 'eGK (Postfach)',
307 number = adr['Postfach'],
308
309 street = _('PO Box'),
310 urb = adr['PostfachOrt'],
311 region_code = '',
312 zip = adr['PostfachPLZ'],
313 country_code = map_CCRdr_region_code2country[adr['PostfachWohnsitzlaendercode']]
314 )
315 except ValueError:
316 _log.exception('invalid PO Box address on card')
317 except KeyError:
318 _log.error('unknown country code [%s] on card in PO Box address', adr['Wohnsitzlaendercode'])
319 except KeyError:
320 _log.warning('no PO Box address on card')
321
322 if not (self.card_is_expired or self.card_is_rejected):
323 self.insuree_number = None
324 try:
325 self.insuree_number = self.raw_data['Versicherten_ID']
326 except KeyError:
327 pass
328 try:
329 self.insuree_number = self.raw_data['Versicherten_ID_KVK']
330 except KeyError:
331 pass
332 try:
333 self.insuree_number = self.raw_data['Versicherten_ID_PKV']
334 except KeyError:
335 pass
336 if self.insuree_number is not None:
337 try:
338 self.remember_external_id (
339 name = '%s (%s)' % (
340 EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
341 self.raw_data['Karte']
342 ),
343 value = self.insuree_number,
344 issuer = EXTERNAL_ID_ISSUER_TEMPLATE % (self.raw_data['KostentraegerName'], self.raw_data['Kostentraegerkennung']),
345 comment = 'Nummer (eGK) des Versicherten bei der Krankenkasse, gültig: %s - %s' % (
346 gmDateTime.pydt_strftime(self.valid_since, '%Y %b %d'),
347 gmDateTime.pydt_strftime(self.valid_until, '%Y %b %d')
348 )
349 )
350 except KeyError:
351 _log.exception('no insurance information on eGK')
352
353
355
356 kvkd_card_id_string = 'Elektronische Gesundheitskarte'
357
358 - def __init__(self, filename=None, strict=True):
359 self.card_type = 'eGK'
360 self.dob_format = '%d%m%Y'
361 self.valid_since_format = '%d%m%Y'
362 self.last_read_time_format = '%H:%M:%S'
363 self.last_read_date_format = '%d.%m.%Y'
364 self.filename = filename
365
366 self.__parse_egk_file(strict = strict)
367
368
369
370
371
372
373
375 old_idents = gmPerson.cDTO_person.get_candidate_identities(self, can_create = can_create)
376
377 cmd = """
378 select pk_identity from dem.v_external_ids4identity where
379 value = %(val)s and
380 name = %(name)s and
381 issuer = %(kk)s
382 """
383 args = {
384 'val': self.insuree_number,
385 'name': EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
386 'kk': EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number)
387 }
388 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
389
390
391 new_idents = []
392 for r in rows:
393 for oid in old_idents:
394 if r[0] == oid.ID:
395 break
396 new_idents.append(gmPerson.cPerson(aPK_obj = r['pk_identity']))
397
398 old_idents.extend(new_idents)
399
400 return old_idents
401
403
404
405
406 identity.add_external_id (
407 type_name = EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER_EGK,
408 value = self.insuree_number,
409 issuer = EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number),
410 comment = 'Nummer (eGK) des Versicherten bei der Krankenkasse'
411 )
412
413 street = self.street
414 number = regex.findall(' \d+.*', street)
415 if len(number) == 0:
416 number = None
417 else:
418 street = street.replace(number[0], '')
419 number = number[0].strip()
420 identity.link_address (
421 number = number,
422 street = street,
423 postcode = self.zip,
424 urb = self.urb,
425 region_code = '',
426 country_code = 'DE'
427 )
428
429
436
437
438
440
441 _log.debug('parsing eGK data in [%s]', self.filename)
442
443 egk_file = io.open(self.filename, mode = 'rt', encoding = 'utf8')
444
445 card_type_seen = False
446 for line in egk_file:
447 line = line.replace('\n', '').replace('\r', '')
448 tag, content = line.split(':', 1)
449 content = content.strip()
450
451 if tag == 'Kartentyp':
452 card_type_seen = True
453 if content != cDTO_eGK.kvkd_card_id_string:
454 _log.error('parsing wrong card type')
455 _log.error('found : %s', content)
456 _log.error('expected: %s', cDTO_KVK.kvkd_card_id_string)
457 if strict:
458 raise ValueError('wrong card type: %s, expected %s', content, cDTO_KVK.kvkd_card_id_string)
459 else:
460 _log.debug('trying to parse anyway')
461
462 if tag == 'Geburtsdatum':
463 tmp = time.strptime(content, self.dob_format)
464 content = pyDT.datetime(tmp.tm_year, tmp.tm_mon, tmp.tm_mday, tzinfo = gmDateTime.gmCurrentLocalTimezone)
465
466 try:
467 setattr(self, map_kvkd_tags2dto[tag], content)
468 except KeyError:
469 _log.exception('unknown KVKd eGK file key [%s]' % tag)
470
471
472 ts = time.strptime (
473 '%s20%s' % (self.valid_since[:4], self.valid_since[4:]),
474 self.valid_since_format
475 )
476
477
478 ts = time.strptime (
479 '%s %s' % (self.last_read_date, self.last_read_time),
480 '%s %s' % (self.last_read_date_format, self.last_read_time_format)
481 )
482 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)
483
484
485 self.gender = gmTools.coalesce(gmPerson.map_firstnames2gender(firstnames=self.firstnames), 'f')
486
487 if not card_type_seen:
488 _log.warning('no line with card type found, unable to verify')
489
490
492
493 kvkd_card_id_string = 'Krankenversichertenkarte'
494
495 - def __init__(self, filename=None, strict=True):
496 self.card_type = 'KVK'
497 self.dob_format = '%d%m%Y'
498 self.valid_until_format = '%d%m%Y'
499 self.last_read_time_format = '%H:%M:%S'
500 self.last_read_date_format = '%d.%m.%Y'
501 self.filename = filename
502
503 self.__parse_kvk_file(strict = strict)
504
505
506
507
508
509
510
512 old_idents = gmPerson.cDTO_person.get_candidate_identities(self, can_create = can_create)
513
514 cmd = """
515 select pk_identity from dem.v_external_ids4identity where
516 value = %(val)s and
517 name = %(name)s and
518 issuer = %(kk)s
519 """
520 args = {
521 'val': self.insuree_number,
522 'name': EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
523 'kk': EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number)
524 }
525 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
526
527
528 new_idents = []
529 for r in rows:
530 for oid in old_idents:
531 if r[0] == oid.ID:
532 break
533 new_idents.append(gmPerson.cPerson(aPK_obj = r['pk_identity']))
534
535 old_idents.extend(new_idents)
536
537 return old_idents
538
540
541 identity.add_external_id (
542 type_name = EXTERNAL_ID_TYPE_VK_INSUREE_NUMBER,
543 value = self.insuree_number,
544 issuer = EXTERNAL_ID_ISSUER_TEMPLATE % (self.insurance_company, self.insurance_number),
545 comment = 'Nummer des Versicherten bei der Krankenkasse'
546 )
547
548 street = self.street
549 number = regex.findall(' \d+.*', street)
550 if len(number) == 0:
551 number = None
552 else:
553 street = street.replace(number[0], '')
554 number = number[0].strip()
555 identity.link_address (
556 number = number,
557 street = street,
558 postcode = self.zip,
559 urb = self.urb,
560 region_code = '',
561 country_code = 'DE'
562 )
563
564
571
572
573
575
576 _log.debug('parsing KVK data in [%s]', self.filename)
577
578 kvk_file = io.open(self.filename, mode = 'rt', encoding = 'utf8')
579
580 card_type_seen = False
581 for line in kvk_file:
582 line = line.replace('\n', '').replace('\r', '')
583 tag, content = line.split(':', 1)
584 content = content.strip()
585
586 if tag == 'Kartentyp':
587 card_type_seen = True
588 if content != cDTO_KVK.kvkd_card_id_string:
589 _log.error('parsing wrong card type')
590 _log.error('found : %s', content)
591 _log.error('expected: %s', cDTO_KVK.kvkd_card_id_string)
592 if strict:
593 raise ValueError('wrong card type: %s, expected %s', content, cDTO_KVK.kvkd_card_id_string)
594 else:
595 _log.debug('trying to parse anyway')
596
597 if tag == 'Geburtsdatum':
598 tmp = time.strptime(content, self.dob_format)
599 content = pyDT.datetime(tmp.tm_year, tmp.tm_mon, tmp.tm_mday, tzinfo = gmDateTime.gmCurrentLocalTimezone)
600
601 try:
602 setattr(self, map_kvkd_tags2dto[tag], content)
603 except KeyError:
604 _log.exception('unknown KVKd kvk file key [%s]' % tag)
605
606
607 ts = time.strptime (
608 '28%s20%s' % (self.valid_until[:2], self.valid_until[2:]),
609 self.valid_until_format
610 )
611
612
613 ts = time.strptime (
614 '%s %s' % (self.last_read_date, self.last_read_time),
615 '%s %s' % (self.last_read_date_format, self.last_read_time_format)
616 )
617 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)
618
619
620 self.gender = gmTools.coalesce(gmPerson.map_firstnames2gender(firstnames=self.firstnames), 'f')
621
622 if not card_type_seen:
623 _log.warning('no line with card type found, unable to verify')
624
626
627 data_file = io.open(card_file, mode = 'rt', encoding = 'utf8')
628
629 for line in kvk_file:
630 line = line.replace('\n', '').replace('\r', '')
631 tag, content = line.split(':', 1)
632 content = content.strip()
633
634 if tag == 'Kartentyp':
635 pass
636
638
639 kvk_files = glob.glob(os.path.join(spool_dir, 'KVK-*.dat'))
640 dtos = []
641 for kvk_file in kvk_files:
642 try:
643 dto = cDTO_KVK(filename = kvk_file)
644 except:
645 _log.exception('probably not a KVKd KVK file: [%s]' % kvk_file)
646 continue
647 dtos.append(dto)
648
649 return dtos
650
652
653 egk_files = glob.glob(os.path.join(spool_dir, 'eGK-*.dat'))
654 dtos = []
655 for egk_file in egk_files:
656 try:
657 dto = cDTO_eGK(filename = egk_file)
658 except:
659 _log.exception('probably not a KVKd eGK file: [%s]' % egk_file)
660 continue
661 dtos.append(dto)
662
663 return dtos
664
666
667 ccrdr_files = glob.glob(os.path.join(spool_dir, 'CCReader-*.dat'))
668 dtos = []
669 for ccrdr_file in ccrdr_files:
670 try:
671 dto = cDTO_CCRdr(filename = ccrdr_file)
672 except:
673 _log.exception('probably not a CCReader file: [%s]' % ccrdr_file)
674 continue
675 dtos.append(dto)
676
677 return dtos
678
686
687
688
689
690 if __name__ == "__main__":
691
692 from Gnumed.pycommon import gmI18N
693
694 gmI18N.activate_locale()
695 gmDateTime.init()
696
701
703
704 kvkd_file = sys.argv[2]
705 print("reading eGK data from KVKd file", kvkd_file)
706 dto = cDTO_eGK(filename = kvkd_file, strict = False)
707 print(dto)
708 for attr in true_egk_fields:
709 print(getattr(dto, attr))
710
712
713 kvkd_file = sys.argv[2]
714 print("reading KVK data from KVKd file", kvkd_file)
715 dto = cDTO_KVK(filename = kvkd_file, strict = False)
716 print(dto)
717 for attr in true_kvk_fields:
718 print(getattr(dto, attr))
719
724
725 if (len(sys.argv)) > 1 and (sys.argv[1] == 'test'):
726 if len(sys.argv) < 3:
727 print("give name of KVKd file as first argument")
728 sys.exit(-1)
729 test_vks()
730
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