Interim autoloaded library/classes via composer classmap, take 4. (#422)
[openemr.git] / interface / orders / gen_hl7_order.inc.php
blob2d74905d0bfa945e0c15121a30853f735cdba25d
1 <?php
2 /**
3 * Functions to support HL7 order generation.
5 * Copyright (C) 2012-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>
23 * A bit of documentation that will need to go into the manual:
25 * The lab may want a list of your insurances for mapping into their system.
26 * To produce it, go into phpmyadmin and run this query:
28 * SELECT i.id, i.name, a.line1, a.line2, a.city, a.state, a.zip, p.area_code,
29 * p.prefix, p.number FROM insurance_companies AS i
30 * LEFT JOIN addresses AS a ON a.foreign_id = i.id
31 * LEFT JOIN phone_numbers AS p ON p.type = 2 AND p.foreign_id = i.id
32 * ORDER BY i.name, i.id;
34 * Then export as a CSV file and read it into your favorite spreadsheet app.
37 require_once("$webserver_root/custom/code_types.inc.php");
39 function hl7Text($s) {
40 // See http://www.interfaceware.com/hl7_escape_protocol.html:
41 $s = str_replace('\\', '\\E\\' , $s);
42 $s = str_replace('^' , '\\S\\' , $s);
43 $s = str_replace('|' , '\\F\\' , $s);
44 $s = str_replace('~' , '\\R\\' , $s);
45 $s = str_replace('&' , '\\T\\' , $s);
46 $s = str_replace("\r", '\\X0d\\', $s);
47 return $s;
50 function hl7Zip($s) {
51 return hl7Text(preg_replace('/[-\s]*/','',$s));
54 function hl7Date($s) {
55 return preg_replace('/[^\d]/','',$s);
58 function hl7Time($s) {
59 if (empty($s)) return '';
60 return date('YmdHis', strtotime($s));
63 function hl7Sex($s) {
64 $s = strtoupper(substr($s, 0, 1));
65 if ($s !== 'M' && $s !== 'F') $s = 'U';
66 return $s;
69 function hl7Phone($s) {
70 if (preg_match("/([2-9]\d\d)\D*(\d\d\d)\D*(\d\d\d\d)\D*$/", $s, $tmp)) {
71 return '(' . $tmp[1] . ')' . $tmp[2] . '-' . $tmp[3];
73 if (preg_match("/(\d\d\d)\D*(\d\d\d\d)\D*$/", $s, $tmp)) {
74 return $tmp[1] . '-' . $tmp[2];
76 return '';
79 function hl7SSN($s) {
80 if (preg_match("/(\d\d\d)\D*(\d\d)\D*(\d\d\d\d)\D*$/", $s, $tmp)) {
81 return $tmp[1] . '-' . $tmp[2] . '-' . $tmp[3];
83 return '';
86 function hl7Priority($s) {
87 return strtoupper(substr($s, 0, 1)) == 'H' ? 'S' : 'R';
90 function hl7Relation($s) {
91 $tmp = strtolower($s);
92 if ($tmp == 'self' || $tmp == '') return 'self';
93 else if ($tmp == 'spouse') return 'spouse';
94 else if ($tmp == 'child' ) return 'child';
95 else if ($tmp == 'other' ) return 'other';
96 // Should not get here so this will probably get noticed if we do.
97 return $s;
101 * Get array of insurance payers for the specified patient as of the specified
102 * date. If no date is passed then the current date is used.
104 * @param integer $pid Patient ID.
105 * @param date $encounter_date YYYY-MM-DD date.
106 * @return array Array containing an array of data for each payer.
108 function loadPayerInfo($pid, $date='') {
109 if (empty($date)) $date = date('Y-m-d');
110 $payers = array();
111 $dres = sqlStatement("SELECT * FROM insurance_data WHERE " .
112 "pid = ? AND date <= ? ORDER BY type ASC, date DESC",
113 array($pid, $date));
114 $prevtype = ''; // type is primary, secondary or tertiary
115 while ($drow = sqlFetchArray($dres)) {
116 if (strcmp($prevtype, $drow['type']) == 0) continue;
117 $prevtype = $drow['type'];
118 // Very important to check for a missing provider because
119 // that indicates no insurance as of the given date.
120 if (empty($drow['provider'])) continue;
121 $ins = count($payers);
122 $crow = sqlQuery("SELECT * FROM insurance_companies WHERE id = ?",
123 array($drow['provider']));
124 $orow = new InsuranceCompany($drow['provider']);
125 $payers[$ins] = array();
126 $payers[$ins]['data'] = $drow;
127 $payers[$ins]['company'] = $crow;
128 $payers[$ins]['object'] = $orow;
130 return $payers;
134 * Generate HL7 for the specified procedure order.
136 * @param integer $orderid Procedure order ID.
137 * @param string &$out Container for target HL7 text.
138 * @return string Error text, or empty if no errors.
140 function gen_hl7_order($orderid, &$out) {
142 // Delimiters
143 $d0 = "\r";
144 $d1 = '|';
145 $d2 = '^';
147 $today = time();
148 $out = '';
150 $porow = sqlQuery("SELECT " .
151 "po.date_collected, po.date_ordered, po.order_priority, " .
152 "pp.*, " .
153 "pd.pid, pd.pubpid, pd.fname, pd.lname, pd.mname, pd.DOB, pd.ss, " .
154 "pd.phone_home, pd.phone_biz, pd.sex, pd.street, pd.city, pd.state, pd.postal_code, " .
155 "f.encounter, u.fname AS docfname, u.lname AS doclname, u.npi AS docnpi " .
156 "FROM procedure_order AS po, procedure_providers AS pp, " .
157 "forms AS f, patient_data AS pd, users AS u " .
158 "WHERE " .
159 "po.procedure_order_id = ? AND " .
160 "pp.ppid = po.lab_id AND " .
161 "f.formdir = 'procedure_order' AND " .
162 "f.form_id = po.procedure_order_id AND " .
163 "pd.pid = f.pid AND " .
164 "u.id = po.provider_id",
165 array($orderid));
166 if (empty($porow)) return "Procedure order, ordering provider or lab is missing for order ID '$orderid'";
168 $pcres = sqlStatement("SELECT " .
169 "pc.procedure_code, pc.procedure_name, pc.procedure_order_seq, pc.diagnoses " .
170 "FROM procedure_order_code AS pc " .
171 "WHERE " .
172 "pc.procedure_order_id = ? AND " .
173 "pc.do_not_send = 0 " .
174 "ORDER BY pc.procedure_order_seq",
175 array($orderid));
177 // Message Header
178 $out .= "MSH" .
179 $d1 . "$d2~\\&" . // Encoding Characters (delimiters)
180 $d1 . $porow['send_app_id'] . // Sending Application ID
181 $d1 . $porow['send_fac_id'] . // Sending Facility ID
182 $d1 . $porow['recv_app_id'] . // Receiving Application ID
183 $d1 . $porow['recv_fac_id'] . // Receiving Facility ID
184 $d1 . date('YmdHis', $today) . // Date and time of this message
185 $d1 .
186 $d1 . 'ORM' . $d2 . 'O01' . // Message Type
187 $d1 . $orderid . // Unique Message Number
188 $d1 . $porow['DorP'] . // D=Debugging, P=Production
189 $d1 . '2.3' . // HL7 Version ID
190 $d0;
192 // Patient Identification
193 $out .= "PID" .
194 $d1 . "1" . // Set ID (always just 1 of these)
195 $d1 . $porow['pid'] . // Patient ID (not required)
196 $d1 . $porow['pid'] . // Patient ID (required)
197 $d1 . // Alternate Patient ID (not required)
198 $d1 . hl7Text($porow['lname']) .
199 $d2 . hl7Text($porow['fname']);
200 if ($porow['mname']) $out .= $d2 . hl7Text($porow['mname']);
201 $out .=
202 $d1 .
203 $d1 . hl7Date($porow['DOB']) . // DOB
204 $d1 . hl7Sex($porow['sex']) . // Sex: M, F or U
205 $d1 . $d1 .
206 $d1 . hl7Text($porow['street']) .
207 $d2 .
208 $d2 . hl7Text($porow['city']) .
209 $d2 . hl7Text($porow['state']) .
210 $d2 . hl7Zip($porow['postal_code']) .
211 $d1 .
212 $d1 . hl7Phone($porow['phone_home']) .
213 $d1 . hl7Phone($porow['phone_biz']) .
214 $d1 . $d1 . $d1 .
215 $d1 . $porow['encounter'] .
216 $d1 . hl7SSN($porow['ss']) .
217 $d1 . $d1 . $d1 .
218 $d0;
220 // NTE segment(s) omitted.
222 // Patient Visit.
223 $out .= "PV1" .
224 $d1 . "1" . // Set ID (always just 1 of these)
225 $d1 . // Patient Class (if required, O for Outpatient)
226 $d1 . // Patient Location (for inpatient only?)
227 $d1 . $d1 . $d1 .
228 $d1 . hl7Text($porow['docnpi']) . // Attending Doctor ID
229 $d2 . hl7Text($porow['doclname']) . // Last Name
230 $d2 . hl7Text($porow['docfname']) . // First Name
231 str_repeat($d1, 11) . // PV1 8 to 18 all empty
232 $d1 . $porow['encounter'] . // Encounter Number
233 str_repeat($d1, 13) . // PV1 20 to 32 all empty
234 $d0;
236 // Insurance stuff.
237 $payers = loadPayerInfo($porow['pid'], $porow['date_ordered']);
238 $setid = 0;
239 foreach ($payers as $payer) {
240 $payer_object = $payer['object'];
241 $payer_address = $payer_object->get_address();
242 $out .= "IN1" .
243 $d1 . ++$setid . // Set ID
244 $d1 . // Insurance Plan Identifier ??
245 $d1 . hl7Text($payer['company']['id']) . // Insurance Company ID
246 $d1 . hl7Text($payer['company']['name']) . // Insurance Company Name
247 $d1 . hl7Text($payer_address->get_line1()) . // Street Address
248 $d2 .
249 $d2 . hl7Text($payer_address->get_city()) . // City
250 $d2 . hl7Text($payer_address->get_state()) . // State
251 $d2 . hl7Zip($payer_address->get_zip()) . // Zip Code
252 $d1 .
253 $d1 . hl7Phone($payer_object->get_phone()) . // Phone Number
254 $d1 . hl7Text($payer['data']['group_number']) . // Insurance Company Group Number
255 str_repeat($d1, 7) . // IN1 9-15 all empty
256 $d1 . hl7Text($payer['data']['subscriber_lname']) . // Insured last name
257 $d2 . hl7Text($payer['data']['subscriber_fname']) . // Insured first name
258 $d2 . hl7Text($payer['data']['subscriber_mname']) . // Insured middle name
259 $d1 . hl7Relation($payer['data']['subscriber_relationship']) .
260 $d1 . hl7Date($payer['data']['subscriber_DOB']) . // Insured DOB
261 $d1 . hl7Date($payer['data']['subscriber_street']) . // Insured Street Address
262 $d2 .
263 $d2 . hl7Text($payer['data']['subscriber_city']) . // City
264 $d2 . hl7Text($payer['data']['subscriber_state']) . // State
265 $d2 . hl7Zip($payer['data']['subscriber_postal_code']) . // Zip
266 $d1 .
267 $d1 .
268 $d1 . $setid . // 1=Primary, 2=Secondary, 3=Tertiary
269 str_repeat($d1, 13) . // IN1-23 to 35 all empty
270 $d1 . hl7Text($payer['data']['policy_number']) . // Policy Number
271 str_repeat($d1, 12) . // IN1-37 to 48 all empty
272 $d0;
274 // IN2 segment omitted.
277 // Guarantor. OpenEMR doesn't have these so use the patient.
278 $out .= "GT1" .
279 $d1 . "1" . // Set ID (always just 1 of these)
280 $d1 .
281 $d1 . hl7Text($porow['lname']) .
282 $d2 . hl7Text($porow['fname']);
283 if ($porow['mname']) $out .= $d2 . hl7Text($porow['mname']);
284 $out .=
285 $d1 .
286 $d1 . hl7Text($porow['street']) .
287 $d2 .
288 $d2 . hl7Text($porow['city']) .
289 $d2 . hl7Text($porow['state']) .
290 $d2 . hl7Zip($porow['postal_code']) .
291 $d1 . hl7Phone($porow['phone_home']) .
292 $d1 . hl7Phone($porow['phone_biz']) .
293 $d1 . hl7Date($porow['DOB']) . // DOB
294 $d1 . hl7Sex($porow['sex']) . // Sex: M, F or U
295 $d1 .
296 $d1 . 'self' . // Relationship
297 $d1 . hl7SSN($porow['ss']) .
298 $d0;
300 // Common Order.
301 $out .= "ORC" .
302 $d1 . "NW" . // New Order
303 $d1 . $orderid . // Placer Order Number
304 str_repeat($d1, 6) . // ORC 3-8 not used
305 $d1 . date('YmdHis') . // Transaction date/time
306 $d1 . $d1 .
307 $d1 . hl7Text($porow['docnpi']) . // Ordering Provider
308 $d2 . hl7Text($porow['doclname']) . // Last Name
309 $d2 . hl7Text($porow['docfname']) . // First Name
310 str_repeat($d1, 7) . // ORC 13-19 not used
311 $d1 . "2" . // ABN Status: 2 = Notified & Signed, 4 = Unsigned
312 $d0;
314 $setid = 0;
315 while ($pcrow = sqlFetchArray($pcres)) {
316 // Observation Request.
317 $out .= "OBR" .
318 $d1 . ++$setid . // Set ID
319 $d1 . $orderid . // Placer Order Number
320 $d1 .
321 $d1 . hl7Text($pcrow['procedure_code']) .
322 $d2 . hl7Text($pcrow['procedure_name']) .
323 $d1 . hl7Priority($porow['order_priority']) . // S=Stat, R=Routine
324 $d1 .
325 $d1 . hl7Time($porow['date_collected']) . // Observation Date/Time
326 str_repeat($d1, 8) . // OBR 8-15 not used
327 $d1 . hl7Text($porow['docnpi']) . // Physician ID
328 $d2 . hl7Text($porow['doclname']) . // Last Name
329 $d2 . hl7Text($porow['docfname']) . // First Name
330 $d1 .
331 $d1 . (count($payers) ? 'I' : 'P') . // I=Insurance, C=Client, P=Self Pay
332 str_repeat($d1, 8) . // OBR 19-26 not used
333 $d1 . '0' . // ?
334 $d0;
336 // Diagnoses. Currently hard-coded for ICD9 and we'll surely want to make
337 // this more flexible (probably when some lab needs another diagnosis type).
338 $setid2 = 0;
339 if (!empty($pcrow['diagnoses'])) {
340 $relcodes = explode(';', $pcrow['diagnoses']);
341 foreach ($relcodes as $codestring) {
342 if ($codestring === '') continue;
343 list($codetype, $code) = explode(':', $codestring);
344 if ($codetype !== 'ICD9') continue;
345 $desc = lookup_code_descriptions($codestring);
346 $out .= "DG1" .
347 $d1 . ++$setid2 . // Set ID
348 $d1 . // Diagnosis Coding Method
349 $d1 . $code . // Diagnosis Code
350 $d1 . hl7Text($desc) . // Diagnosis Description
351 $d0;
355 // Order entry questions and answers.
356 $qres = sqlStatement("SELECT " .
357 "a.question_code, a.answer, q.fldtype " .
358 "FROM procedure_answers AS a " .
359 "LEFT JOIN procedure_questions AS q ON " .
360 "q.lab_id = ? " .
361 "AND q.procedure_code = ? AND " .
362 "q.question_code = a.question_code " .
363 "WHERE " .
364 "a.procedure_order_id = ? AND " .
365 "a.procedure_order_seq = ? " .
366 "ORDER BY q.seq, a.answer_seq",
367 array($porow['ppid'], $pcrow['procedure_code'], $orderid, $pcrow['procedure_order_seq']));
368 $setid2 = 0;
369 while ($qrow = sqlFetchArray($qres)) {
370 // Formatting of these answer values may be lab-specific and we'll figure
371 // out how to deal with that as more labs are supported.
372 $answer = trim($qrow['answer']);
373 $fldtype = $qrow['fldtype'];
374 $datatype = 'ST';
375 if ($fldtype == 'N') {
376 $datatype = "NM";
377 } else if ($fldtype == 'D') {
378 $answer = hl7Date($answer);
379 } else if ($fldtype == 'G') {
380 $weeks = intval($answer / 7);
381 $days = $answer % 7;
382 $answer = $weeks . 'wks ' . $days . 'days';
384 $out .= "OBX" .
385 $d1 . ++$setid2 . // Set ID
386 $d1 . $datatype . // Structure of observation value
387 $d1 . hl7Text($qrow['question_code']) . // Clinical question code
388 $d1 .
389 $d1 . hl7Text($answer) . // Clinical question answer
390 $d0;
394 return '';
398 * Transmit HL7 for the specified lab.
400 * @param integer $ppid Procedure provider ID.
401 * @param string $out The HL7 text to be sent.
402 * @return string Error text, or empty if no errors.
404 function send_hl7_order($ppid, $out) {
405 global $srcdir;
407 $d0 = "\r";
409 $pprow = sqlQuery("SELECT * FROM procedure_providers " .
410 "WHERE ppid = ?", array($ppid));
411 if (empty($pprow)) return xl('Procedure provider') . " $ppid " . xl('not found');
413 $protocol = $pprow['protocol'];
414 $remote_host = $pprow['remote_host'];
416 // Extract MSH-10 which is the message control ID.
417 $segmsh = explode(substr($out, 3, 1), substr($out, 0, strpos($out, $d0)));
418 $msgid = $segmsh[9];
419 if (empty($msgid)) return xl('Internal error: Cannot find MSH-10');
421 if ($protocol == 'DL' || $pprow['orders_path'] === '') {
422 header("Pragma: public");
423 header("Expires: 0");
424 header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
425 header("Content-Type: application/force-download");
426 header("Content-Disposition: attachment; filename=order_$msgid.hl7");
427 header("Content-Description: File Transfer");
428 echo $out;
429 exit;
432 else if ($protocol == 'SFTP') {
433 // Compute the target path/file name.
434 $filename = $msgid . '.txt';
435 if ($pprow['orders_path']) $filename = $pprow['orders_path'] . '/' . $filename;
437 // Connect to the server and write the file.
438 $sftp = new \phpseclib\Net\SFTP($remote_host);
439 if (!$sftp->login($pprow['login'], $pprow['password'])) {
440 return xl('Login to this remote host failed') . ": '$remote_host'";
442 if (!$sftp->put($filename, $out)) {
443 return xl('Creating this file on remote host failed') . ": '$filename'";
447 else if ($protocol == 'FS') {
448 // Compute the target path/file name.
449 $filename = $msgid . '.txt';
450 if ($pprow['orders_path']) $filename = $pprow['orders_path'] . '/' . $filename;
451 $fh = fopen("$filename", 'w');
452 if ($fh) {
453 fwrite($fh, $out);
454 fclose($fh);
456 else {
457 return xl('Cannot create file') . ' "' . "$filename" . '"';
461 // TBD: Insert "else if ($protocol == '???') {...}" to support other protocols.
463 else {
464 return xl('This protocol is not implemented') . ": '$protocol'";
467 // Falling through to here indicates success.
468 newEvent("proc_order_xmit", $_SESSION['authUser'], $_SESSION['authProvider'], 1,
469 "ID: $msgid Protocol: $protocol Host: $remote_host");
470 return '';