[med-svn] [gnumed-client] 01/06: Imported Upstream version 1.6.5+dfsg

Andreas Tille tille at debian.org
Mon Apr 25 22:43:30 UTC 2016


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository gnumed-client.

commit 4eefb56b1028fd39fed5fe882945fdf1051354fc
Author: Andreas Tille <tille at debian.org>
Date:   Tue Apr 26 00:21:19 2016 +0200

    Imported Upstream version 1.6.5+dfsg
---
 client/CHANGELOG                                   |  15 ++
 client/business/gmHL7.py                           | 162 +-------------
 client/business/gmIncomingData.py                  | 203 +++++++++++++++++
 client/doc/gm-import_incoming.1                    | 119 ++++++++++
 client/doc/schema/gnumed-entire_schema.html        |   2 +-
 client/exporters/gmPatientExporter.py              |  22 +-
 client/gnumed                                      |  28 +--
 client/gnumed.py                                   |   2 +-
 client/importers/__init__.py                       |   1 +
 client/importers/gmImportIncoming.py               | 225 +++++++++++++++++++
 client/po/es-gnumed.mo                             | Bin 510162 -> 510739 bytes
 client/po/es.po                                    |  64 +++---
 client/pycommon/gmPG2.py                           |  13 +-
 client/pycommon/gmTools.py                         |  11 +-
 .../wxgSelectablySortedDocTreePnl.py               | 169 +++++++-------
 client/wxpython/gmDocumentWidgets.py               |  53 ++++-
 client/wxpython/gmListWidgets.py                   | 249 ++++++++++++++++-----
 client/wxpython/gmMeasurementWidgets.py            |   5 +-
 external-tools/gm-describe_file                    |  16 +-
 external-tools/gm-import_incoming                  |  26 +++
 20 files changed, 1029 insertions(+), 356 deletions(-)

diff --git a/client/CHANGELOG b/client/CHANGELOG
index f2a542f..715f572 100644
--- a/client/CHANGELOG
+++ b/client/CHANGELOG
@@ -6,6 +6,21 @@
 # rel-1-6-patches
 ------------------------------------------------
 
+	1.6.5
+
+IMPROVED: list context menu: operate on _selected_ rows
+
+	1.6.4
+
+FIX: EMR journal exporter on Windows [thanks Marc]
+
+IMPROVED: by-org sort mode in document tree
+IMPROVED: file describer script
+IMPROVED: STIKO tetanus auto hint
+IMPROVED: ES translation [thanks Uwe]
+
+NEW: gm-import_incoming script for external use
+
 	1.6.3
 
 FIX: exception on creating invoice from bill [thanks Marc]
diff --git a/client/business/gmHL7.py b/client/business/gmHL7.py
index 8e8fef8..784c7a8 100644
--- a/client/business/gmHL7.py
+++ b/client/business/gmHL7.py
@@ -28,6 +28,7 @@ from Gnumed.pycommon import gmBusinessDBObject
 from Gnumed.pycommon import gmPG2
 from Gnumed.pycommon import gmDateTime
 
+from Gnumed.business import gmIncomingData
 from Gnumed.business import gmPathLab
 from Gnumed.business import gmPerson
 from Gnumed.business import gmPraxis
@@ -171,156 +172,6 @@ HL7_GENDERS = {
 }
 
 #============================================================
-# class to handle unmatched incoming clinical data
-#------------------------------------------------------------
-_SQL_get_incoming_data = u"""SELECT * FROM clin.v_incoming_data_unmatched WHERE %s"""
-
-class cIncomingData(gmBusinessDBObject.cBusinessDBObject):
-	"""Represents items of incoming data, say, HL7 snippets."""
-
-	_cmd_fetch_payload = _SQL_get_incoming_data % u"pk_incoming_data_unmatched = %s"
-	_cmds_store_payload = [
-		u"""UPDATE clin.incoming_data_unmatched SET
-				fk_patient_candidates = %(pk_patient_candidates)s,
-				fk_identity_disambiguated = %(pk_identity_disambiguated)s,
-				fk_provider_disambiguated = %(pk_provider_disambiguated)s,
-				request_id = gm.nullify_empty_string(%(request_id)s),
-				firstnames = gm.nullify_empty_string(%(firstnames)s),
-				lastnames = gm.nullify_empty_string(%(lastnames)s),
-				dob = %(dob)s,
-				postcode = gm.nullify_empty_string(%(postcode)s),
-				other_info = gm.nullify_empty_string(%(other_info)s),
-				type = gm.nullify_empty_string(%(data_type)s),
-				gender = gm.nullify_empty_string(%(gender)s),
-				requestor = gm.nullify_empty_string(%(requestor)s),
-				external_data_id = gm.nullify_empty_string(%(external_data_id)s),
-				comment = gm.nullify_empty_string(%(comment)s)
-			WHERE
-				pk = %(pk_incoming_data_unmatched)s
-					AND
-				xmin = %(xmin_incoming_data_unmatched)s
-			RETURNING
-				xmin as xmin_incoming_data_unmatched,
-				octet_length(data) as data_size
-		"""
-	]
-	# view columns that can be updated:
-	_updatable_fields = [
-		u'pk_patient_candidates',
-		u'request_id',						# request ID as found in <data>
-		u'firstnames',
-		u'lastnames',
-		u'dob',
-		u'postcode',
-		u'other_info',						# other identifying info in .data
-		u'data_type',
-		u'gender',
-		u'requestor',						# Requestor of data (e.g. who ordered test results) if available in source data.
-		u'external_data_id',				# ID of content of .data in external system (e.g. importer) where appropriate
-		u'comment',							# a free text comment on this row, eg. why is it here, error logs etc
-		u'pk_identity_disambiguated',
-		u'pk_provider_disambiguated'		# The provider the data is relevant to.
-	]
-	#--------------------------------------------------------
-	def format(self):
-		return u'%s' % self
-	#--------------------------------------------------------
-	def _format_patient_identification(self):
-		tmp = u'%s %s %s' % (
-			gmTools.coalesce(self._payload[self._idx['lastnames']], u'', u'last=%s'),
-			gmTools.coalesce(self._payload[self._idx['firstnames']], u'', u'first=%s'),
-			gmTools.coalesce(self._payload[self._idx['gender']], u'', u'gender=%s')
-		)
-		if self._payload[self._idx['dob']] is not None:
-			tmp += u' dob=%s' % gmDateTime.pydt_strftime(self._payload[self._idx['dob']], '%Y %b %d')
-		return tmp
-
-	patient_identification = property(_format_patient_identification, lambda x:x)
-	#--------------------------------------------------------
-	def update_data_from_file(self, fname=None):
-		# sanity check
-		if not (os.access(fname, os.R_OK) and os.path.isfile(fname)):
-			_log.error('[%s] is not a readable file' % fname)
-			return False
-
-		gmPG2.file2bytea (
-			query = u"UPDATE clin.incoming_data_unmatched SET data = %(data)s::bytea WHERE pk = %(pk)s",
-			filename = fname,
-			args = {'pk': self.pk_obj}
-		)
-
-		# must update XMIN now ...
-		self.refetch_payload()
-		return True
-
-	#--------------------------------------------------------
-	def export_to_file(self, aChunkSize=0, filename=None):
-
-		if self._payload[self._idx['data_size']] == 0:
-			return None
-
-		if self._payload[self._idx['data_size']] is None:
-			return None
-
-		if filename is None:
-			filename = gmTools.get_unique_filename(prefix = 'gm-incoming_data_unmatched-')
-
-		success = gmPG2.bytea2file (
-			data_query = {
-				'cmd': u'SELECT substring(data from %(start)s for %(size)s) FROM clin.incoming_data_unmatched WHERE pk = %(pk)s',
-				'args': {'pk': self.pk_obj}
-			},
-			filename = filename,
-			chunk_size = aChunkSize,
-			data_size = self._payload[self._idx['data_size']]
-		)
-
-		if not success:
-			return None
-
-		return filename
-
-	#--------------------------------------------------------
-	def lock(self, exclusive=False):
-		return gmPG2.lock_row(table = u'clin.incoming_data_unmatched', pk = self.pk_obj, exclusive = exclusive)
-
-	#--------------------------------------------------------
-	def unlock(self, exclusive=False):
-		return gmPG2.unlock_row(table = u'clin.incoming_data_unmatched', pk = self.pk_obj, exclusive = exclusive)
-
-#------------------------------------------------------------
-def get_incoming_data(order_by=None):
-	if order_by is None:
-		order_by = u'true'
-	else:
-		order_by = u'true ORDER BY %s' % order_by
-	cmd = _SQL_get_incoming_data % order_by
-	rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
-	return [ cIncomingData(row = {'data': r, 'idx': idx, 'pk_field': 'pk_incoming_data_unmatched'}) for r in rows ]
-
-#------------------------------------------------------------
-def create_incoming_data(data_type, filename):
-	args = {'typ': data_type}
-	cmd = u"""
-		INSERT INTO clin.incoming_data_unmatched (type, data)
-		VALUES (%(typ)s, 'new data'::bytea)
-		RETURNING pk"""
-	rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
-	pk = rows[0]['pk']
-	incoming = cIncomingData(aPK_obj = pk)
-	if not incoming.update_data_from_file(fname = filename):
-		delete_incoming_data(incoming_data = pk)
-		return None
-	return incoming
-
-#------------------------------------------------------------
-def delete_incoming_data(pk_incoming_data=None):
-	args = {'pk': pk_incoming_data}
-	cmd = u"DELETE FROM clin.incoming_data_unmatched WHERE pk = %(pk)s"
-	gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
-	return True
-
-#============================================================
 # public API
 #============================================================
 def extract_HL7_from_XML_CDATA(filename, xml_path, target_dir=None):
@@ -557,7 +408,7 @@ def stage_single_PID_hl7_file(filename, source=None, encoding='utf8'):
 
 	# stage
 	try:
-		incoming = create_incoming_data(u'HL7%s' % gmTools.coalesce(source, u'', u' (%s)'), filename)
+		incoming = gmIncomingData.create_incoming_data(u'HL7%s' % gmTools.coalesce(source, u'', u' (%s)'), filename)
 		if incoming is None:
 			_log.error(u'cannot stage PID file: %s', filename)
 			root_logger.removeHandler(local_logger)
