Added administrative utility to merge two patient charts.
[openemr.git] / interface / patient_file / merge_patients.php
blob24ae0635554c60d98cf06ed7ab39e41690c12ee1
1 <?php
2 /**
3 * This script merges two patient charts into a single patient chart.
4 * It is to correct the error of creating a duplicate patient.
6 * Copyright (C) 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>
23 set_time_limit(0);
25 $sanitize_all_escapes = true;
26 $fake_register_globals = false;
28 require_once("../globals.php");
29 require_once("$srcdir/acl.inc");
30 require_once("$srcdir/log.inc");
32 // Set this to true for production use. If false you will get a "dry run" with no updates.
33 $PRODUCTION = true;
35 if (!acl_check('admin', 'super')) die(xlt('Not authorized'));
37 <html>
39 <head>
40 <link rel="stylesheet" href='<?php echo $css_header ?>' type='text/css'>
41 <title><?php echo xlt('Merge Patients'); ?></title>
43 <script type="text/javascript" src="../../library/dialog.js"></script>
45 <script language="JavaScript">
47 var mypcc = '<?php echo $GLOBALS['phone_country_code']; ?>';
49 var el_pt_name;
50 var el_pt_id;
52 // This is for callback by the find-patient popup.
53 function setpatient(pid, lname, fname, dob) {
54 el_pt_name.value = lname + ', ' + fname + ' (' + pid + ')';
55 el_pt_id.value = pid;
58 // This invokes the find-patient popup.
59 function sel_patient(ename, epid) {
60 el_pt_name = ename;
61 el_pt_id = epid;
62 dlgopen('../main/calendar/find_patient_popup.php', '_blank', 500, 400);
65 </script>
67 </head>
69 <body class="body_top">
71 <center><h2><?php echo xlt('Merge Patients') ?></h2></center>
73 <?php
75 function deleteRows($tblname, $colname, $source_pid) {
76 global $PRODUCTION;
77 $crow = sqlQuery("SELECT COUNT(*) AS count FROM `$tblname` WHERE `$colname` = $source_pid");
78 $count = $crow['count'];
79 if ($count) {
80 $sql = "DELETE FROM `$tblname` WHERE `$colname` = $source_pid";
81 echo "<br />$sql ($count)";
82 if ($PRODUCTION) sqlStatement($sql);
86 function updateRows($tblname, $colname, $source_pid, $target_pid) {
87 global $PRODUCTION;
88 $crow = sqlQuery("SELECT COUNT(*) AS count FROM `$tblname` WHERE `$colname` = $source_pid");
89 $count = $crow['count'];
90 if ($count) {
91 $sql = "UPDATE `$tblname` SET `$colname` = '$target_pid' WHERE `$colname` = $source_pid";
92 echo "<br />$sql ($count)";
93 if ($PRODUCTION) sqlStatement($sql);
97 if (!empty($_POST['form_submit'])) {
98 $target_pid = intval($_POST['form_target_pid']);
99 $source_pid = intval($_POST['form_source_pid']);
101 $fatal = 0;
103 if ($target_pid == $source_pid) die(xlt('Target and source pid may not be the same!'));
105 $tprow = sqlQuery("SELECT * FROM patient_data WHERE pid = ?", array($target_pid));
106 $sprow = sqlQuery("SELECT * FROM patient_data WHERE pid = ?", array($source_pid));
108 // Do some checking to make sure source and target are the same person.
109 if (empty($tprow['ss'])) die(xlt('Target patient not found or has no SSN'));
110 if (empty($sprow['ss'])) die(xlt('Source patient not found or has no SSN'));
111 if ($tprow['ss'] != $sprow['ss']) die(xlt('Target and source SSN do not match'));
112 if (empty($tprow['DOB']) || $tprow['DOB'] == '0000-00-00') die(xlt('Target patient has no DOB'));
113 if (empty($sprow['DOB']) || $sprow['DOB'] == '0000-00-00') die(xlt('Source patient has no DOB'));
114 if ($tprow['DOB'] != $sprow['DOB']) die(xlt('Target and source DOB do not match'));
116 $tdocdir = "$OE_SITE_DIR/documents/$target_pid";
117 $sdocdir = "$OE_SITE_DIR/documents/$source_pid";
118 $sencdir = "$sdocdir/encounters";
119 $tencdir = "$tdocdir/encounters";
121 // Check for any duplicate document names.
122 if (is_dir($sdocdir)) {
123 $dh = opendir($sdocdir);
124 if (!$dh) die(xlt('Cannot read directory') . " '$sdocdir'");
125 while (false !== ($sfname = readdir($dh))) {
126 if ($sfname == '.' || $sfname == '..') continue;
127 if ($sfname == 'index.html') continue;
128 if ($sfname == 'encounters') continue;
129 if (file_exists("$tdocdir/$sfname")) {
130 ++$fatal;
131 echo "<br />" . xlt('Duplicate document name') . " '$sfname' " . xlt('in source and target');
134 closedir($dh);
137 if ($fatal) die("<br />" . xlt('Aborted due to document duplication'));
139 // Move scanned encounter documents and delete their container.
140 if (is_dir($sencdir)) {
141 if ($PRODUCTION && !file_exists($tdocdir)) mkdir($tdocdir);
142 if ($PRODUCTION && !file_exists($tencdir)) mkdir($tencdir);
143 $dh = opendir($sencdir);
144 if (!$dh) die(xlt('Cannot read directory') . " '$sencdir'");
145 while (false !== ($sfname = readdir($dh))) {
146 if ($sfname == '.' || $sfname == '..') continue;
147 if ($sfname == 'index.html') {
148 echo "<br />" . xlt('Deleting') . " $sencdir/$sfname";
149 if ($PRODUCTION) {
150 if (!unlink("$sencdir/$sfname"))
151 die("<br />" . xlt('Delete failed!'));
153 continue;
155 echo "<br />" . xlt('Moving') . " $sencdir/$sfname " . xlt('to') . " $tencdir/$sfname";
156 if ($PRODUCTION) {
157 if (!rename("$sencdir/$sfname", "$tencdir/$sfname"))
158 die("<br />" . xlt('Move failed!'));
161 closedir($dh);
162 echo "<br />" . xlt('Deleting') . " $sencdir";
163 if ($PRODUCTION) {
164 if (!rmdir($sencdir))
165 die("<br />" . xlt('Delete failed!'));
169 // Move normal documents and delete their container.
170 if (is_dir($sdocdir)) {
171 if ($PRODUCTION && !file_exists($tdocdir)) mkdir($tdocdir);
172 $dh = opendir($sdocdir);
173 if (!$dh) die(xlt('Cannot read directory') . " '$sdocdir'");
174 while (false !== ($sfname = readdir($dh))) {
175 if ($sfname == '.' || $sfname == '..') continue;
176 if ($sfname == 'encounters') continue;
177 if ($sfname == 'index.html') {
178 echo "<br />" . xlt('Deleting') . " $sdocdir/$sfname";
179 if ($PRODUCTION) {
180 if (!unlink("$sdocdir/$sfname"))
181 die("<br />" . xlt('Delete failed!'));
183 continue;
185 echo "<br />" . xlt('Moving') . " $sdocdir/$sfname " . xlt('to') . " $tdocdir/$sfname";
186 if ($PRODUCTION) {
187 if (!rename("$sdocdir/$sfname", "$tdocdir/$sfname"))
188 die("<br />" . xlt('Move failed!'));
191 closedir($dh);
192 echo "<br />" . xlt('Deleting') . " $sdocdir";
193 if ($PRODUCTION) {
194 if (!rmdir($sdocdir))
195 die("<br />" . xlt('Delete failed!'));
199 $tres = sqlStatement("SHOW TABLES");
200 while ($trow = sqlFetchArray($tres)) {
201 $tblname = array_shift($trow);
202 if ($tblname == 'patient_data' || $tblname == 'history_data' || $tblname == 'insurance_data') {
203 deleteRows($tblname, 'pid', $source_pid);
205 else if ($tblname == 'chart_tracker') {
206 updateRows($tblname, 'ct_pid', $source_pid, $target_pid);
208 else if ($tblname == 'documents') {
209 $crow = sqlQuery("SELECT COUNT(*) AS count FROM `$tblname` WHERE `foreign_id` = '$source_pid'");
210 $count = $crow['count'];
211 if ($count) {
212 $sql = "UPDATE `$tblname` SET " .
213 "`url` = replace(`url`, '/documents/$source_pid/', '/documents/$target_pid/') " .
214 "WHERE `foreign_id` = '$source_pid'";
215 echo "<br />$sql ($count)";
216 if ($PRODUCTION) sqlStatement($sql);
218 updateRows($tblname, 'foreign_id', $source_pid, $target_pid);
220 else if ($tblname == 'openemr_postcalendar_events') {
221 updateRows($tblname, 'pc_pid', $source_pid, $target_pid);
223 else if ($tblname == 'log') {
224 // Don't mess with log data.
226 else {
227 $crow = sqlQuery("SHOW COLUMNS FROM `$tblname` WHERE " .
228 "`Field` LIKE 'pid' OR `Field` LIKE 'patient_id'");
229 if (!empty($crow['Field'])) {
230 $colname = $crow['Field'];
231 updateRows($tblname, $colname, $source_pid, $target_pid);
236 echo "<br />" . xlt('Merge complete.');
238 exit(0);
244 </p>
246 <form method='post' action='merge_patients.php'>
247 <center>
248 <table style='width:90%'>
249 <tr>
250 <td>
251 <?php echo xlt('Target Patient') ?>
252 </td>
253 <td>
254 <input type='text' size='30' name='form_target_patient'
255 value=' (<?php echo xla('Click to select'); ?>)'
256 onclick='sel_patient(this, this.form.form_target_pid)'
257 title='Click to select patient' readonly />
258 <input type='hidden' name='form_target_pid' value='0' />
259 </td>
260 <td>
261 <?php echo xlt('This is the main chart that is to receive the merged data.'); ?>
262 </td>
263 </tr>
264 <tr>
265 <td>
266 <?php echo xlt('Source Patient') ?>
267 </td>
268 <td>
269 <input type='text' size='30' name='form_source_patient'
270 value=' (<?php echo xla('Click to select'); ?>)'
271 onclick='sel_patient(this, this.form.form_source_pid)'
272 title='Click to select patient' readonly />
273 <input type='hidden' name='form_source_pid' value='0' />
274 </td>
275 <td>
276 <?php echo xlt('This is the chart that is to be merged into the main chart and then deleted.'); ?>
277 </td>
278 </tr>
279 </table>
280 <p><input type='submit' name='form_submit' value='<?php echo xla('Merge'); ?>' /></p>
281 </center>
282 </form>
284 <!-- I don't think it's good to run big globs of text through the current translation
285 system. Let's find another way. -->
287 <p>This utility is experimental. Back up your database and documents before using it!</p>
289 <?php if (!$PRODUCTION) { ?>
290 <p>This will be a "dry run" with no physical data updates.</p>
291 <?php } ?>
293 <p>This will merge two patient charts into one. It is useful when a patient has been
294 duplicated by mistake. If that happens often, fix your office procedures - do not run this
295 routinely!</p>
297 <p>The first ("target") chart is the one that is considered the most complete and accurate.
298 Demographics, history and insurance sections for this one will be retained.</p>
300 <p>The second ("source") chart will have its demographics, history and insurance sections
301 discarded. Its other data will be merged into the target chart.</p>
303 <p>The merge will not run unless SSN and DOB for the two charts are present and identical.
304 Also there must not be any documents with identical names. If any of these problems are
305 found then you should fix them and retry the merge.</p>
307 </body>
308 </html>