php syntax error bug fix in eye form
[openemr.git] / library / FeeSheet.class.php
blob32f1b73d91d32683cabe2410b5d4898f922b1b5e
1 <?php
2 /**
3 * library/FeeSheet.class.php
4 *
5 * Base class for implementations of the Fee Sheet.
6 * This should not include UI but may be extended by a class that does.
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 3
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
18 * http://www.gnu.org/licenses/licenses.html#GPL .
20 * @package OpenEMR
21 * @license http://www.gnu.org/licenses/licenses.html#GPL GNU GPL V3+
22 * @author Rod Roark <rod@sunsetsystems.com>
23 * @link http://www.open-emr.org
26 $fake_register_globals = false;
27 $sanitize_all_escapes = true;
29 require_once(dirname(__FILE__) . "/../interface/globals.php");
30 require_once(dirname(__FILE__) . "/acl.inc");
31 require_once(dirname(__FILE__) . "/../custom/code_types.inc.php");
32 require_once(dirname(__FILE__) . "/../interface/drugs/drugs.inc.php");
33 require_once(dirname(__FILE__) . "/formatting.inc.php");
34 require_once(dirname(__FILE__) . "/options.inc.php");
35 require_once(dirname(__FILE__) . "/appointment_status.inc.php");
36 require_once(dirname(__FILE__) . "/classes/Prescription.class.php");
37 require_once(dirname(__FILE__) . "/forms.inc");
38 require_once(dirname(__FILE__) . "/log.inc");
40 // For logging checksums set this to true.
41 define('CHECKSUM_LOGGING', true);
43 // require_once(dirname(__FILE__) . "/api.inc");
44 // require_once(dirname(__FILE__) . "/forms.inc");
45 // require_once(dirname(__FILE__) . "/formdata.inc.php");
47 class FeeSheet {
49 public $pid; // patient id
50 public $encounter; // encounter id
51 public $got_warehouses = false; // if there is more than 1 warehouse
52 public $default_warehouse = ''; // logged-in user's default warehouse
53 public $visit_date = ''; // YYYY-MM-DD date of this visit
54 public $match_services_to_products = false; // For IPPF
55 public $patient_age = 0; // Age in years as of the visit date
56 public $patient_male = 0; // 1 if male
57 public $patient_pricelevel = ''; // From patient_data.pricelevel
58 public $provider_id = 0;
59 public $supervisor_id = 0;
60 public $code_is_in_fee_sheet = false; // Set by genCodeSelectorValue()
62 // Possible units of measure for NDC drug quantities.
63 public $ndc_uom_choices = array(
64 'ML' => 'ML',
65 'GR' => 'Grams',
66 'ME' => 'Milligrams',
67 'F2' => 'I.U.',
68 'UN' => 'Units'
71 // Set by checkRelatedForContraception():
72 public $line_contra_code = '';
73 public $line_contra_cyp = 0;
74 public $line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult
76 // Array of line items generated by addServiceLineItem().
77 // Each element is an array of line item attributes.
78 public $serviceitems = array();
80 // Array of line items generated by addProductLineItem().
81 // Each element is an array of line item attributes.
82 public $productitems = array();
84 // Indicates if any line item has a fee.
85 public $hasCharges = false;
87 // Indicates if any clinical services or products are in the fee sheet.
88 public $required_code_count = 0;
90 // These variables are used to compute the initial consult service with highest CYP (IPPF).
91 public $contraception_code = '';
92 public $contraception_cyp = 0;
94 public $ALLOW_COPAYS = false;
96 function __construct($pid=0, $encounter=0) {
97 if (empty($pid)) $pid = $GLOBALS['pid'];
98 if (empty($encounter)) $encounter = $GLOBALS['encounter'];
99 $this->pid = $pid;
100 $this->encounter = $encounter;
102 // IPPF doesn't want any payments to be made or displayed in the Fee Sheet.
103 $this->ALLOW_COPAYS = !$GLOBALS['ippf_specific'];
105 // Get the user's default warehouse and an indicator if there's a choice of warehouses.
106 $wrow = sqlQuery("SELECT count(*) AS count FROM list_options WHERE list_id = 'warehouse' AND activity = 1");
107 $this->got_warehouses = $wrow['count'] > 1;
108 $wrow = sqlQuery("SELECT default_warehouse FROM users WHERE username = ?",
109 array($_SESSION['authUser']));
110 $this->default_warehouse = empty($wrow['default_warehouse']) ? '' : $wrow['default_warehouse'];
112 // Get some info about this visit.
113 $visit_row = sqlQuery("SELECT fe.date, fe.provider_id, fe.supervisor_id, " .
114 "opc.pc_catname, fac.extra_validation " .
115 "FROM form_encounter AS fe " .
116 "LEFT JOIN openemr_postcalendar_categories AS opc ON opc.pc_catid = fe.pc_catid " .
117 "LEFT JOIN facility AS fac ON fac.id = fe.facility_id " .
118 "WHERE fe.pid = ? AND fe.encounter = ? LIMIT 1", array($this->pid, $this->encounter) );
119 $this->visit_date = substr($visit_row['date'], 0, 10);
120 $this->provider_id = $visit_row['provider_id'];
121 if (empty($this->provider_id)) $this->provider_id = findProvider();
122 $this->supervisor_id = $visit_row['supervisor_id'];
123 // This flag is specific to IPPF validation at form submit time. It indicates
124 // that most contraceptive services and products should match up on the fee sheet.
125 $this->match_services_to_products = $GLOBALS['ippf_specific'] &&
126 !empty($visit_row['extra_validation']);
128 // Get some information about the patient.
129 $patientrow = getPatientData($this->pid, "DOB, sex, pricelevel");
130 $this->patient_age = $this->getAge($patientrow['DOB'], $this->visit_date);
131 $this->patient_male = strtoupper(substr($patientrow['sex'], 0, 1)) == 'M' ? 1 : 0;
132 $this->patient_pricelevel = $patientrow['pricelevel'];
135 // Convert numeric code type to the alpha version.
137 public static function alphaCodeType($id) {
138 global $code_types;
139 foreach ($code_types as $key => $value) {
140 if ($value['id'] == $id) return $key;
142 return '';
145 // Compute age in years given a DOB and "as of" date.
147 public static function getAge($dob, $asof='') {
148 if (empty($asof)) $asof = date('Y-m-d');
149 $a1 = explode('-', substr($dob , 0, 10));
150 $a2 = explode('-', substr($asof, 0, 10));
151 $age = $a2[0] - $a1[0];
152 if ($a2[1] < $a1[1] || ($a2[1] == $a1[1] && $a2[2] < $a1[2])) --$age;
153 return $age;
156 // Gets the provider from the encounter, logged-in user or patient demographics.
157 // Adapted from work by Terry Hill.
159 public function findProvider() {
160 $find_provider = sqlQuery("SELECT provider_id FROM form_encounter " .
161 "WHERE pid = ? AND encounter = ? ORDER BY id DESC LIMIT 1",
162 array($this->pid, $this->encounter));
163 $providerid = $find_provider['provider_id'];
164 if (!$providerid) {
165 $get_authorized = $_SESSION['userauthorized'];
166 if ($get_authorized == 1) {
167 $providerid = $_SESSION['authUserID'];
170 if (!$providerid) {
171 $find_provider = sqlQuery("SELECT providerID FROM patient_data " .
172 "WHERE pid = ?", array($this->pid) );
173 $providerid = $find_provider['providerID'];
175 return intval($providerid);
178 // Log a message that is easy for the Re-Opened Visits Report to interpret.
180 public function logFSMessage($action) {
181 newEvent('fee-sheet', $_SESSION['authUser'], $_SESSION['authProvider'], 1,
182 $action, $this->pid, $this->encounter);
185 // Compute a current checksum of this encounter's Fee Sheet data from the database.
187 public function visitChecksum($saved=false) {
188 $rowb = sqlQuery("SELECT BIT_XOR(CRC32(CONCAT_WS(',', " .
189 "id, code, modifier, units, fee, authorized, provider_id, ndc_info, justify, billed" .
190 "))) AS checksum FROM billing WHERE " .
191 "pid = ? AND encounter = ? AND activity = 1",
192 array($this->pid, $this->encounter));
193 $rowp = sqlQuery("SELECT BIT_XOR(CRC32(CONCAT_WS(',', " .
194 "sale_id, inventory_id, prescription_id, quantity, fee, sale_date, billed" .
195 "))) AS checksum FROM drug_sales WHERE " .
196 "pid = ? AND encounter = ?",
197 array($this->pid, $this->encounter));
198 $ret = intval($rowb['checksum']) ^ intval($rowp['checksum']);
199 if (CHECKSUM_LOGGING) {
200 $comment = "Checksum = '$ret'";
201 $comment .= ", Saved = " . ($saved ? "true" : "false");
202 newEvent("checksum", $_SESSION['authUser'], $_SESSION['authProvider'], 1, $comment, $this->pid);
204 return $ret;
207 // IPPF-specific; get contraception attributes of the related codes.
209 public function checkRelatedForContraception($related_code, $is_initial_consult=false) {
210 $this->line_contra_code = '';
211 $this->line_contra_cyp = 0;
212 $this->line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult
213 if (!empty($related_code)) {
214 $relcodes = explode(';', $related_code);
215 foreach ($relcodes as $relstring) {
216 if ($relstring === '') continue;
217 list($reltype, $relcode) = explode(':', $relstring);
218 if ($reltype !== 'IPPFCM') continue;
219 $methtype = $is_initial_consult ? 2 : 1;
220 $tmprow = sqlQuery("SELECT cyp_factor FROM codes WHERE " .
221 "code_type = '32' AND code = ? LIMIT 1", array($relcode));
222 $cyp = 0 + $tmprow['cyp_factor'];
223 if ($cyp > $this->line_contra_cyp) {
224 $this->line_contra_cyp = $cyp;
225 // Note this is an IPPFCM code, not an IPPF2 code.
226 $this->line_contra_code = $relcode;
227 $this->line_contra_methtype = $methtype;
233 // Insert a row into the lbf_data table. Returns a new form ID if that is not provided.
234 // This is only needed for auto-creating Contraception forms.
236 public function insert_lbf_item($form_id, $field_id, $field_value) {
237 if ($form_id) {
238 sqlInsert("INSERT INTO lbf_data (form_id, field_id, field_value) " .
239 "VALUES (?, ?, ?)", array($form_id, $field_id, $field_value));
241 else {
242 $form_id = sqlInsert("INSERT INTO lbf_data (field_id, field_value) " .
243 "VALUES (?, ?)", array($field_id, $field_value));
245 return $form_id;
248 // Create an array of data for a particular billing table item that is useful
249 // for building a user interface form row. $args is an array containing:
250 // codetype
251 // code
252 // modifier
253 // ndc_info
254 // auth
255 // del
256 // units
257 // fee
258 // id
259 // billed
260 // code_text
261 // justify
262 // provider_id
263 // notecodes
264 // pricelevel
265 public function addServiceLineItem($args) {
266 global $code_types;
268 // echo "<!-- \n"; // debugging
269 // print_r($args); // debugging
270 // echo "--> \n"; // debugging
272 $li = array();
273 $li['hidden'] = array();
275 $codetype = $args['codetype'];
276 $code = $args['code'];
277 $modifier = isset($args['modifier']) ? $args['modifier'] : '';
278 $code_text = isset($args['code_text']) ? $args['code_text'] : '';
279 $units = isset($args['units']) ? $args['units'] : 0;
280 $units = max(1, intval($units));
281 $billed = !empty($args['billed']);
282 $auth = !empty($args['auth']);
283 $id = isset($args['id']) ? intval($args['id']) : 0;
284 $ndc_info = isset($args['ndc_info']) ? $args['ndc_info'] : '';
285 $provider_id = isset($args['provider_id']) ? intval($args['provider_id']) : 0;
286 $justify = isset($args['justify']) ? $args['justify'] : '';
287 $notecodes = isset($args['notecodes']) ? $args['notecodes'] : '';
288 $fee = isset($args['fee']) ? (0 + $args['fee']) : 0;
289 // Price level should be unset only if adding a new line item.
290 $pricelevel = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel;
291 $del = !empty($args['del']);
293 // If using line item billing and user wishes to default to a selected provider, then do so.
294 if(!empty($GLOBALS['default_fee_sheet_line_item_provider']) && !empty($GLOBALS['support_fee_sheet_line_item_provider'])) {
295 if ($provider_id == 0) {
296 $provider_id = 0 + $this->findProvider();
300 if ($codetype == 'COPAY') {
301 if (!$code_text) $code_text = 'Cash';
302 if ($fee > 0) $fee = 0 - $fee;
305 // Get the matching entry from the codes table.
306 $sqlArray = array();
307 $query = "SELECT id, units, code_text FROM codes WHERE " .
308 "code_type = ? AND code = ?";
309 array_push($sqlArray, $code_types[$codetype]['id'], $code);
310 if ($modifier) {
311 $query .= " AND modifier = ?";
312 array_push($sqlArray, $modifier);
314 else {
315 $query .= " AND (modifier IS NULL OR modifier = '')";
317 $result = sqlQuery($query, $sqlArray);
318 $codes_id = $result['id'];
320 if (!$code_text) {
321 $code_text = $result['code_text'];
322 if (empty($units)) $units = max(1, intval($result['units']));
323 if (!isset($args['fee'])) {
324 // Fees come from the prices table now.
325 $query = "SELECT pr_price FROM prices WHERE " .
326 "pr_id = ? AND pr_selector = '' AND pr_level = ? " .
327 "LIMIT 1";
328 // echo "\n<!-- $query -->\n"; // debugging
329 $prrow = sqlQuery($query, array($codes_id, $pricelevel));
330 $fee = empty($prrow) ? 0 : $prrow['pr_price'];
333 $fee = sprintf('%01.2f', $fee);
335 $li['hidden']['code_type'] = $codetype;
336 $li['hidden']['code' ] = $code;
337 $li['hidden']['mod' ] = $modifier;
338 $li['hidden']['billed' ] = $billed;
339 $li['hidden']['id' ] = $id;
340 $li['hidden']['codes_id' ] = $codes_id;
342 // This logic is only used for family planning clinics, and then only when
343 // the option is chosen to use or auto-generate Contraception forms.
344 // It adds contraceptive method and effectiveness to relevant lines.
345 if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy'] && $codetype == 'MA') {
346 $codesrow = sqlQuery("SELECT related_code, cyp_factor FROM codes WHERE " .
347 "code_type = ? AND code = ? LIMIT 1",
348 array($code_types[$codetype]['id'], $code));
349 $this->checkRelatedForContraception($codesrow['related_code'], $codesrow['cyp_factor']);
350 if ($this->line_contra_code) {
351 $li['hidden']['method' ] = $this->line_contra_code;
352 $li['hidden']['cyp' ] = $this->line_contra_cyp;
353 $li['hidden']['methtype'] = $this->line_contra_methtype;
354 // contraception_code is only concerned with initial consults.
355 if ($this->line_contra_cyp > $this->contraception_cyp && $this->line_contra_methtype == 2) {
356 $this->contraception_cyp = $this->line_contra_cyp;
357 $this->contraception_code = $this->line_contra_code;
362 if($codetype == 'COPAY') {
363 $li['codetype'] = xl($codetype);
364 if ($ndc_info) $li['codetype'] .= " ($ndc_info)";
365 $ndc_info = '';
367 else {
368 $li['codetype'] = $codetype;
371 $li['code' ] = $codetype == 'COPAY' ? '' : $code;
372 $li['mod' ] = $modifier;
373 $li['fee' ] = $fee;
374 $li['price' ] = $fee / $units;
375 $li['pricelevel'] = $pricelevel;
376 $li['units' ] = $units;
377 $li['provid' ] = $provider_id;
378 $li['justify' ] = $justify;
379 $li['notecodes'] = $notecodes;
380 $li['del' ] = $id && $del;
381 $li['code_text'] = $code_text;
382 $li['auth' ] = $auth;
384 $li['hidden']['price'] = $li['price'];
386 // If NDC info exists or may be required, add stuff for it.
387 if ($codetype == 'HCPCS' && !$billed) {
388 $ndcnum = ''; $ndcuom = ''; $ndcqty = '';
389 if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndc_info, $tmp)) {
390 $ndcnum = $tmp[1]; $ndcuom = $tmp[2]; $ndcqty = $tmp[3];
392 $li['ndcnum' ] = $ndcnum;
393 $li['ndcuom' ] = $ndcuom;
394 $li['ndcqty' ] = $ndcqty;
396 else if ($ndc_info) {
397 $li['ndc_info' ] = $ndc_info;
400 // For Family Planning.
401 if ($codetype == 'MA') ++$this->required_code_count;
402 if ($fee != 0) $this->hasCharges = true;
404 $this->serviceitems[] = $li;
407 // Create an array of data for a particular drug_sales table item that is useful
408 // for building a user interface form row. $args is an array containing:
409 // drug_id
410 // selector
411 // sale_id
412 // rx (boolean)
413 // del (boolean)
414 // units
415 // fee
416 // billed
417 // warehouse_id
418 // pricelevel
420 public function addProductLineItem($args) {
421 global $code_types;
423 $li = array();
424 $li['hidden'] = array();
426 $drug_id = $args['drug_id'];
427 $selector = isset($args['selector']) ? $args['selector'] : '';
428 $sale_id = isset($args['sale_id']) ? intval($args['sale_id']) : 0;
429 $units = isset($args['units']) ? $args['units'] : 0;
430 $units = max(1, intval($units));
431 $billed = !empty($args['billed']);
432 $rx = !empty($args['rx']);
433 $del = !empty($args['del']);
434 $fee = isset($args['fee']) ? (0 + $args['fee']) : 0;
435 $pricelevel = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel;
436 $warehouse_id = isset($args['warehouse_id']) ? $args['warehouse_id'] : '';
438 $drow = sqlQuery("SELECT name, related_code FROM drugs WHERE drug_id = ?", array($drug_id) );
439 $code_text = $drow['name'];
441 // If no warehouse ID passed, use the logged-in user's default.
442 if ($this->got_warehouses && $warehouse_id === '') $warehouse_id = $this->default_warehouse;
444 // If fee is not provided, get it from the prices table.
445 // It is assumed in this case that units will match what is in the product template.
446 if (!isset($args['fee'])) {
447 $query = "SELECT pr_price FROM prices WHERE " .
448 "pr_id = ? AND pr_selector = ? AND pr_level = ? " .
449 "LIMIT 1";
450 $prrow = sqlQuery($query, array($drug_id, $selector, $pricelevel));
451 $fee = empty($prrow) ? 0 : $prrow['pr_price'];
454 $fee = sprintf('%01.2f', $fee);
456 $li['fee' ] = $fee;
457 $li['price' ] = $fee / $units;
458 $li['pricelevel'] = $pricelevel;
459 $li['units' ] = $units;
460 $li['del' ] = $sale_id && $del;
461 $li['code_text'] = $code_text;
462 $li['warehouse'] = $warehouse_id;
463 $li['rx' ] = $rx;
465 $li['hidden']['drug_id'] = $drug_id;
466 $li['hidden']['selector'] = $selector;
467 $li['hidden']['sale_id'] = $sale_id;
468 $li['hidden']['billed' ] = $billed;
469 $li['hidden']['price' ] = $li['price'];
471 // This logic is only used for family planning clinics, and then only when
472 // the option is chosen to use or auto-generate Contraception forms.
473 // It adds contraceptive method and effectiveness to relevant lines.
474 if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy']) {
475 $this->checkRelatedForContraception($drow['related_code']);
476 if ($this->line_contra_code) {
477 $li['hidden']['method' ] = $this->line_contra_code;
478 $li['hidden']['methtype'] = $this->line_contra_methtype;
482 // For Family Planning.
483 ++$this->required_code_count;
484 if ($fee != 0) $this->hasCharges = true;
486 $this->productitems[] = $li;
489 // Generate rows for items already in the billing table for this encounter.
491 public function loadServiceItems() {
492 $billresult = getBillingByEncounter($this->pid, $this->encounter, "*");
493 if ($billresult) {
494 foreach ($billresult as $iter) {
495 if (!$this->ALLOW_COPAYS && $iter["code_type"] == 'COPAY') continue;
496 $justify = trim($iter['justify']);
497 if ($justify) $justify = substr(str_replace(':', ',', $justify), 0, strlen($justify) - 1);
498 $this->addServiceLineItem(array(
499 'id' => $iter['id'],
500 'codetype' => $iter['code_type'],
501 'code' => trim($iter['code']),
502 'modifier' => trim($iter["modifier"]),
503 'code_text' => trim($iter['code_text']),
504 'units' => $iter['units'],
505 'fee' => $iter['fee'],
506 'pricelevel' => $iter['pricelevel'],
507 'billed' => $iter['billed'],
508 'ndc_info' => $iter['ndc_info'],
509 'provider_id' => $iter['provider_id'],
510 'justify' => $justify,
511 'notecodes' => trim($iter['notecodes']),
515 // echo "<!-- \n"; // debugging
516 // print_r($this->serviceitems); // debugging
517 // echo "--> \n"; // debugging
520 // Generate rows for items already in the drug_sales table for this encounter.
522 public function loadProductItems() {
523 $query = "SELECT ds.*, di.warehouse_id FROM drug_sales AS ds, drug_inventory AS di WHERE " .
524 "ds.pid = ? AND ds.encounter = ? AND di.inventory_id = ds.inventory_id " .
525 "ORDER BY ds.sale_id";
526 $sres = sqlStatement($query, array($this->pid, $this->encounter));
527 while ($srow = sqlFetchArray($sres)) {
528 $this->addProductLineItem(array(
529 'drug_id' => $srow['drug_id'],
530 'selector' => $srow['selector'],
531 'sale_id' => $srow['sale_id'],
532 'rx' => !empty($srow['prescription_id']),
533 'units' => $srow['quantity'],
534 'fee' => $srow['fee'],
535 'pricelevel' => $srow['pricelevel'],
536 'billed' => $srow['billed'],
537 'warehouse_id' => $srow['warehouse_id'],
542 // Check for insufficient product inventory levels.
543 // Returns an error message if any product items cannot be filled.
544 // You must call this before save().
546 public function checkInventory(&$prod) {
547 $alertmsg = '';
548 $insufficient = 0;
549 $expiredlots = false;
550 if (is_array($prod)) foreach ($prod as $iter) {
551 if (!empty($iter['billed'])) continue;
552 $drug_id = $iter['drug_id'];
553 $sale_id = empty($iter['sale_id']) ? 0 : intval($iter['sale_id']); // present only if already saved
554 $units = empty($iter['units']) ? 1 : intval($iter['units']);
555 $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse'];
557 // Deleting always works.
558 if (!empty($iter['del'])) continue;
560 // If the item is already in the database...
561 if ($sale_id) {
562 $query = "SELECT ds.quantity, ds.inventory_id, di.on_hand, di.warehouse_id " .
563 "FROM drug_sales AS ds " .
564 "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " .
565 "WHERE ds.sale_id = ?";
566 $dirow = sqlQuery($query, array($sale_id));
567 // There's no inventory ID when this is a non-dispensible product (i.e. no inventory).
568 if (!empty($dirow['inventory_id'])) {
569 if ($warehouse_id && $warehouse_id != $dirow['warehouse_id']) {
570 // Changing warehouse so check inventory in the new warehouse.
571 // Nothing is updated by this call.
572 if (!sellDrug($drug_id, $units, 0, $this->pid, $this->encounter, 0,
573 $this->visit_date, '', $warehouse_id, true, $expiredlots)) {
574 $insufficient = $drug_id;
577 else {
578 if (($dirow['on_hand'] + $dirow['quantity'] - $units) < 0) {
579 $insufficient = $drug_id;
584 // Otherwise it's a new item...
585 else {
586 // This only checks for sufficient inventory, nothing is updated.
587 if (!sellDrug($drug_id, $units, 0, $this->pid, $this->encounter, 0,
588 $this->visit_date, '', $warehouse_id, true, $expiredlots)) {
589 $insufficient = $drug_id;
592 } // end for
593 if ($insufficient) {
594 $drow = sqlQuery("SELECT name FROM drugs WHERE drug_id = ?", array($insufficient));
595 $alertmsg = xl('Insufficient inventory for product') . ' "' . $drow['name'] . '".';
596 if ($expiredlots) $alertmsg .= " " . xl('Check expiration dates.');
598 return $alertmsg;
601 // Save posted data to the database. $bill and $prod are the incoming arrays of line items, with
602 // key names corresponding to those generated by addServiceLineItem() and addProductLineItem().
604 public function save(&$bill, &$prod, $main_provid=NULL, $main_supid=NULL, $default_warehouse=NULL,
605 $mark_as_closed=false) {
606 global $code_types;
608 if (isset($main_provid) && $main_supid == $main_provid) $main_supid = 0;
610 $copay_update = FALSE;
611 $update_session_id = '';
612 $ct0 = ''; // takes the code type of the first fee type code type entry from the fee sheet, against which the copay is posted
613 $cod0 = ''; // takes the code of the first fee type code type entry from the fee sheet, against which the copay is posted
614 $mod0 = ''; // takes the modifier of the first fee type code type entry from the fee sheet, against which the copay is posted
616 if (is_array($bill)) foreach ($bill as $iter) {
617 // Skip disabled (billed) line items.
618 if (!empty($iter['billed'])) continue;
620 $id = $iter['id'];
621 $code_type = $iter['code_type'];
622 $code = $iter['code'];
623 $del = !empty($iter['del']);
624 $units = empty($iter['units']) ? 1 : intval($iter['units']);
625 $price = empty($iter['price']) ? 0 : (0 + trim($iter['price']));
626 $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel'];
627 $modifier = empty($iter['mod']) ? '' : trim($iter['mod']);
628 $justify = empty($iter['justify' ]) ? '' : trim($iter['justify']);
629 $notecodes = empty($iter['notecodes']) ? '' : trim($iter['notecodes']);
630 $provid = empty($iter['provid' ]) ? 0 : intval($iter['provid']);
632 $fee = sprintf('%01.2f', $price * $units);
634 if(!$cod0 && $code_types[$code_type]['fee'] == 1) {
635 $mod0 = $modifier;
636 $cod0 = $code;
637 $ct0 = $code_type;
640 if ($code_type == 'COPAY') {
641 if ($fee < 0) {
642 $fee = $fee * -1;
644 if (!$id) {
645 // adding new copay from fee sheet into ar_session and ar_activity tables
646 $session_id = idSqlStatement("INSERT INTO ar_session " .
647 "(payer_id, user_id, pay_total, payment_type, description, patient_id, payment_method, " .
648 "adjustment_code, post_to_date) " .
649 "VALUES ('0',?,?,'patient','COPAY',?,'','patient_payment',now())",
650 array($_SESSION['authId'], $fee, $this->pid));
651 sqlBeginTrans();
652 $sequence_no = sqlQuery("SELECT IFNULL(MAX(sequence_no),0) + 1 AS increment FROM ar_activity WHERE " .
653 "pid = ? AND encounter = ?", array($this->pid, $this->encounter));
654 SqlStatement("INSERT INTO ar_activity (pid, encounter, sequence_no, code_type, code, modifier, " .
655 "payer_type, post_time, post_user, session_id, " .
656 "pay_amount, account_code) VALUES (?,?,?,?,?,?,0,now(),?,?,?,'PCP')",
657 array($this->pid, $this->encounter, $sequence_no['increment'], $ct0, $cod0, $mod0,
658 $_SESSION['authId'], $session_id, $fee));
659 sqlCommitTrans();
661 else {
662 // editing copay saved to ar_session and ar_activity
663 $session_id = $id;
664 $res_amount = sqlQuery("SELECT pay_amount FROM ar_activity WHERE pid=? AND encounter=? AND session_id=?",
665 array($this->pid, $this->encounter, $session_id));
666 if ($fee != $res_amount['pay_amount']) {
667 sqlStatement("UPDATE ar_session SET user_id=?,pay_total=?,modified_time=now(),post_to_date=now() WHERE session_id=?",
668 array($_SESSION['authId'], $fee, $session_id));
669 sqlStatement("UPDATE ar_activity SET code_type=?, code=?, modifier=?, post_user=?, post_time=now(),".
670 "pay_amount=?, modified_time=now() WHERE pid=? AND encounter=? AND account_code='PCP' AND session_id=?",
671 array($ct0, $cod0, $mod0, $_SESSION['authId'], $fee, $this->pid, $this->encounter, $session_id));
674 if (!$cod0){
675 $copay_update = TRUE;
676 $update_session_id = $session_id;
678 continue;
681 # Code to create justification for all codes based on first justification
682 if ($GLOBALS['replicate_justification'] == '1') {
683 if ($justify != '') {
684 $autojustify = $justify;
687 if (($GLOBALS['replicate_justification'] == '1') && ($justify == '') && check_is_code_type_justify($code_type)) {
688 $justify = $autojustify;
691 if ($justify) $justify = str_replace(',', ':', $justify) . ':';
692 $auth = "1";
694 $ndc_info = '';
695 if (!empty($iter['ndcnum'])) {
696 $ndc_info = 'N4' . trim($iter['ndcnum']) . ' ' . $iter['ndcuom'] .
697 trim($iter['ndcqty']);
700 // If the item is already in the database...
701 if ($id) {
702 if ($del) {
703 $this->logFSMessage(xl('Service deleted'));
704 deleteBilling($id);
706 else {
707 $tmp = sqlQuery("SELECT * FROM billing WHERE id = ? AND billed = 0 AND activity = 1",
708 array($id));
709 if (!empty($tmp)) {
710 $tmparr = array('code' => $code, 'authorized' => $auth);
711 if (isset($iter['units' ])) $tmparr['units' ] = $units;
712 if (isset($iter['price' ])) $tmparr['fee' ] = $fee;
713 if (isset($iter['pricelevel'])) $tmparr['pricelevel'] = $pricelevel;
714 if (isset($iter['mod' ])) $tmparr['modifier' ] = $modifier;
715 if (isset($iter['provid' ])) $tmparr['provider_id'] = $provid;
716 if (isset($iter['ndcnum' ])) $tmparr['ndc_info' ] = $ndc_info;
717 if (isset($iter['justify' ])) $tmparr['justify' ] = $justify;
718 if (isset($iter['notecodes'])) $tmparr['notecodes' ] = $notecodes;
719 foreach ($tmparr AS $key => $value) {
720 if ($tmp[$key] != $value) {
721 if ('fee' == $key) $this->logFSMessage(xl('Price changed'));
722 if ('units' == $key) $this->logFSMessage(xl('Quantity changed'));
723 if ('provider_id' == $key) $this->logFSMessage(xl('Service provider changed'));
724 sqlStatement("UPDATE billing SET `$key` = ? WHERE id = ?", array($value, $id));
730 // Otherwise it's a new item...
731 else if (!$del) {
732 $this->logFSMessage(xl('Service added'));
733 $code_text = lookup_code_descriptions($code_type.":".$code);
734 addBilling($this->encounter, $code_type, $code, $code_text, $this->pid, $auth,
735 $provid, $modifier, $units, $fee, $ndc_info, $justify, 0, $notecodes, $pricelevel);
737 } // end for
739 // if modifier is not inserted during loop update the record using the first
740 // non-empty modifier and code
741 if($copay_update == TRUE && $update_session_id != '' && $mod0 != '') {
742 sqlStatement("UPDATE ar_activity SET code_type = ?, code = ?, modifier = ?".
743 " WHERE pid = ? AND encounter = ? AND account_code = 'PCP' AND session_id = ?",
744 array($ct0, $cod0, $mod0, $this->pid, $this->encounter, $update_session_id));
747 // Doing similarly to the above but for products.
748 if (is_array($prod)) foreach ($prod as $iter) {
749 // Skip disabled (billed) line items.
750 if (!empty($iter['billed'])) continue;
752 $drug_id = $iter['drug_id'];
753 $selector = empty($iter['selector']) ? '' : $iter['selector'];
754 $sale_id = $iter['sale_id']; // present only if already saved
755 $units = max(1, intval(trim($iter['units'])));
756 $price = empty($iter['price']) ? 0 : (0 + trim($iter['price']));
757 $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel'];
758 $fee = sprintf('%01.2f', $price * $units);
759 $del = !empty($iter['del']);
760 $rxid = 0;
761 $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse'];
762 $somechange = false;
764 // If the item is already in the database...
765 if ($sale_id) {
766 $tmprow = sqlQuery("SELECT ds.prescription_id, ds.quantity, ds.inventory_id, ds.fee, " .
767 "ds.sale_date, di.warehouse_id " .
768 "FROM drug_sales AS ds " .
769 "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " .
770 "WHERE ds.sale_id = ?", array($sale_id));
771 $rxid = 0 + $tmprow['prescription_id'];
772 if ($del) {
773 if (!empty($tmprow)) {
774 // Delete this sale and reverse its inventory update.
775 $this->logFSMessage(xl('Product deleted'));
776 sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id));
777 if (!empty($tmprow['inventory_id'])) {
778 sqlStatement("UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?",
779 array($tmprow['quantity'], $tmprow['inventory_id']));
782 if ($rxid) {
783 sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid));
786 else {
787 // Modify the sale and adjust inventory accordingly.
788 if (!empty($tmprow)) {
789 foreach (array(
790 'quantity' => $units,
791 'fee' => $fee,
792 'pricelevel' => $pricelevel,
793 'selector' => $selector,
794 'sale_date' => $this->visit_date,
795 ) AS $key => $value) {
796 if ($tmprow[$key] != $value) {
797 $somechange = true;
798 if ('fee' == $key) $this->logFSMessage(xl('Price changed'));
799 if ('pricelevel' == $key) $this->logFSMessage(xl('Price level changed'));
800 if ('selector' == $key) $this->logFSMessage(xl('Template selector changed'));
801 if ('quantity' == $key) $this->logFSMessage(xl('Quantity changed'));
802 sqlStatement("UPDATE drug_sales SET `$key` = ? WHERE sale_id = ?",
803 array($value, $sale_id));
804 if ($key == 'quantity' && $tmprow['inventory_id']) {
805 sqlStatement("UPDATE drug_inventory SET on_hand = on_hand - ? WHERE inventory_id = ?",
806 array($units - $tmprow['quantity'], $tmprow['inventory_id']));
810 if ($tmprow['inventory_id'] && $warehouse_id && $warehouse_id != $tmprow['warehouse_id']) {
811 // Changing warehouse. Requires deleting and re-adding the sale.
812 // Not setting $somechange because this alone does not affect a prescription.
813 $this->logFSMessage(xl('Warehouse changed'));
814 sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id));
815 sqlStatement("UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?",
816 array($units, $tmprow['inventory_id']));
817 $tmpnull = null;
818 $sale_id = sellDrug($drug_id, $units, $fee, $this->pid, $this->encounter,
819 (empty($iter['rx']) ? 0 : $rxid), $this->visit_date, '', $warehouse_id,
820 false, $tmpnull, $pricelevel, $selector);
823 // Delete Rx if $rxid and flag not set.
824 if ($GLOBALS['gbl_auto_create_rx'] && $rxid && empty($iter['rx'])) {
825 sqlStatement("UPDATE drug_sales SET prescription_id = 0 WHERE sale_id = ?", array($sale_id));
826 sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid));
831 // Otherwise it's a new item...
832 else if (! $del) {
833 $somechange = true;
834 $this->logFSMessage(xl('Product added'));
835 $tmpnull = null;
836 $sale_id = sellDrug($drug_id, $units, $fee, $this->pid, $this->encounter, 0,
837 $this->visit_date, '', $warehouse_id, false, $tmpnull, $pricelevel, $selector);
838 if (!$sale_id) die(xlt("Insufficient inventory for product ID") . " \"" . text($drug_id) . "\".");
841 // If a prescription applies, create or update it.
842 if (!empty($iter['rx']) && !$del && ($somechange || empty($rxid))) {
843 // If an active rx already exists for this drug and date we will
844 // replace it, otherwise we'll make a new one.
845 if (empty($rxid)) $rxid = '';
846 // Get default drug attributes; prefer the template with the matching selector.
847 $drow = sqlQuery("SELECT dt.*, " .
848 "d.name, d.form, d.size, d.unit, d.route, d.substitute " .
849 "FROM drugs AS d, drug_templates AS dt WHERE " .
850 "d.drug_id = ? AND dt.drug_id = d.drug_id " .
851 "ORDER BY (dt.selector = ?) DESC, dt.quantity, dt.dosage, dt.selector LIMIT 1",
852 array($drug_id, $selector));
853 if (!empty($drow)) {
854 $rxobj = new Prescription($rxid);
855 $rxobj->set_patient_id($this->pid);
856 $rxobj->set_provider_id(isset($main_provid) ? $main_provid : $this->provider_id);
857 $rxobj->set_drug_id($drug_id);
858 $rxobj->set_quantity($units);
859 $rxobj->set_per_refill($units);
860 $rxobj->set_start_date_y(substr($this->visit_date,0,4));
861 $rxobj->set_start_date_m(substr($this->visit_date,5,2));
862 $rxobj->set_start_date_d(substr($this->visit_date,8,2));
863 $rxobj->set_date_added($this->visit_date);
864 // Remaining attributes are the drug and template defaults.
865 $rxobj->set_drug($drow['name']);
866 $rxobj->set_unit($drow['unit']);
867 $rxobj->set_dosage($drow['dosage']);
868 $rxobj->set_form($drow['form']);
869 $rxobj->set_refills($drow['refills']);
870 $rxobj->set_size($drow['size']);
871 $rxobj->set_route($drow['route']);
872 $rxobj->set_interval($drow['period']);
873 $rxobj->set_substitute($drow['substitute']);
875 $rxobj->persist();
876 // Set drug_sales.prescription_id to $rxobj->get_id().
877 $oldrxid = $rxid;
878 $rxid = 0 + $rxobj->get_id();
879 if ($rxid != $oldrxid) {
880 sqlStatement("UPDATE drug_sales SET prescription_id = ? WHERE sale_id = ?",
881 array($rxid, $sale_id));
885 } // end for
887 // Set default and/or supervising provider for the encounter.
888 if (isset($main_provid) && $main_provid != $this->provider_id) {
889 $this->logFSMessage(xl('Default provider changed'));
890 sqlStatement("UPDATE form_encounter SET provider_id = ? WHERE pid = ? AND encounter = ?",
891 array($main_provid, $this->pid, $this->encounter));
892 $this->provider_id = $main_provid;
894 if (isset($main_supid) && $main_supid != $this->supervisor_id) {
895 sqlStatement("UPDATE form_encounter SET supervisor_id = ? WHERE pid = ? AND encounter = ?",
896 array($main_supid, $this->pid, $this->encounter));
897 $this->supervisor_id = $main_supid;
900 // Save-and-Close is currently specific to Family Planning but might be more
901 // generally useful. It provides the ability to mark an encounter as billed
902 // directly from the Fee Sheet, if there are no charges.
903 if ($mark_as_closed) {
904 $tmp1 = sqlQuery("SELECT SUM(ABS(fee)) AS sum FROM drug_sales WHERE " .
905 "pid = ? AND encounter = ? AND billed = 0",
906 array($this->pid, $this->encounter));
907 $tmp2 = sqlQuery("SELECT SUM(ABS(fee)) AS sum FROM billing WHERE " .
908 "pid = ? AND encounter = ? AND billed = 0 AND activity = 1",
909 array($this->pid, $this->encounter));
910 if ($tmp1['sum'] + $tmp2['sum'] == 0) {
911 sqlStatement("update drug_sales SET billed = 1 WHERE " .
912 "pid = ? AND encounter = ? AND billed = 0",
913 array($this->pid, $this->encounter));
914 sqlStatement("UPDATE billing SET billed = 1, bill_date = NOW() WHERE " .
915 "pid = ? AND encounter = ? AND billed = 0 AND activity = 1",
916 array($this->pid, $this->encounter));
918 else {
919 // Would be good to display an error message here... they clicked
920 // Save and Close but the close could not be done. However the
921 // framework does not provide an easy way to do that.
926 // Call this after save() for Family Planning implementations.
927 // It checks the contraception form, or makes a new one if $newmauser is set.
928 // Returns 0 unless user intervention is required to fix a missing or incorrect form,
929 // and in that case the return value is an existing form ID, or -1 if none.
931 // Returns FALSE if user intervention is required to fix a missing or incorrect form.
933 public function doContraceptionForm($ippfconmeth=NULL, $newmauser=NULL, $main_provid=0) {
934 if (!empty($ippfconmeth)) {
935 $csrow = sqlQuery("SELECT f.form_id, ld.field_value FROM forms AS f " .
936 "LEFT JOIN lbf_data AS ld ON ld.form_id = f.form_id AND ld.field_id = 'newmethod' " .
937 "WHERE " .
938 "f.pid = ? AND f.encounter = ? AND " .
939 "f.formdir = 'LBFccicon' AND f.deleted = 0 " .
940 "ORDER BY f.form_id DESC LIMIT 1",
941 array($this->pid, $this->encounter));
942 if (isset($newmauser)) {
943 // pastmodern is 0 iff new to modern contraception
944 $pastmodern = $newmauser == '2' ? 0 : 1;
945 if ($newmauser == '2') $newmauser = '1';
946 // Add contraception form but only if it does not already exist
947 // (if it does, must be 2 users working on the visit concurrently).
948 if (empty($csrow)) {
949 $newid = $this->insert_lbf_item(0, 'newmauser', $newmauser);
950 $this->insert_lbf_item($newid, 'newmethod', "IPPFCM:$ippfconmeth");
951 $this->insert_lbf_item($newid, 'pastmodern', $pastmodern);
952 // Do we care about a service-specific provider here?
953 $this->insert_lbf_item($newid, 'provider', $main_provid);
954 addForm($this->encounter, 'Contraception', $newid, 'LBFccicon', $this->pid, $GLOBALS['userauthorized']);
957 else if (empty($csrow) || $csrow['field_value'] != "IPPFCM:$ippfconmeth") {
958 // Contraceptive method does not match what is in an existing Contraception
959 // form for this visit, or there is no such form. User intervention is needed.
960 return empty($csrow) ? -1 : intval($csrow['form_id']);
963 return 0;
966 // Get price level from patient demographics.
968 public function getPriceLevel() {
969 return $this->patient_pricelevel;
972 // Update price level in patient demographics if it's changed.
974 public function updatePriceLevel($pricelevel) {
975 if (!empty($pricelevel)) {
976 if ($this->patient_pricelevel != $pricelevel) {
977 $this->logFSMessage(xl('Price level changed'));
978 sqlStatement("UPDATE patient_data SET pricelevel = ? WHERE pid = ?",
979 array($pricelevel, $this->pid));
980 $this->patient_pricelevel = $pricelevel;
985 // Create JSON string representing code type, code and selector.
986 // This can be a checkbox value for parsing when the checkbox is clicked.
987 // As a side effect note if the code is already selected in the Fee Sheet.
989 public function genCodeSelectorValue($codes) {
990 global $code_types;
991 list($codetype, $code, $selector) = explode(':', $codes);
992 if ($codetype == 'PROD') {
993 $crow = sqlQuery("SELECT sale_id " .
994 "FROM drug_sales WHERE pid = ? AND encounter = ? AND drug_id = ? " .
995 "LIMIT 1",
996 array($this->pid, $this->encounter, $code));
997 $this->code_is_in_fee_sheet = !empty($crow['sale_id']);
998 $cbarray = array($codetype, $code, $selector);
1000 else {
1001 $crow = sqlQuery("SELECT c.id AS code_id, b.id " .
1002 "FROM codes AS c " .
1003 "LEFT JOIN billing AS b ON b.pid = ? AND b.encounter = ? AND b.code_type = ? AND b.code = c.code AND b.activity = 1 " .
1004 "WHERE c.code_type = ? AND c.code = ? LIMIT 1",
1005 array($this->pid, $this->encounter, $codetype, $code_types[$codetype]['id'], $code));
1006 $this->code_is_in_fee_sheet = !empty($crow['id']);
1007 $cbarray = array($codetype, $code);
1009 $cbval = json_encode($cbarray);
1010 return $cbval;