@@ -654,7 +505,7 @@ def process_staged_single_PID_hl7_file(staged_item):
 	try:
 		success = __import_single_PID_hl7_file(filename, emr = emr)
 		if success:
-			delete_incoming_data(pk_incoming_data = staged_item['pk_incoming_data_unmatched'])
+			gmIncomingData.delete_incoming_data(pk_incoming_data = staged_item['pk_incoming_data_unmatched'])
 			staged_item.unlock()
 			root_logger.removeHandler(import_logger)
 			return True, log_name
@@ -1367,7 +1218,7 @@ def __stage_MSH_as_incoming_data(filename, source=None, logfile=None):
 	del raw_hl7
 
 	# import file
-	incoming = create_incoming_data(u'HL7%s' % gmTools.coalesce(source, u'', u' (%s)'), filename)
+	incoming = gmIncomingData.create_incoming_data(u'HL7%s' % gmTools.coalesce(source, u'', u' (%s)'), filename)
 	if incoming is None:
 		return None
 	incoming.update_data_from_file(fname = filename)
@@ -1441,10 +1292,6 @@ if __name__ == "__main__":
 		#for name in PID_fnames:
 		#	print " ", name
 	#-------------------------------------------------------
-	def test_incoming_data():
-		for d in get_incoming_data():
-			print d
-	#-------------------------------------------------------
 	def test_stage_hl7_from_xml():
 		hl7 = extract_HL7_from_XML_CDATA(sys.argv[2], u'.//Message')
 		print "HL7:", hl7
@@ -1527,7 +1374,6 @@ if __name__ == "__main__":
 	#-------------------------------------------------------
 	#test_import_HL7(sys.argv[2])
 	#test_xml_extract()
-	#test_incoming_data()
 	#test_stage_hl7_from_xml()
 	#test_stage_hl7()
 	#test_format_hl7_message()
diff --git a/client/business/gmIncomingData.py b/client/business/gmIncomingData.py
new file mode 100644
index 0000000..37a1c64
--- /dev/null
+++ b/client/business/gmIncomingData.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+"""Handling of <INCOMING> area."""
+#============================================================
+__author__ = "K.Hilbert <Karsten.Hilbert at gmx.net>"
+__license__ = "GPL v2 or later"
+
+
+import sys
+import os
+import logging
+
+
+if __name__ == '__main__':
+	sys.path.insert(0, '../../')
+
+from Gnumed.pycommon import gmI18N
+if __name__ == '__main__':
+	gmI18N.activate_locale()
+	gmI18N.install_domain()
+from Gnumed.pycommon import gmTools
+from Gnumed.pycommon import gmBusinessDBObject
+from Gnumed.pycommon import gmPG2
+from Gnumed.pycommon import gmDateTime
+
+
+_log = logging.getLogger('gm.import')
+
+#============================================================
+# class to handle unmatched incoming clinical data
+#------------------------------------------------------------
+_SQL_get_incoming_data = u"""SELECT * FROM clin.v_incoming_data_unmatched WHERE %s"""
+
+class cIncomingData(gmBusinessDBObject.cBusinessDBObject):
+	"""Represents items of incoming data, say, HL7 snippets."""
+
+	_cmd_fetch_payload = _SQL_get_incoming_data % u"pk_incoming_data_unmatched = %s"
+	_cmds_store_payload = [
+		u"""UPDATE clin.incoming_data_unmatched SET
+				fk_patient_candidates = %(pk_patient_candidates)s,
+				fk_identity_disambiguated = %(pk_identity_disambiguated)s,
+				fk_provider_disambiguated = %(pk_provider_disambiguated)s,
+				request_id = gm.nullify_empty_string(%(request_id)s),
+				firstnames = gm.nullify_empty_string(%(firstnames)s),
+				lastnames = gm.nullify_empty_string(%(lastnames)s),
+				dob = %(dob)s,
+				postcode = gm.nullify_empty_string(%(postcode)s),
+				other_info = gm.nullify_empty_string(%(other_info)s),
+				type = gm.nullify_empty_string(%(data_type)s),
+				gender = gm.nullify_empty_string(%(gender)s),
+				requestor = gm.nullify_empty_string(%(requestor)s),
+				external_data_id = gm.nullify_empty_string(%(external_data_id)s),
+				comment = gm.nullify_empty_string(%(comment)s)
+			WHERE
+				pk = %(pk_incoming_data_unmatched)s
+					AND
+				xmin = %(xmin_incoming_data_unmatched)s
+			RETURNING
+				xmin as xmin_incoming_data_unmatched,
+				octet_length(data) as data_size
+		"""
+	]
+	# view columns that can be updated:
+	_updatable_fields = [
+		u'pk_patient_candidates',
+		u'request_id',						# request ID as found in <data>
+		u'firstnames',
+		u'lastnames',
+		u'dob',
+		u'postcode',
+		u'other_info',						# other identifying info in .data
+		u'data_type',
+		u'gender',
+		u'requestor',						# Requestor of data (e.g. who ordered test results) if available in source data.
+		u'external_data_id',				# ID of content of .data in external system (e.g. importer) where appropriate
+		u'comment',							# a free text comment on this row, eg. why is it here, error logs etc
+		u'pk_identity_disambiguated',
+		u'pk_provider_disambiguated'		# The provider the data is relevant to.
+	]
+	#--------------------------------------------------------
+	def format(self):
+		return u'%s' % self
+	#--------------------------------------------------------
+	def _format_patient_identification(self):
+		tmp = u'%s %s %s' % (
+			gmTools.coalesce(self._payload[self._idx['lastnames']], u'', u'last=%s'),
+			gmTools.coalesce(self._payload[self._idx['firstnames']], u'', u'first=%s'),
+			gmTools.coalesce(self._payload[self._idx['gender']], u'', u'gender=%s')
+		)
+		if self._payload[self._idx['dob']] is not None:
+			tmp += u' dob=%s' % gmDateTime.pydt_strftime(self._payload[self._idx['dob']], '%Y %b %d')
+		return tmp
+
+	patient_identification = property(_format_patient_identification, lambda x:x)
+
+	#--------------------------------------------------------
+	def update_data_from_file(self, fname=None):
+		# sanity check
+		if not (os.access(fname, os.R_OK) and os.path.isfile(fname)):
+			_log.error('[%s] is not a readable file' % fname)
+			return False
+
+		_log.debug('updating [pk=%s] from [%s]', self.pk_obj, fname)
+		gmPG2.file2bytea (
+			query = u"UPDATE clin.incoming_data_unmatched SET data = %(data)s::bytea WHERE pk = %(pk)s",
+			filename = fname,
+			args = {'pk': self.pk_obj}
+		)
+
+		# must update XMIN now ...
+		self.refetch_payload()
+		return True
+
+	#--------------------------------------------------------
+	def export_to_file(self, aChunkSize=0, filename=None):
+
+		if self._payload[self._idx['data_size']] == 0:
+			return None
+
+		if self._payload[self._idx['data_size']] is None:
+			return None
+
+		if filename is None:
+			filename = gmTools.get_unique_filename(prefix = 'gm-incoming_data_unmatched-')
+
+		success = gmPG2.bytea2file (
+			data_query = {
+				'cmd': u'SELECT substring(data from %(start)s for %(size)s) FROM clin.incoming_data_unmatched WHERE pk = %(pk)s',
+				'args': {'pk': self.pk_obj}
+			},
+			filename = filename,
+			chunk_size = aChunkSize,
+			data_size = self._payload[self._idx['data_size']]
+		)
+
+		if not success:
+			return None
+
+		return filename
+
+	#--------------------------------------------------------
+	def lock(self, exclusive=False):
+		return gmPG2.lock_row(table = u'clin.incoming_data_unmatched', pk = self.pk_obj, exclusive = exclusive)
+
+	#--------------------------------------------------------
+	def unlock(self, exclusive=False):
+		return gmPG2.unlock_row(table = u'clin.incoming_data_unmatched', pk = self.pk_obj, exclusive = exclusive)
+
+#------------------------------------------------------------
+def get_incoming_data(order_by=None):
+	if order_by is None:
+		order_by = u'true'
+	else:
+		order_by = u'true ORDER BY %s' % order_by
+	cmd = _SQL_get_incoming_data % order_by
+	rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
+	return [ cIncomingData(row = {'data': r, 'idx': idx, 'pk_field': 'pk_incoming_data_unmatched'}) for r in rows ]
+
+#------------------------------------------------------------
+def create_incoming_data(data_type, filename):
+	args = {'typ': data_type}
+	cmd = u"""
+		INSERT INTO clin.incoming_data_unmatched (type, data)
+		VALUES (%(typ)s, 'new data'::bytea)
+		RETURNING pk"""
+	rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
+	pk = rows[0]['pk']
+	incoming = cIncomingData(aPK_obj = pk)
+	if not incoming.update_data_from_file(fname = filename):
+		_log.debug('cannot update newly created incoming_data record from file, deleting stub')
+		delete_incoming_data(incoming_data = pk)
+		return None
+	return incoming
+
+#------------------------------------------------------------
+def delete_incoming_data(pk_incoming_data=None):
+	args = {'pk': pk_incoming_data}
+	cmd = u"DELETE FROM clin.incoming_data_unmatched WHERE pk = %(pk)s"
+	gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
+	return True
+
+#============================================================
+# main
+#------------------------------------------------------------
+if __name__ == "__main__":
+
+	if len(sys.argv) < 2:
+		sys.exit()
+
+	if sys.argv[1] != 'test':
+		sys.exit()
+
+	from Gnumed.pycommon import gmLog2
+
+	gmDateTime.init()
+	gmTools.gmPaths()
+
+	#-------------------------------------------------------
+	def test_incoming_data():
+		for d in get_incoming_data():
+			print d
+
+	#-------------------------------------------------------
+	test_incoming_data()
diff --git a/client/doc/gm-import_incoming.1 b/client/doc/gm-import_incoming.1
new file mode 100644
index 0000000..b846298
--- /dev/null
+++ b/client/doc/gm-import_incoming.1
@@ -0,0 +1,119 @@
+.\" ========================================================
+.\" license: GPL v2 or later
+.\" ========================================================
+
+.TH gm-import_incoming 1 "2016 April 19th" "gm-import_incoming"
+
+.SH NAME
+.B gm-import_incoming
+- a script to import a file into the INCOMING area of a GNUmed database.
+
+
+.SH SYNOPSIS
+.B gm-import_incoming
+.RB [-h|?]
+.RB [--help]
+.RB [--local-import]
+.RB --file2import=FILE
+.RB --data-type=TYPE
+.RB --user=USER
+.RB [--host=HOST]
+.RB [--port=PORT]
+
+
+.SH DESCRIPTION
+.B gm-import_incoming
+is a script for importing a file into the GNUmed database
+area where incoming files are stored which have not been
+linked to any patient. A type is associated with the data for
+easier recognition.
+
+This script can be used to import for later processing fax
+images, lab data, referral letters, and similar data which
+electronically arrives on your system.
+
+
+.SH OPTIONS
+.PP
+.TP
+.B \--help, -h, -?
+Show a help screen.
+.TP
+.B \--local-import
+At startup adjust the PYTHONPATH such that the GNUmed client
+is run from a local copy of the source tree (say, an unpacked
+tarball) rather than from a proper system-wide installation.
+.TP
+.B \--file2import=FILE
+This is the file to be imported into GNUmed.
+
+Successfully imported files are renamed to FILE.imported
+within the directory in which FILE resides.
+.TP
+.B \--data-type=TYPE
+A short moniker, say, a word or two, used to describe the
+data in FILE to the user when viewed inside GNUmed.
+
+The actual content of TYPE is only limited by the execution
+environment (locale, encoding, ...) of gm-import_incoming.
+GNUmed does not need to understand what TYPE means to the
+user.
+.TP
+.B \--user=USER
+The PostgreSQL user to be used for connecting to the
+database.
+
+Note that this must currently be a GNUmed staff account. It
+will also work with the cluster superuser (usually
+"postgres") or the GNUmed database owner (typically "gm-dbo")
+but using these in production is
+.B strongly
+discouraged for data security reasons.
+.TP
+.B \--host=HOST
+The hostname of the machine PostgreSQL is running on, if
+required.
+
+If this option is not used (or set to an empty string) it
+will default to connecting over UNIX domain sockets.
+.TP
+.B \--port=PORT
+The port PostgreSQL is listening on. Default PostgreSQL
+installations listen on port 5432.
+.PP
+There are no options for database name or password.
+
+The script uses the default database name of the GNUmed
+version the script is released with thereby assuring data
+does not get imported into an older database following an
+upgrade.
+
+If a password is needed (that is, if TRUST, IDENT, or PEER
+authentication is not in use) it must be supplied by either
+setting the $PGPASSFILE environment variable or using a
+standard ~/.pgpass file.
+
+
+.SH ENVIRONMENT
+.TP
+.B PGPASSFILE
+.PP
+	http://www.postgresql.org/docs/devel/static/libpq-pgpass.html
+
+.SH FILES
+.B ~/.pgpass
+.PP
+	http://www.postgresql.org/docs/devel/static/libpq-pgpass.html
+
+
+.SH SEE ALSO
+.PP
+.TP
+.B http://wiki.gnumed.de
+Detailed Wiki-style documentation
+.TP
+.B /usr/share/doc/gnumed/
+Local documentation
+.TP
+.B man -k gm-*
+List man pages on gm-* commands.
diff --git a/client/doc/schema/gnumed-entire_schema.html b/client/doc/schema/gnumed-entire_schema.html
index 95b0a78..c421c7c 100644
--- a/client/doc/schema/gnumed-entire_schema.html
+++ b/client/doc/schema/gnumed-entire_schema.html
@@ -112,7 +112,7 @@
   <body>
 
     <!-- Primary Index -->
