1 __doc__ = """GNUmed simple ASCII EMR export tool.
2
3 TODO:
4 - output modes:
5 - HTML - post-0.1 !
6 """
7
8 __author__ = "Carlos Moro, Karsten Hilbert"
9 __license__ = 'GPL v2 or later'
10
11 import os.path
12 import sys
13 import time
14 import io
15 import logging
16 import shutil
17
18
19 if __name__ == '__main__':
20 sys.path.insert(0, '../../')
21 from Gnumed.pycommon import gmI18N
22 gmI18N.activate_locale()
23 gmI18N.install_domain()
24
25 from Gnumed.pycommon import gmExceptions
26 from Gnumed.pycommon import gmPG2
27 from Gnumed.pycommon import gmTools
28 from Gnumed.pycommon import gmDateTime
29
30 from Gnumed.business import gmClinicalRecord
31 from Gnumed.business import gmAllergy
32 from Gnumed.business import gmDemographicRecord
33 from Gnumed.business import gmSoapDefs
34
35
36 _log = logging.getLogger('gm.export')
37
39
40
41 - def __init__(self, constraints = None, fileout = None, patient = None):
42 """
43 Constructs a new instance of exporter
44
45 constraints - Exporter constraints for filtering clinical items
46 fileout - File-like object as target for dumping operations
47 """
48 if constraints is None:
49
50 self.__constraints = {
51 'since': None,
52 'until': None,
53 'encounters': None,
54 'episodes': None,
55 'issues': None
56 }
57 else:
58 self.__constraints = constraints
59 self.__target = fileout
60 self.__patient = patient
61 self.lab_new_encounter = True
62 self.__filtered_items = []
63
65 """Sets exporter constraints.
66
67 constraints - Exporter constraints for filtering clinical items
68 """
69 if constraints is None:
70
71 self.__constraints = {
72 'since': None,
73 'until': None,
74 'encounters': None,
75 'episodes': None,
76 'issues': None
77 }
78 else:
79 self.__constraints = constraints
80 return True
81
83 """
84 Retrieve exporter constraints
85 """
86 return self.__constraints
87
89 """
90 Sets exporter patient
91
92 patient - Patient whose data are to be dumped
93 """
94 if patient is None:
95 _log.error("can't set None patient for exporter")
96 return
97 self.__patient = patient
98
100 """
101 Sets exporter output file
102
103 @param file_name - The file to dump the EMR to
104 @type file_name - FileType
105 """
106 self.__target = target
107
109 """
110 Retrieves patient whose data are to be dumped
111 """
112 return self.__patient
113
115 """
116 Exporter class cleanup code
117 """
118 pass
119
121 """
122 Retrieves string containg ASCII vaccination table
123 """
124 emr = self.__patient.emr
125
126 patient_dob = self.__patient['dob']
127 date_length = len(gmDateTime.pydt_strftime(patient_dob, '%Y %b %d')) + 2
128
129
130 vaccinations4regimes = {}
131 for a_vacc_regime in vacc_regimes:
132 indication = a_vacc_regime['indication']
133 vaccinations4regimes[indication] = emr.get_scheduled_vaccinations(indications=[indication])
134
135 chart_columns = len(vacc_regimes)
136
137 foot_headers = ['last booster', 'next booster']
138
139 ending_str = '='
140
141
142 column_widths = []
143 chart_rows = -1
144 vaccinations = {}
145 temp = -1
146 for foot_header in foot_headers:
147 if len(foot_header) > temp:
148 temp = len(foot_header)
149 column_widths.append(temp)
150 for a_vacc_regime in vacc_regimes:
151 if a_vacc_regime['shots'] > chart_rows:
152 chart_rows = a_vacc_regime['shots']
153 if (len(a_vacc_regime['l10n_indication'])) > date_length:
154 column_widths.append(len(a_vacc_regime['l10n_indication']))
155 else:
156 column_widths.append(date_length)
157 vaccinations[a_vacc_regime['indication']] = emr.get_vaccinations(indications=[a_vacc_regime['indication']])
158
159
160 txt = '\nDOB: %s' % (gmDateTime.pydt_strftime(patient_dob, '%Y %b %d')) + '\n'
161
162
163
164 for column_width in column_widths:
165 txt += column_width * '-' + '-'
166 txt += '\n'
167
168 txt += column_widths[0] * ' ' + '|'
169 col_index = 1
170 for a_vacc_regime in vacc_regimes:
171 txt += a_vacc_regime['l10n_indication'] + (column_widths[col_index] - len(a_vacc_regime['l10n_indication'])) * ' ' + '|'
172 col_index += 1
173 txt += '\n'
174
175 for column_width in column_widths:
176 txt += column_width * '-' + '-'
177 txt += '\n'
178
179
180 due_date = None
181
182 prev_displayed_date = [patient_dob]
183 for a_regime in vacc_regimes:
184 prev_displayed_date.append(patient_dob)
185
186 for row_index in range(0, chart_rows):
187 row_header = '#%s' %(row_index+1)
188 txt += row_header + (column_widths[0] - len(row_header)) * ' ' + '|'
189
190 for col_index in range(1, chart_columns+1):
191 indication =vacc_regimes[col_index-1]['indication']
192 seq_no = vacc_regimes[col_index-1]['shots']
193 if row_index == seq_no:
194 txt += ending_str * column_widths[col_index] + '|'
195 elif row_index < seq_no:
196 try:
197 vacc_date = vaccinations[indication][row_index]['date']
198 vacc_date_str = gmDateTime.pydt_strftime(vacc_date, '%Y %b %d')
199 txt += vacc_date_str + (column_widths[col_index] - len(vacc_date_str)) * ' ' + '|'
200 prev_displayed_date[col_index] = vacc_date
201 except Exception:
202 if row_index == 0:
203 due_date = prev_displayed_date[col_index] + vaccinations4regimes[indication][row_index]['age_due_min']
204 else:
205 due_date = prev_displayed_date[col_index] + vaccinations4regimes[indication][row_index]['min_interval']
206 txt += '('+ due_date.strftime('%Y-%m-%d') + ')' + (column_widths[col_index] - date_length) * ' ' + '|'
207 prev_displayed_date[col_index] = due_date
208 else:
209 txt += column_widths[col_index] * ' ' + '|'
210 txt += '\n'
211 for column_width in column_widths:
212 txt += column_width * '-' + '-'
213 txt += '\n'
214
215
216 all_vreg_boosters = []
217 for a_vacc_regime in vacc_regimes:
218 vaccs4indication = vaccinations[a_vacc_regime['indication']]
219 given_boosters = []
220 for a_vacc in vaccs4indication:
221 try:
222 if a_vacc['is_booster']:
223 given_boosters.append(a_vacc)
224 except Exception:
225
226 pass
227 if len(given_boosters) > 0:
228 all_vreg_boosters.append(given_boosters[len(given_boosters)-1])
229 else:
230 all_vreg_boosters.append(None)
231
232
233 all_next_boosters = []
234 for a_booster in all_vreg_boosters:
235 all_next_boosters.append(None)
236
237 cont = 0
238 for a_vacc_regime in vacc_regimes:
239 vaccs = vaccinations4regimes[a_vacc_regime['indication']]
240 if vaccs[len(vaccs)-1]['is_booster'] == False:
241 all_vreg_boosters[cont] = ending_str * column_widths[cont+1]
242 all_next_boosters[cont] = ending_str * column_widths[cont+1]
243 else:
244 indication = vacc_regimes[cont]['indication']
245 if len(vaccinations[indication]) > vacc_regimes[cont]['shots']:
246 all_vreg_boosters[cont] = vaccinations[indication][len(vaccinations[indication])-1]['date'].strftime('%Y-%m-%d')
247 scheduled_booster = vaccinations4regimes[indication][len(vaccinations4regimes[indication])-1]
248 booster_date = vaccinations[indication][len(vaccinations[indication])-1]['date'] + scheduled_booster['min_interval']
249 if booster_date < gmDateTime.pydt_now_here():
250 all_next_boosters[cont] = '<(' + booster_date.strftime('%Y-%m-%d') + ')>'
251 else:
252 all_next_boosters[cont] = booster_date.strftime('%Y-%m-%d')
253 elif len(vaccinations[indication]) == vacc_regimes[cont]['shots']:
254 all_vreg_boosters[cont] = column_widths[cont+1] * ' '
255 scheduled_booster = vaccinations4regimes[indication][len(vaccinations4regimes[indication])-1]
256 booster_date = vaccinations[indication][len(vaccinations[indication])-1]['date'] + scheduled_booster['min_interval']
257 if booster_date < gmDateTime.pydt_now_here():
258 all_next_boosters[cont] = '<(' + booster_date.strftime('%Y-%m-%d') + ')>'
259 else:
260 all_next_boosters[cont] = booster_date.strftime('%Y-%m-%d')
261 else:
262 all_vreg_boosters[cont] = column_widths[cont+1] * ' '
263 all_next_boosters[cont] = column_widths[cont+1] * ' '
264 cont += 1
265
266
267 foot_header = foot_headers[0]
268 col_index = 0
269 txt += foot_header + (column_widths[0] - len(foot_header)) * ' ' + '|'
270 col_index += 1
271 for a_vacc_regime in vacc_regimes:
272 txt += str(all_vreg_boosters[col_index-1]) + (column_widths[col_index] - len(str(all_vreg_boosters[col_index-1]))) * ' ' + '|'
273 col_index += 1
274 txt += '\n'
275 for column_width in column_widths:
276 txt += column_width * '-' + '-'
277 txt += '\n'
278
279
280 foot_header = foot_headers[1]
281 col_index = 0
282 txt += foot_header + (column_widths[0] - len(foot_header)) * ' ' + '|'
283 col_index += 1
284 for a_vacc_regime in vacc_regimes:
285 txt += str(all_next_boosters[col_index-1]) + (column_widths[col_index] - len(str(all_next_boosters[col_index-1]))) * ' ' + '|'
286 col_index += 1
287 txt += '\n'
288 for column_width in column_widths:
289 txt += column_width * '-' + '-'
290 txt += '\n'
291
292 self.__target.write(txt)
293
295 """
296 Iterate over patient scheduled regimes preparing vacc tables dump
297 """
298
299 emr = self.__patient.emr
300
301
302 all_vacc_regimes = emr.get_scheduled_vaccination_regimes()
303
304
305 max_regs_per_table = 4
306
307
308
309 reg_count = 0
310 vacc_regimes = []
311 for total_reg_count in range(0,len(all_vacc_regimes)):
312 if reg_count%max_regs_per_table == 0:
313 if len(vacc_regimes) > 0:
314 self.__dump_vacc_table(vacc_regimes)
315 vacc_regimes = []
316 reg_count = 0
317 vacc_regimes.append(all_vacc_regimes[total_reg_count])
318 reg_count += 1
319 if len(vacc_regimes) > 0:
320 self.__dump_vacc_table(vacc_regimes)
321
322
324 """
325 Dump information related to the fields of a clinical item
326 offset - Number of left blank spaces
327 item - Item of the field to dump
328 fields - Fields to dump
329 """
330 txt = ''
331 for a_field in field_list:
332 if type(a_field) is not str:
333 a_field = str(a_field, encoding='latin1', errors='replace')
334 txt += '%s%s%s' % ((offset * ' '), a_field, gmTools.coalesce(item[a_field], '\n', template4value = ': %s\n'))
335 return txt
336
338 """
339 Dumps allergy item data
340 allergy - Allergy item to dump
341 left_margin - Number of spaces on the left margin
342 """
343 txt = ''
344 txt += left_margin*' ' + _('Allergy') + ': \n'
345 txt += self.dump_item_fields((left_margin+3), allergy, ['allergene', 'substance', 'generic_specific','l10n_type', 'definite', 'reaction'])
346 return txt
347
349 """
350 Dumps vaccination item data
351 vaccination - Vaccination item to dump
352 left_margin - Number of spaces on the left margin
353 """
354 txt = ''
355 txt += left_margin*' ' + _('Vaccination') + ': \n'
356 txt += self.dump_item_fields((left_margin+3), vaccination, ['l10n_indication', 'vaccine', 'batch_no', 'site', 'narrative'])
357 return txt
358
360 """
361 Dumps lab result item data
362 lab_request - Lab request item to dump
363 left_margin - Number of spaces on the left margin
364 """
365 txt = ''
366 if self.lab_new_encounter:
367 txt += (left_margin)*' ' + _('Lab result') + ': \n'
368 txt += (left_margin+3) * ' ' + lab_result['unified_name'] + ': ' + lab_result['unified_val']+ ' ' + lab_result['val_unit'] + ' (' + lab_result['material'] + ')' + '\n'
369 return txt
370
372 """
373 Obtains formatted clinical item output dump
374 item - The clinical item to dump
375 left_margin - Number of spaces on the left margin
376 """
377 txt = ''
378 if isinstance(item, gmAllergy.cAllergy):
379 txt += self.get_allergy_output(item, left_margin)
380
381
382
383
384
385 return txt
386
388 """
389 Retrieve patient clinical items filtered by multiple constraints
390 """
391
392
393 emr = self.__patient.emr
394 filtered_items = []
395 filtered_items.extend(emr.get_allergies(
396 since=self.__constraints['since'],
397 until=self.__constraints['until'],
398 encounters=self.__constraints['encounters'],
399 episodes=self.__constraints['episodes'],
400 issues=self.__constraints['issues'])
401 )
402 self.__filtered_items = filtered_items
403 return True
404
406 """
407 Dumps allergy item data summary
408 allergy - Allergy item to dump
409 left_margin - Number of spaces on the left margin
410 """
411 txt = _('%sAllergy: %s, %s (noted %s)\n') % (
412 left_margin * ' ',
413 allergy['descriptor'],
414 gmTools.coalesce(allergy['reaction'], _('unknown reaction')),
415 gmDateTime.pydt_strftime(allergy['date'], '%Y %b %d')
416 )
417
418
419
420
421
422
423 return txt
424
426 """
427 Dumps vaccination item data summary
428 vaccination - Vaccination item to dump
429 left_margin - Number of spaces on the left margin
430 """
431 txt = left_margin*' ' + _('Vaccination') + ': ' + vaccination['l10n_indication'] + ', ' + \
432 vaccination['narrative'] + '\n'
433 return txt
434
436 """
437 Dumps lab result item data summary
438 lab_request - Lab request item to dump
439 left_margin - Number of spaces on the left margin
440 """
441 txt = ''
442 if self.lab_new_encounter:
443 txt += (left_margin+3)*' ' + _('Lab') + ': ' + \
444 lab_result['unified_name'] + '-> ' + lab_result['unified_val'] + \
445 ' ' + lab_result['val_unit']+ '\n' + '(' + lab_result['req_when'].strftime('%Y-%m-%d') + ')'
446 return txt
447
449 """
450 Obtains formatted clinical item summary dump
451 item - The clinical item to dump
452 left_margin - Number of spaces on the left margin
453 """
454 txt = ''
455 if isinstance(item, gmAllergy.cAllergy):
456 txt += self.get_allergy_summary(item, left_margin)
457
458
459
460
461
462
463
464 return txt
465
467 """
468 checks a emr_tree constructed with this.get_historical_tree()
469 and sees if any new items need to be inserted.
470 """
471
472 self._traverse_health_issues( emr_tree, self._update_health_issue_branch)
473
475 self._traverse_health_issues( emr_tree, self._add_health_issue_branch)
476
478 """
479 Retrieves patient's historical in form of a wx tree of health issues
480 -> episodes
481 -> encounters
482 Encounter object is associated with item to allow displaying its information
483 """
484
485
486
487
488
489 if not self.__fetch_filtered_items():
490 return
491 emr = self.__patient.emr
492 unlinked_episodes = emr.get_episodes(issues = [None])
493 h_issues = []
494 h_issues.extend(emr.get_health_issues(id_list = self.__constraints['issues']))
495
496
497 if len(unlinked_episodes) > 0:
498 h_issues.insert(0, {
499 'description': _('Unattributed episodes'),
500 'pk_health_issue': None
501 })
502
503 for a_health_issue in h_issues:
504 health_issue_action( emr_tree, a_health_issue)
505
506 root_item = emr_tree.GetRootItem()
507 if len(h_issues) == 0:
508 emr_tree.SetItemHasChildren(root_item, False)
509 else:
510 emr_tree.SetItemHasChildren(root_item, True)
511 emr_tree.SortChildren(root_item)
512
514 """appends to a wx emr_tree , building wx treenodes from the health_issue make this reusable for non-collapsing tree updates"""
515 emr = self.__patient.emr
516 root_node = emr_tree.GetRootItem()
517 issue_node = emr_tree.AppendItem(root_node, a_health_issue['description'])
518 emr_tree.SetItemData(issue_node, a_health_issue)
519 episodes = emr.get_episodes(id_list=self.__constraints['episodes'], issues = [a_health_issue['pk_health_issue']])
520 if len(episodes) == 0:
521 emr_tree.SetItemHasChildren(issue_node, False)
522 else:
523 emr_tree.SetItemHasChildren(issue_node, True)
524 for an_episode in episodes:
525 self._add_episode_to_tree( emr, emr_tree, issue_node,a_health_issue, an_episode)
526 emr_tree.SortChildren(issue_node)
527
529 episode_node = emr_tree.AppendItem(issue_node, an_episode['description'])
530 emr_tree.SetItemData(episode_node, an_episode)
531 if an_episode['episode_open']:
532 emr_tree.SetItemBold(issue_node, True)
533
534 encounters = self._get_encounters( an_episode, emr )
535 if len(encounters) == 0:
536 emr_tree.SetItemHasChildren(episode_node, False)
537 else:
538 emr_tree.SetItemHasChildren(episode_node, True)
539 self._add_encounters_to_tree( encounters, emr_tree, episode_node )
540 emr_tree.SortChildren(episode_node)
541 return episode_node
542
544 for an_encounter in encounters:
545
546 label = '%s: %s' % (
547 an_encounter['started'].strftime('%Y-%m-%d'),
548 gmTools.unwrap (
549 gmTools.coalesce (
550 gmTools.coalesce (
551 gmTools.coalesce (
552 an_encounter.get_latest_soap (
553 soap_cat = 'a',
554 episode = emr_tree.GetItemData(episode_node)['pk_episode']
555 ),
556 an_encounter['assessment_of_encounter']
557 ),
558 an_encounter['reason_for_encounter']
559 ),
560 an_encounter['l10n_type']
561 ),
562 max_length = 40
563 )
564 )
565 encounter_node_id = emr_tree.AppendItem(episode_node, label)
566 emr_tree.SetItemData(encounter_node_id, an_encounter)
567 emr_tree.SetItemHasChildren(encounter_node_id, False)
568
574
576 emr = self.__patient.emr
577 root_node = emr_tree.GetRootItem()
578 id, cookie = emr_tree.GetFirstChild(root_node)
579 found = False
580 while id.IsOk():
581 if emr_tree.GetItemText(id) == a_health_issue['description']:
582 found = True
583 break
584 id,cookie = emr_tree.GetNextChild( root_node, cookie)
585
586 if not found:
587 _log.error("health issue %s should exist in tree already", a_health_issue['description'] )
588 return
589 issue_node = id
590 episodes = emr.get_episodes(id_list=self.__constraints['episodes'], issues = [a_health_issue['pk_health_issue']])
591
592
593 tree_episodes = {}
594 id_episode, cookie = emr_tree.GetFirstChild(issue_node)
595 while id_episode.IsOk():
596 tree_episodes[ emr_tree.GetItemData(id_episode)['pk_episode'] ]= id_episode
597 id_episode,cookie = emr_tree.GetNextChild( issue_node, cookie)
598
599 existing_episode_pk = [ e['pk_episode'] for e in episodes]
600 missing_tree_pk = [ pk for pk in tree_episodes.keys() if pk not in existing_episode_pk]
601 for pk in missing_tree_pk:
602 emr_tree.Remove( tree_episodes[pk] )
603
604 added_episode_pk = [pk for pk in existing_episode_pk if pk not in tree_episodes.keys()]
605 add_episodes = [ e for e in episodes if e['pk_episode'] in added_episode_pk]
606
607
608 for an_episode in add_episodes:
609 node = self._add_episode_to_tree( emr, emr_tree, issue_node, a_health_issue, an_episode)
610 tree_episodes[an_episode['pk_episode']] = node
611
612 for an_episode in episodes:
613
614 try:
615
616 id_episode = tree_episodes[an_episode['pk_episode']]
617 except Exception:
618 import pdb
619 pdb.set_trace()
620
621 tree_enc = {}
622 id_encounter, cookie = emr_tree.GetFirstChild(id_episode)
623 while id_encounter.IsOk():
624 tree_enc[ emr_tree.GetItemData(id_encounter)['pk_encounter'] ] = id_encounter
625 id_encounter,cookie = emr_tree.GetNextChild(id_episode, cookie)
626
627
628
629 encounters = self._get_encounters( an_episode, emr )
630 existing_enc_pk = [ enc['pk_encounter'] for enc in encounters]
631 missing_enc_pk = [ pk for pk in tree_enc.keys() if pk not in existing_enc_pk]
632 for pk in missing_enc_pk:
633 emr_tree.Remove( tree_enc[pk] )
634
635
636 added_enc_pk = [ pk for pk in existing_enc_pk if pk not in tree_enc.keys() ]
637 add_encounters = [ enc for enc in encounters if enc['pk_encounter'] in added_enc_pk]
638 if add_encounters != []:
639
640 self._add_encounters_to_tree( add_encounters, emr_tree, id_episode)
641
643 """
644 Dumps patient EMR summary
645 """
646 txt = ''
647 for an_item in self.__filtered_items:
648 txt += self.get_item_summary(an_item, left_margin)
649 return txt
650
652 """Dumps episode specific data"""
653 emr = self.__patient.emr
654 encs = emr.get_encounters(episodes = [episode['pk_episode']])
655 if encs is None:
656 txt = left_margin * ' ' + _('Error retrieving encounters for episode\n%s') % str(episode)
657 return txt
658 no_encs = len(encs)
659 if no_encs == 0:
660 txt = left_margin * ' ' + _('There are no encounters for this episode.')
661 return txt
662 if episode['episode_open']:
663 status = _('active')
664 else:
665 status = _('finished')
666 first_encounter = emr.get_first_encounter(episode_id = episode['pk_episode'])
667 last_encounter = emr.get_last_encounter(episode_id = episode['pk_episode'])
668 txt = _(
669 '%sEpisode "%s" [%s]\n'
670 '%sEncounters: %s (%s - %s)\n'
671 '%sLast worked on: %s\n'
672 ) % (
673 left_margin * ' ', episode['description'], status,
674 left_margin * ' ', no_encs, first_encounter['started'].strftime('%m/%Y'), last_encounter['last_affirmed'].strftime('%m/%Y'),
675 left_margin * ' ', last_encounter['last_affirmed'].strftime('%Y-%m-%d %H:%M')
676 )
677 return txt
678
680 """
681 Dumps encounter specific data (rfe, aoe and soap)
682 """
683 emr = self.__patient.emr
684
685 txt = (' ' * left_margin) + '#%s: %s - %s %s' % (
686 encounter['pk_encounter'],
687 encounter['started'].strftime('%Y-%m-%d %H:%M'),
688 encounter['last_affirmed'].strftime('%H:%M (%Z)'),
689 encounter['l10n_type']
690 )
691 if (encounter['assessment_of_encounter'] is not None) and (len(encounter['assessment_of_encounter']) > 0):
692 txt += ' "%s"' % encounter['assessment_of_encounter']
693 txt += '\n\n'
694
695
696 txt += (' ' * left_margin) + '%s: %s\n' % (_('RFE'), encounter['reason_for_encounter'])
697 txt += (' ' * left_margin) + '%s: %s\n' % (_('AOE'), encounter['assessment_of_encounter'])
698
699
700 soap_cat_labels = {
701 's': _('Subjective'),
702 'o': _('Objective'),
703 'a': _('Assessment'),
704 'p': _('Plan'),
705 'u': _('Unspecified'),
706 None: _('Administrative')
707 }
708 eol_w_margin = '\n' + (' ' * (left_margin+3))
709 for soap_cat in 'soapu':
710 soap_cat_narratives = emr.get_clin_narrative (
711 episodes = [episode['pk_episode']],
712 encounters = [encounter['pk_encounter']],
713 soap_cats = [soap_cat]
714 )
715 if soap_cat_narratives is None:
716 continue
717 if len(soap_cat_narratives) == 0:
718 continue
719 txt += (' ' * left_margin) + soap_cat_labels[soap_cat] + ':\n'
720 for soap_entry in soap_cat_narratives:
721 txt += gmTools.wrap (
722 '%s %.8s: %s\n' % (
723 soap_entry['date'].strftime('%d.%m. %H:%M'),
724 soap_entry['modified_by'],
725 soap_entry['narrative']
726 ), 75
727 )
728
729
730
731
732
733
734
735
736
737
738 for an_item in self.__filtered_items:
739 if an_item['pk_encounter'] == encounter['pk_encounter']:
740 txt += self.get_item_output(an_item, left_margin)
741 return txt
742
743
745 """Dumps patient's historical in form of a tree of health issues
746 -> episodes
747 -> encounters
748 -> clinical items
749 """
750
751
752 self.__fetch_filtered_items()
753 emr = self.__patient.emr
754
755
756 for an_item in self.__filtered_items:
757 self.__target.write(self.get_item_summary(an_item, 3))
758
759
760 h_issues = []
761 h_issues.extend(emr.get_health_issues(id_list = self.__constraints['issues']))
762
763 unlinked_episodes = emr.get_episodes(issues = [None])
764 if len(unlinked_episodes) > 0:
765 h_issues.insert(0, {'description':_('Unattributed episodes'), 'pk_health_issue':None})
766 for a_health_issue in h_issues:
767 self.__target.write('\n' + 3*' ' + 'Health Issue: ' + a_health_issue['description'] + '\n')
768 episodes = emr.get_episodes(id_list=self.__constraints['episodes'], issues = [a_health_issue['pk_health_issue']])
769 for an_episode in episodes:
770 self.__target.write('\n' + 6*' ' + 'Episode: ' + an_episode['description'] + '\n')
771 if a_health_issue['pk_health_issue'] is None:
772 issues = None
773 else:
774 issues = [a_health_issue['pk_health_issue']]
775 encounters = emr.get_encounters (
776 since = self.__constraints['since'],
777 until = self.__constraints['until'],
778 id_list = self.__constraints['encounters'],
779 episodes = [an_episode['pk_episode']],
780 issues = issues
781 )
782 for an_encounter in encounters:
783
784 self.lab_new_encounter = True
785 self.__target.write(
786 '\n %s %s: %s - %s (%s)\n' % (
787 _('Encounter'),
788 an_encounter['l10n_type'],
789 an_encounter['started'].strftime('%A, %Y-%m-%d %H:%M'),
790 an_encounter['last_affirmed'].strftime('%m-%d %H:%M'),
791 an_encounter['assessment_of_encounter']
792 )
793 )
794 self.__target.write(self.get_encounter_info(an_episode, an_encounter, 12))
795
797 """
798 Dumps in ASCII format patient's clinical record
799 """
800 emr = self.__patient.emr
801 if emr is None:
802 _log.error('cannot get EMR text dump')
803 print((_(
804 'An error occurred while retrieving a text\n'
805 'dump of the EMR for the active patient.\n\n'
806 'Please check the log file for details.'
807 )))
808 return None
809 self.__target.write('\nOverview\n')
810 self.__target.write('--------\n')
811 self.__target.write("1) Allergy status (for details, see below):\n\n")
812 for allergy in emr.get_allergies():
813 self.__target.write(" " + allergy['descriptor'] + "\n\n")
814 self.__target.write("2) Vaccination status (* indicates booster):\n")
815
816 self.__target.write("\n3) Historical:\n\n")
817 self.dump_historical_tree()
818
819 try:
820 emr.cleanup()
821 except Exception:
822 print("error cleaning up EMR")
823
825 """
826 Dumps patient stored medical documents
827
828 """
829 doc_folder = self.__patient.get_document_folder()
830
831 self.__target.write('\n4) Medical documents: (date) reference - type "comment"\n')
832 self.__target.write(' object - comment')
833
834 docs = doc_folder.get_documents()
835 for doc in docs:
836 self.__target.write('\n\n (%s) %s - %s "%s"' % (
837 doc['clin_when'].strftime('%Y-%m-%d'),
838 doc['ext_ref'],
839 doc['l10n_type'],
840 doc['comment'])
841 )
842 for part in doc.parts:
843 self.__target.write('\n %s - %s' % (
844 part['seq_idx'],
845 part['obj_comment'])
846 )
847 self.__target.write('\n\n')
848
850 """
851 Dumps in ASCII format some basic patient's demographic data
852 """
853 if self.__patient is None:
854 _log.error('cannot get Demographic export')
855 print((_(
856 'An error occurred while Demographic record export\n'
857 'Please check the log file for details.'
858 )))
859 return None
860
861 self.__target.write('\n\n\nDemographics')
862 self.__target.write('\n------------\n')
863 self.__target.write(' Id: %s \n' % self.__patient['pk_identity'])
864 cont = 0
865 for name in self.__patient.get_names():
866 if cont == 0:
867 self.__target.write(' Name (Active): %s, %s\n' % (name['firstnames'], name['lastnames']) )
868 else:
869 self.__target.write(' Name %s: %s, %s\n' % (cont, name['firstnames'], name['lastnames']))
870 cont += 1
871 self.__target.write(' Gender: %s\n' % self.__patient['gender'])
872 self.__target.write(' Title: %s\n' % self.__patient['title'])
873 self.__target.write(' Dob: %s\n' % self.__patient.get_formatted_dob(format = '%Y-%m-%d'))
874 self.__target.write(' Medical age: %s\n' % self.__patient.get_medical_age())
875
877 """
878 Dumps exporter filtering constraints
879 """
880 self.__first_constraint = True
881 if not self.__constraints['since'] is None:
882 self.dump_constraints_header()
883 self.__target.write('\nSince: %s' % self.__constraints['since'].strftime('%Y-%m-%d'))
884
885 if not self.__constraints['until'] is None:
886 self.dump_constraints_header()
887 self.__target.write('\nUntil: %s' % self.__constraints['until'].strftime('%Y-%m-%d'))
888
889 if not self.__constraints['encounters'] is None:
890 self.dump_constraints_header()
891 self.__target.write('\nEncounters: ')
892 for enc in self.__constraints['encounters']:
893 self.__target.write(str(enc) + ' ')
894
895 if not self.__constraints['episodes'] is None:
896 self.dump_constraints_header()
897 self.__target.write('\nEpisodes: ')
898 for epi in self.__constraints['episodes']:
899 self.__target.write(str(epi) + ' ')
900
901 if not self.__constraints['issues'] is None:
902 self.dump_constraints_header()
903 self.__target.write('\nIssues: ')
904 for iss in self.__constraints['issues']:
905 self.__target.write(str(iss) + ' ')
906
908 """
909 Dumps constraints header
910 """
911 if self.__first_constraint == True:
912 self.__target.write('\nClinical items dump constraints\n')
913 self.__target.write('-'*(len(head_txt)-2))
914 self.__first_constraint = False
915
916
918 """Exports patient EMR into a simple chronological journal.
919 """
921 self.__narrative_wrap_len = 72
922
923
924
925
927 if patient is None:
928 raise ValueError('[%s].save_to_file_by_mod_time(): no patient' % self.__class__.__name__)
929
930 if filename is None:
931 filename = gmTools.get_unique_filename(prefix = 'gm-emr_by_mod_time-', suffix = '.txt')
932
933 f = io.open(filename, mode = 'w+t', encoding = 'utf8', errors = 'replace')
934
935 self.__narrative_wrap_len = 80
936
937
938 txt = _('EMR Journal sorted by last modification time\n')
939 f.write(txt)
940 f.write('=' * (len(txt)-1))
941 f.write('\n')
942 f.write(_('Patient: %s (%s), No: %s\n') % (patient['description'], patient['gender'], patient['pk_identity']))
943 f.write(_('Born : %s, age: %s\n\n') % (
944 patient.get_formatted_dob(format = '%Y %b %d'),
945 patient.get_medical_age()
946 ))
947
948
949 cmd = """
950 SELECT
951 vemrj.*,
952 to_char(vemrj.modified_when, 'YYYY-MM-DD HH24:MI') AS date_modified
953 FROM clin.v_emr_journal vemrj
954 WHERE pk_patient = %(pat)s
955 ORDER BY modified_when
956 """
957 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': patient['pk_identity']}}])
958
959 f.write ((gmTools.u_box_horiz_single * 100) + '\n')
960 f.write ('%16.16s %s %9.9s %s %1.1s %s %s \n' % (
961 _('Last modified'),
962 gmTools.u_box_vert_light,
963 _('By'),
964 gmTools.u_box_vert_light,
965 ' ',
966 gmTools.u_box_vert_light,
967 _('Entry')
968 ))
969 f.write ((gmTools.u_box_horiz_single * 100) + '\n')
970
971 for r in rows:
972 txt = '%16.16s %s %9.9s %s %1.1s %s %s \n' % (
973 r['date_modified'],
974 gmTools.u_box_vert_light,
975 r['modified_by'],
976 gmTools.u_box_vert_light,
977 gmSoapDefs.soap_cat2l10n[r['soap_cat']],
978 gmTools.u_box_vert_light,
979 gmTools.wrap (
980 text = r['narrative'].replace('\r', '') + ' (%s: %s)' % (_('When'), gmDateTime.pydt_strftime(r['clin_when'], '%Y %b %d %H:%M')),
981 width = self.__narrative_wrap_len,
982 subsequent_indent = '%31.31s%1.1s %s ' % (' ', gmSoapDefs.soap_cat2l10n[r['soap_cat']], gmTools.u_box_vert_light)
983 )
984 )
985 f.write(txt)
986
987 f.write ((gmTools.u_box_horiz_single * 100) + '\n')
988 f.write(_('Exported: %s\n') % gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%Y %b %d %H:%M:%S'))
989
990 f.close()
991 return filename
992
993
995 """Export medical record into a file.
996
997 @type filename: None (creates filename by itself) or string
998 @type patient: <cPerson> instance
999 """
1000 if patient is None:
1001 raise ValueError('[%s].save_to_file_by_encounter(): no patient' % self.__class__.__name__)
1002
1003 if filename is None:
1004 filename = gmTools.get_unique_filename(prefix = 'gm-emr_journal-', suffix = '.txt')
1005
1006 f = io.open(filename, mode = 'w+t', encoding = 'utf8', errors = 'replace')
1007 self.__export_by_encounter(target = f, patient = patient)
1008 f.close()
1009 return filename
1010
1011
1012
1013
1015 """
1016 Export medical record into a Python object.
1017
1018 @type target: a python object supporting the write() API
1019 @type patient: <cPerson> instance
1020 """
1021 txt = _('Chronological EMR Journal\n')
1022 target.write(txt)
1023 target.write('=' * (len(txt)-1))
1024 target.write('\n')
1025
1026 target.write(_('Patient: %s (%s), No: %s\n') % (patient['description'], patient['gender'], patient['pk_identity']))
1027 target.write(_('Born : %s, age: %s\n\n') % (
1028 patient.get_formatted_dob(format = '%Y %b %d'),
1029 patient.get_medical_age()
1030 ))
1031 for ext_id in patient.external_ids:
1032 target.write('%s: %s (@%s)\n' % (ext_id['name'], ext_id['value'], ext_id['issuer']))
1033 for ch in patient.comm_channels:
1034 target.write('%s: %s\n' % (ch['l10n_comm_type'], ch['url']))
1035
1036 target.write('%s%12.12s%s%11.11s%s%s%s%72.72s\n' % (
1037 gmTools.u_box_top_left_arc,
1038 gmTools.u_box_horiz_single * 12,
1039 gmTools.u_box_T_down,
1040 gmTools.u_box_horiz_single * 11,
1041 gmTools.u_box_T_down,
1042 gmTools.u_box_horiz_single * 5,
1043 gmTools.u_box_T_down,
1044 gmTools.u_box_horiz_single * self.__narrative_wrap_len
1045 ))
1046 target.write('%s %10.10s %s %9.9s %s %s %s\n' % (
1047 gmTools.u_box_vert_light,
1048 _('Encounter'),
1049 gmTools.u_box_vert_light,
1050 _('Doc'),
1051 gmTools.u_box_vert_light,
1052 gmTools.u_box_vert_light,
1053 _('Narrative')
1054 ))
1055 target.write('%s%12.12s%s%11.11s%s%s%s%72.72s\n' % (
1056 gmTools.u_box_T_right,
1057 gmTools.u_box_horiz_single * 12,
1058 gmTools.u_box_plus,
1059 gmTools.u_box_horiz_single * 11,
1060 gmTools.u_box_plus,
1061 gmTools.u_box_horiz_single * 5,
1062 gmTools.u_box_plus,
1063 gmTools.u_box_horiz_single * self.__narrative_wrap_len
1064 ))
1065
1066 cmd = """
1067 SELECT
1068 to_char(vemrj.clin_when, 'YYYY-MM-DD') AS date,
1069 vemrj.*,
1070 (SELECT rank FROM clin.soap_cat_ranks WHERE soap_cat = vemrj.soap_cat) AS scr,
1071 to_char(vemrj.modified_when, 'YYYY-MM-DD HH24:MI') AS date_modified
1072 FROM clin.v_emr_journal vemrj
1073 WHERE pk_patient = %s
1074 ORDER BY date, pk_episode, scr, src_table
1075 """
1076 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [patient['pk_identity']]}], get_col_idx = True)
1077
1078
1079 prev_date = ''
1080 prev_doc = ''
1081 prev_soap = ''
1082 for row in rows:
1083
1084 if row['narrative'] is None:
1085 continue
1086
1087 txt = gmTools.wrap (
1088 text = row['narrative'].replace('\r', '') + (' (%s)' % row['date_modified']),
1089 width = self.__narrative_wrap_len
1090 ).split('\n')
1091
1092
1093 curr_doc = row['modified_by']
1094 if curr_doc != prev_doc:
1095 prev_doc = curr_doc
1096 else:
1097 curr_doc = ''
1098
1099
1100 curr_soap = row['soap_cat']
1101 if curr_soap != prev_soap:
1102 prev_soap = curr_soap
1103
1104
1105 curr_date = row['date']
1106 if curr_date != prev_date:
1107 prev_date = curr_date
1108 curr_doc = row['modified_by']
1109 prev_doc = curr_doc
1110 curr_soap = row['soap_cat']
1111 prev_soap = curr_soap
1112 else:
1113 curr_date = ''
1114
1115
1116 target.write('%s %10.10s %s %9.9s %s %3.3s %s %s\n' % (
1117 gmTools.u_box_vert_light,
1118 curr_date,
1119 gmTools.u_box_vert_light,
1120 curr_doc,
1121 gmTools.u_box_vert_light,
1122 gmSoapDefs.soap_cat2l10n[curr_soap],
1123 gmTools.u_box_vert_light,
1124 txt[0]
1125 ))
1126
1127
1128 if len(txt) == 1:
1129 continue
1130
1131 template = '%s %10.10s %s %9.9s %s %3.3s %s %s\n'
1132 for part in txt[1:]:
1133 line = template % (
1134 gmTools.u_box_vert_light,
1135 '',
1136 gmTools.u_box_vert_light,
1137 '',
1138 gmTools.u_box_vert_light,
1139 ' ',
1140 gmTools.u_box_vert_light,
1141 part
1142 )
1143 target.write(line)
1144
1145
1146 target.write('%s%12.12s%s%11.11s%s%5.5s%s%72.72s\n\n' % (
1147 gmTools.u_box_bottom_left_arc,
1148 gmTools.u_box_horiz_single * 12,
1149 gmTools.u_box_T_up,
1150 gmTools.u_box_horiz_single * 11,
1151 gmTools.u_box_T_up,
1152 gmTools.u_box_horiz_single * 5,
1153 gmTools.u_box_T_up,
1154 gmTools.u_box_horiz_single * self.__narrative_wrap_len
1155 ))
1156
1157 target.write(_('Exported: %s\n') % gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %b %d %H:%M:%S'))
1158
1159 return
1160
1161
1163 """Export SOAP data per encounter into Medistar import format."""
1164
1169
1170
1171
1172
1173 - def save_to_file(self, filename=None, encounter=None, soap_cats='soapu', export_to_import_file=False):
1174 if not self.__pat.connected:
1175 return (False, 'no active patient')
1176
1177 if filename is None:
1178 path = os.path.abspath(os.path.expanduser('~/gnumed/export'))
1179 filename = '%s-%s-%s-%s-%s.txt' % (
1180 os.path.join(path, 'Medistar-MD'),
1181 time.strftime('%Y-%m-%d',time.localtime()),
1182 self.__pat['lastnames'].replace(' ', '-'),
1183 self.__pat['firstnames'].replace(' ', '_'),
1184 self.__pat.get_formatted_dob(format = '%Y-%m-%d')
1185 )
1186
1187 f = io.open(filename, mode = 'w+t', encoding = 'cp437', errors='replace')
1188 status = self.__export(target = f, encounter = encounter, soap_cats = soap_cats)
1189 f.close()
1190
1191 if export_to_import_file:
1192
1193 medistar_found = False
1194 for drive in 'cdefghijklmnopqrstuvwxyz':
1195 path = drive + ':\\medistar\\inst'
1196 if not os.path.isdir(path):
1197 continue
1198 try:
1199 import_fname = path + '\\soap.txt'
1200 open(import_fname, mode = 'w+b').close()
1201 _log.debug('exporting narrative to [%s] for Medistar import', import_fname)
1202 shutil.copyfile(filename, import_fname)
1203 medistar_found = True
1204 except IOError:
1205 continue
1206
1207 if not medistar_found:
1208 _log.debug('no Medistar installation found (no <LW>:\\medistar\\inst\\)')
1209
1210 return (status, filename)
1211
1212 - def export(self, target, encounter=None, soap_cats='soapu'):
1213 return self.__export(target, encounter = encounter, soap_cats = soap_cats)
1214
1215
1216
1217 - def __export(self, target=None, encounter=None, soap_cats='soapu'):
1233
1234
1235
1236
1238 """
1239 Prints application usage options to stdout.
1240 """
1241 print('usage: python gmPatientExporter [--fileout=<outputfilename>] [--conf-file=<file>] [--text-domain=<textdomain>]')
1242 sys.exit(0)
1243
1277
1278
1279
1280
1281 if __name__ == "__main__":
1282
1283 from Gnumed.business import gmPersonSearch
1284
1285
1304
1313
1314 print("\n\nGNUmed ASCII EMR Export")
1315 print("=======================")
1316
1317
1318 export_forensics()
1319
1320
1321