2 // Copyright (C) 2005-2006 Rod Roark <rod@sunsetsystems.com>
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License
6 // as published by the Free Software Foundation; either version 2
7 // of the License, or (at your option) any later version.
9 // This is the first of two pages to support posting of EOBs.
10 // The second is sl_eob_invoice.php.
12 include_once("../globals.php");
13 include_once("../../library/patient.inc");
14 include_once("../../library/sql-ledger.inc");
15 include_once("../../library/invoice_summary.inc.php");
16 include_once("../../custom/statement.inc.php");
17 include_once("../../library/parse_era.inc.php");
18 include_once("../../library/sl_eob.inc.php");
20 $DEBUG = 0; // set to 0 for production, 1 to test
27 // This is called back by parse_era() if we are processing X12 835's.
29 function era_callback(&$out) {
30 global $where, $eracount, $eraname;
31 // print_r($out); // debugging
33 // $eraname = $out['isa_control_number'];
34 $eraname = $out['gs_date'] . '_' . ltrim($out['isa_control_number'], '0') .
35 '_' . ltrim($out['payer_id'], '0');
36 list($pid, $encounter, $invnumber) = slInvoiceNumber($out);
38 if ($pid && $encounter) {
39 if ($where) $where .= ' OR ';
40 $where .= "invnumber = '$invnumber'";
44 function bucks($amount) {
46 printf("%.2f", $amount);
49 $today = date("Y-m-d");
53 // Print statements if requested.
55 if ($_POST['form_print'] && $_POST['form_cb']) {
57 $fhprint = fopen($STMT_TEMP_FILE, 'w');
60 foreach ($_POST['form_cb'] as $key => $value) $where .= " OR ar.id = $key";
61 $where = substr($where, 4);
63 // Sort by patient so that multiple invoices can be
64 // represented on a single statement.
65 $res = SLQuery("SELECT ar.*, customer.name, " .
66 "customer.address1, customer.address2, " .
67 "customer.city, customer.state, customer.zipcode, " .
68 "substring(trim(both from customer.name) from '% #\"%#\"' for '#') AS lname, " .
69 "substring(trim(both from customer.name) from '#\"%#\" %' for '#') AS fname " .
70 "FROM ar, customer WHERE ( $where ) AND " .
71 "customer.id = ar.customer_id " .
72 "ORDER BY lname, fname, ar.customer_id, ar.transdate");
73 if ($sl_err) die($sl_err);
77 for ($irow = 0; $irow < SLRowCount($res); ++
$irow) {
78 $row = SLGetRow($res, $irow);
80 // Determine the date of service. An 8-digit encounter number is
81 // presumed to be a date of service imported during conversion.
82 // Otherwise look it up in the form_encounter table.
85 list($pid, $encounter) = explode(".", $row['invnumber']);
86 if (strlen($encounter) == 8) {
87 $svcdate = substr($encounter, 0, 4) . "-" . substr($encounter, 4, 2) .
88 "-" . substr($encounter, 6, 2);
89 } else if ($encounter) {
90 $tmp = sqlQuery("SELECT date FROM form_encounter WHERE " .
91 "encounter = $encounter");
92 $svcdate = substr($tmp['date'], 0, 10);
95 // How many times have we dunned them for this invoice?
96 $intnotes = trim($row['intnotes']);
97 $duncount = substr_count(strtolower($intnotes), "statement sent");
99 // If this is a new patient then print the pending statement
100 // and start a new one. This is an associative array:
102 // cid = SQL-Ledger customer ID
103 // pid = OpenEMR patient ID
104 // patient = patient name
105 // amount = total amount due
106 // adjust = adjustments (already applied to amount)
107 // duedate = due date of the oldest included invoice
108 // age = number of days from duedate to today
109 // to = array of addressee name/address lines
111 // dos = date of service "yyyy-mm-dd"
112 // desc = description
113 // amount = charge less adjustments
114 // paid = amount paid
115 // notice = 1 for first notice, 2 for second, etc.
116 // detail = array of details, see invoice_summary.inc.php
118 if ($stmt['cid'] != $row['customer_id']) {
119 fwrite($fhprint, create_statement($stmt));
120 $stmt['cid'] = $row['customer_id'];
122 $stmt['patient'] = $row['name'];
123 $stmt['to'] = array($row['name']);
124 if ($row['address1']) $stmt['to'][] = $row['address1'];
125 if ($row['address2']) $stmt['to'][] = $row['address2'];
126 $stmt['to'][] = $row['city'] . ", " . $row['state'] . " " . $row['zipcode'];
127 $stmt['lines'] = array();
128 $stmt['amount'] = '0.00';
129 $stmt['today'] = $today;
130 $stmt['duedate'] = $row['duedate'];
132 // Report the oldest due date.
133 if ($row['duedate'] < $stmt['duedate']) {
134 $stmt['duedate'] = $row['duedate'];
138 $stmt['age'] = round((strtotime($today) - strtotime($stmt['duedate'])) /
141 $invlines = get_invoice_summary($row['id'], true); // true added by Rod 2006-06-09
142 foreach ($invlines as $key => $value) {
144 $line['dos'] = $svcdate;
145 $line['desc'] = ($key == 'CO-PAY') ?
"Patient Payment" : "Procedure $key";
146 $line['amount'] = sprintf("%.2f", $value['chg']);
147 $line['adjust'] = sprintf("%.2f", $value['adj']);
148 $line['paid'] = sprintf("%.2f", $value['chg'] - $value['bal']);
149 $line['notice'] = $duncount +
1;
150 $line['detail'] = $value['dtl']; // Added by Rod 2006-06-09
151 $stmt['lines'][] = $line;
152 $stmt['amount'] = sprintf("%.2f", $stmt['amount'] +
$value['bal']);
155 // Record something in ar.intnotes about this statement run.
156 if ($intnotes) $intnotes .= "\n";
157 $intnotes = addslashes($intnotes . "Statement sent $today");
158 if (! $DEBUG && ! $_POST['form_without']) {
159 SLQuery("UPDATE ar SET intnotes = '$intnotes' WHERE id = " . $row['id']);
160 if ($sl_err) die($sl_err);
164 fwrite($fhprint, create_statement($stmt));
167 $alertmsg = xl("Printing skipped; see test output in ").$STMT_TEMP_FILE;
169 exec("$STMT_PRINT_CMD $STMT_TEMP_FILE");
170 if ($_POST['form_without']) {
171 $alertmsg = xl("Now printing statements; invoices will not be updated.");
173 $alertmsg = xl("Now printing statements and updating invoices.");
180 <link rel
=stylesheet href
="<?echo $css_header;?>" type
="text/css">
181 <title
><?
xl('EOB Posting - Search','e')?
></title
>
182 <script type
="text/javascript" src
="../../library/textformat.js"></script
>
184 <script language
="JavaScript">
188 function checkAll(checked
) {
189 var f
= document
.forms
[0];
190 for (var i
= 0; i
< f
.elements
.length
; ++i
) {
191 var ename
= f
.elements
[i
].name
;
192 if (ename
.indexOf('form_cb[') == 0)
193 f
.elements
[i
].checked
= checked
;
197 function npopup(pid
) {
198 window
.open('sl_eob_patient_note.php?patient_id=' + pid
, '_blank', 'width=500,height=250,resizable=1');
206 <body leftmargin
='0' topmargin
='0' marginwidth
='0' marginheight
='0'>
209 <form method
='post' action
='sl_eob_search.php' enctype
='multipart/form-data'>
211 <table border
='0' cellpadding
='5' cellspacing
='0'>
214 <td height
="1" colspan
="10">
223 <?
xl('Source:','e')?
>
226 <input type
='text' name
='form_source' size
='10' value
='<?php echo $_POST['form_source
']; ?>'
227 title
='<?xl("A check number or claim number to identify the payment","e")?>'>
230 <?
xl('Pay Date:','e')?
>
233 <input type
='text' name
='form_paydate' size
='10' value
='<?php echo $_POST['form_paydate
']; ?>'
234 onkeyup
='datekeyup(this,mypcc)' onblur
='dateblur(this,mypcc)'
235 title
='<?xl("Date of payment yyyy-mm-dd","e")?>'>
238 <?
xl('Amount:','e')?
>
241 <input type
='text' name
='form_amount' size
='10' value
='<?php echo $_POST['form_amount
']; ?>'
242 title
='<?xl("Paid amount that you will allocate","e")?>'>
244 <td colspan
='2' align
='right'>
245 <a href
='sl_eob_help.php' target
='_blank'><?
xl('Help','e')?
></a
>
250 <td height
="1" colspan
="10">
254 <tr bgcolor
='#ddddff'>
259 <input type
='text' name
='form_name' size
='10' value
='<?php echo $_POST['form_name
']; ?>'
260 title
='<?xl("Any part of the patient name, or 'last
,first
', or 'X
-Y
'","e")?>'>
263 <?
xl('Chart ID:','e')?
>
266 <input type
='text' name
='form_pid' size
='10' value
='<?php echo $_POST['form_pid
']; ?>'
267 title
='<?xl("Patient chart ID","e")?>'>
270 <?
xl('Encounter:','e')?
>
273 <input type
='text' name
='form_encounter' size
='10' value
='<?php echo $_POST['form_encounter
']; ?>'
274 title
='<?xl("Encounter number","e")?>'>
277 <?
xl('Svc Date:','e')?
>
280 <input type
='text' name
='form_date' size
='10' value
='<?php echo $_POST['form_date
']; ?>'
281 title
='<?xl("Date of service mm/dd/yyyy","e")?>'>
287 <input type
='text' name
='form_to_date' size
='10' value
='<?php echo $_POST['form_to_date
']; ?>'
288 title
='<?xl("Ending DOS mm/dd/yyyy if you wish to enter a range","e")?>'>
291 <select name
='form_category'>
293 foreach (array(xl('Open'), xl('All'), xl('Due Pt'), xl('Due Ins')) as $value) {
294 echo " <option value='$value'";
295 if ($_POST['form_category'] == $value) echo " selected";
296 echo ">$value</option>\n";
302 <input type
='submit' name
='form_search' value
='<?xl("Search","e")?>'>
306 <!-- Support
for X12
835 upload
-->
307 <tr bgcolor
='#ddddff'>
309 <?
xl('Or upload ERA file:','e')?
>
310 <input type
="hidden" name
="MAX_FILE_SIZE" value
="5000000" />
311 <input name
="form_erafile" type
="file" />
316 <td height
="1" colspan
="10">
323 if ($_POST['form_search'] ||
$_POST['form_print']) {
324 $form_name = trim($_POST['form_name']);
325 $form_pid = trim($_POST['form_pid']);
326 $form_encounter = trim($_POST['form_encounter']);
327 $form_date = fixDate($_POST['form_date'], "");
328 $form_to_date = fixDate($_POST['form_to_date'], "");
332 // Handle X12 835 file upload.
334 if ($_FILES['form_erafile']['size']) {
335 $tmp_name = $_FILES['form_erafile']['tmp_name'];
337 // Handle .zip extension if present. Probably won't work on Windows.
338 if (strtolower(substr($_FILES['form_erafile']['name'], -4)) == '.zip') {
339 rename($tmp_name, "$tmp_name.zip");
340 exec("unzip -p $tmp_name.zip > $tmp_name");
341 unlink("$tmp_name.zip");
344 echo "<!-- Notes from ERA upload processing:\n";
345 $alertmsg .= parse_era($tmp_name, 'era_callback');
347 $erafullname = "$webserver_root/era/$eraname.edi";
349 if (is_file($erafullname)) {
350 $alertmsg .= "Warning: Set $eraname was already uploaded ";
351 if (is_file("$webserver_root/era/$eraname.html"))
352 $alertmsg .= "and processed. ";
354 $alertmsg .= "but not yet processed. ";
356 // if (!move_uploaded_file($_FILES['form_erafile']['tmp_name'], $erafullname)) {
357 // die("Upload failed! $alertmsg");
359 rename($tmp_name, $erafullname);
363 if (! $where) $where = '1 = 2';
367 if ($where) $where .= " AND ";
368 // Allow the last name to be followed by a comma and some part of a first name.
369 if (preg_match('/^(.*\S)\s*,\s*(.*)/', $form_name, $matches)) {
370 $where .= "customer.name ILIKE '" . $matches[2] . '% ' . $matches[1] . "%'";
371 // Allow a filter like "A-C" on the first character of the last name.
372 } else if (preg_match('/^(\S)\s*-\s*(\S)$/', $form_name, $matches)) {
374 while (ord($matches[1]) <= ord($matches[2])) {
375 $tmp .= " OR customer.name ILIKE '% " . $matches[1] . "%'";
376 $matches[1] = chr(ord($matches[1]) +
1);
378 $where .= "( $tmp ) ";
380 $where .= "customer.name ILIKE '%$form_name%'";
383 if ($form_pid && $form_encounter) {
384 if ($where) $where .= " AND ";
385 $where .= "ar.invnumber = '$form_pid.$form_encounter'";
387 else if ($form_pid) {
388 if ($where) $where .= " AND ";
389 $where .= "ar.invnumber LIKE '$form_pid.%'";
391 else if ($form_encounter) {
392 if ($where) $where .= " AND ";
393 $where .= "ar.invnumber like '%.$form_encounter'";
397 if ($where) $where .= " AND ";
398 $date1 = substr($form_date, 0, 4) . substr($form_date, 5, 2) .
399 substr($form_date, 8, 2);
401 $date2 = substr($form_to_date, 0, 4) . substr($form_to_date, 5, 2) .
402 substr($form_to_date, 8, 2);
403 $where .= "((CAST (substring(ar.invnumber from position('.' in ar.invnumber) + 1 for 8) AS integer) " .
404 "BETWEEN '$date1' AND '$date2')";
405 $tmp = "date >= '$form_date' AND date <= '$form_to_date'";
408 // This catches old converted invoices where we have no encounters:
409 $where .= "(ar.invnumber LIKE '%.$date1'";
410 $tmp = "date = '$form_date'";
412 // Pick out the encounters from MySQL with the desired DOS:
413 $rez = sqlStatement("SELECT pid, encounter FROM form_encounter WHERE $tmp");
414 while ($row = sqlFetchArray($rez)) {
415 $where .= " OR ar.invnumber = '" . $row['pid'] . "." . $row['encounter'] . "'";
421 if ($_POST['form_category'] == 'All') {
422 die("At least one search parameter is required if you select All.");
429 $query = "SELECT ar.id, ar.invnumber, ar.duedate, ar.amount, ar.paid, " .
430 "ar.intnotes, ar.notes, ar.shipvia, customer.name, " .
431 "substring(trim(both from customer.name) from '% #\"%#\"' for '#') AS lname, " .
432 "substring(trim(both from customer.name) from '#\"%#\" %' for '#') AS fname, " .
433 "(SELECT SUM(invoice.fxsellprice) FROM invoice WHERE " .
434 "invoice.trans_id = ar.id AND invoice.fxsellprice > 0) AS charges, " .
435 "(SELECT SUM(invoice.fxsellprice) FROM invoice WHERE " .
436 "invoice.trans_id = ar.id AND invoice.fxsellprice < 0) AS adjustments " .
437 "FROM ar, customer WHERE ( $where ) AND customer.id = ar.customer_id ";
438 if ($_POST['form_category'] != 'All' && !$eracount) {
439 $query .= "AND ar.amount != ar.paid ";
440 // if ($_POST['form_category'] == 'Due') {
441 // $query .= "AND ar.duedate <= CURRENT_DATE ";
444 $query .= "ORDER BY lname, fname, ar.invnumber";
446 echo "<!-- $query -->\n"; // debugging
448 $t_res = SLQuery($query);
449 if ($sl_err) die($sl_err);
451 $num_invoices = SLRowCount($t_res);
452 if ($eracount && $num_invoices != $eracount) {
453 $alertmsg .= "Of $eracount remittances, there are $num_invoices " .
454 "matching claims in OpenEMR. ";
458 <table border
='0' cellpadding
='1' cellspacing
='2' width
='98%'>
460 <tr bgcolor
="#dddddd">
462  
;<?
xl('Patient','e')?
>
465  
;<?
xl('Invoice','e')?
>
468  
;<?
xl('Svc Date','e')?
>
471  
;<?
xl('Due Date','e')?
>
473 <td
class="dehead" align
="right">
474 <?
xl('Charge','e')?
> 
;
476 <td
class="dehead" align
="right">
477 <?
xl('Adjust','e')?
> 
;
479 <td
class="dehead" align
="right">
480 <?
xl('Paid','e')?
> 
;
482 <td
class="dehead" align
="right">
483 <?
xl('Balance','e')?
> 
;
485 <td
class="dehead" align
="center">
488 <?php
if (!$eracount) { ?
>
489 <td
class="dehead" align
="center">
497 for ($irow = 0; $irow < $num_invoices; ++
$irow) {
498 $row = SLGetRow($t_res, $irow);
500 // $duncount was originally supposed to be the number of times that
501 // the patient was sent a statement for this invoice.
503 $duncount = substr_count(strtolower($row['intnotes']), "statement sent");
505 // But if we have not yet billed the patient, then compute $duncount as a
506 // negative count of the number of insurance plans for which we have not
507 // yet closed out insurance.
510 $insgot = strtolower($row['notes']);
511 $inseobs = strtolower($row['shipvia']);
512 foreach (array('ins1', 'ins2', 'ins3') as $value) {
513 if (strpos($insgot, $value) !== false &&
514 strpos($inseobs, $value) === false)
519 // $isdue = ($row['duedate'] <= $today && $row['amount'] > $row['paid']) ? " checked" : "";
521 $isdueany = sprintf("%.2f",$row['amount']) > sprintf("%.2f",$row['paid']);
523 // An invoice is now due from the patient if money is owed and we are
524 // not waiting for insurance to pay. We no longer look at the due date
527 $isduept = ($duncount >= 0 && $isdueany) ?
" checked" : "";
529 // Skip invoices not in the desired "Due..." category.
531 if (substr($_POST['form_category'], 0, 3) == 'Due' && !$isdueany) continue;
532 if ($_POST['form_category'] == 'Due Ins' && ($duncount >= 0 ||
!$isdueany)) continue;
533 if ($_POST['form_category'] == 'Due Pt' && ($duncount < 0 ||
!$isdueany)) continue;
535 $bgcolor = ((++
$orow & 1) ?
"#ffdddd" : "#ddddff");
537 // Determine the date of service. If this was a search parameter
538 // then we already know it. Or an 8-digit encounter number is
539 // presumed to be a date of service imported during conversion.
540 // Otherwise look it up in the form_encounter table.
543 list($pid, $encounter) = explode(".", $row['invnumber']);
545 // $svcdate = $form_date;
547 if (strlen($encounter) == 8) {
548 $svcdate = substr($encounter, 0, 4) . "-" . substr($encounter, 4, 2) .
549 "-" . substr($encounter, 6, 2);
551 else if ($encounter) {
552 $tmp = sqlQuery("SELECT date FROM form_encounter WHERE " .
553 "encounter = $encounter");
554 $svcdate = substr($tmp['date'], 0, 10);
557 <tr bgcolor
='<?php echo $bgcolor ?>'>
559  
;<a href
="" onclick
="return npopup(<?php echo $pid ?>)"
560 ><?php
echo $row['lname'] . ', ' . $row['fname']; ?
></a
>
563  
;<a href
="sl_eob_invoice.php?id=<?php echo $row['id'] ?>"
564 target
="_blank"><?php
echo $row['invnumber'] ?
></a
>
567  
;<?php
echo $svcdate ?
>
570  
;<?php
echo $row['duedate'] ?
>
572 <td
class="detail" align
="right">
573 <?php
bucks($row['charges']) ?
> 
;
575 <td
class="detail" align
="right">
576 <?php
bucks($row['adjustments']) ?
> 
;
578 <td
class="detail" align
="right">
579 <?php
bucks($row['paid']) ?
> 
;
581 <td
class="detail" align
="right">
582 <?php
bucks($row['charges'] +
$row['adjustments'] - $row['paid']) ?
> 
;
584 <td
class="detail" align
="center">
585 <?php
echo $duncount ?
$duncount : " " ?
>
587 <?php
if (!$eracount) { ?
>
588 <td
class="detail" align
="center">
589 <input type
='checkbox' name
='form_cb[<?php echo($row['id
']) ?>]'<?php
echo $isduept ?
> />
602 <?php
if ($eracount) { ?
>
603 <input type
='button' value
='Process ERA File' onclick
='processERA()' />  
;
605 <input type
='button' value
='Select All' onclick
='checkAll(true)' />  
;
606 <input type
='button' value
='Clear All' onclick
='checkAll(false)' />  
;
607 <input type
='submit' name
='form_print' value
='Print Selected Statements' />  
;
609 <input type
='checkbox' name
='form_without' value
='1' /> <?
xl('Without Update','e')?
>
614 <script language
="JavaScript">
615 function processERA() {
616 var f
= document
.forms
[0];
617 var debug
= f
.form_without
.checked ?
'1' : '0';
618 var paydate
= f
.form_paydate
.value
;
619 window
.open('sl_eob_process.php?eraname=<?php echo $eraname ?>&debug=' + debug +
'&paydate=' + paydate
, '_blank');
624 echo "alert('" . htmlentities($alertmsg) . "');\n";