-	<p><br><br>Dumped on 2016-04-06</p>
+	<p><br><br>Dumped on 2016-04-25</p>
 <h1><a name="index">Index of database - gnumed_v21</a></h1>
 <ul>
     
diff --git a/client/exporters/gmPatientExporter.py b/client/exporters/gmPatientExporter.py
index 2db8ded..0fedba0 100644
--- a/client/exporters/gmPatientExporter.py
+++ b/client/exporters/gmPatientExporter.py
@@ -19,10 +19,10 @@ import shutil
 
 if __name__ == '__main__':
 	sys.path.insert(0, '../../')
-from Gnumed.pycommon import gmI18N
-if __name__ == '__main__':
+	from Gnumed.pycommon import gmI18N
 	gmI18N.activate_locale()
 	gmI18N.install_domain()
+
 from Gnumed.pycommon import gmExceptions
 from Gnumed.pycommon import gmPG2
 from Gnumed.pycommon import gmTools
@@ -940,7 +940,7 @@ class cEMRJournalExporter:
 		f.write(u'\n')
 		f.write(_('Patient: %s (%s), No: %s\n') % (patient['description'], patient['gender'], patient['pk_identity']))
 		f.write(_('Born   : %s, age: %s\n\n') % (
-			patient.get_formatted_dob(format = '%Y %b %d', encoding = gmI18N.get_encoding()),
+			patient.get_formatted_dob(format = '%Y %b %d', encoding = 'utf8'),
 			patient.get_medical_age()
 		))
 
