1 """GNUmed simple ASCII EMR export tool.
2
3 TODO:
4 - GUI mode:
5 - post-0.1 !
6 - allow user to select patient
7 - allow user to pick episodes/encounters/etc from list
8 - output modes:
9 - HTML - post-0.1 !
10 """
11
12 __version__ = "$Revision: 1.138 $"
13 __author__ = "Carlos Moro"
14 __license__ = 'GPL'
15
16 import os.path, sys, types, time, codecs, datetime as pyDT, logging, shutil
17
18
19 import mx.DateTime.Parser as mxParser
20 import mx.DateTime as mxDT
21
22
23 if __name__ == '__main__':
24 sys.path.insert(0, '../../')
25 from Gnumed.pycommon import gmI18N, gmExceptions, gmNull, gmPG2, gmTools
26 from Gnumed.business import gmClinicalRecord, gmPerson, gmAllergy, gmDemographicRecord, gmClinNarrative, gmPersonSearch
27
28
29 _log = logging.getLogger('gm.export')
30 _log.info(__version__)
31
33
34
35 - def __init__(self, constraints = None, fileout = None, patient = None):
36 """
37 Constructs a new instance of exporter
38
39 constraints - Exporter constraints for filtering clinical items
40 fileout - File-like object as target for dumping operations
41 """
42 if constraints is None:
43
44 self.__constraints = {
45 'since': None,
46 'until': None,
47 'encounters': None,
48 'episodes': None,
49 'issues': None
50 }
51 else:
52 self.__constraints = constraints
53 self.__target = fileout
54 self.__patient = patient
55 self.lab_new_encounter = True
56 self.__filtered_items = []
57
59 """Sets exporter constraints.
60
61 constraints - Exporter constraints for filtering clinical items
62 """
63 if constraints is None:
64
65 self.__constraints = {
66 'since': None,
67 'until': None,
68 'encounters': None,
69 'episodes': None,
70 'issues': None
71 }
72 else:
73 self.__constraints = constraints
74 return True
75
77 """
78 Retrieve exporter constraints
79 """
80 return self.__constraints
81
83 """
84 Sets exporter patient
85
86 patient - Patient whose data are to be dumped
87 """
88 if patient is None:
89 _log.error("can't set None patient for exporter")
90 return
91 self.__patient = patient
92
94 """
95 Sets exporter output file
96
97 @param file_name - The file to dump the EMR to
98 @type file_name - FileType
99 """
100 self.__target = target
101
103 """
104 Retrieves patient whose data are to be dumped
105 """
106 return self.__patient
107
109 """
110 Exporter class cleanup code
111 """
112 pass
113
115 """
116 Retrieves string containg ASCII vaccination table
117 """
118 emr = self.__patient.get_emr()
119
120 patient_dob = self.__patient['dob']
121 date_length = len(patient_dob.strftime('%x')) + 2
122
123
124 vaccinations4regimes = {}
125 for a_vacc_regime in vacc_regimes:
126 indication = a_vacc_regime['indication']
127 vaccinations4regimes[indication] = emr.get_scheduled_vaccinations(indications=[indication])
128
129 chart_columns = len(vacc_regimes)
130
131 foot_headers = ['last booster', 'next booster']
132
133 ending_str = '='
134
135
136 column_widths = []
137 chart_rows = -1
138 vaccinations = {}
139 temp = -1
140 for foot_header in foot_headers:
141 if len(foot_header) > temp:
142 temp = len(foot_header)
143 column_widths.append(temp)
144 for a_vacc_regime in vacc_regimes:
145 if a_vacc_regime['shots'] > chart_rows:
146 chart_rows = a_vacc_regime['shots']
147 if (len(a_vacc_regime['l10n_indication'])) > date_length:
148 column_widths.append(len(a_vacc_regime['l10n_indication']))
149 else:
150 column_widths.append(date_length)
151 vaccinations[a_vacc_regime['indication']] = emr.get_vaccinations(indications=[a_vacc_regime['indication']])
152
153
154 txt = '\nDOB: %s' %(patient_dob.strftime('%x')) + '\n'
155
156
157
158 for column_width in column_widths:
159 txt += column_width * '-' + '-'
160 txt += '\n'
161
162 txt += column_widths[0] * ' ' + '|'
163 col_index = 1
164 for a_vacc_regime in vacc_regimes:
165 txt += a_vacc_regime['l10n_indication'] + (column_widths[col_index] - len(a_vacc_regime['l10n_indication'])) * ' ' + '|'
166 col_index += 1
167 txt += '\n'
168
169 for column_width in column_widths:
170 txt += column_width * '-' + '-'
171 txt += '\n'
172
173
174 due_date = None
175
176 prev_displayed_date = [patient_dob]
177 for a_regime in vacc_regimes:
178 prev_displayed_date.append(patient_dob)
179
180 for row_index in range(0, chart_rows):
181 row_header = '#%s' %(row_index+1)
182 txt += row_header + (column_widths[0] - len(row_header)) * ' ' + '|'
183
184 for col_index in range(1, chart_columns+1):
185 indication =vacc_regimes[col_index-1]['indication']
186 seq_no = vacc_regimes[col_index-1]['shots']
187 if row_index == seq_no:
188 txt += ending_str * column_widths[col_index] + '|'
189 elif row_index < seq_no:
190 try:
191 vacc_date = vaccinations[indication][row_index]['date']
192 vacc_date_str = vacc_date.strftime('%x')
193 txt += vacc_date_str + (column_widths[col_index] - len(vacc_date_str)) * ' ' + '|'
194 prev_displayed_date[col_index] = vacc_date
195 except:
196 if row_index == 0:
197 due_date = prev_displayed_date[col_index] + vaccinations4regimes[indication][row_index]['age_due_min']
198 else:
199 due_date = prev_displayed_date[col_index] + vaccinations4regimes[indication][row_index]['min_interval']
200 txt += '('+ due_date.strftime('%Y-%m-%d') + ')' + (column_widths[col_index] - date_length) * ' ' + '|'
201 prev_displayed_date[col_index] = due_date
202 else:
203 txt += column_widths[col_index] * ' ' + '|'
204 txt += '\n'
205 for column_width in column_widths:
206 txt += column_width * '-' + '-'
207 txt += '\n'
208
209
210 all_vreg_boosters = []
211 for a_vacc_regime in vacc_regimes:
212 vaccs4indication = vaccinations[a_vacc_regime['indication']]
213 given_boosters = []
214 for a_vacc in vaccs4indication:
215 try:
216 if a_vacc['is_booster']:
217 given_boosters.append(a_vacc)
218 except:
219
220 pass
221 if len(given_boosters) > 0:
222 all_vreg_boosters.append(given_boosters[len(given_boosters)-1])
223 else:
224 all_vreg_boosters.append(None)
225
226
227 all_next_boosters = []
228 for a_booster in all_vreg_boosters:
229 all_next_boosters.append(None)
230
231 cont = 0
232 for a_vacc_regime in vacc_regimes:
233 vaccs = vaccinations4regimes[a_vacc_regime['indication']]
234 if vaccs[len(vaccs)-1]['is_booster'] == False:
235 all_vreg_boosters[cont] = ending_str * column_widths[cont+1]
236 all_next_boosters[cont] = ending_str * column_widths[cont+1]
237 else:
238 indication = vacc_regimes[cont]['indication']
239 if len(vaccinations[indication]) > vacc_regimes[cont]['shots']:
240 all_vreg_boosters[cont] = vaccinations[indication][len(vaccinations[indication])-1]['date'].strftime('%Y-%m-%d')
241 scheduled_booster = vaccinations4regimes[indication][len(vaccinations4regimes[indication])-1]
242 booster_date = vaccinations[indication][len(vaccinations[indication])-1]['date'] + scheduled_booster['min_interval']
243 if booster_date < mxDT.today():
244 all_next_boosters[cont] = '<(' + booster_date.strftime('%Y-%m-%d') + ')>'
245 else:
246 all_next_boosters[cont] = booster_date.strftime('%Y-%m-%d')
247 elif len(vaccinations[indication]) == vacc_regimes[cont]['shots']:
248 all_vreg_boosters[cont] = column_widths[cont+1] * ' '
249 scheduled_booster = vaccinations4regimes[indication][len(vaccinations4regimes[indication])-1]
250 booster_date = vaccinations[indication][len(vaccinations[indication])-1]['date'] + scheduled_booster['min_interval']
251 if booster_date < mxDT.today():
252 all_next_boosters[cont] = '<(' + booster_date.strftime('%Y-%m-%d') + ')>'
253 else:
254 all_next_boosters[cont] = booster_date.strftime('%Y-%m-%d')
255 else:
256 all_vreg_boosters[cont] = column_widths[cont+1] * ' '
257 all_next_boosters[cont] = column_widths[cont+1] * ' '
258 cont += 1
259
260
261 foot_header = foot_headers[0]
262 col_index = 0
263 txt += foot_header + (column_widths[0] - len(foot_header)) * ' ' + '|'
264 col_index += 1
265 for a_vacc_regime in vacc_regimes:
266 txt += str(all_vreg_boosters[col_index-1]) + (column_widths[col_index] - len(str(all_vreg_boosters[col_index-1]))) * ' ' + '|'
267 col_index += 1
268 txt += '\n'
269 for column_width in column_widths:
270 txt += column_width * '-' + '-'
271 txt += '\n'
272
273
274 foot_header = foot_headers[1]
275 col_index = 0
276 txt += foot_header + (column_widths[0] - len(foot_header)) * ' ' + '|'
277 col_index += 1
278 for a_vacc_regime in vacc_regimes:
279 txt += str(all_next_boosters[col_index-1]) + (column_widths[col_index] - len(str(all_next_boosters[col_index-1]))) * ' ' + '|'
280 col_index += 1
281 txt += '\n'
282 for column_width in column_widths:
283 txt += column_width * '-' + '-'
284 txt += '\n'
285
286 self.__target.write(txt)
287
289 """
290 Iterate over patient scheduled regimes preparing vacc tables dump
291 """
292
293 emr = self.__patient.get_emr()
294
295
296 all_vacc_regimes = emr.get_scheduled_vaccination_regimes()
297
298
299 max_regs_per_table = 4
300
301
302
303 reg_count = 0
304 vacc_regimes = []
305 for total_reg_count in range(0,len(all_vacc_regimes)):
306 if reg_count%max_regs_per_table == 0:
307 if len(vacc_regimes) > 0:
308 self.__dump_vacc_table(vacc_regimes)
309 vacc_regimes = []
310 reg_count = 0
311 vacc_regimes.append(all_vacc_regimes[total_reg_count])
312 reg_count += 1
313 if len(vacc_regimes) > 0:
314 self.__dump_vacc_table(vacc_regimes)
315
316
318 """
319 Dump information related to the fields of a clinical item
320 offset - Number of left blank spaces
321 item - Item of the field to dump
322 fields - Fields to dump
323 """
324 txt = ''
325 for a_field in field_list:
326 if type(a_field) is not types.UnicodeType:
327 a_field = unicode(a_field, encoding='latin1', errors='replace')
328 txt += u'%s%s%s' % ((offset * u' '), a_field, gmTools.coalesce(item[a_field], u'\n', template_initial = u': %s\n'))
329 return txt
330
332 """
333 Dumps allergy item data
334 allergy - Allergy item to dump
335 left_margin - Number of spaces on the left margin
336 """
337 txt = ''
338 txt += left_margin*' ' + _('Allergy') + ': \n'
339 txt += self.dump_item_fields((left_margin+3), allergy, ['allergene', 'substance', 'generic_specific','l10n_type', 'definite', 'reaction'])
340 return txt
341
343 """
344 Dumps vaccination item data
345 vaccination - Vaccination item to dump
346 left_margin - Number of spaces on the left margin
347 """
348 txt = ''
349 txt += left_margin*' ' + _('Vaccination') + ': \n'
350 txt += self.dump_item_fields((left_margin+3), vaccination, ['l10n_indication', 'vaccine', 'batch_no', 'site', 'narrative'])
351 return txt
352
354 """
355 Dumps lab result item data
356 lab_request - Lab request item to dump
357 left_margin - Number of spaces on the left margin
358 """
359 txt = ''
360 if self.lab_new_encounter:
361 txt += (left_margin)*' ' + _('Lab result') + ': \n'
362 txt += (left_margin+3) * ' ' + lab_result['unified_name'] + ': ' + lab_result['unified_val']+ ' ' + lab_result['val_unit'] + ' (' + lab_result['material'] + ')' + '\n'
363 return txt
364
366 """
367 Obtains formatted clinical item output dump
368 item - The clinical item to dump
369 left_margin - Number of spaces on the left margin
370 """
371 txt = ''
372 if isinstance(item, gmAllergy.cAllergy):
373 txt += self.get_allergy_output(item, left_margin)
374
375
376
377
378
379 return txt
380
382 """
383 Retrieve patient clinical items filtered by multiple constraints
384 """
385 if not self.__patient.connected:
386 return False
387 emr = self.__patient.get_emr()
388 filtered_items = []
389 filtered_items.extend(emr.get_allergies(
390 since=self.__constraints['since'],
391 until=self.__constraints['until'],
392 encounters=self.__constraints['encounters'],
393 episodes=self.__constraints['episodes'],
394 issues=self.__constraints['issues']))
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411 self.__filtered_items = filtered_items
412 return True
413
415 """
416 Dumps allergy item data summary
417 allergy - Allergy item to dump
418 left_margin - Number of spaces on the left margin
419 """
420 txt = _('%sAllergy: %s, %s (noted %s)\n') % (
421 left_margin * u' ',
422 allergy['descriptor'],
423 gmTools.coalesce(allergy['reaction'], _('unknown reaction')),
424 allergy['date'].strftime('%x')
425 )
426
427
428
429
430
431
432 return txt
433
435 """
436 Dumps vaccination item data summary
437 vaccination - Vaccination item to dump
438 left_margin - Number of spaces on the left margin
439 """
440 txt = left_margin*' ' + _('Vaccination') + ': ' + vaccination['l10n_indication'] + ', ' + \
441 vaccination['narrative'] + '\n'
442 return txt
443
445 """
446 Dumps lab result item data summary
447 lab_request - Lab request item to dump
448 left_margin - Number of spaces on the left margin
449 """
450 txt = ''
451 if self.lab_new_encounter:
452 txt += (left_margin+3)*' ' + _('Lab') + ': ' + \
453 lab_result['unified_name'] + '-> ' + lab_result['unified_val'] + \
454 ' ' + lab_result['val_unit']+ '\n' + '(' + lab_result['req_when'].strftime('%Y-%m-%d') + ')'
455 return txt
456
458 """
459 Obtains formatted clinical item summary dump
460 item - The clinical item to dump
461 left_margin - Number of spaces on the left margin
462 """
463 txt = ''
464 if isinstance(item, gmAllergy.cAllergy):
465 txt += self.get_allergy_summary(item, left_margin)
466
467
468
469
470
471
472
473 return txt
474
476 """
477 checks a emr_tree constructed with this.get_historical_tree()
478 and sees if any new items need to be inserted.
479 """
480
481 self._traverse_health_issues( emr_tree, self._update_health_issue_branch)
482
484 self._traverse_health_issues( emr_tree, self._add_health_issue_branch)
485
487 """
488 Retrieves patient's historical in form of a wx tree of health issues
489 -> episodes
490 -> encounters
491 Encounter object is associated with item to allow displaying its information
492 """
493
494
495
496
497
498 if not self.__fetch_filtered_items():
499 return
500 emr = self.__patient.get_emr()
501 unlinked_episodes = emr.get_episodes(issues = [None])
502 h_issues = []
503 h_issues.extend(emr.get_health_issues(id_list = self.__constraints['issues']))
504
505
506 if len(unlinked_episodes) > 0:
507 h_issues.insert(0, {
508 'description': _('Unattributed episodes'),
509 'pk_health_issue': None
510 })
511
512 for a_health_issue in h_issues:
513 health_issue_action( emr_tree, a_health_issue)
514
515 root_item = emr_tree.GetRootItem()
516 if len(h_issues) == 0:
517 emr_tree.SetItemHasChildren(root_item, False)
518 else:
519 emr_tree.SetItemHasChildren(root_item, True)
520 emr_tree.SortChildren(root_item)
521
523 """appends to a wx emr_tree , building wx treenodes from the health_issue make this reusable for non-collapsing tree updates"""
524 emr = self.__patient.get_emr()
525 root_node = emr_tree.GetRootItem()
526 issue_node = emr_tree.AppendItem(root_node, a_health_issue['description'])
527 emr_tree.SetItemPyData(issue_node, a_health_issue)
528 episodes = emr.get_episodes(id_list=self.__constraints['episodes'], issues = [a_health_issue['pk_health_issue']])
529 if len(episodes) == 0:
530 emr_tree.SetItemHasChildren(issue_node, False)
531 else:
532 emr_tree.SetItemHasChildren(issue_node, True)
533 for an_episode in episodes:
534 self._add_episode_to_tree( emr, emr_tree, issue_node,a_health_issue, an_episode)
535 emr_tree.SortChildren(issue_node)
536
538 episode_node = emr_tree.AppendItem(issue_node, an_episode['description'])
539 emr_tree.SetItemPyData(episode_node, an_episode)
540 if an_episode['episode_open']:
541 emr_tree.SetItemBold(issue_node, True)
542
543 encounters = self._get_encounters( an_episode, emr )
544 if len(encounters) == 0:
545 emr_tree.SetItemHasChildren(episode_node, False)
546 else:
547 emr_tree.SetItemHasChildren(episode_node, True)
548 self._add_encounters_to_tree( encounters, emr_tree, episode_node )
549 emr_tree.SortChildren(episode_node)
550 return episode_node
551
553 for an_encounter in encounters:
554
555 label = u'%s: %s' % (
556 an_encounter['started'].strftime('%Y-%m-%d'),
557 gmTools.unwrap (
558 gmTools.coalesce (
559 gmTools.coalesce (
560 gmTools.coalesce (
561 an_encounter.get_latest_soap (
562 soap_cat = 'a',
563 episode = emr_tree.GetPyData(episode_node)['pk_episode']
564 ),
565 an_encounter['assessment_of_encounter']
566 ),
567 an_encounter['reason_for_encounter']
568 ),
569 an_encounter['l10n_type']
570 ),
571 max_length = 40
572 )
573 )
574 encounter_node_id = emr_tree.AppendItem(episode_node, label)
575 emr_tree.SetItemPyData(encounter_node_id, an_encounter)
576 emr_tree.SetItemHasChildren(encounter_node_id, False)
577
583
585 emr = self.__patient.get_emr()
586 root_node = emr_tree.GetRootItem()
587 id, cookie = emr_tree.GetFirstChild(root_node)
588 found = False
589 while id.IsOk():
590 if emr_tree.GetItemText(id) == a_health_issue['description']:
591 found = True
592 break
593 id,cookie = emr_tree.GetNextChild( root_node, cookie)
594
595 if not found:
596 _log.error("health issue %s should exist in tree already", a_health_issue['description'] )
597 return
598 issue_node = id
599 episodes = emr.get_episodes(id_list=self.__constraints['episodes'], issues = [a_health_issue['pk_health_issue']])
600
601
602 tree_episodes = {}
603 id_episode, cookie = emr_tree.GetFirstChild(issue_node)
604 while id_episode.IsOk():
605 tree_episodes[ emr_tree.GetPyData(id_episode)['pk_episode'] ]= id_episode
606 id_episode,cookie = emr_tree.GetNextChild( issue_node, cookie)
607
608 existing_episode_pk = [ e['pk_episode'] for e in episodes]
609 missing_tree_pk = [ pk for pk in tree_episodes.keys() if pk not in existing_episode_pk]
610 for pk in missing_tree_pk:
611 emr_tree.Remove( tree_episodes[pk] )
612
613 added_episode_pk = [pk for pk in existing_episode_pk if pk not in tree_episodes.keys()]
614 add_episodes = [ e for e in episodes if e['pk_episode'] in added_episode_pk]
615
616
617 for an_episode in add_episodes:
618 node = self._add_episode_to_tree( emr, emr_tree, issue_node, a_health_issue, an_episode)
619 tree_episodes[an_episode['pk_episode']] = node
620
621 for an_episode in episodes:
622
623 try:
624
625 id_episode = tree_episodes[an_episode['pk_episode']]
626 except:
627 import pdb
628 pdb.set_trace()
629
630 tree_enc = {}
631 id_encounter, cookie = emr_tree.GetFirstChild(id_episode)
632 while id_encounter.IsOk():
633 tree_enc[ emr_tree.GetPyData(id_encounter)['pk_encounter'] ] = id_encounter
634 id_encounter,cookie = emr_tree.GetNextChild(id_episode, cookie)
635
636
637
638 encounters = self._get_encounters( an_episode, emr )
639 existing_enc_pk = [ enc['pk_encounter'] for enc in encounters]
640 missing_enc_pk = [ pk for pk in tree_enc.keys() if pk not in existing_enc_pk]
641 for pk in missing_enc_pk:
642 emr_tree.Remove( tree_enc[pk] )
643
644
645 added_enc_pk = [ pk for pk in existing_enc_pk if pk not in tree_enc.keys() ]
646 add_encounters = [ enc for enc in encounters if enc['pk_encounter'] in added_enc_pk]
647 if add_encounters != []:
648
649 self._add_encounters_to_tree( add_encounters, emr_tree, id_episode)
650
652 """
653 Dumps patient EMR summary
654 """
655 txt = ''
656 for an_item in self.__filtered_items:
657 txt += self.get_item_summary(an_item, left_margin)
658 return txt
659
661 """Dumps episode specific data"""
662 emr = self.__patient.get_emr()
663 encs = emr.get_encounters(episodes = [episode['pk_episode']])
664 if encs is None:
665 txt = left_margin * ' ' + _('Error retrieving encounters for episode\n%s') % str(episode)
666 return txt
667 no_encs = len(encs)
668 if no_encs == 0:
669 txt = left_margin * ' ' + _('There are no encounters for this episode.')
670 return txt
671 if episode['episode_open']:
672 status = _('active')
673 else:
674 status = _('finished')
675 first_encounter = emr.get_first_encounter(episode_id = episode['pk_episode'])
676 last_encounter = emr.get_last_encounter(episode_id = episode['pk_episode'])
677 txt = _(
678 '%sEpisode "%s" [%s]\n'
679 '%sEncounters: %s (%s - %s)\n'
680 '%sLast worked on: %s\n'
681 ) % (
682 left_margin * ' ', episode['description'], status,
683 left_margin * ' ', no_encs, first_encounter['started'].strftime('%m/%Y'), last_encounter['last_affirmed'].strftime('%m/%Y'),
684 left_margin * ' ', last_encounter['last_affirmed'].strftime('%Y-%m-%d %H:%M')
685 )
686 return txt
687
689 """
690 Dumps encounter specific data (rfe, aoe and soap)
691 """
692 emr = self.__patient.get_emr()
693
694 txt = (' ' * left_margin) + '#%s: %s - %s %s' % (
695 encounter['pk_encounter'],
696 encounter['started'].strftime('%Y-%m-%d %H:%M'),
697 encounter['last_affirmed'].strftime('%H:%M (%Z)'),
698 encounter['l10n_type']
699 )
700 if (encounter['assessment_of_encounter'] is not None) and (len(encounter['assessment_of_encounter']) > 0):
701 txt += ' "%s"' % encounter['assessment_of_encounter']
702 txt += '\n\n'
703
704
705 txt += (' ' * left_margin) + '%s: %s\n' % (_('RFE'), encounter['reason_for_encounter'])
706 txt += (' ' * left_margin) + '%s: %s\n' % (_('AOE'), encounter['assessment_of_encounter'])
707
708
709 soap_cat_labels = {
710 's': _('Subjective'),
711 'o': _('Objective'),
712 'a': _('Assessment'),
713 'p': _('Plan'),
714 None: _('Administrative')
715 }
716 eol_w_margin = '\n' + (' ' * (left_margin+3))
717 for soap_cat in 'soap':
718 soap_cat_narratives = emr.get_clin_narrative (
719 episodes = [episode['pk_episode']],
720 encounters = [encounter['pk_encounter']],
721 soap_cats = [soap_cat]
722 )
723 if soap_cat_narratives is None:
724 continue
725 if len(soap_cat_narratives) == 0:
726 continue
727 txt += (' ' * left_margin) + soap_cat_labels[soap_cat] + ':\n'
728 for soap_entry in soap_cat_narratives:
729 txt += gmTools.wrap (
730 '%s %.8s: %s\n' % (
731 soap_entry['date'].strftime('%d.%m. %H:%M'),
732 soap_entry['provider'],
733 soap_entry['narrative']
734 ), 75
735 )
736
737
738
739
740
741
742
743
744
745
746 for an_item in self.__filtered_items:
747 if an_item['pk_encounter'] == encounter['pk_encounter']:
748 txt += self.get_item_output(an_item, left_margin)
749 return txt
750
752 """Dumps patient's historical in form of a tree of health issues
753 -> episodes
754 -> encounters
755 -> clinical items
756 """
757
758
759 self.__fetch_filtered_items()
760 emr = self.__patient.get_emr()
761
762
763 for an_item in self.__filtered_items:
764 self.__target.write(self.get_item_summary(an_item, 3))
765
766
767 h_issues = []
768 h_issues.extend(emr.get_health_issues(id_list = self.__constraints['issues']))
769
770 unlinked_episodes = emr.get_episodes(issues = [None])
771 if len(unlinked_episodes) > 0:
772 h_issues.insert(0, {'description':_('Unattributed episodes'), 'pk_health_issue':None})
773 for a_health_issue in h_issues:
774 self.__target.write('\n' + 3*' ' + 'Health Issue: ' + a_health_issue['description'] + '\n')
775 episodes = emr.get_episodes(id_list=self.__constraints['episodes'], issues = [a_health_issue['pk_health_issue']])
776 for an_episode in episodes:
777 self.__target.write('\n' + 6*' ' + 'Episode: ' + an_episode['description'] + '\n')
778 if a_health_issue['pk_health_issue'] is None:
779 issues = None
780 else:
781 issues = [a_health_issue['pk_health_issue']]
782 encounters = emr.get_encounters (
783 since = self.__constraints['since'],
784 until = self.__constraints['until'],
785 id_list = self.__constraints['encounters'],
786 episodes = [an_episode['pk_episode']],
787 issues = issues
788 )
789 for an_encounter in encounters:
790
791 self.lab_new_encounter = True
792 self.__target.write(
793 '\n %s %s: %s - %s (%s)\n' % (
794 _('Encounter'),
795 an_encounter['l10n_type'],
796 an_encounter['started'].strftime('%A, %Y-%m-%d %H:%M'),
797 an_encounter['last_affirmed'].strftime('%m-%d %H:%M'),
798 an_encounter['assessment_of_encounter']
799 )
800 )
801 self.__target.write(self.get_encounter_info(an_episode, an_encounter, 12))
802
804 """
805 Dumps in ASCII format patient's clinical record
806 """
807 emr = self.__patient.get_emr()
808 if emr is None:
809 _log.error('cannot get EMR text dump')
810 print(_(
811 'An error occurred while retrieving a text\n'
812 'dump of the EMR for the active patient.\n\n'
813 'Please check the log file for details.'
814 ))
815 return None
816 self.__target.write('\nOverview\n')
817 self.__target.write('--------\n')
818 self.__target.write("1) Allergy status (for details, see below):\n\n")
819 for allergy in emr.get_allergies():
820 self.__target.write(" " + allergy['descriptor'] + "\n\n")
821 self.__target.write("2) Vaccination status (* indicates booster):\n")
822
823 self.__target.write("\n3) Historical:\n\n")
824 self.dump_historical_tree()
825
826 try:
827 emr.cleanup()
828 except:
829 print "error cleaning up EMR"
830
832 """
833 Dumps patient stored medical documents
834
835 """
836 doc_folder = self.__patient.get_document_folder()
837
838 self.__target.write('\n4) Medical documents: (date) reference - type "comment"\n')
839 self.__target.write(' object - comment')
840
841 docs = doc_folder.get_documents()
842 for doc in docs:
843 self.__target.write('\n\n (%s) %s - %s "%s"' % (
844 doc['clin_when'].strftime('%Y-%m-%d'),
845 doc['ext_ref'],
846 doc['l10n_type'],
847 doc['comment'])
848 )
849 for part in doc.parts:
850 self.__target.write('\n %s - %s' % (
851 part['seq_idx'],
852 part['obj_comment'])
853 )
854 self.__target.write('\n\n')
855
857 """
858 Dumps in ASCII format some basic patient's demographic data
859 """
860 if self.__patient is None:
861 _log.error('cannot get Demographic export')
862 print(_(
863 'An error occurred while Demographic record export\n'
864 'Please check the log file for details.'
865 ))
866 return None
867
868 self.__target.write('\n\n\nDemographics')
869 self.__target.write('\n------------\n')
870 self.__target.write(' Id: %s \n' % self.__patient['pk_identity'])
871 cont = 0
872 for name in self.__patient.get_names():
873 if cont == 0:
874 self.__target.write(' Name (Active): %s, %s\n' % (name['firstnames'], name['lastnames']) )
875 else:
876 self.__target.write(' Name %s: %s, %s\n' % (cont, name['firstnames'], name['lastnames']))
877 cont += 1
878 self.__target.write(' Gender: %s\n' % self.__patient['gender'])
879 self.__target.write(' Title: %s\n' % self.__patient['title'])
880 self.__target.write(' Dob: %s\n' % self.__patient.get_formatted_dob(format = '%Y-%m-%d'))
881 self.__target.write(' Medical age: %s\n' % self.__patient.get_medical_age())
882
884 """
885 Dumps exporter filtering constraints
886 """
887 self.__first_constraint = True
888 if not self.__constraints['since'] is None:
889 self.dump_constraints_header()
890 self.__target.write('\nSince: %s' % self.__constraints['since'].strftime('%Y-%m-%d'))
891
892 if not self.__constraints['until'] is None:
893 self.dump_constraints_header()
894 self.__target.write('\nUntil: %s' % self.__constraints['until'].strftime('%Y-%m-%d'))
895
896 if not self.__constraints['encounters'] is None:
897 self.dump_constraints_header()
898 self.__target.write('\nEncounters: ')
899 for enc in self.__constraints['encounters']:
900 self.__target.write(str(enc) + ' ')
901
902 if not self.__constraints['episodes'] is None:
903 self.dump_constraints_header()
904 self.__target.write('\nEpisodes: ')
905 for epi in self.__constraints['episodes']:
906 self.__target.write(str(epi) + ' ')
907
908 if not self.__constraints['issues'] is None:
909 self.dump_constraints_header()
910 self.__target.write('\nIssues: ')
911 for iss in self.__constraints['issues']:
912 self.__target.write(str(iss) + ' ')
913
915 """
916 Dumps constraints header
917 """
918 if self.__first_constraint == True:
919 self.__target.write('\nClinical items dump constraints\n')
920 self.__target.write('-'*(len(head_txt)-2))
921 self.__first_constraint = False
922
924 """Exports patient EMR into a simple chronological journal.
925
926 Note that this export will emit u'' strings only.
927 """
930
931
932
934 """Export medical record into a file.
935
936 @type filename: None (creates filename by itself) or string
937 @type patient: None (use currently active patient) or <gmPerson.cIdentity> instance
938 """
939 if patient is None:
940 patient = gmPerson.gmCurrentPatient()
941 if not patient.connected:
942 raise ValueError('[%s].export_to_file(): no active patient' % self.__class__.__name__)
943
944 if filename is None:
945 filename = u'%s-%s-%s-%s.txt' % (
946 _('emr-journal'),
947 patient['lastnames'].replace(u' ', u'_'),
948 patient['firstnames'].replace(u' ', u'_'),
949 patient.get_formatted_dob(format = '%Y-%m-%d')
950 )
951 path = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'EMR', patient['dirname'], filename))
952
953 f = codecs.open(filename = filename, mode = 'w+b', encoding = 'utf8', errors = 'replace')
954 self.export(target = f, patient = patient)
955 f.close()
956 return filename
957
958
959
960 - def export(self, target=None, patient=None):
961 """
962 Export medical record into a Python object.
963
964 @type target: a python object supporting the write() API
965 @type patient: None (use currently active patient) or <gmPerson.cIdentity> instance
966 """
967 if patient is None:
968 patient = gmPerson.gmCurrentPatient()
969 if not patient.connected:
970 raise ValueError('[%s].export(): no active patient' % self.__class__.__name__)
971
972
973 txt = _('Chronological EMR Journal\n')
974 target.write(txt)
975 target.write(u'=' * (len(txt)-1))
976 target.write('\n')
977 target.write(_('Patient: %s (%s), No: %s\n') % (patient['description'], patient['gender'], patient['pk_identity']))
978 target.write(_('Born : %s, age: %s\n\n') % (
979 patient.get_formatted_dob(format = '%x', encoding = gmI18N.get_encoding()),
980 patient.get_medical_age()
981 ))
982 target.write(u'.-%10.10s---%9.9s-------%72.72s\n' % (u'-' * 10, u'-' * 9, u'-' * self.__part_len))
983 target.write(u'| %10.10s | %9.9s | | %s\n' % (_('Happened'), _('Doc'), _('Narrative')))
984 target.write(u'|-%10.10s---%9.9s-------%72.72s\n' % (u'-' * 10, u'-' * 9, u'-' * self.__part_len))
985
986
987 cmd = u"""
988 select
989 to_char(vemrj.clin_when, 'YYYY-MM-DD') as date,
990 vemrj.*,
991 (select rank from clin.soap_cat_ranks where soap_cat = vemrj.soap_cat) as scr,
992 to_char(vemrj.modified_when, 'YYYY-MM-DD HH24:MI') as date_modified
993 from clin.v_emr_journal vemrj
994 where pk_patient = %s
995 order by date, pk_episode, scr, src_table"""
996 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [patient['pk_identity']]}], get_col_idx = True)
997
998
999 prev_date = u''
1000 prev_doc = u''
1001 prev_soap = u''
1002 for row in rows:
1003
1004 if row['narrative'] is None:
1005 continue
1006
1007 txt = gmTools.wrap (
1008 text = row['narrative'].replace(u'\r', u'') + (u' (%s)' % row['date_modified']),
1009 width = self.__part_len
1010 ).split('\n')
1011
1012
1013 curr_doc = row['modified_by']
1014 if curr_doc != prev_doc:
1015 prev_doc = curr_doc
1016 else:
1017 curr_doc = u''
1018
1019
1020 curr_soap = row['soap_cat']
1021 if curr_soap != prev_soap:
1022 prev_soap = curr_soap
1023
1024
1025 curr_date = row['date']
1026 if curr_date != prev_date:
1027 prev_date = curr_date
1028 curr_doc = row['modified_by']
1029 prev_doc = curr_doc
1030 curr_soap = row['soap_cat']
1031 prev_soap = curr_soap
1032 else:
1033 curr_date = u''
1034
1035
1036 target.write(u'| %10.10s | %9.9s | %3.3s | %s\n' % (
1037 curr_date,
1038 curr_doc,
1039 gmClinNarrative.soap_cat2l10n[curr_soap],
1040 txt[0]
1041 ))
1042
1043
1044 if len(txt) == 1:
1045 continue
1046
1047 template = u'| %10.10s | %9.9s | %3.3s | %s\n'
1048 for part in txt[1:]:
1049 line = template % (u'', u'', u' ', part)
1050 target.write(line)
1051
1052
1053 target.write(u'`-%10.10s---%9.9s-------%72.72s\n\n' % (u'-' * 10, u'-' * 9, u'-' * self.__part_len))
1054 target.write(_('Exported: %s\n') % pyDT.datetime.now().strftime('%c').decode(gmI18N.get_encoding()))
1055
1056 return
1057
1059 """Export SOAP data per encounter into Medistar import format."""
1067
1068
1069
1070 - def export_to_file(self, filename=None, encounter=None, soap_cats=u'soap', export_to_import_file=False):
1071 if not self.__pat.connected:
1072 return (False, 'no active patient')
1073
1074 if filename is None:
1075 path = os.path.abspath(os.path.expanduser('~/gnumed/export'))
1076 filename = '%s-%s-%s-%s-%s.txt' % (
1077 os.path.join(path, 'Medistar-MD'),
1078 time.strftime('%Y-%m-%d',time.localtime()),
1079 self.__pat['lastnames'].replace(' ', '-'),
1080 self.__pat['firstnames'].replace(' ', '_'),
1081 self.__pat.get_formatted_dob(format = '%Y-%m-%d')
1082 )
1083
1084 f = codecs.open(filename = filename, mode = 'w+b', encoding = 'cp437', errors='replace')
1085 status = self.__export(target = f, encounter = encounter, soap_cats = soap_cats)
1086 f.close()
1087
1088 if export_to_import_file:
1089
1090 medistar_found = False
1091 for drive in u'cdefghijklmnopqrstuvwxyz':
1092 path = drive + ':\\medistar\\inst'
1093 if not os.path.isdir(path):
1094 continue
1095 try:
1096 import_fname = path + '\\soap.txt'
1097 open(import_fname, mode = 'w+b').close()
1098 _log.debug('exporting narrative to [%s] for Medistar import', import_fname)
1099 shutil.copyfile(filename, import_fname)
1100 medistar_found = True
1101 except IOError:
1102 continue
1103
1104 if not medistar_found:
1105 _log.debug('no Medistar installation found (no <LW>:\\medistar\\inst\\)')
1106
1107 return (status, filename)
1108
1109 - def export(self, target, encounter=None, soap_cats=u'soap'):
1110 return self.__export(target, encounter = encounter, soap_cats = soap_cats)
1111
1112
1113
1114 - def __export(self, target=None, encounter=None, soap_cats=u'soap'):
1115
1116 cmd = u"select narrative from clin.v_emr_journal where pk_patient=%s and pk_encounter=%s and soap_cat=%s"
1117 for soap_cat in soap_cats:
1118 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': (self.__pat['pk_identity'], encounter['pk_encounter'], soap_cat)}])
1119 target.write('*MD%s*\r\n' % gmClinNarrative.soap_cat2l10n[soap_cat])
1120 for row in rows:
1121 text = row[0]
1122 if text is None:
1123 continue
1124 target.write('%s\r\n' % gmTools.wrap (
1125 text = text,
1126 width = 64,
1127 eol = u'\r\n'
1128 ))
1129 return True
1130
1131
1132
1134 """
1135 Prints application usage options to stdout.
1136 """
1137 print 'usage: python gmPatientExporter [--fileout=<outputfilename>] [--conf-file=<file>] [--text-domain=<textdomain>]'
1138 sys.exit(0)
1139
1174
1175
1176
1177 if __name__ == "__main__":
1178 gmI18N.activate_locale()
1179 gmI18N.install_domain()
1180
1181
1200
1201 print "\n\nGNUmed ASCII EMR Export"
1202 print "======================="
1203
1204
1205 export_journal()
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656