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, gmDemographicRecord, gmClinNarrative, gmPersonSearch
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
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 for part in doc.parts:
838 self.__target.write('\n %s - %s' % (
839 part['seq_idx'],
840 part['obj_comment'])
841 )
842 self.__target.write('\n\n')
843
845 """
846 Dumps in ASCII format some basic patient's demographic data
847 """
848 if self.__patient is None:
849 _log.error('cannot get Demographic export')
850 print(_(
851 'An error occurred while Demographic record export\n'
852 'Please check the log file for details.'
853 ))
854 return None
855
856 self.__target.write('\n\n\nDemographics')
857 self.__target.write('\n------------\n')
858 self.__target.write(' Id: %s \n' % self.__patient['pk_identity'])
859 cont = 0
860 for name in self.__patient.get_names():
861 if cont == 0:
862 self.__target.write(' Name (Active): %s, %s\n' % (name['firstnames'], name['lastnames']) )
863 else:
864 self.__target.write(' Name %s: %s, %s\n' % (cont, name['firstnames'], name['lastnames']))
865 cont += 1
866 self.__target.write(' Gender: %s\n' % self.__patient['gender'])
867 self.__target.write(' Title: %s\n' % self.__patient['title'])
868 self.__target.write(' Dob: %s\n' % self.__patient.get_formatted_dob(format = '%Y-%m-%d'))
869 self.__target.write(' Medical age: %s\n' % self.__patient.get_medical_age())
870
872 """
873 Dumps exporter filtering constraints
874 """
875 self.__first_constraint = True
876 if not self.__constraints['since'] is None:
877 self.dump_constraints_header()
878 self.__target.write('\nSince: %s' % self.__constraints['since'].strftime('%Y-%m-%d'))
879
880 if not self.__constraints['until'] is None:
881 self.dump_constraints_header()
882 self.__target.write('\nUntil: %s' % self.__constraints['until'].strftime('%Y-%m-%d'))
883
884 if not self.__constraints['encounters'] is None:
885 self.dump_constraints_header()
886 self.__target.write('\nEncounters: ')
887 for enc in self.__constraints['encounters']:
888 self.__target.write(str(enc) + ' ')
889
890 if not self.__constraints['episodes'] is None:
891 self.dump_constraints_header()
892 self.__target.write('\nEpisodes: ')
893 for epi in self.__constraints['episodes']:
894 self.__target.write(str(epi) + ' ')
895
896 if not self.__constraints['issues'] is None:
897 self.dump_constraints_header()
898 self.__target.write('\nIssues: ')
899 for iss in self.__constraints['issues']:
900 self.__target.write(str(iss) + ' ')
901
903 """
904 Dumps constraints header
905 """
906 if self.__first_constraint == True:
907 self.__target.write('\nClinical items dump constraints\n')
908 self.__target.write('-'*(len(head_txt)-2))
909 self.__first_constraint = False
910
912 """Exports patient EMR into a simple chronological journal.
913
914 Note that this export will emit u'' strings only.
915 """
918
919
920
922 """Export medical record into a file.
923
924 @type filename: None (creates filename by itself) or string
925 @type patient: None (use currently active patient) or <gmPerson.cIdentity> instance
926 """
927 if patient is None:
928 patient = gmPerson.gmCurrentPatient()
929 if not patient.connected:
930 raise ValueError('[%s].export_to_file(): no active patient' % self.__class__.__name__)
931
932 if filename is None:
933 filename = u'%s-%s-%s-%s.txt' % (
934 _('emr-journal'),
935 patient['lastnames'].replace(u' ', u'_'),
936 patient['firstnames'].replace(u' ', u'_'),
937 patient.get_formatted_dob(format = '%Y-%m-%d')
938 )
939 path = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'EMR', patient['dirname'], filename))
940
941 f = codecs.open(filename = filename, mode = 'w+b', encoding = 'utf8', errors = 'replace')
942 self.export(target = f, patient = patient)
943 f.close()
944 return filename
945
946
947
948 - def export(self, target=None, patient=None):
949 """
950 Export medical record into a Python object.
951
952 @type target: a python object supporting the write() API
953 @type patient: None (use currently active patient) or <gmPerson.cIdentity> instance
954 """
955 if patient is None:
956 patient = gmPerson.gmCurrentPatient()
957 if not patient.connected:
958 raise ValueError('[%s].export(): no active patient' % self.__class__.__name__)
959
960
961 txt = _('Chronological EMR Journal\n')
962 target.write(txt)
963 target.write(u'=' * (len(txt)-1))
964 target.write('\n')
965 target.write(_('Patient: %s (%s), No: %s\n') % (patient['description'], patient['gender'], patient['pk_identity']))
966 target.write(_('Born : %s, age: %s\n\n') % (
967 patient.get_formatted_dob(format = '%x', encoding = gmI18N.get_encoding()),
968 patient.get_medical_age()
969 ))
970 target.write(u'.-%10.10s---%9.9s-------%72.72s\n' % (u'-' * 10, u'-' * 9, u'-' * self.__part_len))
971 target.write(u'| %10.10s | %9.9s | | %s\n' % (_('Happened'), _('Doc'), _('Narrative')))
972 target.write(u'|-%10.10s---%9.9s-------%72.72s\n' % (u'-' * 10, u'-' * 9, u'-' * self.__part_len))
973
974
975 cmd = u"""
976 select
977 to_char(vemrj.clin_when, 'YYYY-MM-DD') as date,
978 vemrj.*,
979 (select rank from clin.soap_cat_ranks where soap_cat = vemrj.soap_cat) as scr,
980 to_char(vemrj.modified_when, 'YYYY-MM-DD HH24:MI') as date_modified
981 from clin.v_emr_journal vemrj
982 where pk_patient = %s
983 order by date, pk_episode, scr, src_table"""
984 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [patient['pk_identity']]}], get_col_idx = True)
985
986
987 prev_date = u''
988 prev_doc = u''
989 prev_soap = u''
990 for row in rows:
991
992 if row['narrative'] is None:
993 continue
994
995 txt = gmTools.wrap (
996 text = row['narrative'].replace(u'\r', u'') + (u' (%s)' % row['date_modified']),
997 width = self.__part_len
998 ).split('\n')
999
1000
1001 curr_doc = row['modified_by']
1002 if curr_doc != prev_doc:
1003 prev_doc = curr_doc
1004 else:
1005 curr_doc = u''
1006
1007
1008 curr_soap = row['soap_cat']
1009 if curr_soap != prev_soap:
1010 prev_soap = curr_soap
1011
1012
1013 curr_date = row['date']
1014 if curr_date != prev_date:
1015 prev_date = curr_date
1016 curr_doc = row['modified_by']
1017 prev_doc = curr_doc
1018 curr_soap = row['soap_cat']
1019 prev_soap = curr_soap
1020 else:
1021 curr_date = u''
1022
1023
1024 target.write(u'| %10.10s | %9.9s | %3.3s | %s\n' % (
1025 curr_date,
1026 curr_doc,
1027 gmClinNarrative.soap_cat2l10n[curr_soap],
1028 txt[0]
1029 ))
1030
1031
1032 if len(txt) == 1:
1033 continue
1034
1035 template = u'| %10.10s | %9.9s | %3.3s | %s\n'
1036 for part in txt[1:]:
1037 line = template % (u'', u'', u' ', part)
1038 target.write(line)
1039
1040
1041 target.write(u'`-%10.10s---%9.9s-------%72.72s\n\n' % (u'-' * 10, u'-' * 9, u'-' * self.__part_len))
1042 target.write(_('Exported: %s\n') % pyDT.datetime.now().strftime('%c').decode(gmI18N.get_encoding()))
1043
1044 return
1045
1047 """Export SOAP data per encounter into Medistar import format."""
1055
1056
1057
1058 - def export_to_file(self, filename=None, encounter=None, soap_cats=u'soap', export_to_import_file=False):
1059 if not self.__pat.connected:
1060 return (False, 'no active patient')
1061
1062 if filename is None:
1063 path = os.path.abspath(os.path.expanduser('~/gnumed/export'))
1064 filename = '%s-%s-%s-%s-%s.txt' % (
1065 os.path.join(path, 'Medistar-MD'),
1066 time.strftime('%Y-%m-%d',time.localtime()),
1067 self.__pat['lastnames'].replace(' ', '-'),
1068 self.__pat['firstnames'].replace(' ', '_'),
1069 self.__pat.get_formatted_dob(format = '%Y-%m-%d')
1070 )
1071
1072 f = codecs.open(filename = filename, mode = 'w+b', encoding = 'cp437', errors='replace')
1073 status = self.__export(target = f, encounter = encounter, soap_cats = soap_cats)
1074 f.close()
1075
1076 if export_to_import_file:
1077
1078 medistar_found = False
1079 for drive in u'cdefghijklmnopqrstuvwxyz':
1080 path = drive + ':\\medistar\\inst'
1081 if not os.path.isdir(path):
1082 continue
1083 try:
1084 import_fname = path + '\\soap.txt'
1085 open(import_fname, mode = 'w+b').close()
1086 _log.debug('exporting narrative to [%s] for Medistar import', import_fname)
1087 shutil.copyfile(filename, import_fname)
1088 medistar_found = True
1089 except IOError:
1090 continue
1091
1092 if not medistar_found:
1093 _log.debug('no Medistar installation found (no <LW>:\\medistar\\inst\\)')
1094
1095 return (status, filename)
1096
1097 - def export(self, target, encounter=None, soap_cats=u'soap'):
1098 return self.__export(target, encounter = encounter, soap_cats = soap_cats)
1099
1100
1101
1102 - def __export(self, target=None, encounter=None, soap_cats=u'soap'):
1103
1104 cmd = u"select narrative from clin.v_emr_journal where pk_patient=%s and pk_encounter=%s and soap_cat=%s"
1105 for soap_cat in soap_cats:
1106 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': (self.__pat['pk_identity'], encounter['pk_encounter'], soap_cat)}])
1107 target.write('*MD%s*\r\n' % gmClinNarrative.soap_cat2l10n[soap_cat])
1108 for row in rows:
1109 text = row[0]
1110 if text is None:
1111 continue
1112 target.write('%s\r\n' % gmTools.wrap (
1113 text = text,
1114 width = 64,
1115 eol = u'\r\n'
1116 ))
1117 return True
1118
1119
1120
1122 """
1123 Prints application usage options to stdout.
1124 """
1125 print 'usage: python gmPatientExporter [--fileout=<outputfilename>] [--conf-file=<file>] [--text-domain=<textdomain>]'
1126 sys.exit(0)
1127
1162
1163
1164
1165 if __name__ == "__main__":
1166 gmI18N.activate_locale()
1167 gmI18N.install_domain()
1168
1169
1188
1189 print "\n\nGNUmed ASCII EMR Export"
1190 print "======================="
1191
1192
1193 export_journal()
1194
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