@@ -1017,14 +1017,14 @@ class cEMRJournalExporter:
 		@type target: a python object supporting the write() API
 		@type patient: <cPerson> instance
 		"""
-		txt = _('Chronological EMR Journal\n')
+		txt = _(u'Chronological EMR Journal\n')
 		target.write(txt)
 		target.write(u'=' * (len(txt)-1))
 		target.write(u'\n')
 		# demographics
-		target.write(_('Patient: %s (%s), No: %s\n') % (patient['description'], patient['gender'], patient['pk_identity']))
-		target.write(_('Born   : %s, age: %s\n\n') % (
-			patient.get_formatted_dob(format = '%Y %b %d', encoding = gmI18N.get_encoding()),
+		target.write(_(u'Patient: %s (%s), No: %s\n') % (patient['description'], patient['gender'], patient['pk_identity']))
+		target.write(_(u'Born   : %s, age: %s\n\n') % (
+			patient.get_formatted_dob(format = '%Y %b %d', encoding = 'utf8'),
 			patient.get_medical_age()
 		))
 		for ext_id in patient.external_ids:
@@ -1044,12 +1044,12 @@ class cEMRJournalExporter:
 		))
 		target.write(u'%s %10.10s %s %9.9s %s     %s %s\n' % (
 			gmTools.u_box_vert_light,
-			_('Encounter'),
+			_(u'Encounter'),
 			gmTools.u_box_vert_light,
-			_('Doc'),
+			_(u'Doc'),
 			gmTools.u_box_vert_light,
 			gmTools.u_box_vert_light,
-			_('Narrative')
+			_(u'Narrative')
 		))
 		target.write(u'%s%12.12s%s%11.11s%s%s%s%72.72s\n' % (
 			gmTools.u_box_T_right,
@@ -1153,7 +1153,7 @@ class cEMRJournalExporter:
 			gmTools.u_box_horiz_single * self.__narrative_wrap_len
 		))
 
-		target.write(_('Exported: %s\n') % gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %b %d  %H:%M:%S'))
+		target.write(_(u'Exported: %s\n') % gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %b %d  %H:%M:%S'))
 
 		return
 
diff --git a/client/gnumed b/client/gnumed
index a451d6c..9ffa23a 100755
--- a/client/gnumed
+++ b/client/gnumed
@@ -23,18 +23,6 @@ if [ ! -e ${SYSCONF} ] ; then
 fi
 
 
-# source systemwide startup extension shell script if it exists
-if [ -r /etc/gnumed/gnumed-startup-local.sh ] ; then
-	. /etc/gnumed/gnumed-startup-local.sh
-fi
-
-
-# source local startup extension shell script if it exists
-if [ -r ${HOME}/.gnumed/scripts/gnumed-startup-local.sh ] ; then
-	. ${HOME}/.gnumed/scripts/gnumed-startup-local.sh
-fi
-
-
 # packages which install the GNUmed python modules into a path not
 # already accessible for imports via sys.path (say, /usr/share/gnumed/)
 # may need to adjust PYTHONPATH appropriately here
@@ -47,6 +35,18 @@ fi
 #export PATH="${PATH}:/usr/share/gnumed/bin"
 
 
+# source systemwide startup extension shell script if it exists
+if [ -r /etc/gnumed/gnumed-startup-local.sh ] ; then
+	. /etc/gnumed/gnumed-startup-local.sh
+fi
+
+
+# source local startup extension shell script if it exists
+if [ -r ${HOME}/.gnumed/scripts/gnumed-startup-local.sh ] ; then
+	. ${HOME}/.gnumed/scripts/gnumed-startup-local.sh
+fi
+
+
 # now run the client
 python -m Gnumed.gnumed ${OPTIONS}
 
@@ -63,7 +63,7 @@ if [ -r ${HOME}/.gnumed/scripts/gnumed-shutdown-local.sh ] ; then
 fi
 
 
-# sync the discs just in case so we don't lose log files
-sync
+# maybe sync the discs just in case so we don't loose log files
+#sync
 
 #==================================================
diff --git a/client/gnumed.py b/client/gnumed.py
index e7e7bca..a6dc05d 100644
--- a/client/gnumed.py
+++ b/client/gnumed.py
@@ -88,7 +88,7 @@ against. Please run GNUmed as a non-root user.
 	sys.exit(1)
 
 #----------------------------------------------------------
-current_client_version = u'1.6.3'
+current_client_version = u'1.6.5'
 current_client_branch = u'1.6'
 
 _log = None
diff --git a/client/importers/__init__.py b/client/importers/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/client/importers/__init__.py
@@ -0,0 +1 @@
+
diff --git a/client/importers/gmImportIncoming.py b/client/importers/gmImportIncoming.py
new file mode 100644
index 0000000..16e2fc6
--- /dev/null
+++ b/client/importers/gmImportIncoming.py
@@ -0,0 +1,225 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+
+__doc__ = """Incoming data importer."""
+__author__ = "K.Hilbert <Karsten.Hilbert at gmx.net>"
+__license__ = "GPL v2 or later"
+#============================================================
+
+# stdlib
+import sys
+import os
+
+
+# do not run as root
+if os.name in ['posix'] and os.geteuid() == 0:
+	print("""
+%s should not be run as root.
+
+Running as <root> can potentially put all your
+medical data at risk. It is strongly advised
+against. Please run as a non-root user.
+""") % sys.argv[0]
+	sys.exit(1)
+
+
+# when attempting to run from a tarball:
+if '--local-import' in sys.argv:
+	sys.path.insert(0, '../../')
+
+
+# stdlib
+import logging
+
+# GNUmed
+from Gnumed.pycommon import gmCfg2
+from Gnumed.pycommon import gmPG2
+from Gnumed.pycommon import gmTools
+
+from Gnumed.business import gmIncomingData
+
+
+_log = logging.getLogger('importer')
+
+# update man page and help text when changing options
+_known_short_options = u'h?'
+_known_long_options = [
+	u'help',
+	u'local-import',
+	u'data-type=',
+	u'file2import=',
+	u'user=',
+	u'host=',
+	u'port='
+]
+
+# update man page when changing options
+_help_text = u"""
+--------------------------------------------------------------------------------------
+Command line:
+ %s
+
+Usage synopsis:
+ %s --local-import --file2import=DATA --data-type=TYPE --user=USER --host=HOST --port=PORT
+
+  --local-import: use when running from a tarball
+
+  DATA: full path of data file to import
+  TYPE: short description of file to be shown in GNUmed (say, "fax")
+  USER: PostgreSQL user in database [%s]
+  HOST: host name of machine running PostgreSQL, if needed
+  PORT: port at which PostgreSQL listens on HOST, if needed, typically 5432
+
+See the man page for more details.
+
+Log file:
+ %s
+--------------------------------------------------------------------------------------"""
+
+#============================================================
+def import_file(data_type, filename):
+
+	_log.info('[%s]: %s', data_type, filename)
+
+	inc = gmIncomingData.create_incoming_data(data_type, filename)
+	if inc is None:
+		_log.error('import failed')
+	else:
+		_log.info('success')
+		target_filename = filename + u'.imported'
+		_log.debug('[%s] -> [%s]', filename, target_filename)
+		try:
+			os.rename(filename, target_filename)
+		except OSError:
+			_log.exception('cannot rename [%s] to [%s]', filename, target_filename)
+	return inc
+
+#	u'request_id',						# request ID as found in <data>
+#	u'firstnames',
+#	u'lastnames',
+#	u'dob',
+#	u'postcode',
+##	u'other_info',						# other identifying info in .data
+#	u'gender',
+#	u'requestor',						# Requestor of data (e.g. who ordered test results) if available in source data.
+#	u'external_data_id',				# ID of content of .data in external system (e.g. importer) where appropriate
+#	u'comment',							# a free text comment on this row, eg. why is it here, error logs etc
+
+#============================================================
+def process_options():
+
+	if _cfg.get(option = '-h', source_order = [('cli', 'return')]):
+		show_usage()
+		sys.exit(0)
+
+	if _cfg.get(option = '-?', source_order = [('cli', 'return')]):
+		show_usage()
+		sys.exit(0)
+
+	if _cfg.get(option = '--help', source_order = [('cli', 'return')]):
+		show_usage()
+		sys.exit(0)
+
+	file2import = _cfg.get(option = '--file2import', source_order = [('cli', 'return')])
+	if file2import is None:
+		exit_with_message('ERROR: option --file2import missing')
+	if file2import is True:
+		exit_with_message('ERROR: data file missing in option --file2import=')
+	try:
+		open(file2import).close()
+	except IOError:
+		_log.exception('cannot open data file')
+		exit_with_message('ERROR: cannot open data file in option --file2import=%s' % file2import)
+
+	datatype = _cfg.get(option = '--data-type', source_order = [('cli', 'return')])
+	if datatype is None:
+		exit_with_message('ERROR: option --data-type missing')
+	if datatype is True:
+		exit_with_message('ERROR: data type missing in option --data-type=')
+	if datatype.strip() == u'':
+		exit_with_message('ERROR: invalid data type in option --data-type=>>>%s<<<' % datatype)
+
+	db_user = _cfg.get(option = '--user', source_order = [('cli', 'return')])
+	if db_user is None:
+		exit_with_message('ERROR: option --user missing')
+	if db_user is True:
+		exit_with_message('ERROR: user name missing in option --user=')
+	if db_user.strip() == u'':
+		exit_with_message('ERROR: invalid user name in option --user=>>>%s<<<' % db_user)
+
+	db_host = _cfg.get(option = '--host', source_order = [('cli', 'return')])
+	if db_host is None:
+		_log.debug('option --host not set, using <UNIX domain socket> on <localhost>')
+	elif db_host is True:
+		exit_with_message('ERROR: host name missing in option --host=')
+	elif db_host.strip() == u'':
+		_log.debug('option --host set to "", using <UNIX domain socket> on <localhost>')
+		db_host = None
+
+	db_port = _cfg.get(option = '--port', source_order = [('cli', 'return')])
+	if db_port is None:
+		_log.debug('option --port not set, using <UNIX domain socket> on <localhost>')
+	elif db_port is True:
+		exit_with_message('ERROR: port value missing in option --port=')
+	elif db_port.strip() == u'':
+		_log.debug('option --port set to "", using <UNIX domain socket> on <localhost>')
+		db_port = None
+	else:
+		converted, db_port = gmTools.input2int(initial = db_port, minval = 1024, maxval = 65535)
+		if not converted:
+			exit_with_message('ERROR: invalid port in option --port=%s (must be 1024...65535)' % db_port)
+
+	gmPG2.log_auth_environment()
+	dsn = gmPG2.make_psycopg2_dsn (
+		database = gmPG2.default_database,
+		host = db_host,
+		port = db_port,
+		user = db_user,
+		password = None			# None = force not-required (TRUST/IDENT/PEER) or use-.pgpass-or-$PGPASSFILE
+	)
+	gmPG2._default_dsn = dsn
+
+	return datatype, file2import
+
+#	val = _cfg.get(option = '--debug', source_order = [('cli', 'return')])
+#	_cfg.set_option (
+#		option = u'debug',
+#		value = val
+#	)
+
+#============================================================
+def exit_with_message(message=None):
+	if message is not None:
+		_log.error(message)
+		print('')
+		print(message)
+	show_usage()
+	sys.exit(1)
+
+#============================================================
+def show_usage():
+	from Gnumed.pycommon import gmLog2
+	print(_help_text % (
+		u' '.join(sys.argv),
+		sys.argv[0],
+		gmPG2.default_database,
+		gmLog2._logfile_name
+	))
+
+#============================================================
+if __name__ == '__main__':
+
+	_cfg = gmCfg2.gmCfgData()
+	_cfg.add_cli (
+		short_options = _known_short_options,
+		long_options = _known_long_options
+	)
+	datatype, file2import = process_options()
+	inc = import_file(datatype, file2import)
+	sys.exit(0)
+
+#	print('Log file:')
+#	from Gnumed.pycommon import gmLog2
+#	print(' %s' % gmLog2._logfile_name)
diff --git a/client/po/es-gnumed.mo b/client/po/es-gnumed.mo
index 584d1d6..842caba 100644
Binary files a/client/po/es-gnumed.mo and b/client/po/es-gnumed.mo differ
diff --git a/client/po/es.po b/client/po/es.po
index f42b49b..318e24a 100644
--- a/client/po/es.po
+++ b/client/po/es.po
@@ -9,7 +9,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: es\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-06 11:40+0200\n"
+"POT-Creation-Date: 2016-04-18 21:14+0200\n"
 "PO-Revision-Date: 2016-02-24 18:28+0100\n"
 "Last-Translator: Karsten Hilbert <Karsten.Hilbert at gmx.net>\n"
 "Language-Team: colombiano <es at li.org>\n"
@@ -5552,20 +5552,18 @@ msgstr "editar los detalles del contacto"
 msgid "Close this dialog."
 msgstr "Cerrar este diálogo."
 
-msgid "age"
-msgstr "edad"
+msgid "Age"
+msgstr "Edad"
 
-msgid "review status"
+#, fuzzy
+msgid "Review status"
 msgstr "revisar estado"
 
-msgid "episode"
-msgstr "episodio"
-
-msgid "health issue"
-msgstr "problema de salud"
+msgid "Health issue"
+msgstr "Problema de salud"
 
-msgid "type"
-msgstr "tipo"
+msgid "Organization"
+msgstr "Organización"
 
 msgid "Sort newest documents to top of tree."
 msgstr "Ordenar los documentos más nuevos al principio del árbol."
@@ -5582,6 +5580,10 @@ msgstr "Ordene documentos por el problema de salud al cual pertenecen."
 msgid "Sort documents by their type."
 msgstr "Ordenar documentos por tipo."
 
+#, fuzzy
+msgid "Sort documents by the organization they are from."
+msgstr "Ordenar documentos por el episodio al que pertenecen."
+
 msgid "Sort documents by"
 msgstr "Ordenar documentos por"
 
@@ -5907,9 +5909,6 @@ msgstr "El nombre de la unidad de la organización."
 msgid "The category of the organizational unit."
 msgstr "La categoría de la unidad de la organización."
 
-msgid "Organization"
-msgstr "Organización"
-
 msgid "Unit"
 msgstr "Unidad de medida"
 
@@ -6020,13 +6019,14 @@ msgid "&Event"
 msgstr "Evento"
 
 msgid "Order by start of encounter a chart entry is linked to."
-msgstr ""
+msgstr "Ordeno por inicio del encuentro enlazado a un ingreso de cartilla."
 
 msgid "Order by time of most recent edit of each chart entry."
-msgstr ""
+msgstr "Ordeno por hora de la más reciente edición de ingreso de cartilla."
 
 msgid "Order by time documented as actual occurrence of each chart entry."
 msgstr ""
+"Ordeno por hora documentada como ocurrencia real de cada ingreso de cartilla."
 
 msgid "Edit the selected chart entry."
 msgstr "Edito el ingreso de cartilla seleccionado."
@@ -8027,9 +8027,6 @@ msgstr "resultado correcto"
 msgid "missing, reported later"
 msgstr "perdido, reportado posteriormente"
 
-msgid "Health issue"
-msgstr "Problema de salud"
-
 msgid "External care"
 msgstr "Cuidado externo"
 
@@ -9801,6 +9798,9 @@ msgstr "cuándo"
 msgid "who"
 msgstr "quién"
 
+msgid "type"
+msgstr "tipo"
+
 msgid "entry"
 msgstr "ingreso"
 
@@ -12144,9 +12144,6 @@ msgstr "Notas"
 msgid "Progress Note"
 msgstr "Nota de Progreso"
 
-msgid "Age"
-msgstr "Edad"
-
 msgid "N/A"
 msgstr "N/D"
 
@@ -13958,17 +13955,17 @@ msgid "Find next [%s] (<CTRL-N>)"
 msgstr "Encuentra siguiente [%s] (<CTRL-N>)"
 
 msgid "Tooltips of selected rows"
-msgstr ""
+msgstr "Herramientas de ayuda sobre filas seleccionadas"
 
 #, fuzzy
 msgid "Data (formatted as text) of selected rows"
 msgstr "Datos de fila (en una linea)"
 
 msgid "Content (as one line each) of selected rows"
-msgstr ""
+msgstr "Contenido (individual por linea) de las filas seleccionadas"
 
 msgid "Tooltip of current row"
-msgstr ""
+msgstr "Herrmienta de ayuda de la fila actual"
 
 #, fuzzy
 msgid "Data (formatted as text) of current row"
@@ -15738,6 +15735,10 @@ msgstr "ordenado por tema de salud"
 msgid "sorted by type"
 msgstr "ordenado por tipo"
 
+#, fuzzy
+msgid "sorted by organization"
+msgstr "Agregando una nueva organización"
+
 msgid "Cannot load documents. No active patient."
 msgstr "No se pueden cargar documentos. No hay paciente activo."
 
@@ -15823,6 +15824,10 @@ msgstr "%s%7s (%s):%s (%s)"
 msgid "%s (unattributed episode)"
 msgstr "%s (episodio no atribuido)"
 
+#, fuzzy
+msgid "unknown organization"
+msgstr "reacción desconocida"
+
 #, python-format
 msgid "part %2s"
 msgstr "parte %2s"
@@ -20894,6 +20899,15 @@ msgstr "Aborto"
 msgid "Abort and do NOT connect to GNUmed."
 msgstr "Aborto y NO conecto a GNUMed."
 
+#~ msgid "age"
+#~ msgstr "edad"
+
+#~ msgid "episode"
+#~ msgstr "episodio"
+
+#~ msgid "health issue"
+#~ msgstr "problema de salud"
+
 #~ msgid "Row tooltip"
 #~ msgstr "Fila de herramienta de ayuda"
 
diff --git a/client/pycommon/gmPG2.py b/client/pycommon/gmPG2.py
index d3f837d..acc0d09 100644
--- a/client/pycommon/gmPG2.py
+++ b/client/pycommon/gmPG2.py
@@ -88,6 +88,8 @@ FixedOffsetTimezone = dbapi.tz.FixedOffsetTimezone
 _default_dsn = None
 _default_login = None
 
+default_database = 'gnumed_v21'
+
 postgresql_version_string = None
 postgresql_version = None			# accuracy: major.minor
 
@@ -289,6 +291,7 @@ def set_default_client_encoding(encoding = None):
 	_log.info('setting default client encoding from [%s] to [%s]' % (_default_client_encoding, str(encoding)))
 	_default_client_encoding = encoding
 	return True
+
 #---------------------------------------------------
 def set_default_client_timezone(timezone = None):
 
@@ -301,6 +304,7 @@ def set_default_client_timezone(timezone = None):
 	_sql_set_timezone = u'set timezone to %s'
 
 	return True
+
 #---------------------------------------------------
 def __validate_timezone(conn=None, timezone=None):
 
@@ -334,6 +338,7 @@ def __validate_timezone(conn=None, timezone=None):
 	conn.rollback()
 
 	return is_valid
+
 #---------------------------------------------------
 def __expand_timezone(conn=None, timezone=None):
 	"""some timezone defs are abbreviations so try to expand
