fix: fix ci (#7614)
[openemr.git] / interface / orders / gen_hl7_order.inc.php
blobc8a27a7bc6e4fee4f42394383c07eb97e5659d06
1 <?php
3 /**
4 * Functions to support HL7 order generation.
6 * Copyright (C) 2012-2013 Rod Roark <rod@sunsetsystems.com>
8 * LICENSE: This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>.
19 * @package OpenEMR
20 * @author Rod Roark <rod@sunsetsystems.com>
21 * @author Jerry Padgett <sjpadgett@gmail.com>
25 * A bit of documentation that will need to go into the manual:
27 * The lab may want a list of your insurances for mapping into their system.
28 * To produce it, go into phpmyadmin and run this query:
30 * SELECT i.id, i.name, a.line1, a.line2, a.city, a.state, a.zip, p.area_code,
31 * p.prefix, p.number FROM insurance_companies AS i
32 * LEFT JOIN addresses AS a ON a.foreign_id = i.id
33 * LEFT JOIN phone_numbers AS p ON p.type = 2 AND p.foreign_id = i.id
34 * ORDER BY i.name, i.id;
36 * Then export as a CSV file and read it into your favorite spreadsheet app.
39 require_once("$webserver_root/custom/code_types.inc.php");
41 use OpenEMR\Common\Logging\EventAuditLogger;
43 function hl7Text($s)
45 // See http://www.interfaceware.com/hl7_escape_protocol.html:
46 $s = str_replace('\\', '\\E\\', $s);
47 $s = str_replace('^', '\\S\\', $s);
48 $s = str_replace('|', '\\F\\', $s);
49 $s = str_replace('~', '\\R\\', $s);
50 $s = str_replace('&', '\\T\\', $s);
51 $s = str_replace("\r", '\\X0d\\', $s);
52 return $s;
55 function hl7Zip($s)
57 return hl7Text(preg_replace('/[-\s]*/', '', $s));
60 function hl7Date($s)
62 return preg_replace('/[^\d]/', '', $s);
65 function hl7Time($s)
67 if (empty($s)) {
68 return '';
71 return date('YmdHis', strtotime($s));
74 function hl7Sex($s)
76 $s = strtoupper(substr($s, 0, 1));
77 if ($s !== 'M' && $s !== 'F') {
78 $s = 'U';
81 return $s;
84 function hl7Phone($s)
86 if (preg_match("/([2-9]\d\d)\D*(\d\d\d)\D*(\d\d\d\d)\D*$/", $s, $tmp)) {
87 return '(' . $tmp[1] . ')' . $tmp[2] . '-' . $tmp[3];
90 if (preg_match("/(\d\d\d)\D*(\d\d\d\d)\D*$/", $s, $tmp)) {
91 return $tmp[1] . '-' . $tmp[2];
94 return '';
97 function hl7SSN($s)
99 if (preg_match("/(\d\d\d)\D*(\d\d)\D*(\d\d\d\d)\D*$/", $s, $tmp)) {
100 return $tmp[1] . '-' . $tmp[2] . '-' . $tmp[3];
103 return '';
106 function hl7Priority($s)
108 return strtoupper(substr($s, 0, 1)) == 'H' ? 'S' : 'R';
111 function hl7Relation($s)
113 $tmp = strtolower($s);
114 if ($tmp == 'self' || $tmp == '') {
115 return 'self';
116 } elseif ($tmp == 'spouse') {
117 return 'spouse';
118 } elseif ($tmp == 'child') {
119 return 'child';
120 } elseif ($tmp == 'other') {
121 return 'other';
124 // Should not get here so this will probably get noticed if we do.
125 return $s;
129 * Get array of insurance payers for the specified patient as of the specified
130 * date. If no date is passed then the current date is used.
132 * @param integer $pid Patient ID.
133 * @param date $encounter_date YYYY-MM-DD date.
134 * @return array Array containing an array of data for each payer.
136 function loadPayerInfo($pid, $date = '')
138 if (empty($date)) {
139 $date = date('Y-m-d');
142 $payers = getEffectiveInsurances($pid, $date);
144 foreach ($payers as $key => $drow) {
145 // Very important to check for a missing provider because
146 // that indicates no insurance as of the given date.
147 if (empty($drow['provider'])) {
148 continue;
151 $crow = sqlQuery(
152 "SELECT * FROM insurance_companies WHERE id = ?",
153 array($drow['provider'])
156 $orow = new InsuranceCompany($drow['provider']);
157 $payers[$key] = array();
158 $payers[$key]['data'] = $drow;
159 $payers[$key]['company'] = $crow;
160 $payers[$key]['object'] = $orow;
163 return $payers;
167 * Generate HL7 for the specified procedure order.
169 * @param integer $orderid Procedure order ID.
170 * @param string &$out Container for target HL7 text.
171 * @return string Error text, or empty if no errors.
173 function gen_hl7_order($orderid, &$out)
176 // Delimiters
177 $d0 = "\r";
178 $d1 = '|';
179 $d2 = '^';
181 $today = time();
182 $out = '';
184 $porow = sqlQuery(
185 "SELECT " .
186 "po.date_collected, po.date_ordered, po.order_priority, " .
187 "pp.*, " .
188 "pd.pid, pd.pubpid, pd.fname, pd.lname, pd.mname, pd.DOB, pd.ss, " .
189 "pd.phone_home, pd.phone_biz, pd.sex, pd.street, pd.city, pd.state, pd.postal_code, " .
190 "f.encounter, u.fname AS docfname, u.lname AS doclname, u.npi AS docnpi " .
191 "FROM procedure_order AS po, procedure_providers AS pp, " .
192 "forms AS f, patient_data AS pd, users AS u " .
193 "WHERE " .
194 "po.procedure_order_id = ? AND " .
195 "pp.ppid = po.lab_id AND " .
196 "f.formdir = 'procedure_order' AND " .
197 "f.form_id = po.procedure_order_id AND " .
198 "pd.pid = f.pid AND " .
199 "u.id = po.provider_id",
200 array($orderid)
202 if (empty($porow)) {
203 return "Procedure order, ordering provider or lab is missing for order ID '$orderid'";
206 $pcres = sqlStatement(
207 "SELECT " .
208 "pc.procedure_code, pc.procedure_name, pc.procedure_order_seq, pc.diagnoses, pt.specimen " .
209 "FROM procedure_order_code AS pc, procedure_type as pt " .
210 "WHERE " .
211 "pc.procedure_order_id = ? AND " .
212 "pc.procedure_name = pt.name AND " .
213 "pc.do_not_send = 0 " .
214 "ORDER BY pc.procedure_order_seq",
215 array($orderid)
218 // Message Header
219 $out .= "MSH" .
220 $d1 . "$d2~\\&" . // Encoding Characters (delimiters)
221 $d1 . $porow['send_app_id'] . // Sending Application ID
222 $d1 . $porow['send_fac_id'] . // Sending Facility ID
223 $d1 . $porow['recv_app_id'] . // Receiving Application ID
224 $d1 . $porow['recv_fac_id'] . // Receiving Facility ID
225 $d1 . date('YmdHis', $today) . // Date and time of this message
226 $d1 .
227 $d1 . 'ORM' . $d2 . 'O01' . // Message Type
228 $d1 . str_pad((string)$orderid, 4, "0", STR_PAD_LEFT) . // Unique Message Number
229 $d1 . $porow['DorP'] . // D=Debugging, P=Production
230 $d1 . '2.3' . // HL7 Version ID
231 $d0;
233 // Patient Identification
234 $out .= "PID" .
235 $d1 . "1" . // Set ID (always just 1 of these)
236 $d1 . $porow['pid'] . // Patient ID (not required)
237 $d1 . $porow['pid'] . // Patient ID (required)
238 $d1 . // Alternate Patient ID (not required)
239 $d1 . hl7Text($porow['lname']) .
240 $d2 . hl7Text($porow['fname']);
241 if ($porow['mname']) {
242 $out .= $d2 . hl7Text($porow['mname']);
245 $out .=
246 $d1 .
247 $d1 . hl7Date($porow['DOB']) . // DOB
248 $d1 . hl7Sex($porow['sex']) . // Sex: M, F or U
249 $d1 . $d1 .
250 $d1 . hl7Text($porow['street']) .
251 $d2 .
252 $d2 . hl7Text($porow['city']) .
253 $d2 . hl7Text($porow['state']) .
254 $d2 . hl7Zip($porow['postal_code']) .
255 $d1 .
256 $d1 . hl7Phone($porow['phone_home']) .
257 $d1 . hl7Phone($porow['phone_biz']) .
258 $d1 . $d1 . $d1 .
259 $d1 . $porow['encounter'] .
260 $d1 . hl7SSN($porow['ss']) .
261 $d1 . $d1 . $d1 .
262 $d0;
264 // NTE segment(s).
265 //$msql = sqlStatement("SELECT title FROM lists WHERE type='medication' AND pid='".$porow['pid']."'");
266 $msql = sqlStatement("SELECT drug FROM prescriptions WHERE active=1 AND patient_id=?", [$porow['pid']]);
267 $drugs = array();
268 while ($mres = sqlFetchArray($msql)) {
269 $drugs[] = trim($mres['drug']);
271 $med_list = count($drugs) > 0 ? implode(",", $drugs) : 'NONE';
273 $out .= "NTE" .
274 $d1 . "1" .
275 $d1 . "L" .
276 $d1 . "Medications:" . $med_list .
277 $d0;
279 // Patient Visit.
280 $out .= "PV1" .
281 $d1 . "1" . // Set ID (always just 1 of these)
282 $d1 . // Patient Class (if required, O for Outpatient)
283 $d1 . // Patient Location (for inpatient only?)
284 $d1 . $d1 . $d1 .
285 $d1 . hl7Text($porow['docnpi']) . // Attending Doctor ID
286 $d2 . hl7Text($porow['doclname']) . // Last Name
287 $d2 . hl7Text($porow['docfname']) . // First Name
288 str_repeat($d1, 11) . // PV1 8 to 18 all empty
289 $d1 . $porow['encounter'] . // Encounter Number
290 str_repeat($d1, 13) . // PV1 20 to 32 all empty
291 $d0;
293 // Insurance stuff.
294 $payers = loadPayerInfo($porow['pid'], $porow['date_ordered']);
295 $setid = 0;
296 foreach ($payers as $payer) {
297 $payer_object = $payer['object'];
298 $payer_address = $payer_object->get_address();
299 $out .= "IN1" .
300 $d1 . ++$setid . // Set ID
301 $d1 . // Insurance Plan Identifier ??
302 $d1 . hl7Text($payer['company']['id']) . // Insurance Company ID
303 $d1 . hl7Text($payer['company']['name']) . // Insurance Company Name
304 $d1 . hl7Text($payer_address->get_line1()) . // Street Address
305 $d2 .
306 $d2 . hl7Text($payer_address->get_city()) . // City
307 $d2 . hl7Text($payer_address->get_state()) . // State
308 $d2 . hl7Zip($payer_address->get_zip()) . // Zip Code
309 $d1 .
310 $d1 . hl7Phone($payer_object->get_phone()) . // Phone Number
311 $d1 . hl7Text($payer['data']['group_number']) . // Insurance Company Group Number
312 str_repeat($d1, 7) . // IN1 9-15 all empty
313 $d1 . hl7Text($payer['data']['subscriber_lname']) . // Insured last name
314 $d2 . hl7Text($payer['data']['subscriber_fname']) . // Insured first name
315 $d2 . hl7Text($payer['data']['subscriber_mname']) . // Insured middle name
316 $d1 . hl7Relation($payer['data']['subscriber_relationship']) .
317 $d1 . hl7Date($payer['data']['subscriber_DOB']) . // Insured DOB
318 $d1 . hl7Date($payer['data']['subscriber_street']) . // Insured Street Address
319 $d2 .
320 $d2 . hl7Text($payer['data']['subscriber_city']) . // City
321 $d2 . hl7Text($payer['data']['subscriber_state']) . // State
322 $d2 . hl7Zip($payer['data']['subscriber_postal_code']) . // Zip
323 $d1 .
324 $d1 .
325 $d1 . $setid . // 1=Primary, 2=Secondary, 3=Tertiary
326 str_repeat($d1, 13) . // IN1-23 to 35 all empty
327 $d1 . hl7Text($payer['data']['policy_number']) . // Policy Number
328 str_repeat($d1, 12) . // IN1-37 to 48 all empty
329 $d0;
331 // IN2 segment omitted.
334 // Guarantor. OpenEMR doesn't have these so use the patient.
335 $out .= "GT1" .
336 $d1 . "1" . // Set ID (always just 1 of these)
337 $d1 .
338 $d1 . hl7Text($porow['lname']) .
339 $d2 . hl7Text($porow['fname']);
340 if ($porow['mname']) {
341 $out .= $d2 . hl7Text($porow['mname']);
344 $out .=
345 $d1 .
346 $d1 . hl7Text($porow['street']) .
347 $d2 .
348 $d2 . hl7Text($porow['city']) .
349 $d2 . hl7Text($porow['state']) .
350 $d2 . hl7Zip($porow['postal_code']) .
351 $d1 . hl7Phone($porow['phone_home']) .
352 $d1 . hl7Phone($porow['phone_biz']) .
353 $d1 . hl7Date($porow['DOB']) . // DOB
354 $d1 . hl7Sex($porow['sex']) . // Sex: M, F or U
355 $d1 .
356 $d1 . 'self' . // Relationship
357 $d1 . hl7SSN($porow['ss']) .
358 $d0;
360 // Common Order.
361 $out .= "ORC" .
362 $d1 . "NW" . // New Order
363 $d1 . str_pad((string)$orderid, 4, "0", STR_PAD_LEFT) . // Placer Order Number
364 str_repeat($d1, 6) . // ORC 3-8 not used
365 $d1 . date('YmdHis') . // Transaction date/time
366 $d1 . $d1 .
367 $d1 . hl7Text($porow['docnpi']) . // Ordering Provider
368 $d2 . hl7Text($porow['doclname']) . // Last Name
369 $d2 . hl7Text($porow['docfname']) . // First Name
370 str_repeat($d1, 7) . // ORC 13-19 not used
371 $d1 . "2" . // ABN Status: 2 = Notified & Signed, 4 = Unsigned
372 $d0;
374 $setid = 0;
375 while ($pcrow = sqlFetchArray($pcres)) {
376 // Observation Request.
378 $dl = '';
379 $out .= "OBR" .
380 $d1 . ++$setid . // Set ID
381 $d1 . str_pad((string)$orderid, 4, "0", STR_PAD_LEFT) . // Placer Order Number
382 $d1 .
383 $d1 . hl7Text($pcrow['procedure_code']) .
384 $d2 . hl7Text($pcrow['procedure_name']) .
385 $d1 . hl7Priority($porow['order_priority']) . // S=Stat, R=Routine
386 $d1 .
387 $d1 . hl7Time($porow['date_collected']) . // Observation Date/Time
388 str_repeat($d1, 8) . // OBR 8-15 not used
389 $dl . hl7Text($pcrow['specimen']) . // Specimen source aka OBR-15
390 $d1 . hl7Text($porow['docnpi']) . // Physician ID
391 $d2 . hl7Text($porow['doclname']) . // Last Name
392 $d2 . hl7Text($porow['docfname']) . // First Name
393 $d1 .
394 $d1 . (count($payers) ? 'I' : 'P') . // I=Insurance, C=Client, P=Self Pay
395 str_repeat($d1, 8) . // OBR 19-26 not used
396 $d1 . '0' . // ?
397 $d0;
399 // Diagnoses. Currently hard-coded for ICD9 and we'll surely want to make
400 // this more flexible (probably when some lab needs another diagnosis type).
401 $setid2 = 0;
402 if (!empty($pcrow['diagnoses'])) {
403 $relcodes = explode(';', $pcrow['diagnoses']);
404 foreach ($relcodes as $codestring) {
405 if ($codestring === '') {
406 continue;
409 list($codetype, $code) = explode(':', $codestring);
410 if ($codetype !== 'ICD10') {
411 continue;
414 $desc = lookup_code_descriptions($codestring);
415 $out .= "DG1" .
416 $d1 . ++$setid2 . // Set ID
417 $d1 . // Diagnosis Coding Method
418 $d1 . $code . // Diagnosis Code
419 $d2 . hl7Text($desc) . // Diagnosis Description
420 $d2 . "I10" . // Diagnosis Type
421 $d1 . $d0;
425 // Order entry questions and answers.
426 $qres = sqlStatement(
427 "SELECT " .
428 "a.question_code, a.answer, q.fldtype " .
429 "FROM procedure_answers AS a " .
430 "LEFT JOIN procedure_questions AS q ON " .
431 "q.lab_id = ? " .
432 "AND q.procedure_code = ? AND " .
433 "q.question_code = a.question_code " .
434 "WHERE " .
435 "a.procedure_order_id = ? AND " .
436 "a.procedure_order_seq = ? " .
437 "ORDER BY q.seq, a.answer_seq",
438 array($porow['ppid'], $pcrow['procedure_code'], $orderid, $pcrow['procedure_order_seq'])
440 $setid2 = 0;
441 while ($qrow = sqlFetchArray($qres)) {
442 // Formatting of these answer values may be lab-specific and we'll figure
443 // out how to deal with that as more labs are supported.
444 $answer = trim($qrow['answer']);
445 $fldtype = $qrow['fldtype'];
446 $datatype = 'ST';
447 if ($fldtype == 'N') {
448 $datatype = "NM";
449 } elseif ($fldtype == 'D') {
450 $answer = hl7Date($answer);
451 } elseif ($fldtype == 'G') {
452 $weeks = intval($answer / 7);
453 $days = $answer % 7;
454 $answer = $weeks . 'wks ' . $days . 'days';
457 $out .= "OBX" .
458 $d1 . ++$setid2 . // Set ID
459 $d1 . $datatype . // Structure of observation value
460 $d1 . hl7Text($qrow['question_code']) . // Clinical question code
461 $d1 .
462 $d1 . hl7Text($answer) . // Clinical question answer
463 $d0;
467 return '';
471 * Transmit HL7 for the specified lab.
473 * @param integer $ppid Procedure provider ID.
474 * @param string $out The HL7 text to be sent.
475 * @return string Error text, or empty if no errors.
477 function send_hl7_order($ppid, $out)
479 global $srcdir;
481 $d0 = "\r";
483 $pprow = sqlQuery("SELECT * FROM procedure_providers " .
484 "WHERE ppid = ?", array($ppid));
485 if (empty($pprow)) {
486 return xl('Procedure provider') . " $ppid " . xl('not found');
489 $protocol = $pprow['protocol'];
490 $remote_host = $pprow['remote_host'];
492 // Extract MSH-10 which is the message control ID.
493 $segmsh = explode(substr($out, 3, 1), substr($out, 0, strpos($out, $d0)));
494 $msgid = $segmsh[9];
495 if (empty($msgid)) {
496 return xl('Internal error: Cannot find MSH-10');
499 if ($protocol == 'DL' || $pprow['orders_path'] === '') {
500 header("Pragma: public");
501 header("Expires: 0");
502 header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
503 header("Content-Type: application/force-download");
504 header("Content-Disposition: attachment; filename=order_$msgid.hl7");
505 header("Content-Description: File Transfer");
506 echo $out;
507 exit;
508 } elseif ($protocol == 'SFTP') {
509 // Compute the target path/file name.
510 $filename = $msgid . '.txt';
511 if ($pprow['orders_path']) {
512 $filename = $pprow['orders_path'] . '/' . $filename;
515 // Connect to the server and write the file.
516 $sftp = new \phpseclib3\Net\SFTP($remote_host);
517 if (!$sftp->login($pprow['login'], $pprow['password'])) {
518 return xl('Login to this remote host failed') . ": '$remote_host'";
521 if (!$sftp->put($filename, $out)) {
522 return xl('Creating this file on remote host failed') . ": '$filename'";
524 } elseif ($protocol == 'FS') {
525 // Compute the target path/file name.
526 $filename = $msgid . '.txt';
527 if ($pprow['orders_path']) {
528 $filename = $pprow['orders_path'] . '/' . $filename;
531 $fh = fopen("$filename", 'w');
532 if ($fh) {
533 fwrite($fh, $out);
534 fclose($fh);
535 } else {
536 return xl('Cannot create file') . ' "' . "$filename" . '"';
538 } else { // TBD: Insert "else if ($protocol == '???') {...}" to support other protocols.
539 return xl('This protocol is not implemented') . ": '$protocol'";
542 // Falling through to here indicates success.
543 EventAuditLogger::instance()->newEvent(
544 "proc_order_xmit",
545 $_SESSION['authUser'],
546 $_SESSION['authProvider'],
548 "ID: $msgid Protocol: $protocol Host: $remote_host"
550 return '';