Initial revision
[openemr.git] / sl_convert.php
blob8a5d687e5ed2b427000a987ffaf5cde16de35a36
1 <?php
2 // Copyright (C) 2008, 2009 Rod Roark <rod@sunsetsystems.com>
3 //
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.
8 //
9 // This may be run after an upgraded OpenEMR has been installed.
10 // Its purpose is to extract A/R information from SQL-Ledger and
11 // convert it to the OpenEMR tables that maintain A/R internally,
12 // thus eliminating SQL-Ledger.
14 // Significant changes were made around November 2009: SQL-Ledger
15 // data is now considered authoritative, and the billing table is
16 // modified to reflect that. This is so that financial reports in
17 // the new system will (hopefully) match up with the old system.
18 // Discrepancies are logged to the display during conversion.
20 // Disable PHP timeout. This will not work in safe mode.
21 ini_set('max_execution_time', '0');
23 $ignoreAuth=true; // no login required
25 require_once('interface/globals.php');
26 require_once('library/sql-ledger.inc');
27 require_once('library/invoice_summary.inc.php');
28 require_once('library/sl_eob.inc.php');
30 // Set this to true to skip all database changes.
31 $dry_run = false;
33 if (!$dry_run) {
34 $tmp = sqlQuery("SELECT count(*) AS count FROM ar_activity");
35 if ($tmp['count']) die("ar_activity and ar_session must be empty to run this script!");
38 <html>
39 <head>
40 <title>OpenEMR Conversion from SQL-Ledger</title>
41 <link rel='STYLESHEET' href='interface/themes/style_blue.css'>
42 </head>
43 <body>
44 <span class='title'>OpenEMR Conversion from SQL-Ledger</span>
45 <br><br>
46 <span class='text'>
47 <?php
48 SLConnect();
50 echo "<p>Be patient, this will take a while...</p>";
51 flush();
53 $invoice_count = 0;
54 $activity_count = 0;
56 $res = SLQuery("SELECT id, invnumber, transdate, shipvia, intnotes " .
57 "FROM ar WHERE invnumber LIKE '%.%' ORDER BY id");
59 for ($irow = 0; $irow < SLRowCount($res); ++$irow) {
60 $row = SLGetRow($res, $irow);
61 list($pid, $encounter) = explode(".", $row['invnumber']);
62 $billing = array();
63 $provider_id = 0;
64 $last_biller = 0;
65 $svcdate = $row['transdate'];
67 if (!$dry_run) {
68 // Delete any TAX rows from billing for encounters in SQL-Ledger.
69 sqlStatement("UPDATE billing SET activity = 0 WHERE " .
70 "pid = '$pid' AND encounter = '$encounter' AND " .
71 "code_type = 'TAX'");
74 // Get all billing table items with money for this encounter, and
75 // compute provider ID and billing status.
76 $bres = sqlStatement("SELECT * FROM billing WHERE " .
77 "pid = '$pid' AND encounter = '$encounter' AND activity = 1 " .
78 "AND code_type != 'TAX' AND fee != 0 ORDER BY fee DESC");
79 while ($brow = sqlFetchArray($bres)) {
80 if (!$provider_id) $provider_id = $brow['provider_id'];
81 if (!$last_biller && $brow['billed'] && !empty($brow['payer_id']))
82 $last_biller = $brow['payer_id'];
83 $billing[$brow['id']] = $brow;
86 // Get invoice details.
87 $invlines = get_invoice_summary($row['id'], true);
88 // print_r($invlines); // debugging
89 ksort($invlines);
91 // For each line item or payment from the invoice...
92 foreach ($invlines as $codekey => $codeinfo) {
93 ksort($codeinfo['dtl']);
94 $code = strtoupper($codekey);
95 if ($code == 'CO-PAY' || $code == 'UNKNOWN') $code = '';
97 $is_product = substr($code, 0, 5) == 'PROD:';
99 $codeonly = $code;
100 $modifier = '';
101 $tmp = explode(":", $code);
102 if (!empty($tmp[1])) {
103 $codeonly = $tmp[0];
104 $modifier = $tmp[1];
107 foreach ($codeinfo['dtl'] as $dtlkey => $dtlinfo) {
108 $dtldate = trim(substr($dtlkey, 0, 10));
110 if (empty($dtldate)) { // if this is a charge
111 $charge = $dtlinfo['chg'];
113 // Zero charges don't matter.
114 if ($charge == 0) continue;
116 // Insert taxes but ignore other charges.
117 if ($code == 'TAX') {
118 if (!$dry_run) {
119 sqlInsert("INSERT INTO billing ( date, encounter, code_type, code, code_text, " .
120 "pid, authorized, user, groupname, activity, billed, provider_id, " .
121 "modifier, units, fee, ndc_info, justify ) values ( " .
122 "'$svcdate 00:00:00', '$encounter', 'TAX', 'TAX', '" .
123 addslashes($dtlinfo['dsc']) . "', " .
124 "'$pid', '1', '$provider_id', 'Default', 1, 1, 0, '', '1', " .
125 "'$charge', '', '' )");
128 else {
129 // Non-tax charges for products are in the drug_sales table.
130 // We won't bother trying to make sure they match the invoice.
131 if ($is_product) continue;
133 // Look up this charge in the $billing array.
134 // If found, remove it from the array and skip to the next detail item.
135 // Otherwise add it to the billing table and log the discrepancy.
136 $posskey = 0;
137 foreach ($billing as $key => $brow) {
138 $bcode = strtoupper($brow['code']);
139 $bcodeonly = $bcode;
140 if ($brow['modifier']) $bcode .= ':' . strtoupper($brow['modifier']);
141 if ($bcode === $code && $brow['fee'] == $charge) {
142 unset($billing[$key]);
143 continue 2; // done with this detail item
145 else if (($bcodeonly === $codeonly || (empty($codeonly) && $charge != 0)) && $brow['fee'] == $charge) {
146 $posskey = $key;
149 if ($posskey) {
150 // There was no exact match, but there was a match if the modifiers
151 // are ignored or if the SL code is empty. Good enough.
152 unset($billing[$posskey]);
153 continue;
155 // This charge is not in the billing table!
156 $codetype = preg_match('/^[A-V]/', $code) ? 'HCPCS' : 'CPT4';
157 // Note that get_invoice_summary() loses the code type. The above
158 // statement works for normal U.S. clinics, but sites that have
159 // charges other than CPT4 and HCPCS will need to have their code
160 // types for these generated entries, if any, fixed.
161 if (!$dry_run) {
162 sqlInsert("INSERT INTO billing ( date, encounter, code_type, code, code_text, " .
163 "pid, authorized, user, groupname, activity, billed, provider_id, " .
164 "modifier, units, fee, ndc_info, justify ) values ( " .
165 "'$svcdate 00:00:00', '$encounter', '$codetype', '$codeonly',
166 'Copied from SQL-Ledger by sl_convert.php', " .
167 "'$pid', '1', '$provider_id', 'Default', 1, 1, 0, '$modifier', '1', " .
168 "'$charge', '', '' )");
170 echo "Billing code '$code' with charge \$$charge was copied from " .
171 "SQL-Ledger invoice $pid.$encounter.<br />\n";
172 flush();
173 } // end non-tax charge
175 // End charge item logic. Continue to the next invoice detail item.
176 continue;
178 } // end if charge
180 $payer_id = empty($dtlinfo['ins']) ? 0 : $dtlinfo['ins'];
181 $session_id = 0;
183 // Compute a reasonable "source" value. For payments this will
184 // commonly be a check number, for adjustments we have none.
185 $source = empty($dtlinfo['src']) ? '' : $dtlinfo['src'];
186 $source = preg_replace('!^Ins[123]/!i', '', $source);
187 $source = preg_replace('!^Pt/!i', '', $source);
188 if ($source == '' && empty($dtlinfo['pmt'])) {
189 $source = 'From SQL-Ledger';
192 // For insurance payers look up or create the session table entry.
193 if ($payer_id) {
194 if (!$dry_run) {
195 $session_id = arGetSession($payer_id, addslashes($source), $dtldate);
198 // For non-insurance payers deal with copay duplication.
199 else if ($code == '') {
200 if (!empty($dtlinfo['pmt'])) {
201 // Skip payments that are already present in the billing table as copays.
202 foreach ($billing as $key => $brow) {
203 if ($brow['code_type'] == 'COPAY' && (0 - $brow['fee']) == $dtlinfo['pmt']) {
204 unset($billing[$key]);
205 continue 2; // done with this detail item
208 } // end if payment
209 } // end not insurance
211 $payer_type = 0;
213 if (!empty($dtlinfo['pmt'])) { // it's a payment
214 $tmp = strtolower($dtlinfo['src']);
215 for ($i = 1; $i <= 3; ++$i) {
216 if (strpos($tmp, "ins$i") !== false) $payer_type = $i;
218 if (!$dry_run) {
219 arPostPayment($pid, $encounter, $session_id, $dtlinfo['pmt'], $code,
220 $payer_type, addslashes($source), 0, "$dtldate 00:00:00");
221 if ($session_id) {
222 sqlStatement("UPDATE ar_session SET pay_total = pay_total + '" .
223 $dtlinfo['pmt'] . "' WHERE session_id = '$session_id'");
227 else { // it's an adjustment
228 $tmp = strtolower($dtlinfo['rsn']);
229 for ($i = 1; $i <= 3; ++$i) {
230 if (strpos($tmp, "ins$i") !== false) $payer_type = $i;
232 if (!$dry_run) {
233 arPostAdjustment($pid, $encounter, $session_id, 0 - $dtlinfo['chg'],
234 $code, $payer_type, addslashes($dtlinfo['rsn']), 0, "$dtldate 00:00:00");
238 ++$activity_count;
239 } // end detail item
240 } // end code
242 // Compute last insurance level billed.
243 $last_level_billed = 0;
244 if ($last_biller) {
245 $invdate = $row['transdate'];
246 $tmp = sqlQuery("SELECT type FROM insurance_data WHERE " .
247 "pid = '$patient_id' AND provider = '$last_biller' AND " .
248 "date <= '$invdate' ORDER BY date DESC, id ASC LIMIT 1");
249 $last_level_billed = ($tmp['type'] == 'tertiary') ?
250 3 : ($tmp['type'] == 'secondary') ? 2 : 1;
253 // Compute last insurance level closed.
254 $last_level_closed = 0;
255 $tmp = strtolower($row['shipvia']);
256 for ($i = 1; $i <= 3; ++$i) {
257 if (strpos($tmp, "ins$i") !== false) $last_level_closed = $i;
260 // Compute last statement date and number of statements sent.
261 $last_stmt_date = "NULL";
262 $stmt_count = 0;
263 $i = 0;
264 $tmp = strtolower($row['intnotes']);
265 while (($i = strpos($tmp, 'statement sent ', $i)) !== false) {
266 $i += 15;
267 $last_stmt_date = "'" . substr($tmp, $i, 10) . "'";
268 ++$stmt_count;
271 if (!$dry_run) {
272 sqlStatement("UPDATE form_encounter SET " .
273 "last_level_billed = '$last_level_billed', " .
274 "last_level_closed = '$last_level_closed', " .
275 "last_stmt_date = $last_stmt_date, " .
276 "stmt_count = '$stmt_count' " .
277 "WHERE pid = '$pid' AND encounter = '$encounter'");
280 // Delete and show a warning for any unmatched copays or charges.
281 foreach ($billing as $key => $brow) {
282 if (!$dry_run) {
283 sqlStatement("UPDATE billing SET activity = 0 WHERE id = '$key'");
285 if ($brow['code_type'] == 'COPAY') {
286 echo "Patient payment of \$" . sprintf('%01.2f', 0 - $brow['fee']);
288 else {
289 echo "Charge item '" . $brow['code'] . "' with amount \$" .
290 sprintf('%01.2f', $brow['fee']);
292 echo " was not found in SQL-Ledger invoice $pid.$encounter " .
293 "and has been removed from the encounter.<br />\n";
294 flush();
297 ++$invoice_count;
298 } // end invoice
299 SLClose();
300 echo "<br />\n";
301 echo "$invoice_count SQL-Ledger invoices were processed.<br />\n";
302 echo "$activity_count payments and adjustments were posted.<br />\n";
304 </span>
306 </body>
307 </html>