@@ -366,6 +371,7 @@ where
 	conn.rollback()
 
 	return result
+
 #---------------------------------------------------
 def __detect_client_timezone(conn=None):
 	"""This is run on the very first connection."""
@@ -412,6 +418,7 @@ def __detect_client_timezone(conn=None):
 		_sql_set_timezone = u"set time zone interval %s hour to minute"
 
 	_log.info('client system time zone detected as equivalent to [%s]', _default_client_timezone)
+
 # =======================================================================
 # login API
 # =======================================================================
@@ -423,7 +430,7 @@ def __request_login_params_tui():
 	print "\nPlease enter the required login parameters:"
 	try:
 		login.host = prompted_input(prompt = "host ('' = non-TCP/IP)", default = '')
-		login.database = prompted_input(prompt = "database", default = 'gnumed_v21')
+		login.database = prompted_input(prompt = "database", default = default_database)
 		login.user = prompted_input(prompt = "user name", default = '')
 		tmp = 'password for "%s" (not shown): ' % login.user
 		login.password = getpass.getpass(tmp)
@@ -502,11 +509,13 @@ def make_psycopg2_dsn(database=None, host=None, port=5432, user=None, password=N
 	dsn_parts.append('sslmode=prefer')
 
 	return ' '.join(dsn_parts)
+
 # ------------------------------------------------------
 def get_default_login():
 	# make sure we do have a login
 	get_default_dsn()
 	return _default_login
+
 # ------------------------------------------------------
 def get_default_dsn():
 	global _default_dsn
@@ -517,6 +526,7 @@ def get_default_dsn():
 	set_default_login(login=login)
 
 	return _default_dsn
+
 # ------------------------------------------------------
 def set_default_login(login=None):
 	if login is None:
@@ -563,6 +573,7 @@ def log_auth_environment():
 				_log.debug('$PGPASSFILE=%s not found')
 	except Exception:
 		_log.exception('cannot detect .pgpass and or $PGPASSFILE')
+
 # =======================================================================
 # netadata API
 # =======================================================================
diff --git a/client/pycommon/gmTools.py b/client/pycommon/gmTools.py
index d1c20dc..9e5fb8b 100644
--- a/client/pycommon/gmTools.py
+++ b/client/pycommon/gmTools.py
@@ -630,7 +630,7 @@ def fname_from_path(filename):
 	return os.path.split(filename)[1]
 
 #---------------------------------------------------------------------------
-def get_unique_filename(prefix=None, suffix=None, tmp_dir=None):
+def get_unique_filename(prefix=None, suffix=None, tmp_dir=None, include_timestamp=False):
 	"""This introduces a race condition between the file.close() and
 	actually using the filename.
 
@@ -645,6 +645,11 @@ def get_unique_filename(prefix=None, suffix=None, tmp_dir=None):
 			_log.warning('cannot find temporary dir [%s], using system default', tmp_dir)
 			tmp_dir = None
 
+	if include_timestamp:
+		ts = pydt.datetime.now().strftime('%m%d-%H%M%S-')
+	else:
+		ts = u''
+
 	kwargs = {
 		'dir': tmp_dir,
 		#  make sure file gets deleted as soon as
@@ -653,9 +658,9 @@ def get_unique_filename(prefix=None, suffix=None, tmp_dir=None):
 	}
 
 	if prefix is None:
-		kwargs['prefix'] = 'gmd-'
+		kwargs['prefix'] = 'gmd-%s' % ts
 	else:
-		kwargs['prefix'] = prefix
+		kwargs['prefix'] = prefix + ts
 
 	if suffix in [None, u'']:
 		kwargs['suffix'] = '.tmp'
diff --git a/client/wxGladeWidgets/wxgSelectablySortedDocTreePnl.py b/client/wxGladeWidgets/wxgSelectablySortedDocTreePnl.py
index 9e63140..d5f4644 100644
--- a/client/wxGladeWidgets/wxgSelectablySortedDocTreePnl.py
+++ b/client/wxGladeWidgets/wxgSelectablySortedDocTreePnl.py
@@ -1,89 +1,100 @@
 #!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# generated by wxGlade 0.6.3 from "/home/ncq/Projekte/gm-git/gnumed/gnumed/client/wxg/wxgSelectablySortedDocTreePnl.wxg"
+# -*- coding: UTF-8 -*-
+#
+# generated by wxGlade 0.7.1
+#
 
 import wx
 
-# begin wxGlade: extracode
+# begin wxGlade: dependencies
+import gettext
 # end wxGlade
 
+# begin wxGlade: extracode
+# end wxGlade
 
 
 class wxgSelectablySortedDocTreePnl(wx.ScrolledWindow):
-    def __init__(self, *args, **kwds):
-
-        from Gnumed.wxpython import gmDocumentWidgets
-
-        # begin wxGlade: wxgSelectablySortedDocTreePnl.__init__
-        kwds["style"] = wx.TAB_TRAVERSAL
-        wx.ScrolledWindow.__init__(self, *args, **kwds)
-        self._rbtn_sort_by_age = wx.RadioButton(self, -1, _("age"), style=wx.RB_GROUP)
-        self._rbtn_sort_by_review = wx.RadioButton(self, -1, _("review status"))
-        self._rbtn_sort_by_episode = wx.RadioButton(self, -1, _("episode"))
-        self._rbtn_sort_by_issue = wx.RadioButton(self, -1, _("health issue"))
-        self._rbtn_sort_by_type = wx.RadioButton(self, -1, _("type"))
-        self._doc_tree = gmDocumentWidgets.cDocTree(self, -1)
-
-        self.__set_properties()
-        self.__do_layout()
-
-        self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_age_selected, self._rbtn_sort_by_age)
-        self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_review_selected, self._rbtn_sort_by_review)
-        self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_episode_selected, self._rbtn_sort_by_episode)
-        self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_issue_selected, self._rbtn_sort_by_issue)
-        self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_type_selected, self._rbtn_sort_by_type)
-        # end wxGlade
-
-    def __set_properties(self):
-        # begin wxGlade: wxgSelectablySortedDocTreePnl.__set_properties
-        self.SetScrollRate(10, 10)
-        self._rbtn_sort_by_age.SetToolTipString(_("Sort newest documents to top of tree."))
-        self._rbtn_sort_by_age.SetValue(1)
-        self._rbtn_sort_by_review.SetToolTipString(_("Sort unreviewed documents to top of tree."))
-        self._rbtn_sort_by_episode.SetToolTipString(_("Sort documents by the episode they belong to."))
-        self._rbtn_sort_by_issue.SetToolTipString(_("Sort documents by the health issue they belong to."))
-        self._rbtn_sort_by_type.SetToolTipString(_("Sort documents by their type."))
-        # end wxGlade
-
-    def __do_layout(self):
-        # begin wxGlade: wxgSelectablySortedDocTreePnl.__do_layout
-        __szr_main = wx.BoxSizer(wx.VERTICAL)
-        __szr_top_radio = wx.BoxSizer(wx.HORIZONTAL)
-        __lbl_sort = wx.StaticText(self, -1, _("Sort documents by"))
-        __szr_top_radio.Add(__lbl_sort, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, 5)
-        __szr_top_radio.Add(self._rbtn_sort_by_age, 0, wx.LEFT|wx.ALIGN_CENTER_VERTICAL, 10)
-        __szr_top_radio.Add(self._rbtn_sort_by_review, 0, wx.LEFT|wx.ALIGN_CENTER_VERTICAL, 10)
-        __szr_top_radio.Add(self._rbtn_sort_by_episode, 0, wx.LEFT|wx.ALIGN_CENTER_VERTICAL, 10)
-        __szr_top_radio.Add(self._rbtn_sort_by_issue, 0, wx.LEFT|wx.ALIGN_CENTER_VERTICAL, 10)
-        __szr_top_radio.Add(self._rbtn_sort_by_type, 0, wx.LEFT|wx.ALIGN_CENTER_VERTICAL, 10)
-        __szr_main.Add(__szr_top_radio, 0, wx.EXPAND, 0)
-        __hline_middle = wx.StaticLine(self, -1)
-        __szr_main.Add(__hline_middle, 0, wx.EXPAND, 0)
-        __szr_main.Add(self._doc_tree, 1, wx.EXPAND, 0)
-        self.SetSizer(__szr_main)
-        __szr_main.Fit(self)
-        # end wxGlade
-
-    def _on_sort_by_age_selected(self, event): # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
-        print "Event handler `_on_sort_by_age_selected' not implemented!"
-        event.Skip()
-
-    def _on_sort_by_review_selected(self, event): # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
-        print "Event handler `_on_sort_by_review_selected' not implemented!"
-        event.Skip()
-
-    def _on_sort_by_episode_selected(self, event): # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
-        print "Event handler `_on_sort_by_episode_selected' not implemented!"
-        event.Skip()
-
-    def _on_sort_by_issue_selected(self, event): # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
-        print "Event handler `_on_sort_by_issue_selected' not implemented!"
-        event.Skip()
-
-    def _on_sort_by_type_selected(self, event): # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
-        print "Event handler `_on_sort_by_type_selected' not implemented!"
-        event.Skip()
-
+	def __init__(self, *args, **kwds):
+
+		from Gnumed.wxpython import gmDocumentWidgets
+
+		# begin wxGlade: wxgSelectablySortedDocTreePnl.__init__
+		kwds["style"] = wx.TAB_TRAVERSAL
+		wx.ScrolledWindow.__init__(self, *args, **kwds)
+		self._rbtn_sort_by_age = wx.RadioButton(self, wx.ID_ANY, _("Age"), style=wx.RB_GROUP)
+		self._rbtn_sort_by_review = wx.RadioButton(self, wx.ID_ANY, _("Review status"))
+		self._rbtn_sort_by_episode = wx.RadioButton(self, wx.ID_ANY, _("Episode"))
+		self._rbtn_sort_by_issue = wx.RadioButton(self, wx.ID_ANY, _("Health issue"))
+		self._rbtn_sort_by_type = wx.RadioButton(self, wx.ID_ANY, _("Type"))
+		self._rbtn_sort_by_org = wx.RadioButton(self, wx.ID_ANY, _("Organization"))
+		self._doc_tree = gmDocumentWidgets.cDocTree(self, wx.ID_ANY)
+
+		self.__set_properties()
+		self.__do_layout()
+
+		self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_age_selected, self._rbtn_sort_by_age)
+		self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_review_selected, self._rbtn_sort_by_review)
+		self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_episode_selected, self._rbtn_sort_by_episode)
+		self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_issue_selected, self._rbtn_sort_by_issue)
+		self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_type_selected, self._rbtn_sort_by_type)
+		self.Bind(wx.EVT_RADIOBUTTON, self._on_sort_by_org_selected, self._rbtn_sort_by_org)
+		# end wxGlade
+
+	def __set_properties(self):
+		# begin wxGlade: wxgSelectablySortedDocTreePnl.__set_properties
+		self.SetScrollRate(10, 10)
+		self._rbtn_sort_by_age.SetToolTipString(_("Sort newest documents to top of tree."))
+		self._rbtn_sort_by_age.SetValue(1)
+		self._rbtn_sort_by_review.SetToolTipString(_("Sort unreviewed documents to top of tree."))
+		self._rbtn_sort_by_episode.SetToolTipString(_("Sort documents by the episode they belong to."))
+		self._rbtn_sort_by_issue.SetToolTipString(_("Sort documents by the health issue they belong to."))
+		self._rbtn_sort_by_type.SetToolTipString(_("Sort documents by their type."))
+		self._rbtn_sort_by_org.SetToolTipString(_("Sort documents by the organization they are from."))
+		# end wxGlade
+
+	def __do_layout(self):
+		# begin wxGlade: wxgSelectablySortedDocTreePnl.__do_layout
+		__szr_main = wx.BoxSizer(wx.VERTICAL)
+		__szr_top_radio = wx.BoxSizer(wx.HORIZONTAL)
+		__lbl_sort = wx.StaticText(self, wx.ID_ANY, _("Sort documents by"))
+		__szr_top_radio.Add(__lbl_sort, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.RIGHT, 5)
+		__szr_top_radio.Add(self._rbtn_sort_by_age, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
+		__szr_top_radio.Add(self._rbtn_sort_by_review, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
+		__szr_top_radio.Add(self._rbtn_sort_by_episode, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
+		__szr_top_radio.Add(self._rbtn_sort_by_issue, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
+		__szr_top_radio.Add(self._rbtn_sort_by_type, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
+		__szr_top_radio.Add(self._rbtn_sort_by_org, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
+		__szr_main.Add(__szr_top_radio, 0, wx.EXPAND, 0)
+		__hline_middle = wx.StaticLine(self, wx.ID_ANY)
+		__szr_main.Add(__hline_middle, 0, wx.EXPAND, 0)
+		__szr_main.Add(self._doc_tree, 1, wx.EXPAND, 0)
+		self.SetSizer(__szr_main)
+		__szr_main.Fit(self)
+		self.Layout()
+		# end wxGlade
+
+	def _on_sort_by_age_selected(self, event):  # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
+		print "Event handler '_on_sort_by_age_selected' not implemented!"
+		event.Skip()
+
+	def _on_sort_by_review_selected(self, event):  # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
+		print "Event handler '_on_sort_by_review_selected' not implemented!"
+		event.Skip()
+
+	def _on_sort_by_episode_selected(self, event):  # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
+		print "Event handler '_on_sort_by_episode_selected' not implemented!"
+		event.Skip()
+
+	def _on_sort_by_issue_selected(self, event):  # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
+		print "Event handler '_on_sort_by_issue_selected' not implemented!"
+		event.Skip()
+
+	def _on_sort_by_type_selected(self, event):  # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
+		print "Event handler '_on_sort_by_type_selected' not implemented!"
+		event.Skip()
+
+	def _on_sort_by_org_selected(self, event):  # wxGlade: wxgSelectablySortedDocTreePnl.<event_handler>
+		print "Event handler '_on_sort_by_org_selected' not implemented!"
+		event.Skip()
 # end of class wxgSelectablySortedDocTreePnl
-
-
diff --git a/client/wxpython/gmDocumentWidgets.py b/client/wxpython/gmDocumentWidgets.py
index d497595..f937da5 100644
--- a/client/wxpython/gmDocumentWidgets.py
+++ b/client/wxpython/gmDocumentWidgets.py
@@ -199,7 +199,7 @@ def save_files_as_new_document(parent=None, filenames=None, document_type=None,
 	if reference is not None:
 		doc['ext_ref'] = reference
 	if pk_org_unit is not None:
-		doc['pk_org_unit'] = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit']
+		doc['pk_org_unit'] = pk_org_unit
 	if date_generated is not None:
 		doc['clin_when'] = date_generated
 	if comment is not None:
@@ -1443,6 +1443,11 @@ class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySor
 		self._doc_tree.sort_mode = 'type'
 		self._doc_tree.SetFocus()
 		self._rbtn_sort_by_type.SetValue(True)
+	#--------------------------------------------------------
+	def _on_sort_by_org_selected(self, evt):
+		self._doc_tree.sort_mode = 'org'
+		self._doc_tree.SetFocus()
+		self._rbtn_sort_by_org.SetValue(True)
 
 #============================================================
 class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.ExpansionState):
@@ -1452,7 +1457,7 @@ class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.Expansion
 
 	This acts on the current patient.
 	"""
-	_sort_modes = ['age', 'review', 'episode', 'type', 'issue']
+	_sort_modes = ['age', 'review', 'episode', 'type', 'issue', 'org']
 	_root_node_labels = None
 	#--------------------------------------------------------
 	def __init__(self, parent, id, *args, **kwds):
@@ -1470,7 +1475,8 @@ class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.Expansion
 			'review': tmp % unsigned,
 			'episode': tmp % _('sorted by episode'),
 			'issue': tmp % _('sorted by health issue'),
-			'type': tmp % _('sorted by type')
+			'type': tmp % _('sorted by type'),
+			'org': tmp % _('sorted by organization')
 		}
 
 		self.root = None
