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