1 """Widgets dealing with patient demographics."""
2
3
4
5 __version__ = "$Revision: 1.175 $"
6 __author__ = "R.Terry, SJ Tan, I Haywood, Carlos Moro <cfmoro1976@yahoo.es>"
7 __license__ = 'GPL (details at http://www.gnu.org)'
8
9
10 import time, string, sys, os, datetime as pyDT, csv, codecs, re as regex, psycopg2, logging
11
12
13 import wx
14 import wx.wizard
15
16
17
18 if __name__ == '__main__':
19 sys.path.insert(0, '../../')
20 from Gnumed.pycommon import gmDispatcher, gmI18N, gmMatchProvider, gmPG2, gmTools, gmCfg
21 from Gnumed.pycommon import gmDateTime, gmShellAPI
22 from Gnumed.business import gmDemographicRecord, gmPerson, gmSurgery
23 from Gnumed.wxpython import gmPlugin, gmPhraseWheel, gmGuiHelpers, gmDateTimeInput
24 from Gnumed.wxpython import gmRegetMixin, gmDataMiningWidgets, gmListWidgets, gmEditArea
25 from Gnumed.wxpython import gmAuthWidgets, gmCfgWidgets
26 from Gnumed.wxGladeWidgets import wxgGenericAddressEditAreaPnl, wxgPersonContactsManagerPnl, wxgPersonIdentityManagerPnl
27 from Gnumed.wxGladeWidgets import wxgNameGenderDOBEditAreaPnl, wxgCommChannelEditAreaPnl, wxgExternalIDEditAreaPnl
28
29
30
31 _log = logging.getLogger('gm.ui')
32
33
34 try:
35 _('dummy-no-need-to-translate-but-make-epydoc-happy')
36 except NameError:
37 _ = lambda x:x
38
39
40
41
58
60
62
63 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
64
65 context = {
66 u'ctxt_zip': {
67 u'where_part': u'and zip ilike %(zip)s',
68 u'placeholder': u'zip'
69 }
70 }
71 query = u"""
72 select code, name from (
73 select distinct on (code, name) code, (name || ' (' || code || ')') as name, rank from (
74
75 -- localized to user
76
77 select
78 code_country as code, l10n_country as name, 1 as rank
79 from dem.v_zip2data
80 where
81 l10n_country %(fragment_condition)s
82 %(ctxt_zip)s
83
84 union all
85
86 select
87 code as code, _(name) as name, 2 as rank
88 from dem.country
89 where
90 _(name) %(fragment_condition)s
91
92 union all
93
94 -- non-localized
95
96 select
97 code_country as code, country as name, 3 as rank
98 from dem.v_zip2data
99 where
100 country %(fragment_condition)s
101 %(ctxt_zip)s
102
103 union all
104
105 select
106 code as code, name as name, 4 as rank
107 from dem.country
108 where
109 name %(fragment_condition)s
110
111 union all
112
113 -- abbreviation
114
115 select
116 code as code, name as name, 5 as rank
117 from dem.country
118 where
119 code %(fragment_condition)s
120
121 ) as q2
122 ) as q1 order by rank, name limit 25"""
123 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
124 mp.setThresholds(2, 5, 9)
125 self.matcher = mp
126
127 self.unset_context(context = u'zip')
128 self.SetToolTipString(_('Type or select a country.'))
129 self.capitalisation_mode = gmTools.CAPS_FIRST
130 self.selection_only = True
131
132
133
134
151
153 ea = cProvinceEAPnl(parent = parent, id = -1, province = province)
154 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = (province is not None))
155 dlg.SetTitle(gmTools.coalesce(province, _('Adding province'), _('Editing province')))
156 result = dlg.ShowModal()
157 dlg.Destroy()
158 return (result == wx.ID_OK)
159
161
162 msg = _(
163 'Are you sure you want to delete this province ?\n'
164 '\n'
165 'Deletion will only work if this province is not\n'
166 'yet in use in any patient addresses.'
167 )
168
169 tt = _(
170 'Also delete any towns/cities/villages known\n'
171 'to be situated in this state as long as\n'
172 'no patients are recorded to live there.'
173 )
174
175 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
176 parent,
177 -1,
178 caption = _('Deleting province'),
179 question = msg,
180 show_checkbox = True,
181 checkbox_msg = _('delete related townships'),
182 checkbox_tooltip = tt,
183 button_defs = [
184 {'label': _('Yes, delete'), 'tooltip': _('Delete province and possibly related townships.'), 'default': False},
185 {'label': _('No'), 'tooltip': _('No, do NOT delete anything.'), 'default': True}
186 ]
187 )
188
189 decision = dlg.ShowModal()
190 if decision != wx.ID_YES:
191 dlg.Destroy()
192 return False
193
194 include_urbs = dlg.checkbox_is_checked()
195 dlg.Destroy()
196
197 return gmDemographicRecord.delete_province(province = province, delete_urbs = include_urbs)
198
200
201 if parent is None:
202 parent = wx.GetApp().GetTopWindow()
203
204
205 def delete(province=None):
206 return delete_province(parent = parent, province = province['pk_state'])
207
208 def edit(province=None):
209 return edit_province(parent = parent, province = province)
210
211 def refresh(lctrl):
212 wx.BeginBusyCursor()
213 provinces = gmDemographicRecord.get_provinces()
214 lctrl.set_string_items([ (p['l10n_country'], p['l10n_state']) for p in provinces ])
215 lctrl.set_data(provinces)
216 wx.EndBusyCursor()
217
218 msg = _(
219 '\n'
220 'This list shows the provinces known to GNUmed.\n'
221 '\n'
222 'In your jurisdiction "province" may correspond to either of "state",\n'
223 '"county", "region", "territory", or some such term.\n'
224 '\n'
225 'Select the province you want to edit !\n'
226 )
227
228 gmListWidgets.get_choices_from_list (
229 parent = parent,
230 msg = msg,
231 caption = _('Editing provinces ...'),
232 columns = [_('Country'), _('Province')],
233 single_selection = True,
234 new_callback = edit,
235
236 delete_callback = delete,
237 refresh_callback = refresh
238 )
239
241
243
244 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
245
246 context = {
247 u'ctxt_country_name': {
248 u'where_part': u'and l10n_country ilike %(country_name)s or country ilike %(country_name)s',
249 u'placeholder': u'country_name'
250 },
251 u'ctxt_zip': {
252 u'where_part': u'and zip ilike %(zip)s',
253 u'placeholder': u'zip'
254 },
255 u'ctxt_country_code': {
256 u'where_part': u'and country in (select code from dem.country where _(name) ilike %(country_name)s or name ilike %(country_name)s)',
257 u'placeholder': u'country_name'
258 }
259 }
260
261 query = u"""
262 select code, name from (
263 select distinct on (name) code, name, rank from (
264 -- 1: find states based on name, context: zip and country name
265 select
266 code_state as code, state as name, 1 as rank
267 from dem.v_zip2data
268 where
269 state %(fragment_condition)s
270 %(ctxt_country_name)s
271 %(ctxt_zip)s
272
273 union all
274
275 -- 2: find states based on code, context: zip and country name
276 select
277 code_state as code, state as name, 2 as rank
278 from dem.v_zip2data
279 where
280 code_state %(fragment_condition)s
281 %(ctxt_country_name)s
282 %(ctxt_zip)s
283
284 union all
285
286 -- 3: find states based on name, context: country
287 select
288 code as code, name as name, 3 as rank
289 from dem.state
290 where
291 name %(fragment_condition)s
292 %(ctxt_country_code)s
293
294 union all
295
296 -- 4: find states based on code, context: country
297 select
298 code as code, name as name, 3 as rank
299 from dem.state
300 where
301 code %(fragment_condition)s
302 %(ctxt_country_code)s
303
304 ) as q2
305 ) as q1 order by rank, name limit 50"""
306
307 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
308 mp.setThresholds(2, 5, 6)
309 mp.word_separators = u'[ \t]+'
310 self.matcher = mp
311
312 self.unset_context(context = u'zip')
313 self.unset_context(context = u'country_name')
314 self.SetToolTipString(_('Type or select a state/region/province/territory.'))
315 self.capitalisation_mode = gmTools.CAPS_FIRST
316 self.selection_only = True
317
318 from Gnumed.wxGladeWidgets import wxgProvinceEAPnl
319
320 -class cProvinceEAPnl(wxgProvinceEAPnl.wxgProvinceEAPnl, gmEditArea.cGenericEditAreaMixin):
321
339
341 self._PRW_province.selection_only = False
342
343
344
372
388
390
391
392
393
394
395
396
397 return True
398
405
407 self._PRW_province.SetText(self.data['l10n_state'], self.data['code_state'])
408 self._TCTRL_code.SetValue(self.data['code_state'])
409 self._PRW_country.SetText(self.data['l10n_country'], self.data['code_country'])
410
411 self._PRW_province.SetFocus()
412
419
420
422
424
425 kwargs['message'] = _("Today's KOrganizer appointments ...")
426 kwargs['button_defs'] = [
427 {'label': _('Reload'), 'tooltip': _('Reload appointments from KOrganizer')},
428 {'label': u''},
429 {'label': u''},
430 {'label': u''},
431 {'label': u'KOrganizer', 'tooltip': _('Launch KOrganizer')}
432 ]
433 gmDataMiningWidgets.cPatientListingPnl.__init__(self, *args, **kwargs)
434
435 self.fname = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp', 'korganizer2gnumed.csv'))
436 self.reload_cmd = 'konsolekalendar --view --export-type csv --export-file %s' % self.fname
437
438
442
452
454 try: os.remove(self.fname)
455 except OSError: pass
456 gmShellAPI.run_command_in_shell(command=self.reload_cmd, blocking=True)
457 try:
458 csv_file = codecs.open(self.fname , mode = 'rU', encoding = 'utf8', errors = 'replace')
459 except IOError:
460 gmDispatcher.send(signal = u'statustext', msg = _('Cannot access KOrganizer transfer file [%s]') % self.fname, beep = True)
461 return
462
463 csv_lines = gmTools.unicode_csv_reader (
464 csv_file,
465 delimiter = ','
466 )
467
468 self._LCTRL_items.set_columns ([
469 _('Place'),
470 _('Start'),
471 u'',
472 u'',
473 _('Patient'),
474 _('Comment')
475 ])
476 items = []
477 data = []
478 for line in csv_lines:
479 items.append([line[5], line[0], line[1], line[3], line[4], line[6]])
480 data.append([line[4], line[7]])
481
482 self._LCTRL_items.set_string_items(items = items)
483 self._LCTRL_items.set_column_widths()
484 self._LCTRL_items.set_data(data = data)
485 self._LCTRL_items.patient_key = 0
486
487
488
491
493
494 pat = gmPerson.gmCurrentPatient()
495 curr_jobs = pat.get_occupations()
496 if len(curr_jobs) > 0:
497 old_job = curr_jobs[0]['l10n_occupation']
498 update = curr_jobs[0]['modified_when'].strftime('%m/%Y')
499 else:
500 old_job = u''
501 update = u''
502
503 msg = _(
504 'Please enter the primary occupation of the patient.\n'
505 '\n'
506 'Currently recorded:\n'
507 '\n'
508 ' %s (last updated %s)'
509 ) % (old_job, update)
510
511 new_job = wx.GetTextFromUser (
512 message = msg,
513 caption = _('Editing primary occupation'),
514 default_value = old_job,
515 parent = None
516 )
517 if new_job.strip() == u'':
518 return
519
520 for job in curr_jobs:
521
522 if job['l10n_occupation'] != new_job:
523 pat.unlink_occupation(occupation = job['l10n_occupation'])
524
525 pat.link_occupation(occupation = new_job)
526
528
529 go_ahead = gmGuiHelpers.gm_show_question (
530 _('Are you sure you really, positively want\n'
531 'to disable the following person ?\n'
532 '\n'
533 ' %s %s %s\n'
534 ' born %s\n'
535 '\n'
536 '%s\n'
537 ) % (
538 identity['firstnames'],
539 identity['lastnames'],
540 identity['gender'],
541 identity['dob'],
542 gmTools.bool2subst (
543 identity.is_patient,
544 _('This patient DID receive care.'),
545 _('This person did NOT receive care.')
546 )
547 ),
548 _('Disabling person')
549 )
550 if not go_ahead:
551 return True
552
553
554 conn = gmAuthWidgets.get_dbowner_connection (
555 procedure = _('Disabling patient')
556 )
557
558 if conn is False:
559 return True
560
561 if conn is None:
562 return False
563
564
565 gmPG2.run_rw_queries(queries = [{'cmd': u"update dem.identity set deleted=True where pk=%s", 'args': [identity['pk_identity']]}])
566
567 return True
568
569
570
572 """A list for managing a person's addresses.
573
574 Does NOT act on/listen to the current patient.
575 """
593
594
595
596 - def refresh(self, *args, **kwargs):
597 if self.__identity is None:
598 self._LCTRL_items.set_string_items()
599 return
600
601 adrs = self.__identity.get_addresses()
602 self._LCTRL_items.set_string_items (
603 items = [ [
604 a['l10n_address_type'],
605 a['street'],
606 gmTools.coalesce(a['notes_street'], u''),
607 a['number'],
608 gmTools.coalesce(a['subunit'], u''),
609 a['postcode'],
610 a['urb'],
611 gmTools.coalesce(a['suburb'], u''),
612 a['l10n_state'],
613 a['l10n_country'],
614 gmTools.coalesce(a['notes_subunit'], u'')
615 ] for a in adrs
616 ]
617 )
618 self._LCTRL_items.set_column_widths()
619 self._LCTRL_items.set_data(data = adrs)
620
621
622
624 self._LCTRL_items.SetToolTipString(_('List of known addresses.'))
625 self._LCTRL_items.set_columns(columns = [
626 _('Type'),
627 _('Street'),
628 _('Street info'),
629 _('Number'),
630 _('Subunit'),
631 _('Postal code'),
632 _('Place'),
633 _('Suburb'),
634 _('Region'),
635 _('Country'),
636 _('Comment')
637 ])
638
647
649 ea = cAddressEditAreaPnl(self, -1, address = address)
650 ea.identity = self.__identity
651 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
652 dlg.SetTitle(_('Editing address'))
653 if dlg.ShowModal() == wx.ID_OK:
654
655
656 if ea.address['pk_address'] != address['pk_address']:
657 self.__identity.unlink_address(address = address)
658 return True
659 return False
660
662 go_ahead = gmGuiHelpers.gm_show_question (
663 _( 'Are you sure you want to remove this\n'
664 "address from the patient's addresses ?\n"
665 '\n'
666 'The address itself will not be deleted\n'
667 'but it will no longer be associated with\n'
668 'this patient.'
669 ),
670 _('Removing address')
671 )
672 if not go_ahead:
673 return False
674 self.__identity.unlink_address(address = address)
675 return True
676
677
678
680 return self.__identity
681
685
686 identity = property(_get_identity, _set_identity)
687
720
722 """An edit area for editing/creating an address.
723
724 Does NOT act on/listen to the current patient.
725 """
727 try:
728 self.address = kwargs['address']
729 del kwargs['address']
730 except KeyError:
731 self.address = None
732
733 wxgGenericAddressEditAreaPnl.wxgGenericAddressEditAreaPnl.__init__(self, *args, **kwargs)
734
735 self.identity = None
736
737 self.__register_interests()
738 self.refresh()
739
740
741
742 - def refresh(self, address = None):
743 if address is not None:
744 self.address = address
745
746 if self.address is not None:
747 self._PRW_type.SetText(self.address['l10n_address_type'])
748 self._PRW_zip.SetText(self.address['postcode'])
749 self._PRW_street.SetText(self.address['street'], data = self.address['street'])
750 self._TCTRL_notes_street.SetValue(gmTools.coalesce(self.address['notes_street'], ''))
751 self._TCTRL_number.SetValue(self.address['number'])
752 self._TCTRL_subunit.SetValue(gmTools.coalesce(self.address['subunit'], ''))
753 self._PRW_suburb.SetText(gmTools.coalesce(self.address['suburb'], ''))
754 self._PRW_urb.SetText(self.address['urb'], data = self.address['urb'])
755 self._PRW_state.SetText(self.address['l10n_state'], data = self.address['code_state'])
756 self._PRW_country.SetText(self.address['l10n_country'], data = self.address['code_country'])
757 self._TCTRL_notes_subunit.SetValue(gmTools.coalesce(self.address['notes_subunit'], ''))
758
759
760
761
763 """Links address to patient, creating new address if necessary"""
764
765 if not self.__valid_for_save():
766 return False
767
768
769 try:
770 adr = self.identity.link_address (
771 number = self._TCTRL_number.GetValue().strip(),
772 street = self._PRW_street.GetValue().strip(),
773 postcode = self._PRW_zip.GetValue().strip(),
774 urb = self._PRW_urb.GetValue().strip(),
775 state = self._PRW_state.GetData(),
776 country = self._PRW_country.GetData(),
777 subunit = gmTools.none_if(self._TCTRL_subunit.GetValue().strip(), u''),
778 suburb = gmTools.none_if(self._PRW_suburb.GetValue().strip(), u''),
779 id_type = self._PRW_type.GetData()
780 )
781 except:
782 _log.exception('cannot save address')
783 gmGuiHelpers.gm_show_error (
784 _('Cannot save address.\n\n'
785 'Does the state [%s]\n'
786 'exist in country [%s] ?'
787 ) % (
788 self._PRW_state.GetValue().strip(),
789 self._PRW_country.GetValue().strip()
790 ),
791 _('Saving address')
792 )
793 return False
794
795 notes = self._TCTRL_notes_street.GetValue().strip()
796 if notes != u'':
797 adr['notes_street'] = notes
798 notes = self._TCTRL_notes_subunit.GetValue().strip()
799 if notes != u'':
800 adr['notes_subunit'] = notes
801 adr.save_payload()
802
803 self.address = adr
804
805 return True
806
807
808
812
814 """Set the street, town, state and country according to entered zip code."""
815 zip_code = self._PRW_zip.GetValue()
816 if zip_code.strip() == u'':
817 self._PRW_street.unset_context(context = u'zip')
818 self._PRW_urb.unset_context(context = u'zip')
819 self._PRW_state.unset_context(context = u'zip')
820 self._PRW_country.unset_context(context = u'zip')
821 else:
822 self._PRW_street.set_context(context = u'zip', val = zip_code)
823 self._PRW_urb.set_context(context = u'zip', val = zip_code)
824 self._PRW_state.set_context(context = u'zip', val = zip_code)
825 self._PRW_country.set_context(context = u'zip', val = zip_code)
826
828 """Set the states according to entered country."""
829 country = self._PRW_country.GetData()
830 if country is None:
831 self._PRW_state.unset_context(context = 'country')
832 else:
833 self._PRW_state.set_context(context = 'country', val = country)
834
835
836
838
839
840 is_any_field_filled = False
841
842 required_fields = (
843 self._PRW_type,
844 self._PRW_zip,
845 self._PRW_street,
846 self._TCTRL_number,
847 self._PRW_urb
848 )
849 for field in required_fields:
850 if len(field.GetValue().strip()) == 0:
851 if is_any_field_filled:
852 field.SetBackgroundColour('pink')
853 field.SetFocus()
854 field.Refresh()
855 gmGuiHelpers.gm_show_error (
856 _('Address details must be filled in completely or not at all.'),
857 _('Saving contact data')
858 )
859 return False
860 else:
861 is_any_field_filled = True
862 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
863 field.Refresh()
864
865 required_fields = (
866 self._PRW_state,
867 self._PRW_country
868 )
869 for field in required_fields:
870 if field.GetData() is None:
871 if is_any_field_filled:
872 field.SetBackgroundColour('pink')
873 field.SetFocus()
874 field.Refresh()
875 gmGuiHelpers.gm_show_error (
876 _('Address details must be filled in completely or not at all.'),
877 _('Saving contact data')
878 )
879 return False
880 else:
881 is_any_field_filled = True
882 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
883 field.Refresh()
884
885 return True
886
888
890
891 query = u"""
892 select * from (
893 (select
894 pk_address,
895 (street || ' ' || number || coalesce(' (' || subunit || ')', '') || ', '
896 || urb || coalesce(' (' || suburb || ')', '') || ', '
897 || postcode
898 || coalesce(', ' || notes_street, '')
899 || coalesce(', ' || notes_subunit, '')
900 ) as address
901 from
902 dem.v_address
903 where
904 street %(fragment_condition)s
905
906 ) union (
907
908 select
909 pk_address,
910 (street || ' ' || number || coalesce(' (' || subunit || ')', '') || ', '
911 || urb || coalesce(' (' || suburb || ')', '') || ', '
912 || postcode
913 || coalesce(', ' || notes_street, '')
914 || coalesce(', ' || notes_subunit, '')
915 ) as address
916 from
917 dem.v_address
918 where
919 postcode_street %(fragment_condition)s
920
921 ) union (
922
923 select
924 pk_address,
925 (street || ' ' || number || coalesce(' (' || subunit || ')', '') || ', '
926 || urb || coalesce(' (' || suburb || ')', '') || ', '
927 || postcode
928 || coalesce(', ' || notes_street, '')
929 || coalesce(', ' || notes_subunit, '')
930 ) as address
931 from
932 dem.v_address
933 where
934 postcode_urb %(fragment_condition)s
935 )
936 ) as union_result
937 order by union_result.address limit 50"""
938
939 gmMatchProvider.cMatchProvider_SQL2.__init__(self, queries = query)
940
941 self.setThresholds(2, 4, 6)
942
943
944
946
960
962
963 pk = self.GetData()
964
965 if pk is None:
966 self.__address = None
967 return None
968
969 if self.__address is None:
970 self.__old_pk = pk
971 self.__address = gmDemographicRecord.cAddress(aPK_obj = pk)
972 else:
973 if pk != self.__old_pk:
974 self.__old_pk = pk
975 self.__address = gmDemographicRecord.cAddress(aPK_obj = pk)
976
977 return self.__address
978
980
982
983 query = u"""
984 select id, type from ((
985 select id, _(name) as type, 1 as rank
986 from dem.address_type
987 where _(name) %(fragment_condition)s
988 ) union (
989 select id, name as type, 2 as rank
990 from dem.address_type
991 where name %(fragment_condition)s
992 )) as ur
993 order by
994 ur.rank, ur.type
995 """
996 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
997 mp.setThresholds(1, 2, 4)
998 mp.word_separators = u'[ \t]+'
999 gmPhraseWheel.cPhraseWheel.__init__ (
1000 self,
1001 *args,
1002 **kwargs
1003 )
1004 self.matcher = mp
1005 self.SetToolTipString(_('Select the type of address.'))
1006
1007 self.selection_only = True
1008
1009
1010
1011
1012
1013
1014
1016
1018
1019 query = u"""
1020 (select distinct postcode, postcode from dem.street where postcode %(fragment_condition)s limit 20)
1021 union
1022 (select distinct postcode, postcode from dem.urb where postcode %(fragment_condition)s limit 20)"""
1023 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1024 mp.setThresholds(2, 3, 15)
1025 gmPhraseWheel.cPhraseWheel.__init__ (
1026 self,
1027 *args,
1028 **kwargs
1029 )
1030 self.SetToolTipString(_("Type or select a zip code (postcode)."))
1031 self.matcher = mp
1032
1034
1036 context = {
1037 u'ctxt_zip': {
1038 u'where_part': u'and zip ilike %(zip)s',
1039 u'placeholder': u'zip'
1040 }
1041 }
1042 query = u"""
1043 select s1, s2 from (
1044 select s1, s2, rank from (
1045 select distinct on (street)
1046 street as s1, street as s2, 1 as rank
1047 from dem.v_zip2data
1048 where
1049 street %(fragment_condition)s
1050 %(ctxt_zip)s
1051
1052 union all
1053
1054 select distinct on (name)
1055 name as s1, name as s2, 2 as rank
1056 from dem.street
1057 where
1058 name %(fragment_condition)s
1059
1060 ) as q2
1061 ) as q1 order by rank, s2 limit 50"""
1062 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
1063 mp.setThresholds(3, 5, 8)
1064 gmPhraseWheel.cPhraseWheel.__init__ (
1065 self,
1066 *args,
1067 **kwargs
1068 )
1069 self.unset_context(context = u'zip')
1070
1071 self.SetToolTipString(_('Type or select a street.'))
1072 self.capitalisation_mode = gmTools.CAPS_FIRST
1073 self.matcher = mp
1074
1076
1078
1079 query = """
1080 select distinct on (suburb) suburb, suburb
1081 from dem.street
1082 where suburb %(fragment_condition)s
1083 order by suburb
1084 limit 50
1085 """
1086 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1087 mp.setThresholds(2, 3, 6)
1088 gmPhraseWheel.cPhraseWheel.__init__ (
1089 self,
1090 *args,
1091 **kwargs
1092 )
1093
1094 self.SetToolTipString(_('Type or select the suburb.'))
1095 self.capitalisation_mode = gmTools.CAPS_FIRST
1096 self.matcher = mp
1097
1099
1101 context = {
1102 u'ctxt_zip': {
1103 u'where_part': u'and zip ilike %(zip)s',
1104 u'placeholder': u'zip'
1105 }
1106 }
1107 query = u"""
1108 select u1, u2 from (
1109 select distinct on (rank, u1)
1110 u1, u2, rank
1111 from (
1112 select
1113 urb as u1, urb as u2, 1 as rank
1114 from dem.v_zip2data
1115 where
1116 urb %(fragment_condition)s
1117 %(ctxt_zip)s
1118
1119 union all
1120
1121 select
1122 name as u1, name as u2, 2 as rank
1123 from dem.urb
1124 where
1125 name %(fragment_condition)s
1126 ) as union_result
1127 order by rank, u1
1128 ) as distincted_union
1129 limit 50
1130 """
1131 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
1132 mp.setThresholds(3, 5, 7)
1133 gmPhraseWheel.cPhraseWheel.__init__ (
1134 self,
1135 *args,
1136 **kwargs
1137 )
1138 self.unset_context(context = u'zip')
1139
1140 self.SetToolTipString(_('Type or select a city/town/village/dwelling.'))
1141 self.capitalisation_mode = gmTools.CAPS_FIRST
1142 self.matcher = mp
1143
1144
1145
1147
1149
1150 query = u"""
1151 select pk, type from ((
1152 select pk, _(description) as type, 1 as rank
1153 from dem.enum_comm_types
1154 where _(description) %(fragment_condition)s
1155 ) union (
1156 select pk, description as type, 2 as rank
1157 from dem.enum_comm_types
1158 where description %(fragment_condition)s
1159 )) as ur
1160 order by
1161 ur.rank, ur.type
1162 """
1163 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1164 mp.setThresholds(1, 2, 4)
1165 mp.word_separators = u'[ \t]+'
1166 gmPhraseWheel.cPhraseWheel.__init__ (
1167 self,
1168 *args,
1169 **kwargs
1170 )
1171 self.matcher = mp
1172 self.SetToolTipString(_('Select the type of communications channel.'))
1173 self.selection_only = True
1174
1176 """An edit area for editing/creating a comms channel.
1177
1178 Does NOT act on/listen to the current patient.
1179 """
1181 try:
1182 self.channel = kwargs['comm_channel']
1183 del kwargs['comm_channel']
1184 except KeyError:
1185 self.channel = None
1186
1187 wxgCommChannelEditAreaPnl.wxgCommChannelEditAreaPnl.__init__(self, *args, **kwargs)
1188
1189 self.identity = None
1190
1191 self.refresh()
1192
1193
1194
1195 - def refresh(self, comm_channel = None):
1196 if comm_channel is not None:
1197 self.channel = comm_channel
1198
1199 if self.channel is None:
1200 self._PRW_type.SetText(u'')
1201 self._TCTRL_url.SetValue(u'')
1202 self._PRW_address.SetText(value = u'', data = None)
1203 self._CHBOX_confidential.SetValue(False)
1204 else:
1205 self._PRW_type.SetText(self.channel['l10n_comm_type'])
1206 self._TCTRL_url.SetValue(self.channel['url'])
1207 self._PRW_address.SetData(data = self.channel['pk_address'])
1208 self._CHBOX_confidential.SetValue(self.channel['is_confidential'])
1209
1211 """Links comm channel to patient."""
1212 if self.channel is None:
1213 if not self.__valid_for_save():
1214 return False
1215 try:
1216 self.channel = self.identity.link_comm_channel (
1217 pk_channel_type = self._PRW_type.GetData(),
1218 url = self._TCTRL_url.GetValue().strip(),
1219 is_confidential = self._CHBOX_confidential.GetValue(),
1220 )
1221 except psycopg2.IntegrityError:
1222 _log.exception('error saving comm channel')
1223 gmDispatcher.send(signal = u'statustext', msg = _('Cannot save communications channel.'), beep = True)
1224 return False
1225 else:
1226 comm_type = self._PRW_type.GetValue().strip()
1227 if comm_type != u'':
1228 self.channel['comm_type'] = comm_type
1229 url = self._TCTRL_url.GetValue().strip()
1230 if url != u'':
1231 self.channel['url'] = url
1232 self.channel['is_confidential'] = self._CHBOX_confidential.GetValue()
1233
1234 self.channel['pk_address'] = self._PRW_address.GetData()
1235 self.channel.save_payload()
1236
1237 return True
1238
1239
1240
1242
1243 no_errors = True
1244
1245 if self._PRW_type.GetData() is None:
1246 self._PRW_type.SetBackgroundColour('pink')
1247 self._PRW_type.SetFocus()
1248 self._PRW_type.Refresh()
1249 no_errors = False
1250 else:
1251 self._PRW_type.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1252 self._PRW_type.Refresh()
1253
1254 if self._TCTRL_url.GetValue().strip() == u'':
1255 self._TCTRL_url.SetBackgroundColour('pink')
1256 self._TCTRL_url.SetFocus()
1257 self._TCTRL_url.Refresh()
1258 no_errors = False
1259 else:
1260 self._TCTRL_url.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1261 self._TCTRL_url.Refresh()
1262
1263 return no_errors
1264
1266 """A list for managing a person's comm channels.
1267
1268 Does NOT act on/listen to the current patient.
1269 """
1287
1288
1289
1290 - def refresh(self, *args, **kwargs):
1301
1302
1303
1305 self._LCTRL_items.SetToolTipString(_('List of known communication channels.'))
1306 self._LCTRL_items.set_columns(columns = [
1307 _('confidential'),
1308 _('Type'),
1309 _('Value')
1310 ])
1311
1320
1329
1331 go_ahead = gmGuiHelpers.gm_show_question (
1332 _( 'Are you sure this patient can no longer\n'
1333 "be contacted via this channel ?"
1334 ),
1335 _('Removing communication channel')
1336 )
1337 if not go_ahead:
1338 return False
1339 self.__identity.unlink_comm_channel(comm_channel = comm)
1340 return True
1341
1342
1343
1345 return self.__identity
1346
1350
1351 identity = property(_get_identity, _set_identity)
1352
1353
1354
1355
1356
1371
1373
1375 query = u"""
1376 (select distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
1377 union
1378 (select distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
1379 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1380 mp.setThresholds(3, 5, 9)
1381 gmPhraseWheel.cPhraseWheel.__init__ (
1382 self,
1383 *args,
1384 **kwargs
1385 )
1386 self.SetToolTipString(_("Type or select a first name (forename/Christian name/given name)."))
1387 self.capitalisation_mode = gmTools.CAPS_NAMES
1388 self.matcher = mp
1389
1391
1393 query = u"""
1394 (select distinct preferred, preferred from dem.names where preferred %(fragment_condition)s order by preferred limit 20)
1395 union
1396 (select distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
1397 union
1398 (select distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
1399 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1400 mp.setThresholds(3, 5, 9)
1401 gmPhraseWheel.cPhraseWheel.__init__ (
1402 self,
1403 *args,
1404 **kwargs
1405 )
1406 self.SetToolTipString(_("Type or select an alias (nick name, preferred name, call name, warrior name, artist name)."))
1407
1408
1409 self.matcher = mp
1410
1412
1414 query = u"select distinct title, title from dem.identity where title %(fragment_condition)s"
1415 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1416 mp.setThresholds(1, 3, 9)
1417 gmPhraseWheel.cPhraseWheel.__init__ (
1418 self,
1419 *args,
1420 **kwargs
1421 )
1422 self.SetToolTipString(_("Type or select a title. Note that the title applies to the person, not to a particular name !"))
1423 self.matcher = mp
1424
1426 """Let user select a gender."""
1427
1428 _gender_map = None
1429
1431
1432 if cGenderSelectionPhraseWheel._gender_map is None:
1433 cmd = u"""
1434 select tag, l10n_label, sort_weight
1435 from dem.v_gender_labels
1436 order by sort_weight desc"""
1437 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx=True)
1438 cGenderSelectionPhraseWheel._gender_map = {}
1439 for gender in rows:
1440 cGenderSelectionPhraseWheel._gender_map[gender[idx['tag']]] = {
1441 'data': gender[idx['tag']],
1442 'label': gender[idx['l10n_label']],
1443 'weight': gender[idx['sort_weight']]
1444 }
1445
1446 mp = gmMatchProvider.cMatchProvider_FixedList(aSeq = cGenderSelectionPhraseWheel._gender_map.values())
1447 mp.setThresholds(1, 1, 3)
1448
1449 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1450 self.selection_only = True
1451 self.matcher = mp
1452 self.picklist_delay = 50
1453
1468
1470
1472 query = u"""
1473 select distinct pk, (name || coalesce(' (%s ' || issuer || ')', '')) as label
1474 from dem.enum_ext_id_types
1475 where name %%(fragment_condition)s
1476 order by label limit 25""" % _('issued by')
1477 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1478 mp.setThresholds(1, 3, 5)
1479 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1480 self.SetToolTipString(_("Enter or select a type for the external ID."))
1481 self.matcher = mp
1482
1497
1498
1499
1501 """An edit area for editing/creating external IDs.
1502
1503 Does NOT act on/listen to the current patient.
1504 """
1506
1507 try:
1508 self.ext_id = kwargs['external_id']
1509 del kwargs['external_id']
1510 except:
1511 self.ext_id = None
1512
1513 wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl.__init__(self, *args, **kwargs)
1514
1515 self.identity = None
1516
1517 self.__register_events()
1518
1519 self.refresh()
1520
1521
1522
1524 if ext_id is not None:
1525 self.ext_id = ext_id
1526
1527 if self.ext_id is not None:
1528 self._PRW_type.SetText(value = self.ext_id['name'], data = self.ext_id['pk_type'])
1529 self._TCTRL_value.SetValue(self.ext_id['value'])
1530 self._PRW_issuer.SetText(self.ext_id['issuer'])
1531 self._TCTRL_comment.SetValue(gmTools.coalesce(self.ext_id['comment'], u''))
1532
1533
1534
1535
1537
1538 if not self.__valid_for_save():
1539 return False
1540
1541
1542 type = regex.split(' \(%s .+\)$' % _('issued by'), self._PRW_type.GetValue().strip(), 1)[0]
1543
1544
1545 if self.ext_id is None:
1546 self.identity.add_external_id (
1547 type_name = type,
1548 value = self._TCTRL_value.GetValue().strip(),
1549 issuer = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u''),
1550 comment = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1551 )
1552
1553 else:
1554 self.identity.update_external_id (
1555 pk_id = self.ext_id['pk_id'],
1556 type = type,
1557 value = self._TCTRL_value.GetValue().strip(),
1558 issuer = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u''),
1559 comment = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1560 )
1561
1562 return True
1563
1564
1565
1568
1570 """Set the issuer according to the selected type.
1571
1572 Matches are fetched from existing records in backend.
1573 """
1574 pk_curr_type = self._PRW_type.GetData()
1575 if pk_curr_type is None:
1576 return True
1577 rows, idx = gmPG2.run_ro_queries(queries = [{
1578 'cmd': u"select issuer from dem.enum_ext_id_types where pk = %s",
1579 'args': [pk_curr_type]
1580 }])
1581 if len(rows) == 0:
1582 return True
1583 wx.CallAfter(self._PRW_issuer.SetText, rows[0][0])
1584 return True
1585
1587
1588 no_errors = True
1589
1590
1591
1592
1593 if self._PRW_type.GetValue().strip() == u'':
1594 self._PRW_type.SetBackgroundColour('pink')
1595 self._PRW_type.SetFocus()
1596 self._PRW_type.Refresh()
1597 else:
1598 self._PRW_type.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1599 self._PRW_type.Refresh()
1600
1601 if self._TCTRL_value.GetValue().strip() == u'':
1602 self._TCTRL_value.SetBackgroundColour('pink')
1603 self._TCTRL_value.SetFocus()
1604 self._TCTRL_value.Refresh()
1605 no_errors = False
1606 else:
1607 self._TCTRL_value.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1608 self._TCTRL_value.Refresh()
1609
1610 return no_errors
1611
1613 """An edit area for editing/creating name/gender/dob.
1614
1615 Does NOT act on/listen to the current patient.
1616 """
1618
1619 self.__name = kwargs['name']
1620 del kwargs['name']
1621 self.__identity = gmPerson.cIdentity(aPK_obj = self.__name['pk_identity'])
1622
1623 wxgNameGenderDOBEditAreaPnl.wxgNameGenderDOBEditAreaPnl.__init__(self, *args, **kwargs)
1624
1625 self.__register_interests()
1626 self.refresh()
1627
1628
1629
1631 if self.__name is None:
1632 return
1633
1634 self._PRW_title.SetText(gmTools.coalesce(self.__name['title'], u''))
1635 self._PRW_firstname.SetText(self.__name['firstnames'])
1636 self._PRW_lastname.SetText(self.__name['lastnames'])
1637 self._PRW_nick.SetText(gmTools.coalesce(self.__name['preferred'], u''))
1638 dob = self.__identity['dob']
1639 self._PRW_dob.SetText(value = dob.strftime('%Y-%m-%d %H:%M'), data = dob)
1640 self._PRW_gender.SetData(self.__name['gender'])
1641 self._CHBOX_active.SetValue(self.__name['active_name'])
1642 self._TCTRL_comment.SetValue(gmTools.coalesce(self.__name['comment'], u''))
1643
1644
1645
1646
1648
1649 if not self.__valid_for_save():
1650 return False
1651
1652 self.__identity['gender'] = self._PRW_gender.GetData()
1653 if self._PRW_dob.GetValue().strip() == u'':
1654 self.__identity['dob'] = None
1655 else:
1656 self.__identity['dob'] = self._PRW_dob.GetData().get_pydt()
1657 self.__identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip(), u'')
1658 self.__identity.save_payload()
1659
1660 active = self._CHBOX_active.GetValue()
1661 first = self._PRW_firstname.GetValue().strip()
1662 last = self._PRW_lastname.GetValue().strip()
1663 old_nick = self.__name['preferred']
1664
1665
1666 old_name = self.__name['firstnames'] + self.__name['lastnames']
1667 if (first + last) != old_name:
1668 self.__name = self.__identity.add_name(first, last, active)
1669
1670 self.__name['active_name'] = active
1671 self.__name['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1672 self.__name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1673
1674 self.__name.save_payload()
1675
1676 return True
1677
1678
1679
1682
1684 """Set the gender according to entered firstname.
1685
1686 Matches are fetched from existing records in backend.
1687 """
1688 firstname = self._PRW_firstname.GetValue().strip()
1689 if firstname == u'':
1690 return True
1691 rows, idx = gmPG2.run_ro_queries(queries = [{
1692 'cmd': u"select gender from dem.name_gender_map where name ilike %s",
1693 'args': [firstname]
1694 }])
1695 if len(rows) == 0:
1696 return True
1697 wx.CallAfter(self._PRW_gender.SetData, rows[0][0])
1698 return True
1699
1700
1701
1703
1704 error_found = True
1705
1706 if self._PRW_gender.GetData() is None:
1707 self._PRW_gender.SetBackgroundColour('pink')
1708 self._PRW_gender.Refresh()
1709 self._PRW_gender.SetFocus()
1710 error_found = False
1711 else:
1712 self._PRW_gender.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1713 self._PRW_gender.Refresh()
1714
1715 if not self._PRW_dob.is_valid_timestamp():
1716 val = self._PRW_dob.GetValue().strip()
1717 gmDispatcher.send(signal = u'statustext', msg = _('Cannot parse <%s> into proper timestamp.') % val)
1718 self._PRW_dob.SetBackgroundColour('pink')
1719 self._PRW_dob.Refresh()
1720 self._PRW_dob.SetFocus()
1721 error_found = False
1722 else:
1723 self._PRW_dob.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1724 self._PRW_dob.Refresh()
1725
1726 if self._PRW_lastname.GetValue().strip() == u'':
1727 self._PRW_lastname.SetBackgroundColour('pink')
1728 self._PRW_lastname.Refresh()
1729 self._PRW_lastname.SetFocus()
1730 error_found = False
1731 else:
1732 self._PRW_lastname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1733 self._PRW_lastname.Refresh()
1734
1735 if self._PRW_firstname.GetValue().strip() == u'':
1736 self._PRW_firstname.SetBackgroundColour('pink')
1737 self._PRW_firstname.Refresh()
1738 self._PRW_firstname.SetFocus()
1739 error_found = False
1740 else:
1741 self._PRW_firstname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1742 self._PRW_firstname.Refresh()
1743
1744 return error_found
1745
1746
1747
1749 """A list for managing a person's names.
1750
1751 Does NOT act on/listen to the current patient.
1752 """
1770
1771
1772
1773 - def refresh(self, *args, **kwargs):
1774 if self.__identity is None:
1775 self._LCTRL_items.set_string_items()
1776 return
1777
1778 names = self.__identity.get_names()
1779 self._LCTRL_items.set_string_items (
1780 items = [ [
1781 gmTools.bool2str(n['active_name'], 'X', ''),
1782 gmTools.coalesce(n['title'], gmPerson.map_gender2salutation(n['gender'])),
1783 n['lastnames'],
1784 n['firstnames'],
1785 gmTools.coalesce(n['preferred'], u''),
1786 gmTools.coalesce(n['comment'], u'')
1787 ] for n in names ]
1788 )
1789 self._LCTRL_items.set_column_widths()
1790 self._LCTRL_items.set_data(data = names)
1791
1792
1793
1795 self._LCTRL_items.set_columns(columns = [
1796 _('Active'),
1797 _('Title'),
1798 _('Lastname'),
1799 _('Firstname(s)'),
1800 _('Preferred Name'),
1801 _('Comment')
1802 ])
1803
1813
1823
1825
1826 if len(self.__identity.get_names()) == 1:
1827 gmDispatcher.send(signal = u'statustext', msg = _('Cannot delete the only name of a person.'), beep = True)
1828 return False
1829
1830 go_ahead = gmGuiHelpers.gm_show_question (
1831 _( 'It is often advisable to keep old names around and\n'
1832 'just create a new "currently active" name.\n'
1833 '\n'
1834 'This allows finding the patient by both the old\n'
1835 'and the new name (think before/after marriage).\n'
1836 '\n'
1837 'Do you still want to really delete\n'
1838 "this name from the patient ?"
1839 ),
1840 _('Deleting name')
1841 )
1842 if not go_ahead:
1843 return False
1844
1845 self.__identity.delete_name(name = name)
1846 return True
1847
1848
1849
1851 return self.__identity
1852
1856
1857 identity = property(_get_identity, _set_identity)
1858
1860 """A list for managing a person's external IDs.
1861
1862 Does NOT act on/listen to the current patient.
1863 """
1881
1882
1883
1884 - def refresh(self, *args, **kwargs):
1902
1903
1904
1906 self._LCTRL_items.set_columns(columns = [
1907 _('ID type'),
1908 _('Value'),
1909 _('Issuer'),
1910 _('Context'),
1911 _('Comment')
1912 ])
1913
1924
1935
1937 go_ahead = gmGuiHelpers.gm_show_question (
1938 _( 'Do you really want to delete this\n'
1939 'external ID from the patient ?'),
1940 _('Deleting external ID')
1941 )
1942 if not go_ahead:
1943 return False
1944 self.__identity.delete_external_id(pk_ext_id = ext_id['pk_id'])
1945 return True
1946
1947
1948
1950 return self.__identity
1951
1955
1956 identity = property(_get_identity, _set_identity)
1957
1958
1959
1961 """A panel for editing identity data for a person.
1962
1963 - provides access to:
1964 - name
1965 - external IDs
1966
1967 Does NOT act on/listen to the current patient.
1968 """
1970
1971 wxgPersonIdentityManagerPnl.wxgPersonIdentityManagerPnl.__init__(self, *args, **kwargs)
1972
1973 self.__identity = None
1974 self.refresh()
1975
1976
1977
1979 self._PNL_names.identity = self.__identity
1980 self._PNL_ids.identity = self.__identity
1981
1982
1983
1985 return self.__identity
1986
1990
1991 identity = property(_get_identity, _set_identity)
1992
1993
1994
1996
1997 dbcfg = gmCfg.cCfgSQL()
1998
1999 def_region = dbcfg.get2 (
2000 option = u'person.create.default_region',
2001 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2002 bias = u'user'
2003 )
2004
2005 if def_region is None:
2006 def_country = dbcfg.get2 (
2007 option = u'person.create.default_country',
2008 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2009 bias = u'user'
2010 )
2011 else:
2012 countries = gmDemographicRecord.get_country_for_region(region = def_region)
2013 if len(countries) == 1:
2014 def_country = countries[0]['l10n_country']
2015
2016 ea = cNewPatientEAPnl(parent = parent, id = -1, country = def_country, region = def_region)
2017 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = True)
2018 dlg.SetTitle(_('Adding new person'))
2019 ea._PRW_lastname.SetFocus()
2020 result = dlg.ShowModal()
2021 pat = ea.data
2022 dlg.Destroy()
2023
2024 if result != wx.ID_OK:
2025 return False
2026
2027 if activate:
2028 from Gnumed.wxpython import gmPatSearchWidgets
2029 gmPatSearchWidgets.set_active_patient(patient = pat)
2030
2031 gmDispatcher.send(signal = 'display_widget', name = 'gmNotebookedPatientEditionPlugin')
2032
2033 return True
2034
2035 from Gnumed.wxGladeWidgets import wxgNewPatientEAPnl
2036
2037 -class cNewPatientEAPnl(wxgNewPatientEAPnl.wxgNewPatientEAPnl, gmEditArea.cGenericEditAreaMixin):
2038
2040
2041 try:
2042 self.default_region = kwargs['region']
2043 del kwargs['region']
2044 except KeyError:
2045 self.default_region = None
2046
2047 try:
2048 self.default_country = kwargs['country']
2049 del kwargs['country']
2050 except KeyError:
2051 self.default_country = None
2052
2053 wxgNewPatientEAPnl.wxgNewPatientEAPnl.__init__(self, *args, **kwargs)
2054 gmEditArea.cGenericEditAreaMixin.__init__(self)
2055
2056 self.mode = 'new'
2057 self.data = None
2058 self._address = None
2059
2060 self.__init_ui()
2061 self.__register_interests()
2062
2063
2064
2066 self._PRW_lastname.final_regex = '.+'
2067 self._PRW_firstnames.final_regex = '.+'
2068 self._PRW_address_searcher.selection_only = False
2069 low = wx.DateTimeFromDMY(1,0,1900)
2070 hi = wx.DateTime()
2071 self._DP_dob.SetRange(low, hi.SetToCurrent())
2072
2073
2074
2075 if self.default_country is not None:
2076 self._PRW_country.SetText(value = self.default_country)
2077
2078 if self.default_region is not None:
2079 self._PRW_region.SetText(value = self.default_region)
2080
2082
2083 adr = self._PRW_address_searcher.get_address()
2084 if adr is None:
2085 return True
2086
2087 if ctrl.GetValue().strip() != adr[field]:
2088 wx.CallAfter(self._PRW_address_searcher.SetText, value = u'', data = None)
2089 return True
2090
2091 return False
2092
2094 adr = self._PRW_address_searcher.get_address()
2095 if adr is None:
2096 return True
2097
2098 self._PRW_zip.SetText(value = adr['postcode'], data = adr['postcode'])
2099
2100 self._PRW_street.SetText(value = adr['street'], data = adr['street'])
2101 self._PRW_street.set_context(context = u'zip', val = adr['postcode'])
2102
2103 self._TCTRL_number.SetValue(adr['number'])
2104
2105 self._PRW_urb.SetText(value = adr['urb'], data = adr['urb'])
2106 self._PRW_urb.set_context(context = u'zip', val = adr['postcode'])
2107
2108 self._PRW_region.SetText(value = adr['l10n_state'], data = adr['code_state'])
2109 self._PRW_region.set_context(context = u'zip', val = adr['postcode'])
2110
2111 self._PRW_country.SetText(value = adr['l10n_country'], data = adr['code_country'])
2112 self._PRW_country.set_context(context = u'zip', val = adr['postcode'])
2113
2115 error = False
2116
2117
2118 if self._PRW_lastname.GetValue().strip() == u'':
2119 error = True
2120 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
2121 self._PRW_lastname.display_as_valid(False)
2122 else:
2123 self._PRW_lastname.display_as_valid(True)
2124
2125 if self._PRW_firstnames.GetValue().strip() == '':
2126 error = True
2127 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
2128 self._PRW_firstnames.display_as_valid(False)
2129 else:
2130 self._PRW_firstnames.display_as_valid(True)
2131
2132
2133 if self._PRW_gender.GetData() is None:
2134 error = True
2135 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
2136 self._PRW_gender.display_as_valid(False)
2137 else:
2138 self._PRW_gender.display_as_valid(True)
2139
2140
2141 if not self._DP_dob.is_valid_timestamp():
2142
2143 gmDispatcher.send(signal = 'statustext', msg = _('Cannot use this date of birth. Does it lie before 1900 ?'))
2144
2145 do_it_anyway = gmGuiHelpers.gm_show_question (
2146 _(
2147 'Are you sure you want to register this person\n'
2148 'without a valid date of birth ?\n'
2149 '\n'
2150 'This can be useful for temporary staff members\n'
2151 'but will provoke nag screens if this person\n'
2152 'becomes a patient.\n'
2153 '\n'
2154 'Note that the date of birth cannot technically\n'
2155 'be before 1900, either :-(\n'
2156 ),
2157 _('Registering new person')
2158 )
2159
2160 if not do_it_anyway:
2161 error = True
2162
2163 if self._DP_dob.GetValue().GetYear() < 1900:
2164 error = True
2165 gmDispatcher.send(signal = 'statustext', msg = _('The year of birth must lie after 1900.'), beep = True)
2166 self._DP_dob.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
2167 self._DP_dob.SetFocus()
2168 else:
2169 self._DP_dob.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
2170 self._DP_dob.Refresh()
2171
2172
2173
2174
2175 return (not error)
2176
2178
2179
2180 if self._PRW_address_searcher.GetData() is not None:
2181 wx.CallAfter(self.__set_fields_from_address_searcher)
2182 return True
2183
2184
2185 fields_to_fill = (
2186 self._TCTRL_number,
2187 self._PRW_zip,
2188 self._PRW_street,
2189 self._PRW_urb,
2190 self._PRW_region,
2191 self._PRW_country
2192 )
2193 no_of_filled_fields = 0
2194
2195 for field in fields_to_fill:
2196 if field.GetValue().strip() != u'':
2197 no_of_filled_fields += 1
2198 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
2199 field.Refresh()
2200
2201
2202 if no_of_filled_fields == 0:
2203 if empty_address_is_valid:
2204 return True
2205 else:
2206 return None
2207
2208
2209 if no_of_filled_fields != len(fields_to_fill):
2210 for field in fields_to_fill:
2211 if field.GetValue().strip() == u'':
2212 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
2213 field.SetFocus()
2214 field.Refresh()
2215 msg = _('To properly create an address, all the related fields must be filled in.')
2216 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
2217 return False
2218
2219
2220
2221
2222 strict_fields = (
2223 self._PRW_region,
2224 self._PRW_country
2225 )
2226 error = False
2227 for field in strict_fields:
2228 if field.GetData() is None:
2229 error = True
2230 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
2231 field.SetFocus()
2232 else:
2233 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
2234 field.Refresh()
2235
2236 if error:
2237 msg = _('This field must contain an item selected from the dropdown list.')
2238 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
2239 return False
2240
2241 return True
2242
2259
2260
2261
2263 """Set the gender according to entered firstname.
2264
2265 Matches are fetched from existing records in backend.
2266 """
2267
2268
2269 if self._PRW_gender.GetData() is not None:
2270 return True
2271
2272 firstname = self._PRW_firstnames.GetValue().strip()
2273 if firstname == u'':
2274 return True
2275
2276 gender = gmPerson.map_firstnames2gender(firstnames = firstname)
2277 if gender is None:
2278 return True
2279
2280 wx.CallAfter(self._PRW_gender.SetData, gender)
2281 return True
2282
2284 self.__perhaps_invalidate_address_searcher(self._PRW_zip, 'postcode')
2285
2286 zip_code = gmTools.none_if(self._PRW_zip.GetValue().strip(), u'')
2287 self._PRW_street.set_context(context = u'zip', val = zip_code)
2288 self._PRW_urb.set_context(context = u'zip', val = zip_code)
2289 self._PRW_region.set_context(context = u'zip', val = zip_code)
2290 self._PRW_country.set_context(context = u'zip', val = zip_code)
2291
2292 return True
2293
2295 self.__perhaps_invalidate_address_searcher(self._PRW_country, 'l10n_country')
2296
2297 country = gmTools.none_if(self._PRW_country.GetValue().strip(), u'')
2298 self._PRW_region.set_context(context = u'country', val = country)
2299
2300 return True
2301
2303 mapping = [
2304 (self._PRW_street, 'street'),
2305 (self._TCTRL_number, 'number'),
2306 (self._PRW_urb, 'urb'),
2307 (self._PRW_region, 'l10n_state')
2308 ]
2309
2310
2311 for ctrl, field in mapping:
2312 if self.__perhaps_invalidate_address_searcher(ctrl, field):
2313 return True
2314
2315 return True
2316
2318 adr = self._PRW_address_searcher.get_address()
2319 if adr is None:
2320 return True
2321
2322 wx.CallAfter(self.__set_fields_from_address_searcher)
2323 return True
2324
2325
2326
2328 return (self.__identity_valid_for_save() and self.__address_valid_for_save(empty_address_is_valid = True))
2329
2331
2332
2333 new_identity = gmPerson.create_identity (
2334 gender = self._PRW_gender.GetData(),
2335 dob = self._DP_dob.get_pydt(),
2336 lastnames = self._PRW_lastname.GetValue().strip(),
2337 firstnames = self._PRW_firstnames.GetValue().strip()
2338 )
2339 _log.debug('identity created: %s' % new_identity)
2340
2341 new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
2342 new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
2343
2344 new_identity.save()
2345
2346 name = new_identity.get_active_name()
2347 name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
2348 name.save()
2349
2350
2351 is_valid = self.__address_valid_for_save(empty_address_is_valid = False)
2352 if is_valid is True:
2353
2354
2355 try:
2356 new_identity.link_address (
2357 number = self._TCTRL_number.GetValue().strip(),
2358 street = self._PRW_street.GetValue().strip(),
2359 postcode = self._PRW_zip.GetValue().strip(),
2360 urb = self._PRW_urb.GetValue().strip(),
2361 state = self._PRW_region.GetData(),
2362 country = self._PRW_country.GetData()
2363 )
2364 except psycopg2.InternalError:
2365
2366 _log.debug('number: >>%s<<', self._TCTRL_number.GetValue().strip())
2367 _log.debug('street: >>%s<<', self._PRW_street.GetValue().strip())
2368 _log.debug('postcode: >>%s<<', self._PRW_zip.GetValue().strip())
2369 _log.debug('urb: >>%s<<', self._PRW_urb.GetValue().strip())
2370 _log.debug('state: >>%s<<', self._PRW_region.GetData().strip())
2371 _log.debug('country: >>%s<<', self._PRW_country.GetData().strip())
2372 _log.exception('cannot link address')
2373 gmGuiHelpers.gm_show_error (
2374 aTitle = _('Saving address'),
2375 aMessage = _(
2376 'Cannot save this address.\n'
2377 '\n'
2378 'You will have to add it via the Demographics plugin.\n'
2379 )
2380 )
2381 elif is_valid is False:
2382 gmGuiHelpers.gm_show_error (
2383 aTitle = _('Saving address'),
2384 aMessage = _(
2385 'Address not saved.\n'
2386 '\n'
2387 'You will have to add it via the Demographics plugin.\n'
2388 )
2389 )
2390
2391
2392
2393 new_identity.link_comm_channel (
2394 comm_medium = u'homephone',
2395 url = gmTools.none_if(self._TCTRL_phone.GetValue().strip(), u''),
2396 is_confidential = False
2397 )
2398
2399
2400 pk_type = self._PRW_external_id_type.GetData()
2401 id_value = self._TCTRL_external_id_value.GetValue().strip()
2402 if (pk_type is not None) and (id_value != u''):
2403 new_identity.add_external_id(value = id_value, pk_type = pk_type)
2404
2405
2406 new_identity.link_occupation (
2407 occupation = gmTools.none_if(self._PRW_occupation.GetValue().strip(), u'')
2408 )
2409
2410 self.data = new_identity
2411 return True
2412
2414 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
2415
2419
2422
2424 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
2425
2426
2427
2428 -class cBasicPatDetailsPage(wx.wizard.WizardPageSimple):
2429 """
2430 Wizard page for entering patient's basic demographic information
2431 """
2432
2433 form_fields = (
2434 'firstnames', 'lastnames', 'nick', 'dob', 'gender', 'title', 'occupation',
2435 'address_number', 'zip_code', 'street', 'town', 'state', 'country', 'phone', 'comment'
2436 )
2437
2438 - def __init__(self, parent, title):
2439 """
2440 Creates a new instance of BasicPatDetailsPage
2441 @param parent - The parent widget
2442 @type parent - A wx.Window instance
2443 @param tile - The title of the page
2444 @type title - A StringType instance
2445 """
2446 wx.wizard.WizardPageSimple.__init__(self, parent)
2447 self.__title = title
2448 self.__do_layout()
2449 self.__register_interests()
2450
2451 - def __do_layout(self):
2452 PNL_form = wx.Panel(self, -1)
2453
2454
2455 STT_lastname = wx.StaticText(PNL_form, -1, _('Last name'))
2456 STT_lastname.SetForegroundColour('red')
2457 self.PRW_lastname = cLastnamePhraseWheel(parent = PNL_form, id = -1)
2458 self.PRW_lastname.SetToolTipString(_('Required: lastname (family name)'))
2459
2460
2461 STT_firstname = wx.StaticText(PNL_form, -1, _('First name(s)'))
2462 STT_firstname.SetForegroundColour('red')
2463 self.PRW_firstname = cFirstnamePhraseWheel(parent = PNL_form, id = -1)
2464 self.PRW_firstname.SetToolTipString(_('Required: surname/given name/first name'))
2465
2466
2467 STT_nick = wx.StaticText(PNL_form, -1, _('Nick name'))
2468 self.PRW_nick = cNicknamePhraseWheel(parent = PNL_form, id = -1)
2469
2470
2471 STT_dob = wx.StaticText(PNL_form, -1, _('Date of birth'))
2472 STT_dob.SetForegroundColour('red')
2473 self.PRW_dob = gmDateTimeInput.cFuzzyTimestampInput(parent = PNL_form, id = -1)
2474 self.PRW_dob.SetToolTipString(_("Required: date of birth, if unknown or aliasing wanted then invent one"))
2475
2476
2477 STT_gender = wx.StaticText(PNL_form, -1, _('Gender'))
2478 STT_gender.SetForegroundColour('red')
2479 self.PRW_gender = cGenderSelectionPhraseWheel(parent = PNL_form, id=-1)
2480 self.PRW_gender.SetToolTipString(_("Required: gender of patient"))
2481
2482
2483 STT_title = wx.StaticText(PNL_form, -1, _('Title'))
2484 self.PRW_title = cTitlePhraseWheel(parent = PNL_form, id = -1)
2485
2486
2487 STT_zip_code = wx.StaticText(PNL_form, -1, _('Postal code'))
2488 STT_zip_code.SetForegroundColour('orange')
2489 self.PRW_zip_code = cZipcodePhraseWheel(parent = PNL_form, id = -1)
2490 self.PRW_zip_code.SetToolTipString(_("primary/home address: zip/postal code"))
2491
2492
2493 STT_street = wx.StaticText(PNL_form, -1, _('Street'))
2494 STT_street.SetForegroundColour('orange')
2495 self.PRW_street = cStreetPhraseWheel(parent = PNL_form, id = -1)
2496 self.PRW_street.SetToolTipString(_("primary/home address: name of street"))
2497
2498
2499 STT_address_number = wx.StaticText(PNL_form, -1, _('Number'))
2500 STT_address_number.SetForegroundColour('orange')
2501 self.TTC_address_number = wx.TextCtrl(PNL_form, -1)
2502 self.TTC_address_number.SetToolTipString(_("primary/home address: address number"))
2503
2504
2505 STT_town = wx.StaticText(PNL_form, -1, _('Place'))
2506 STT_town.SetForegroundColour('orange')
2507 self.PRW_town = cUrbPhraseWheel(parent = PNL_form, id = -1)
2508 self.PRW_town.SetToolTipString(_("primary/home address: city/town/village/dwelling/..."))
2509
2510
2511 STT_state = wx.StaticText(PNL_form, -1, _('Region'))
2512 STT_state.SetForegroundColour('orange')
2513 self.PRW_state = cStateSelectionPhraseWheel(parent=PNL_form, id=-1)
2514 self.PRW_state.SetToolTipString(_("primary/home address: state/province/county/..."))
2515
2516
2517 STT_country = wx.StaticText(PNL_form, -1, _('Country'))
2518 STT_country.SetForegroundColour('orange')
2519 self.PRW_country = cCountryPhraseWheel(parent = PNL_form, id = -1)
2520 self.PRW_country.SetToolTipString(_("primary/home address: country"))
2521
2522
2523 STT_phone = wx.StaticText(PNL_form, -1, _('Phone'))
2524 self.TTC_phone = wx.TextCtrl(PNL_form, -1)
2525 self.TTC_phone.SetToolTipString(_("phone number at home"))
2526
2527
2528 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
2529 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2530
2531
2532 STT_comment = wx.StaticText(PNL_form, -1, _('Comment'))
2533 self.TCTRL_comment = wx.TextCtrl(PNL_form, -1)
2534 self.TCTRL_comment.SetToolTipString(_('A comment on this patient.'))
2535
2536
2537 self.form_DTD = cFormDTD(fields = self.__class__.form_fields)
2538 PNL_form.SetValidator(cBasicPatDetailsPageValidator(dtd = self.form_DTD))
2539
2540
2541 SZR_input = wx.FlexGridSizer(cols = 2, rows = 16, vgap = 4, hgap = 4)
2542 SZR_input.AddGrowableCol(1)
2543 SZR_input.Add(STT_lastname, 0, wx.SHAPED)
2544 SZR_input.Add(self.PRW_lastname, 1, wx.EXPAND)
2545 SZR_input.Add(STT_firstname, 0, wx.SHAPED)
2546 SZR_input.Add(self.PRW_firstname, 1, wx.EXPAND)
2547 SZR_input.Add(STT_nick, 0, wx.SHAPED)
2548 SZR_input.Add(self.PRW_nick, 1, wx.EXPAND)
2549 SZR_input.Add(STT_dob, 0, wx.SHAPED)
2550 SZR_input.Add(self.PRW_dob, 1, wx.EXPAND)
2551 SZR_input.Add(STT_gender, 0, wx.SHAPED)
2552 SZR_input.Add(self.PRW_gender, 1, wx.EXPAND)
2553 SZR_input.Add(STT_title, 0, wx.SHAPED)
2554 SZR_input.Add(self.PRW_title, 1, wx.EXPAND)
2555 SZR_input.Add(STT_zip_code, 0, wx.SHAPED)
2556 SZR_input.Add(self.PRW_zip_code, 1, wx.EXPAND)
2557 SZR_input.Add(STT_street, 0, wx.SHAPED)
2558 SZR_input.Add(self.PRW_street, 1, wx.EXPAND)
2559 SZR_input.Add(STT_address_number, 0, wx.SHAPED)
2560 SZR_input.Add(self.TTC_address_number, 1, wx.EXPAND)
2561 SZR_input.Add(STT_town, 0, wx.SHAPED)
2562 SZR_input.Add(self.PRW_town, 1, wx.EXPAND)
2563 SZR_input.Add(STT_state, 0, wx.SHAPED)
2564 SZR_input.Add(self.PRW_state, 1, wx.EXPAND)
2565 SZR_input.Add(STT_country, 0, wx.SHAPED)
2566 SZR_input.Add(self.PRW_country, 1, wx.EXPAND)
2567 SZR_input.Add(STT_phone, 0, wx.SHAPED)
2568 SZR_input.Add(self.TTC_phone, 1, wx.EXPAND)
2569 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2570 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2571 SZR_input.Add(STT_comment, 0, wx.SHAPED)
2572 SZR_input.Add(self.TCTRL_comment, 1, wx.EXPAND)
2573
2574 PNL_form.SetSizerAndFit(SZR_input)
2575
2576
2577 SZR_main = gmGuiHelpers.makePageTitle(self, self.__title)
2578 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2579
2580
2581
2586
2587 - def on_country_selected(self, data):
2588 """Set the states according to entered country."""
2589 self.PRW_state.set_context(context=u'country', val=data)
2590 return True
2591
2592 - def on_name_set(self):
2593 """Set the gender according to entered firstname.
2594
2595 Matches are fetched from existing records in backend.
2596 """
2597 firstname = self.PRW_firstname.GetValue().strip()
2598 rows, idx = gmPG2.run_ro_queries(queries = [{
2599 'cmd': u"select gender from dem.name_gender_map where name ilike %s",
2600 'args': [firstname]
2601 }])
2602 if len(rows) == 0:
2603 return True
2604 wx.CallAfter(self.PRW_gender.SetData, rows[0][0])
2605 return True
2606
2607 - def on_zip_set(self):
2608 """Set the street, town, state and country according to entered zip code."""
2609 zip_code = self.PRW_zip_code.GetValue().strip()
2610 self.PRW_street.set_context(context=u'zip', val=zip_code)
2611 self.PRW_town.set_context(context=u'zip', val=zip_code)
2612 self.PRW_state.set_context(context=u'zip', val=zip_code)
2613 self.PRW_country.set_context(context=u'zip', val=zip_code)
2614 return True
2615
2617 """
2618 Wizard to create a new patient.
2619
2620 TODO:
2621 - write pages for different "themes" of patient creation
2622 - make it configurable which pages are loaded
2623 - make available sets of pages that apply to a country
2624 - make loading of some pages depend upon values in earlier pages, eg
2625 when the patient is female and older than 13 include a page about
2626 "female" data (number of kids etc)
2627
2628 FIXME: use: wizard.FindWindowById(wx.ID_FORWARD).Disable()
2629 """
2630
2631 - def __init__(self, parent, title = _('Register new person'), subtitle = _('Basic demographic details') ):
2632 """
2633 Creates a new instance of NewPatientWizard
2634 @param parent - The parent widget
2635 @type parent - A wx.Window instance
2636 """
2637 id_wiz = wx.NewId()
2638 wx.wizard.Wizard.__init__(self, parent, id_wiz, title)
2639 self.SetExtraStyle(wx.WS_EX_VALIDATE_RECURSIVELY)
2640 self.__subtitle = subtitle
2641 self.__do_layout()
2642
2644 """Create new patient.
2645
2646 activate, too, if told to do so (and patient successfully created)
2647 """
2648 while True:
2649
2650 if not wx.wizard.Wizard.RunWizard(self, self.basic_pat_details):
2651 return False
2652
2653 try:
2654
2655 ident = create_identity_from_dtd(dtd = self.basic_pat_details.form_DTD)
2656 except:
2657 _log.exception('cannot add new patient - missing identity fields')
2658 gmGuiHelpers.gm_show_error (
2659 _('Cannot create new patient.\n'
2660 'Missing parts of the identity.'
2661 ),
2662 _('Adding new patient')
2663 )
2664 continue
2665
2666 update_identity_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
2667
2668 try:
2669 link_contacts_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
2670 except:
2671 _log.exception('cannot finalize new patient - missing address fields')
2672 gmGuiHelpers.gm_show_error (
2673 _('Cannot add address for the new patient.\n'
2674 'You must either enter all of the address fields or\n'
2675 'none at all. The relevant fields are marked in yellow.\n'
2676 '\n'
2677 'You will need to add the address details in the\n'
2678 'demographics module.'
2679 ),
2680 _('Adding new patient')
2681 )
2682 break
2683
2684 link_occupation_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
2685
2686 break
2687
2688 if activate:
2689 from Gnumed.wxpython import gmPatSearchWidgets
2690 gmPatSearchWidgets.set_active_patient(patient = ident)
2691
2692 return ident
2693
2694
2695
2697 """Arrange widgets.
2698 """
2699
2700 self.basic_pat_details = cBasicPatDetailsPage(self, self.__subtitle )
2701 self.FitToPage(self.basic_pat_details)
2702
2704 """
2705 This validator is used to ensure that the user has entered all
2706 the required conditional values in the page (eg., to properly
2707 create an address, all the related fields must be filled).
2708 """
2709
2710 - def __init__(self, dtd):
2711 """
2712 Validator initialization.
2713 @param dtd The object containing the data model.
2714 @type dtd A cFormDTD instance
2715 """
2716
2717 wx.PyValidator.__init__(self)
2718
2719 self.form_DTD = dtd
2720
2722 """
2723 Standard cloner.
2724 Note that every validator must implement the Clone() method.
2725 """
2726 return cBasicPatDetailsPageValidator(dtd = self.form_DTD)
2727
2728 - def Validate(self, parent = None):
2729 """
2730 Validate the contents of the given text control.
2731 """
2732 _pnl_form = self.GetWindow().GetParent()
2733
2734 error = False
2735
2736
2737 if _pnl_form.PRW_lastname.GetValue().strip() == '':
2738 error = True
2739 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
2740 _pnl_form.PRW_lastname.SetBackgroundColour('pink')
2741 _pnl_form.PRW_lastname.Refresh()
2742 else:
2743 _pnl_form.PRW_lastname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2744 _pnl_form.PRW_lastname.Refresh()
2745
2746 if _pnl_form.PRW_firstname.GetValue().strip() == '':
2747 error = True
2748 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
2749 _pnl_form.PRW_firstname.SetBackgroundColour('pink')
2750 _pnl_form.PRW_firstname.Refresh()
2751 else:
2752 _pnl_form.PRW_firstname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2753 _pnl_form.PRW_firstname.Refresh()
2754
2755
2756 if _pnl_form.PRW_gender.GetData() is None:
2757 error = True
2758 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
2759 _pnl_form.PRW_gender.SetBackgroundColour('pink')
2760 _pnl_form.PRW_gender.Refresh()
2761 else:
2762 _pnl_form.PRW_gender.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2763 _pnl_form.PRW_gender.Refresh()
2764
2765
2766 if (
2767 (_pnl_form.PRW_dob.GetValue().strip() == u'')
2768 or (not _pnl_form.PRW_dob.is_valid_timestamp())
2769 or (_pnl_form.PRW_dob.GetData().timestamp.year < 1900)
2770 ):
2771 error = True
2772 msg = _('Cannot parse <%s> into proper timestamp.') % _pnl_form.PRW_dob.GetValue()
2773 gmDispatcher.send(signal = 'statustext', msg = msg)
2774 _pnl_form.PRW_dob.SetBackgroundColour('pink')
2775 _pnl_form.PRW_dob.Refresh()
2776 else:
2777 _pnl_form.PRW_dob.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2778 _pnl_form.PRW_dob.Refresh()
2779
2780
2781 is_any_field_filled = False
2782 address_fields = (
2783 _pnl_form.TTC_address_number,
2784 _pnl_form.PRW_zip_code,
2785 _pnl_form.PRW_street,
2786 _pnl_form.PRW_town
2787 )
2788 for field in address_fields:
2789 if field.GetValue().strip() == u'':
2790 if is_any_field_filled:
2791 error = True
2792 msg = _('To properly create an address, all the related fields must be filled in.')
2793 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
2794 field.SetBackgroundColour('pink')
2795 field.SetFocus()
2796 field.Refresh()
2797 else:
2798 is_any_field_filled = True
2799 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2800 field.Refresh()
2801
2802 address_fields = (
2803 _pnl_form.PRW_state,
2804 _pnl_form.PRW_country
2805 )
2806 for field in address_fields:
2807 if field.GetData() is None:
2808 if is_any_field_filled:
2809 error = True
2810 msg = _('To properly create an address, all the related fields must be filled in.')
2811 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
2812 field.SetBackgroundColour('pink')
2813 field.SetFocus()
2814 field.Refresh()
2815 else:
2816 is_any_field_filled = True
2817 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2818 field.Refresh()
2819
2820 return (not error)
2821
2822 - def TransferToWindow(self):
2823 """
2824 Transfer data from validator to window.
2825 The default implementation returns False, indicating that an error
2826 occurred. We simply return True, as we don't do any data transfer.
2827 """
2828 _pnl_form = self.GetWindow().GetParent()
2829
2830 _pnl_form.PRW_gender.SetData(self.form_DTD['gender'])
2831 _pnl_form.PRW_dob.SetText(self.form_DTD['dob'])
2832 _pnl_form.PRW_lastname.SetText(self.form_DTD['lastnames'])
2833 _pnl_form.PRW_firstname.SetText(self.form_DTD['firstnames'])
2834 _pnl_form.PRW_title.SetText(self.form_DTD['title'])
2835 _pnl_form.PRW_nick.SetText(self.form_DTD['nick'])
2836 _pnl_form.PRW_occupation.SetText(self.form_DTD['occupation'])
2837 _pnl_form.TTC_address_number.SetValue(self.form_DTD['address_number'])
2838 _pnl_form.PRW_street.SetText(self.form_DTD['street'])
2839 _pnl_form.PRW_zip_code.SetText(self.form_DTD['zip_code'])
2840 _pnl_form.PRW_town.SetText(self.form_DTD['town'])
2841 _pnl_form.PRW_state.SetData(self.form_DTD['state'])
2842 _pnl_form.PRW_country.SetData(self.form_DTD['country'])
2843 _pnl_form.TTC_phone.SetValue(self.form_DTD['phone'])
2844 _pnl_form.TCTRL_comment.SetValue(self.form_DTD['comment'])
2845 return True
2846
2848 """
2849 Transfer data from window to validator.
2850 The default implementation returns False, indicating that an error
2851 occurred. We simply return True, as we don't do any data transfer.
2852 """
2853
2854 if not self.GetWindow().GetParent().Validate():
2855 return False
2856 try:
2857 _pnl_form = self.GetWindow().GetParent()
2858
2859 self.form_DTD['gender'] = _pnl_form.PRW_gender.GetData()
2860 self.form_DTD['dob'] = _pnl_form.PRW_dob.GetData()
2861
2862 self.form_DTD['lastnames'] = _pnl_form.PRW_lastname.GetValue()
2863 self.form_DTD['firstnames'] = _pnl_form.PRW_firstname.GetValue()
2864 self.form_DTD['title'] = _pnl_form.PRW_title.GetValue()
2865 self.form_DTD['nick'] = _pnl_form.PRW_nick.GetValue()
2866
2867 self.form_DTD['occupation'] = _pnl_form.PRW_occupation.GetValue()
2868
2869 self.form_DTD['address_number'] = _pnl_form.TTC_address_number.GetValue()
2870 self.form_DTD['street'] = _pnl_form.PRW_street.GetValue()
2871 self.form_DTD['zip_code'] = _pnl_form.PRW_zip_code.GetValue()
2872 self.form_DTD['town'] = _pnl_form.PRW_town.GetValue()
2873 self.form_DTD['state'] = _pnl_form.PRW_state.GetData()
2874 self.form_DTD['country'] = _pnl_form.PRW_country.GetData()
2875
2876 self.form_DTD['phone'] = _pnl_form.TTC_phone.GetValue()
2877
2878 self.form_DTD['comment'] = _pnl_form.TCTRL_comment.GetValue()
2879 except:
2880 return False
2881 return True
2882
2927
2928
2929
2931 """Notebook displaying demographics editing pages:
2932
2933 - Identity
2934 - Contacts (addresses, phone numbers, etc)
2935
2936 Does NOT act on/listen to the current patient.
2937 """
2938
2940
2941 wx.Notebook.__init__ (
2942 self,
2943 parent = parent,
2944 id = id,
2945 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
2946 name = self.__class__.__name__
2947 )
2948
2949 self.__identity = None
2950 self.__do_layout()
2951 self.SetSelection(0)
2952
2953
2954
2956 """Populate fields in pages with data from model."""
2957 for page_idx in range(self.GetPageCount()):
2958 page = self.GetPage(page_idx)
2959 page.identity = self.__identity
2960
2961 return True
2962
2963
2964
2966 """Build patient edition notebook pages."""
2967
2968 new_page = cPersonContactsManagerPnl(self, -1)
2969 new_page.identity = self.__identity
2970 self.AddPage (
2971 page = new_page,
2972 text = _('Contacts'),
2973 select = True
2974 )
2975
2976
2977 new_page = cPersonIdentityManagerPnl(self, -1)
2978 new_page.identity = self.__identity
2979 self.AddPage (
2980 page = new_page,
2981 text = _('Identity'),
2982 select = False
2983 )
2984
2985
2986
2988 return self.__identity
2989
2992
2993 identity = property(_get_identity, _set_identity)
2994
2995
2996
2997
2999 """Page containing patient occupations edition fields.
3000 """
3001 - def __init__(self, parent, id, ident=None):
3002 """
3003 Creates a new instance of BasicPatDetailsPage
3004 @param parent - The parent widget
3005 @type parent - A wx.Window instance
3006 @param id - The widget id
3007 @type id - An integer
3008 """
3009 wx.Panel.__init__(self, parent, id)
3010 self.__ident = ident
3011 self.__do_layout()
3012
3014 PNL_form = wx.Panel(self, -1)
3015
3016 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
3017 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
3018 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
3019
3020 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
3021 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
3022
3023
3024 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
3025 SZR_input.AddGrowableCol(1)
3026 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
3027 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
3028 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
3029 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
3030 PNL_form.SetSizerAndFit(SZR_input)
3031
3032
3033 SZR_main = wx.BoxSizer(wx.VERTICAL)
3034 SZR_main.Add(PNL_form, 1, wx.EXPAND)
3035 self.SetSizer(SZR_main)
3036
3039
3040 - def refresh(self, identity=None):
3041 if identity is not None:
3042 self.__ident = identity
3043 jobs = self.__ident.get_occupations()
3044 if len(jobs) > 0:
3045 self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
3046 self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
3047 return True
3048
3050 if self.PRW_occupation.IsModified():
3051 new_job = self.PRW_occupation.GetValue().strip()
3052 jobs = self.__ident.get_occupations()
3053 for job in jobs:
3054 if job['l10n_occupation'] == new_job:
3055 continue
3056 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
3057 self.__ident.link_occupation(occupation = new_job)
3058 return True
3059
3061 """Patient demographics plugin for main notebook.
3062
3063 Hosts another notebook with pages for Identity, Contacts, etc.
3064
3065 Acts on/listens to the currently active patient.
3066 """
3067
3073
3074
3075
3076
3077
3078
3080 """Arrange widgets."""
3081 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
3082
3083 szr_main = wx.BoxSizer(wx.VERTICAL)
3084 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
3085 self.SetSizerAndFit(szr_main)
3086
3087
3088
3090 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
3091 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
3092
3094 self._schedule_data_reget()
3095
3097 self._schedule_data_reget()
3098
3099
3100
3110
3112 """
3113 Register a new patient, given the data supplied in the
3114 Data Transfer Dictionary object.
3115
3116 @param basic_details_DTD Data Transfer Dictionary encapsulating all the
3117 supplied data.
3118 @type basic_details_DTD A cFormDTD instance.
3119 """
3120 new_identity = gmPerson.create_identity (
3121 gender = dtd['gender'],
3122 dob = dtd['dob'].get_pydt(),
3123 lastnames = dtd['lastnames'],
3124 firstnames = dtd['firstnames']
3125 )
3126 if new_identity is None:
3127 _log.error('cannot create identity from %s' % str(dtd))
3128 return None
3129 _log.debug('identity created: %s' % new_identity)
3130
3131 if dtd['comment'] is not None:
3132 if dtd['comment'].strip() != u'':
3133 name = new_identity.get_active_name()
3134 name['comment'] = dtd['comment']
3135 name.save_payload()
3136
3137 return new_identity
3138
3140 """
3141 Update patient details with data supplied by
3142 Data Transfer Dictionary object.
3143
3144 @param basic_details_DTD Data Transfer Dictionary encapsulating all the
3145 supplied data.
3146 @type basic_details_DTD A cFormDTD instance.
3147 """
3148
3149 if identity['gender'] != dtd['gender']:
3150 identity['gender'] = dtd['gender']
3151 if identity['dob'] != dtd['dob'].get_pydt():
3152 identity['dob'] = dtd['dob'].get_pydt()
3153 if len(dtd['title']) > 0 and identity['title'] != dtd['title']:
3154 identity['title'] = dtd['title']
3155
3156
3157
3158
3159 identity.save_payload()
3160
3161
3162
3163 if identity['firstnames'] != dtd['firstnames'] or identity['lastnames'] != dtd['lastnames']:
3164 new_name = identity.add_name(firstnames = dtd['firstnames'], lastnames = dtd['lastnames'], active = True)
3165
3166 if len(dtd['nick']) > 0 and identity['preferred'] != dtd['nick']:
3167 identity.set_nickname(nickname = dtd['nick'])
3168
3169 return True
3170
3214
3216 """
3217 Update patient details with data supplied by
3218 Data Transfer Dictionary object.
3219
3220 @param basic_details_DTD Data Transfer Dictionary encapsulating all the
3221 supplied data.
3222 @type basic_details_DTD A cFormDTD instance.
3223 """
3224 identity.link_occupation(occupation = dtd['occupation'])
3225
3226 return True
3227
3229 """
3230 Utility class to test the new patient wizard.
3231 """
3232
3234 """
3235 Create a new instance of TestPanel.
3236 @param parent The parent widget
3237 @type parent A wx.Window instance
3238 """
3239 wx.Panel.__init__(self, parent, id)
3240 wizard = cNewPatientWizard(self)
3241 print wizard.RunWizard()
3242
3243 if __name__ == "__main__":
3244
3245
3247 app = wx.PyWidgetTester(size = (200, 50))
3248 pw = cZipcodePhraseWheel(app.frame, -1)
3249 app.frame.Show(True)
3250 app.MainLoop()
3251
3253 app = wx.PyWidgetTester(size = (200, 50))
3254 pw = cStateSelectionPhraseWheel(app.frame, -1)
3255
3256
3257 app.frame.Show(True)
3258 app.MainLoop()
3259
3261 app = wx.PyWidgetTester(size = (200, 50))
3262 pw = cUrbPhraseWheel(app.frame, -1)
3263 app.frame.Show(True)
3264 pw.set_context(context = u'zip', val = u'04317')
3265 app.MainLoop()
3266
3268 app = wx.PyWidgetTester(size = (200, 50))
3269 pw = cSuburbPhraseWheel(app.frame, -1)
3270 app.frame.Show(True)
3271 app.MainLoop()
3272
3274 app = wx.PyWidgetTester(size = (200, 50))
3275 pw = cAddressTypePhraseWheel(app.frame, -1)
3276 app.frame.Show(True)
3277 app.MainLoop()
3278
3280 app = wx.PyWidgetTester(size = (200, 50))
3281 pw = cAddressPhraseWheel(app.frame, -1)
3282 app.frame.Show(True)
3283 app.MainLoop()
3284
3286 app = wx.PyWidgetTester(size = (200, 50))
3287 pw = cStreetPhraseWheel(app.frame, -1)
3288
3289 app.frame.Show(True)
3290 app.MainLoop()
3291
3293 app = wx.PyWidgetTester(size = (600, 400))
3294 app.SetWidget(cKOrganizerSchedulePnl)
3295 app.MainLoop()
3296
3298 app = wx.PyWidgetTester(size = (600, 400))
3299 widget = cPersonNamesManagerPnl(app.frame, -1)
3300 widget.identity = activate_patient()
3301 app.frame.Show(True)
3302 app.MainLoop()
3303
3305 app = wx.PyWidgetTester(size = (600, 400))
3306 widget = cPersonIDsManagerPnl(app.frame, -1)
3307 widget.identity = activate_patient()
3308 app.frame.Show(True)
3309 app.MainLoop()
3310
3312 app = wx.PyWidgetTester(size = (600, 400))
3313 widget = cPersonIdentityManagerPnl(app.frame, -1)
3314 widget.identity = activate_patient()
3315 app.frame.Show(True)
3316 app.MainLoop()
3317
3322
3327
3329 app = wx.PyWidgetTester(size = (600, 400))
3330 widget = cPersonAddressesManagerPnl(app.frame, -1)
3331 widget.identity = activate_patient()
3332 app.frame.Show(True)
3333 app.MainLoop()
3334
3336 app = wx.PyWidgetTester(size = (600, 400))
3337 widget = cPersonCommsManagerPnl(app.frame, -1)
3338 widget.identity = activate_patient()
3339 app.frame.Show(True)
3340 app.MainLoop()
3341
3348
3350 app = wx.PyWidgetTester(size = (600, 400))
3351 widget = cPersonDemographicsEditorNb(app.frame, -1)
3352 widget.identity = activate_patient()
3353 widget.refresh()
3354 app.frame.Show(True)
3355 app.MainLoop()
3356
3365
3366 if len(sys.argv) > 1 and sys.argv[1] == 'test':
3367
3368 gmI18N.activate_locale()
3369 gmI18N.install_domain(domain='gnumed')
3370 gmPG2.get_connection()
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387 test_urb_prw()
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315