@@ -1758,6 +1764,25 @@ class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.Expansion
 					self.SetItemHasChildren(intermediate_nodes[inter_label], True)
 				parent = intermediate_nodes[inter_label]
 
+			elif self.__sort_mode == 'org':
+				if doc['pk_org'] is None:
+					inter_label = _('unknown organization')
+				else:
+					inter_label = doc['organization']
+				doc_label = _('%s%7s %s:%s (%s)') % (
+					gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
+					doc['clin_when'].strftime('%m/%Y'),
+					doc['l10n_type'][:26],
+					gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'),
+					no_parts
+				)
+				if inter_label not in intermediate_nodes:
+					intermediate_nodes[inter_label] = self.AppendItem(parent = self.root, text = inter_label)
+					self.SetItemBold(intermediate_nodes[inter_label], bold = True)
+					self.SetItemPyData(intermediate_nodes[inter_label], None)
+					self.SetItemHasChildren(intermediate_nodes[inter_label], True)
+				parent = intermediate_nodes[inter_label]
+
 			else:
 				doc_label = _('%s%7s %s:%s (%s)') % (
 					gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
@@ -1819,7 +1844,7 @@ class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.Expansion
 		# expand intermediate nodes as well
 		if self.__expanded_nodes is None:
 			# but only if there are any
-			if self.__sort_mode in ['episode', 'type', 'issue']:
+			if self.__sort_mode in ['episode', 'type', 'issue', 'org']:
 				for key in intermediate_nodes.keys():
 					self.Expand(intermediate_nodes[key])
 
@@ -1914,6 +1939,26 @@ class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.Expansion
 					return 1
 				return 1
 
+			elif self.__sort_mode == 'org':
+				if (data1['organization'] is None) and (data2['organization'] is None):
+					return 0
+				if (data1['organization'] is None) and (data2['organization'] is not None):
+					return 1
+				if (data1['organization'] is not None) and (data2['organization'] is None):
+					return -1
+				txt1 = u'%s %s' % (data1['organization'], data1['unit'])
+				txt2 = u'%s %s' % (data2['organization'], data2['unit'])
+				if txt1 < txt2:
+					return -1
+				if txt1 == txt2:
+					# inner sort: reverse by date
+					if data1[date_field] > data2[date_field]:
+						return -1
+					if data1[date_field] == data2[date_field]:
+						return 0
+					return 1
+				return 1
+
 			else:
 				_log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
 				# reverse sort by date
diff --git a/client/wxpython/gmListWidgets.py b/client/wxpython/gmListWidgets.py
index 14a70d5..fa12810 100644
--- a/client/wxpython/gmListWidgets.py
+++ b/client/wxpython/gmListWidgets.py
@@ -24,9 +24,11 @@ import logging
 import thread
 import time
 import locale
+import os
+import io
+import csv
 import re as regex
-#import io
-#import csv
+import datetime as pydt
 
 
 import wx
@@ -34,6 +36,7 @@ import wx.lib.mixins.listctrl as listmixins
 
 
 from Gnumed.pycommon import gmTools
+from Gnumed.pycommon import gmDispatcher
 
 
 _log = logging.getLogger('gm.list_ui')
@@ -1139,6 +1142,7 @@ class cItemPickerDlg(wxgItemPickerDlg.wxgItemPickerDlg):
 		self._LCTRL_right.set_column_widths()
 #		print u'%s <-> %s (items)' % (self._LCTRL_left.ItemCount, self._LCTRL_right.ItemCount)
 #		print u'%s <-> %s (data)' % (len(self._LCTRL_left.data), len(self._LCTRL_right.data))
+
 	#------------------------------------------------------------
 	def __remove_selected_picks(self):
 		if self._LCTRL_right.get_selected_items(only_one = True) == -1:
@@ -1152,6 +1156,7 @@ class cItemPickerDlg(wxgItemPickerDlg.wxgItemPickerDlg):
 
 #		print u'%s <-> %s (items)' % (self._LCTRL_left.ItemCount, self._LCTRL_right.ItemCount)
 #		print u'%s <-> %s (data)' % (len(self._LCTRL_left.data), len(self._LCTRL_right.data))
+
 	#------------------------------------------------------------
 	# event handlers
 	#------------------------------------------------------------
@@ -1473,7 +1478,7 @@ class cReportListCtrl(listmixins.ListCtrlAutoWidthMixin, listmixins.ColumnSorter
 		labels = []
 		for col_idx in range(self.ColumnCount):
 			col = self.GetColumn(col = col_idx)
-			labels.append(col.GetText())
+			labels.append(col.Text)
 		return labels
 
 	column_labels = property(get_column_labels, lambda x:x)
@@ -1580,6 +1585,12 @@ class cReportListCtrl(listmixins.ListCtrlAutoWidthMixin, listmixins.ColumnSorter
 	#------------------------------------------------------------
 	def __show_context_menu(self, item_idx):
 
+		if item_idx == -1:
+			return
+
+		if self.ItemCount == 0:
+			return
+
 		items = self.selected_items
 		if self.__is_single_selection:
 			if items is None:
@@ -1634,12 +1645,27 @@ class cReportListCtrl(listmixins.ListCtrlAutoWidthMixin, listmixins.ColumnSorter
 			self._rclicked_row_cells.append(cell_content)
 			self._rclicked_row_cells_w_hdr.append(u'%s: %s' % (col_header, cell_content))
 
-#		# export area
-#		exp_menu = wx.Menu()
-#		menu_item = exp_menu.Append(-1, _('&All rows as CSV'))
-#		self.Bind(wx.EVT_MENU, self._all_rows2export_area, menu_item)
-#		menu_item = exp_menu.Append(-1, _('&Selected rows as CSV'))
-#		self.Bind(wx.EVT_MENU, self._selected_rows2export_area, menu_item)
+		# save to file
+		save_menu = wx.Menu()
+		menu_item = save_menu.Append(-1, _('&All rows'))
+		self.Bind(wx.EVT_MENU, self._all_rows2file, menu_item)
+		menu_item = save_menu.Append(-1, _('All rows as &CSV'))
+		self.Bind(wx.EVT_MENU, self._all_rows2csv, menu_item)
+		menu_item = save_menu.Append(-1, _('&Tooltips of all rows'))
+		self.Bind(wx.EVT_MENU, self._all_row_tooltips2file, menu_item)
+		menu_item = save_menu.Append(-1, _('&Data of all rows'))
+		self.Bind(wx.EVT_MENU, self._all_row_data2file, menu_item)
+
+		if no_of_selected_items > 1:
+			save_menu.AppendSeparator()
+			menu_item = save_menu.Append(-1, _('&Selected rows'))
+			self.Bind(wx.EVT_MENU, self._selected_rows2file, menu_item)
+			menu_item = save_menu.Append(-1, _('&Selected rows as CSV'))
+			self.Bind(wx.EVT_MENU, self._selected_rows2csv, menu_item)
+			menu_item = save_menu.Append(-1, _('&Tooltips of selected rows'))
+			self.Bind(wx.EVT_MENU, self._selected_row_tooltips2file, menu_item)
+			menu_item = save_menu.Append(-1, _('&Data of selected rows'))
+			self.Bind(wx.EVT_MENU, self._selected_row_data2file, menu_item)
 
 		# 1) set clipboard to item
 		clip_menu = wx.Menu()
@@ -1751,8 +1777,7 @@ class cReportListCtrl(listmixins.ListCtrlAutoWidthMixin, listmixins.ColumnSorter
 				clip_add_menu.AppendMenu(-1, _(u'Column &%s (current row): %s') % (col_idx+1, col_header), col_add_menu)
 
 		# 3) copy item to export area
-		#export_area_menu = wx.Menu()
-		# put into export area
+		# put into file
 		# current row
 		# - fields as one line
 		# - fields as list
@@ -1766,7 +1791,8 @@ class cReportListCtrl(listmixins.ListCtrlAutoWidthMixin, listmixins.ColumnSorter
 		# send signal
 
 		# show menu
-		self._context_menu.AppendMenu(-1, _('Copy to e&xport area...'), exp_menu)
+		#self._context_menu.AppendMenu(-1, _('Copy to e&xport area...'), exp_menu)
+		self._context_menu.AppendMenu(-1, _('&Save to file...'), save_menu)
 		self._context_menu.AppendMenu(-1, _('&Copy to clipboard...'), clip_menu)
 		self._context_menu.AppendMenu(-1, _('Append (&+) to clipboard...'), clip_add_menu)
 
@@ -2020,44 +2046,167 @@ class cReportListCtrl(listmixins.ListCtrlAutoWidthMixin, listmixins.ColumnSorter
 		evt.Skip()
 		self.__search_match()
 
-#	#------------------------------------------------------------
-#	def _all_rows2export_area(self, evt):
-#		col_labels = self.column_labels
-#
-#		csv_name = gmTools.get_unique_filename(suffix = '.csv')
-#		csv_file = io.open(csv_name, mode = 'wt', encoding = 'utf8')
-#		csv_writer = csv.DictWriter(csv_file, col_labels)
-#		csv_writer.writeheader()
-#
-#		for item_idx in range(self.ItemCount):
-#			row_dict = {}
-#			for col_idx in range(self.ColumnCount):
-#				row_dict[col_labels[col_idx]] = self.GetItem(item_idx, col_idx).Text
-#			csv_writer.writerow(row_dict)
-#
-#		csv_file.close()
-#
-#		# signal export area
-#
-#	#------------------------------------------------------------
-#	def _selected_rows2export_area(self, evt):
-#		col_labels = self.column_labels
-#
-#		csv_name = gmTools.get_unique_filename(suffix = '.csv')
-#		csv_file = io.open(csv_name, mode = 'wb', encoding = 'utf8')
-#		csv_writer = csv.DictWriter(csv_file, col_labels)
-#		csv_writer.writeheader()
-#
-#		for item_idx in self.selected_items:
-#			row_dict = {}
-#			for col_idx in range(self.ColumnCount):
-#				row_dict[col_labels[col_idx]] = self.GetItem(item_idx, col_idx).Text
-#			csv_writer.writerow(row_dict)
-#
-#		csv_file.close()
-#
-#		# signal export area
-#
+	#------------------------------------------------------------
+	def _all_rows2file(self, evt):
+
+		txt_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-all_rows-%s.txt' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		txt_file = io.open(txt_name, mode = 'wt', encoding = 'utf8')
+
+		col_labels = self.column_labels
+		line = u'%s' % u' || '.join(col_labels)
+		txt_file.write(u'%s\n' % line)
+		txt_file.write((u'=' * len(line)) + u'\n')
+
+		for item_idx in range(self.ItemCount):
+			fields = []
+			for col_idx in range(self.ColumnCount):
+				fields.append(self.GetItem(item_idx, col_idx).Text)
+			txt_file.write(u'%s\n' % u' || '.join(fields))
+
+		txt_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('All rows saved to [%s].') % txt_name)
+
+	#------------------------------------------------------------
+	def _all_rows2csv(self, evt):
+
+		csv_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-all_rows-%s.csv' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		csv_file = io.open(csv_name, mode = 'wb')
+
+		csv_writer = csv.writer(csv_file)
+		csv_writer.writerow([ l.encode('utf-8') for l in self.column_labels ])
+		for item_idx in range(self.ItemCount):
+			fields = []
+			for col_idx in range(self.ColumnCount):
+				fields.append(self.GetItem(item_idx, col_idx).Text)
+			csv_writer.writerow([ f.encode('utf-8') for f in fields ])
+
+		csv_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('All rows saved to [%s].') % csv_name)
+
+	#------------------------------------------------------------
+	def _all_row_tooltips2file(self, evt):
+
+		if (self.__data is None) or (self.__item_tooltip_callback is None):
+			return
+
+		txt_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-list_tooltips-%s.txt' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		txt_file = io.open(txt_name, mode = 'wt', encoding = 'utf8')
+
+		for data in self.data:
+			tt = self.__item_tooltip_callback(data)
+			if tt is None:
+				continue
+			txt_file.write(u'%s\n\n' % tt)
+
+		txt_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('All tooltips saved to [%s].') % txt_name)
+
+	#------------------------------------------------------------
+	def _all_row_data2file(self, evt):
+
+		if self.__data is None:
+			return
+
+		txt_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-list_data-%s.txt' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		txt_file = io.open(txt_name, mode = 'wt', encoding = 'utf8')
+
+		for data in self.data:
+			if hasattr(data, 'format'):
+				txt = data.format()
+				if type(txt) is list:
+					txt = u'\n'.join(txt)
+			else:
+				txt = u'%s' % data
+			txt_file.write(u'%s\n\n' % txt)
+
+		txt_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('All data saved to [%s].') % txt_name)
+
+	#------------------------------------------------------------
+	def _selected_rows2file(self, evt):
+
+		txt_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-some_rows-%s.txt' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		txt_file = io.open(txt_name, mode = 'wt', encoding = 'utf8')
+
+		col_labels = self.column_labels
+		line = u'%s' % u' || '.join(col_labels)
+		txt_file.write(u'%s\n' % line)
+		txt_file.write((u'=' * len(line)) + u'\n')
+
+		items = self.selected_items
+		if self.__is_single_selection:
+			items = [items]
+
+		for item_idx in items:
+			fields = []
+			for col_idx in range(self.ColumnCount):
+				fields.append(self.GetItem(item_idx, col_idx).Text)
+			txt_file.write(u'%s\n' % u' || '.join(fields))
+
+		txt_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('Selected rows saved to [%s].') % txt_name)
+
+	#------------------------------------------------------------
+	def _selected_rows2csv(self, evt):
+
+		csv_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-some_rows-%s.csv' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		csv_file = io.open(csv_name, mode = 'wb')
+
+		csv_writer = csv.writer(csv_file)
+		csv_writer.writerow([ l.encode('utf-8') for l in self.column_labels ])
+
+		items = self.selected_items
+		if self.__is_single_selection:
+			items = [items]
+
+		for item_idx in items:
+			fields = []
+			for col_idx in range(self.ColumnCount):
+				fields.append(self.GetItem(item_idx, col_idx).Text)
+			csv_writer.writerow([ f.encode('utf-8') for f in fields ])
+
+		csv_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('Selected rows saved to [%s].') % csv_name)
+
+	#------------------------------------------------------------
+	def _selected_row_tooltips2file(self, evt):
+
+		if (self.__data is None) or (self.__item_tooltip_callback is None):
+			return
+
+		txt_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-list_tooltips-%s.txt' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		txt_file = io.open(txt_name, mode = 'wt', encoding = 'utf8')
+
+		for data in self.selected_item_data:
+			tt = self.__item_tooltip_callback(data)
+			if tt is None:
+				continue
+			txt_file.write(u'%s\n\n' % tt)
+
+		txt_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('Selected tooltips saved to [%s].') % txt_name)
+
+	#------------------------------------------------------------
+	def _selected_row_data2file(self, evt):
+
+		if self.__data is None:
+			return
+
+		txt_name = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', 'gm-list_data-%s.txt' % pydt.datetime.now().strftime('%m%d-%H%M%S'))
+		txt_file = io.open(txt_name, mode = 'wt', encoding = 'utf8')
+
+		for data in self.selected_item_data:
+			if hasattr(data, 'format'):
+				txt = data.format()
+				if type(txt) is list:
+					txt = u'\n'.join(txt)
+			else:
+				txt = u'%s' % data
+			txt_file.write(u'%s\n\n' % txt)
+
+		txt_file.close()
+		gmDispatcher.send(signal = 'statustext', msg = _('Selected data saved to [%s].') % txt_name)
+
 	#------------------------------------------------------------
 	def _tooltip2clipboard(self, evt):
 		if wx.TheClipboard.IsOpened():
