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