b45a80b6b2e38b084818d13f625d016987689c07
[openemr.git] / interface / orders / receive_hl7_results.inc.php
blobb45a80b6b2e38b084818d13f625d016987689c07
1 <?php
2 /**
3 * Functions to support parsing and saving hl7 results.
5 * Copyright (C) 2013 Rod Roark <rod@sunsetsystems.com>
7 * LICENSE: This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>.
18 * @package OpenEMR
19 * @author Rod Roark <rod@sunsetsystems.com>
22 function rhl7InsertRow(&$arr, $tablename) {
23 if (empty($arr)) return;
25 // echo "<!-- "; // debugging
26 // print_r($arr);
27 // echo " -->\n";
29 $query = "INSERT INTO $tablename SET";
30 $binds = array();
31 $sep = '';
32 foreach ($arr as $key => $value) {
33 $query .= "$sep `$key` = ?";
34 $sep = ',';
35 $binds[] = $value;
37 $arr = array();
38 return sqlInsert($query, $binds);
41 function rhl7FlushResult(&$ares) {
42 return rhl7InsertRow($ares, 'procedure_result');
45 function rhl7FlushReport(&$arep) {
46 return rhl7InsertRow($arep, 'procedure_report');
49 function rhl7Text($s) {
50 $s = str_replace('\\S\\' ,'^' , $s);
51 $s = str_replace('\\F\\' ,'|' , $s);
52 $s = str_replace('\\R\\' ,'~' , $s);
53 $s = str_replace('\\T\\' ,'&' , $s);
54 $s = str_replace('\\X0d\\',"\r", $s);
55 $s = str_replace('\\E\\' ,'\\', $s);
56 return $s;
59 function rhl7DateTime($s) {
60 if (empty($s)) return '0000-00-00 00:00:00';
61 $ret = substr($s, 0, 4) . '-' . substr($s, 4, 2) . '-' . substr($s, 6, 2);
62 if (strlen($s) > 8) $ret .= ' ' . substr($s, 8, 2) . ':' . substr($s, 10, 2) . ':';
63 if (strlen($s) > 12) {
64 $ret .= substr($s, 12, 2);
65 } else {
66 $ret .= '00';
68 return $ret;
71 function rhl7Abnormal($s) {
72 if ($s == '' ) return 'no';
73 if ($s == 'A' ) return 'yes';
74 if ($s == 'H' ) return 'high';
75 if ($s == 'L' ) return 'low';
76 if ($s == 'HH') return 'vhigh';
77 if ($s == 'LL') return 'vlow';
78 return rhl7Text($s);
81 function rhl7ReportStatus($s) {
82 if ($s == 'F') return 'final';
83 if ($s == 'P') return 'prelim';
84 if ($s == 'C') return 'correct';
85 return rhl7Text($s);
88 /**
89 * Parse and save.
91 * @param string &$pprow A row from the procedure_providers table.
92 * @param string &$hl7 The input HL7 text.
93 * @return string Error text, or empty if no errors.
95 function receive_hl7_results(&$pprow, &$hl7) {
96 if (substr($hl7, 0, 3) != 'MSH') {
97 return xl('Input does not begin with a MSH segment');
100 // End-of-line delimiter for text in procedure_result.comments
101 $commentdelim = "\n";
103 $today = time();
105 $in_message_id = '';
106 $in_ssn = '';
107 $in_dob = '';
108 $in_lname = '';
109 $in_fname = '';
110 $in_orderid = 0;
111 $in_procedure_code = '';
112 $in_report_status = '';
113 $in_encounter = 0;
115 $porow = false;
116 $pcrow = false;
117 $procedure_report_id = 0;
118 $arep = array(); // holding area for OBR and its NTE data
119 $ares = array(); // holding area for OBX and its NTE data
121 // This is so we know where we are if a segment like NTE that can appear in
122 // different places is encountered.
123 $context = '';
125 // Delimiters
126 $d0 = "\r";
127 $d1 = substr($hl7, 3, 1); // typically |
128 $d2 = substr($hl7, 4, 1); // typically ^
129 $d3 = substr($hl7, 5, 1); // typically ~
131 $segs = explode($d0, $hl7);
133 foreach ($segs as $seg) {
134 if (empty($seg)) continue;
136 $a = explode($d1, $seg);
138 if ($a[0] == 'MSH') {
139 $context = $a[0];
140 if ($a[8] != 'ORU^R01') {
141 return xl('Message type') . " '${a[8]}' " . xl('does not seem valid');
143 $in_message_id = $a[9];
146 else if ($a[0] == 'PID') {
147 $context = $a[0];
148 rhl7FlushResult($ares);
149 // Next line will do something only if there was a report with no results.
150 rhl7FlushReport($arep);
151 $in_ssn = $a[4];
152 $in_dob = $a[7]; // yyyymmdd format
153 $tmp = explode($d2, $a[5]);
154 $in_lname = $tmp[0];
155 $in_fname = $tmp[1];
158 else if ($a[0] == 'PV1') {
159 // Save placer encounter number if present.
160 if (!empty($a[19])) {
161 $tmp = explode($d2, $a[19]);
162 $in_encounter = intval($tmp[0]);
166 else if ($a[0] == 'ORC') {
167 $context = $a[0];
168 rhl7FlushResult($ares);
169 // Next line will do something only if there was a report with no results.
170 rhl7FlushReport($arep);
171 $porow = false;
172 $pcrow = false;
173 if ($a[2]) $in_orderid = intval($a[2]);
176 else if ($a[0] == 'NTE' && $context == 'ORC') {
177 // TBD? Is this ever used?
180 else if ($a[0] == 'OBR') {
181 $context = $a[0];
182 rhl7FlushResult($ares);
183 // Next line will do something only if there was a report with no results.
184 rhl7FlushReport($arep);
185 $procedure_report_id = 0;
186 if ($a[2]) $in_orderid = intval($a[2]);
187 $tmp = explode($d2, $a[4]);
188 $in_procedure_code = $tmp[0];
189 $in_procedure_name = $tmp[1];
190 $in_report_status = rhl7ReportStatus($a[25]);
191 if (empty($porow)) {
192 $porow = sqlQuery("SELECT * FROM procedure_order WHERE " .
193 "procedure_order_id = ?", array($in_orderid));
194 // The order must already exist. Currently we do not handle electronic
195 // results returned for manual orders.
196 if (empty($porow)) {
197 return xl('Procedure order') . " '$in_orderid' " . xl('was not found');
199 if ($in_encounter) {
200 if ($porow['encounter_id'] != $in_encounter) {
201 return xl('Encounter ID') .
202 " '" . $porow['encounter_id'] . "' " .
203 xl('for OBR placer order number') .
204 " '$in_orderid' " .
205 xl('does not match the PV1 encounter number') .
206 " '$in_encounter'";
209 else {
210 // They did not return an encounter number to verify, so more checking
211 // might be done here to make sure the patient seems to match.
214 // Find the order line item (procedure code) that matches this result.
215 $pcquery = "SELECT pc.* FROM procedure_order_code AS pc " .
216 "WHERE pc.procedure_order_id = ? AND pc.procedure_code = ? " .
217 "ORDER BY procedure_order_seq LIMIT 1";
218 $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code));
219 if (empty($pcrow)) {
220 // There is no matching procedure in the order, so it must have been
221 // added after the original order was sent, either as a manual request
222 // from the physician or as a "reflex" from the lab.
223 // procedure_source = '2' indicates this.
224 sqlInsert("INSERT INTO procedure_order_code SET " .
225 "procedure_order_id = ?, " .
226 "procedure_code = ?, " .
227 "procedure_name = ?, " .
228 "procedure_source = '2'",
229 array($in_orderid, $in_procedure_code, $in_procedure_name));
230 $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code));
232 $arep = array();
233 $arep['procedure_order_id'] = $in_orderid;
234 $arep['procedure_order_seq'] = $pcrow['procedure_order_seq'];
235 $arep['date_collected'] = rhl7DateTime($a[7]);
236 $arep['date_report'] = substr(rhl7DateTime($a[22]), 0, 10);
237 $arep['report_status'] = $in_report_status;
238 $arep['report_notes'] = '';
241 else if ($a[0] == 'NTE' && $context == 'OBR') {
242 $arep['report_notes'] .= rhl7Text($a[3]) . "\n";
245 else if ($a[0] == 'OBX') {
246 $context = $a[0];
247 rhl7FlushResult($ares);
248 if (!$procedure_report_id) {
249 $procedure_report_id = rhl7FlushReport($arep);
251 $ares = array();
252 $ares['procedure_report_id'] = $procedure_report_id;
253 // OBX-5 can be a very long string of text with "~" as line separators.
254 // The first line of comments is reserved for such things.
255 if (strlen($a[5]) > 200) {
256 $ares['result_data_type'] = 'L';
257 $ares['result'] = '';
258 $ares['comments'] = rhl7Text($a[5]) . $commentdelim;
260 else {
261 $ares['result_data_type'] = substr($a[2], 0, 1); // N, S or F
262 $ares['result'] = rhl7Text($a[5]);
263 $ares['comments'] = $commentdelim;
265 $tmp = explode($d2, $a[3]);
266 $ares['result_code'] = rhl7Text($tmp[0]);
267 $ares['result_text'] = rhl7Text($tmp[1]);
268 $ares['date'] = rhl7DateTime($a[14]);
269 $ares['facility'] = rhl7Text($a[15]);
270 $ares['units'] = rhl7Text($a[6]);
271 $ares['range'] = rhl7Text($a[7]);
272 $ares['abnormal'] = rhl7Abnormal($a[8]); // values are lab dependent
273 $ares['result_status'] = rhl7ReportStatus($a[11]);
276 else if ($a[0] == 'NTE' && $context == 'OBX') {
277 $ares['comments'] .= rhl7Text($a[3]) . $commentdelim;
280 // Add code here for any other segment types that may be present.
282 else {
283 return xl('Segment name') . " '${a[0]}' " . xl('is misplaced or unknown');
287 rhl7FlushResult($ares);
288 // Next line will do something only if there was a report with no results.
289 rhl7FlushReport($arep);
290 return '';
294 * Poll all eligible labs for new results and store them in the database.
296 * @param array &$messages Receives messages of interest.
297 * @return string Error text, or empty if no errors.
299 function poll_hl7_results(&$messages) {
300 global $srcdir;
302 $messages = array();
303 $filecount = 0;
304 $badcount = 0;
306 $ppres = sqlStatement("SELECT * FROM procedure_providers ORDER BY name");
308 while ($pprow = sqlFetchArray($ppres)) {
309 $protocol = $pprow['protocol'];
310 $remote_host = $pprow['remote_host'];
311 $hl7 = '';
313 if ($protocol == 'SFTP') {
314 ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . "$srcdir/phpseclib");
315 require_once("$srcdir/phpseclib/Net/SFTP.php");
316 // Compute the target path name.
317 $pathname = '.';
318 if ($pprow['results_path']) $pathname = $pprow['results_path'] . '/' . $pathname;
319 // Connect to the server and enumerate files to process.
320 $sftp = new Net_SFTP($remote_host);
321 if (!$sftp->login($pprow['login'], $pprow['password'])) {
322 return xl('Login to remote host') . " '$remote_host' " . xl('failed');
324 $files = $sftp->nlist($pathname);
325 foreach ($files as $file) {
326 if (substr($file, 0, 1) == '.') continue;
327 ++$filecount;
328 $hl7 = $sftp->get("$pathname/$file");
329 // Archive the results file.
330 $prpath = $GLOBALS['OE_SITE_DIR'] . "/procedure_results";
331 if (!file_exists($prpath)) mkdir($prpath);
332 $prpath .= '/' . $pprow['ppid'];
333 if (!file_exists($prpath)) mkdir($prpath);
334 $fh = fopen("$prpath/$file", 'w');
335 if ($fh) {
336 fwrite($fh, $hl7);
337 fclose($fh);
339 else {
340 $messages[] = xl('File') . " '$file' " . xl('cannot be archived, ignored');
341 ++$badcount;
342 continue;
344 // Now delete it from its ftp directory.
345 if (!$sftp->delete("$pathname/$file")) {
346 $messages[] = xl('File') . " '$file' " . xl('cannot be deleted, ignored');
347 ++$badcount;
348 continue;
350 // Parse and process its contents.
351 $msg = receive_hl7_results($pprow, $hl7);
352 if ($msg) {
353 $messages[] = xl('Error processing file') . " '$file':" . $msg;
354 ++$badcount;
355 continue;
357 $messages[] = xl('New file') . " '$file' " . xl('processed successfully');
361 // TBD: Insert "else if ($protocol == '???') {...}" to support other protocols.
365 if ($badcount) return "$badcount " . xl('error(s) encountered from new results');
367 return '';