diff --git a/client/wxpython/gmMeasurementWidgets.py b/client/wxpython/gmMeasurementWidgets.py
index 64536b9..41bcac7 100644
--- a/client/wxpython/gmMeasurementWidgets.py
+++ b/client/wxpython/gmMeasurementWidgets.py
@@ -40,6 +40,7 @@ from Gnumed.business import gmForms
 from Gnumed.business import gmPersonSearch
 from Gnumed.business import gmOrganization
 from Gnumed.business import gmHL7
+from Gnumed.business import gmIncomingData
 
 from Gnumed.wxpython import gmRegetMixin
 from Gnumed.wxpython import gmPlugin
@@ -270,10 +271,10 @@ def browse_incoming_unmatched(parent=None):
 		)
 		if not do_delete:
 			return False
-		return gmHL7.delete_incoming_data(pk_incoming_data = staged_item['pk_incoming_data_unmatched'])
+		return gmIncomingData.delete_incoming_data(pk_incoming_data = staged_item['pk_incoming_data_unmatched'])
 	#------------------------------------------------------------
 	def refresh(lctrl):
-		incoming = gmHL7.get_incoming_data()
+		incoming = gmIncomingData.get_incoming_data()
 		items = [ [
 			gmTools.coalesce(i['data_type'], u''),
 			u'%s, %s (%s) %s' % (
diff --git a/external-tools/gm-describe_file b/external-tools/gm-describe_file
index 4a516dc..8aefc2f 100755
--- a/external-tools/gm-describe_file
+++ b/external-tools/gm-describe_file
@@ -7,17 +7,19 @@ if test -z "${DESCRIPTION_FILE}" ; then
 	DESCRIPTION_FILE="${FILE_2_DESCRIBE}.txt"
 fi
 
-echo "-- file ----" > ${DESCRIPTION_FILE}
+echo "---- file ----" > ${DESCRIPTION_FILE}
 file "${FILE_2_DESCRIBE}" >> "${DESCRIPTION_FILE}" 2>> "${DESCRIPTION_FILE}"
 echo "" >> "${DESCRIPTION_FILE}"
-echo "-- exiftool ----" >> "${DESCRIPTION_FILE}"
-exiftool -ee -m -u "${FILE_2_DESCRIBE}" >> "${DESCRIPTION_FILE}" 2>> "${DESCRIPTION_FILE}"
-#echo "" >> "${DESCRIPTION_FILE}"
-#echo "-- identify ----" >> "${DESCRIPTION_FILE}"
+
+exiftool -g1 -ee -m -u "${FILE_2_DESCRIBE}" >> "${DESCRIPTION_FILE}" 2>> "${DESCRIPTION_FILE}"
+echo "" >> "${DESCRIPTION_FILE}"
+
+#echo "---- identify ----" >> "${DESCRIPTION_FILE}"
 # takes a long time on larger images / PDF files
 #identify -verbose "${FILE_2_DESCRIBE}" >> "${DESCRIPTION_FILE}" 2>> "${DESCRIPTION_FILE}"
-echo "" >> "${DESCRIPTION_FILE}"
-echo "-- sfinfo ----" >> "${DESCRIPTION_FILE}"
+#echo "" >> "${DESCRIPTION_FILE}"
+
+echo "---- sfinfo ----" >> "${DESCRIPTION_FILE}"
 sfinfo "${FILE_2_DESCRIBE}" >> "${DESCRIPTION_FILE}" 2>> "${DESCRIPTION_FILE}"
 
 exit 0
diff --git a/external-tools/gm-import_incoming b/external-tools/gm-import_incoming
new file mode 100755
index 0000000..9c7356e
--- /dev/null
+++ b/external-tools/gm-import_incoming
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+#==================================================
+# This shell script is intended to be placed in
+# /usr/bin/ and should be run to use the importer
+# for external files into the INCOMING area of
+# GNUmed.
+#
+# license: GPL v2 or later
+# Karsten Hilbert
+#--------------------------------------------------
+
+# for debugging this script
+# set -x
+
+OPTIONS=$@
+
+
+# packages which install the GNUmed python modules into a path not
+# already accessible for imports via sys.path (say, /usr/share/gnumed/)
+# may need to adjust PYTHONPATH appropriately here
+#export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}/usr/share/gnumed/"
+
+
+# now run the actual script
+python -m Gnumed.importers.gmImportIncoming ${OPTIONS}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/gnumed-client.git



More information about the debian-med-commit mailing list