* @link http://www.open-emr.org */ $fake_register_globals = false; $sanitize_all_escapes = true; require_once(dirname(__FILE__) . "/../interface/globals.php"); require_once(dirname(__FILE__) . "/acl.inc"); require_once(dirname(__FILE__) . "/../custom/code_types.inc.php"); require_once(dirname(__FILE__) . "/../interface/drugs/drugs.inc.php"); require_once(dirname(__FILE__) . "/formatting.inc.php"); require_once(dirname(__FILE__) . "/options.inc.php"); require_once(dirname(__FILE__) . "/appointment_status.inc.php"); require_once(dirname(__FILE__) . "/classes/Prescription.class.php"); require_once(dirname(__FILE__) . "/forms.inc"); require_once(dirname(__FILE__) . "/log.inc"); // For logging checksums set this to true. define('CHECKSUM_LOGGING', true); // require_once(dirname(__FILE__) . "/api.inc"); // require_once(dirname(__FILE__) . "/forms.inc"); class FeeSheet { public $pid; // patient id public $encounter; // encounter id public $got_warehouses = false; // if there is more than 1 warehouse public $default_warehouse = ''; // logged-in user's default warehouse public $visit_date = ''; // YYYY-MM-DD date of this visit public $match_services_to_products = false; // For IPPF public $patient_age = 0; // Age in years as of the visit date public $patient_male = 0; // 1 if male public $patient_pricelevel = ''; // From patient_data.pricelevel public $provider_id = 0; public $supervisor_id = 0; public $code_is_in_fee_sheet = false; // Set by genCodeSelectorValue() // Possible units of measure for NDC drug quantities. public $ndc_uom_choices = array( 'ML' => 'ML', 'GR' => 'Grams', 'ME' => 'Milligrams', 'F2' => 'I.U.', 'UN' => 'Units' ); // Set by checkRelatedForContraception(): public $line_contra_code = ''; public $line_contra_cyp = 0; public $line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult // Array of line items generated by addServiceLineItem(). // Each element is an array of line item attributes. public $serviceitems = array(); // Array of line items generated by addProductLineItem(). // Each element is an array of line item attributes. public $productitems = array(); // Indicates if any line item has a fee. public $hasCharges = false; // Indicates if any clinical services or products are in the fee sheet. public $required_code_count = 0; // These variables are used to compute the initial consult service with highest CYP (IPPF). public $contraception_code = ''; public $contraception_cyp = 0; public $ALLOW_COPAYS = false; function __construct($pid=0, $encounter=0) { if (empty($pid)) $pid = $GLOBALS['pid']; if (empty($encounter)) $encounter = $GLOBALS['encounter']; $this->pid = $pid; $this->encounter = $encounter; // IPPF doesn't want any payments to be made or displayed in the Fee Sheet. $this->ALLOW_COPAYS = !$GLOBALS['ippf_specific']; // Get the user's default warehouse and an indicator if there's a choice of warehouses. $wrow = sqlQuery("SELECT count(*) AS count FROM list_options WHERE list_id = 'warehouse' AND activity = 1"); $this->got_warehouses = $wrow['count'] > 1; $wrow = sqlQuery("SELECT default_warehouse FROM users WHERE username = ?", array($_SESSION['authUser'])); $this->default_warehouse = empty($wrow['default_warehouse']) ? '' : $wrow['default_warehouse']; // Get some info about this visit. $visit_row = sqlQuery("SELECT fe.date, fe.provider_id, fe.supervisor_id, " . "opc.pc_catname, fac.extra_validation " . "FROM form_encounter AS fe " . "LEFT JOIN openemr_postcalendar_categories AS opc ON opc.pc_catid = fe.pc_catid " . "LEFT JOIN facility AS fac ON fac.id = fe.facility_id " . "WHERE fe.pid = ? AND fe.encounter = ? LIMIT 1", array($this->pid, $this->encounter) ); $this->visit_date = substr($visit_row['date'], 0, 10); $this->provider_id = $visit_row['provider_id']; if (empty($this->provider_id)) $this->provider_id = findProvider(); $this->supervisor_id = $visit_row['supervisor_id']; // This flag is specific to IPPF validation at form submit time. It indicates // that most contraceptive services and products should match up on the fee sheet. $this->match_services_to_products = $GLOBALS['ippf_specific'] && !empty($visit_row['extra_validation']); // Get some information about the patient. $patientrow = getPatientData($this->pid, "DOB, sex, pricelevel"); $this->patient_age = $this->getAge($patientrow['DOB'], $this->visit_date); $this->patient_male = strtoupper(substr($patientrow['sex'], 0, 1)) == 'M' ? 1 : 0; $this->patient_pricelevel = $patientrow['pricelevel']; } // Convert numeric code type to the alpha version. // public static function alphaCodeType($id) { global $code_types; foreach ($code_types as $key => $value) { if ($value['id'] == $id) return $key; } return ''; } // Compute age in years given a DOB and "as of" date. // public static function getAge($dob, $asof='') { if (empty($asof)) $asof = date('Y-m-d'); $a1 = explode('-', substr($dob , 0, 10)); $a2 = explode('-', substr($asof, 0, 10)); $age = $a2[0] - $a1[0]; if ($a2[1] < $a1[1] || ($a2[1] == $a1[1] && $a2[2] < $a1[2])) --$age; return $age; } // Gets the provider from the encounter, logged-in user or patient demographics. // Adapted from work by Terry Hill. // public function findProvider() { $find_provider = sqlQuery("SELECT provider_id FROM form_encounter " . "WHERE pid = ? AND encounter = ? ORDER BY id DESC LIMIT 1", array($this->pid, $this->encounter)); $providerid = $find_provider['provider_id']; if (!$providerid) { $get_authorized = $_SESSION['userauthorized']; if ($get_authorized == 1) { $providerid = $_SESSION['authUserID']; } } if (!$providerid) { $find_provider = sqlQuery("SELECT providerID FROM patient_data " . "WHERE pid = ?", array($this->pid) ); $providerid = $find_provider['providerID']; } return intval($providerid); } // Log a message that is easy for the Re-Opened Visits Report to interpret. // public function logFSMessage($action) { newEvent('fee-sheet', $_SESSION['authUser'], $_SESSION['authProvider'], 1, $action, $this->pid, $this->encounter); } // Compute a current checksum of this encounter's Fee Sheet data from the database. // public function visitChecksum($saved=false) { $rowb = sqlQuery("SELECT BIT_XOR(CRC32(CONCAT_WS(',', " . "id, code, modifier, units, fee, authorized, provider_id, ndc_info, justify, billed" . "))) AS checksum FROM billing WHERE " . "pid = ? AND encounter = ? AND activity = 1", array($this->pid, $this->encounter)); $rowp = sqlQuery("SELECT BIT_XOR(CRC32(CONCAT_WS(',', " . "sale_id, inventory_id, prescription_id, quantity, fee, sale_date, billed" . "))) AS checksum FROM drug_sales WHERE " . "pid = ? AND encounter = ?", array($this->pid, $this->encounter)); $ret = intval($rowb['checksum']) ^ intval($rowp['checksum']); if (CHECKSUM_LOGGING) { $comment = "Checksum = '$ret'"; $comment .= ", Saved = " . ($saved ? "true" : "false"); newEvent("checksum", $_SESSION['authUser'], $_SESSION['authProvider'], 1, $comment, $this->pid); } return $ret; } // IPPF-specific; get contraception attributes of the related codes. // public function checkRelatedForContraception($related_code, $is_initial_consult=false) { $this->line_contra_code = ''; $this->line_contra_cyp = 0; $this->line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult if (!empty($related_code)) { $relcodes = explode(';', $related_code); foreach ($relcodes as $relstring) { if ($relstring === '') continue; list($reltype, $relcode) = explode(':', $relstring); if ($reltype !== 'IPPFCM') continue; $methtype = $is_initial_consult ? 2 : 1; $tmprow = sqlQuery("SELECT cyp_factor FROM codes WHERE " . "code_type = '32' AND code = ? LIMIT 1", array($relcode)); $cyp = 0 + $tmprow['cyp_factor']; if ($cyp > $this->line_contra_cyp) { $this->line_contra_cyp = $cyp; // Note this is an IPPFCM code, not an IPPF2 code. $this->line_contra_code = $relcode; $this->line_contra_methtype = $methtype; } } } } // Insert a row into the lbf_data table. Returns a new form ID if that is not provided. // This is only needed for auto-creating Contraception forms. // public function insert_lbf_item($form_id, $field_id, $field_value) { if ($form_id) { sqlInsert("INSERT INTO lbf_data (form_id, field_id, field_value) " . "VALUES (?, ?, ?)", array($form_id, $field_id, $field_value)); } else { $form_id = sqlInsert("INSERT INTO lbf_data (field_id, field_value) " . "VALUES (?, ?)", array($field_id, $field_value)); } return $form_id; } // Create an array of data for a particular billing table item that is useful // for building a user interface form row. $args is an array containing: // codetype // code // modifier // ndc_info // auth // del // units // fee // id // billed // code_text // justify // provider_id // notecodes // pricelevel public function addServiceLineItem($args) { global $code_types; // echo " \n"; // debugging $li = array(); $li['hidden'] = array(); $codetype = $args['codetype']; $code = $args['code']; $modifier = isset($args['modifier']) ? $args['modifier'] : ''; $code_text = isset($args['code_text']) ? $args['code_text'] : ''; $units = isset($args['units']) ? $args['units'] : 0; $units = max(1, intval($units)); $billed = !empty($args['billed']); $auth = !empty($args['auth']); $id = isset($args['id']) ? intval($args['id']) : 0; $ndc_info = isset($args['ndc_info']) ? $args['ndc_info'] : ''; $provider_id = isset($args['provider_id']) ? intval($args['provider_id']) : 0; $justify = isset($args['justify']) ? $args['justify'] : ''; $notecodes = isset($args['notecodes']) ? $args['notecodes'] : ''; $fee = isset($args['fee']) ? (0 + $args['fee']) : 0; // Price level should be unset only if adding a new line item. $pricelevel = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel; $del = !empty($args['del']); // If using line item billing and user wishes to default to a selected provider, then do so. if(!empty($GLOBALS['default_fee_sheet_line_item_provider']) && !empty($GLOBALS['support_fee_sheet_line_item_provider'])) { if ($provider_id == 0) { $provider_id = 0 + $this->findProvider(); } } if ($codetype == 'COPAY') { if (!$code_text) $code_text = 'Cash'; if ($fee > 0) $fee = 0 - $fee; } // Get the matching entry from the codes table. $sqlArray = array(); $query = "SELECT id, units, code_text FROM codes WHERE " . "code_type = ? AND code = ?"; array_push($sqlArray, $code_types[$codetype]['id'], $code); if ($modifier) { $query .= " AND modifier = ?"; array_push($sqlArray, $modifier); } else { $query .= " AND (modifier IS NULL OR modifier = '')"; } $result = sqlQuery($query, $sqlArray); $codes_id = $result['id']; if (!$code_text) { $code_text = $result['code_text']; if (empty($units)) $units = max(1, intval($result['units'])); if (!isset($args['fee'])) { // Fees come from the prices table now. $query = "SELECT pr_price FROM prices WHERE " . "pr_id = ? AND pr_selector = '' AND pr_level = ? " . "LIMIT 1"; // echo "\n\n"; // debugging $prrow = sqlQuery($query, array($codes_id, $pricelevel)); $fee = empty($prrow) ? 0 : $prrow['pr_price']; } } $fee = sprintf('%01.2f', $fee); $li['hidden']['code_type'] = $codetype; $li['hidden']['code' ] = $code; $li['hidden']['mod' ] = $modifier; $li['hidden']['billed' ] = $billed; $li['hidden']['id' ] = $id; $li['hidden']['codes_id' ] = $codes_id; // This logic is only used for family planning clinics, and then only when // the option is chosen to use or auto-generate Contraception forms. // It adds contraceptive method and effectiveness to relevant lines. if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy'] && $codetype == 'MA') { $codesrow = sqlQuery("SELECT related_code, cyp_factor FROM codes WHERE " . "code_type = ? AND code = ? LIMIT 1", array($code_types[$codetype]['id'], $code)); $this->checkRelatedForContraception($codesrow['related_code'], $codesrow['cyp_factor']); if ($this->line_contra_code) { $li['hidden']['method' ] = $this->line_contra_code; $li['hidden']['cyp' ] = $this->line_contra_cyp; $li['hidden']['methtype'] = $this->line_contra_methtype; // contraception_code is only concerned with initial consults. if ($this->line_contra_cyp > $this->contraception_cyp && $this->line_contra_methtype == 2) { $this->contraception_cyp = $this->line_contra_cyp; $this->contraception_code = $this->line_contra_code; } } } if($codetype == 'COPAY') { $li['codetype'] = xl($codetype); if ($ndc_info) $li['codetype'] .= " ($ndc_info)"; $ndc_info = ''; } else { $li['codetype'] = $codetype; } $li['code' ] = $codetype == 'COPAY' ? '' : $code; $li['mod' ] = $modifier; $li['fee' ] = $fee; $li['price' ] = $fee / $units; $li['pricelevel'] = $pricelevel; $li['units' ] = $units; $li['provid' ] = $provider_id; $li['justify' ] = $justify; $li['notecodes'] = $notecodes; $li['del' ] = $id && $del; $li['code_text'] = $code_text; $li['auth' ] = $auth; $li['hidden']['price'] = $li['price']; // If NDC info exists or may be required, add stuff for it. if ($codetype == 'HCPCS' && !$billed) { $ndcnum = ''; $ndcuom = ''; $ndcqty = ''; if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndc_info, $tmp)) { $ndcnum = $tmp[1]; $ndcuom = $tmp[2]; $ndcqty = $tmp[3]; } $li['ndcnum' ] = $ndcnum; $li['ndcuom' ] = $ndcuom; $li['ndcqty' ] = $ndcqty; } else if ($ndc_info) { $li['ndc_info' ] = $ndc_info; } // For Family Planning. if ($codetype == 'MA') ++$this->required_code_count; if ($fee != 0) $this->hasCharges = true; $this->serviceitems[] = $li; } // Create an array of data for a particular drug_sales table item that is useful // for building a user interface form row. $args is an array containing: // drug_id // selector // sale_id // rx (boolean) // del (boolean) // units // fee // billed // warehouse_id // pricelevel // public function addProductLineItem($args) { global $code_types; $li = array(); $li['hidden'] = array(); $drug_id = $args['drug_id']; $selector = isset($args['selector']) ? $args['selector'] : ''; $sale_id = isset($args['sale_id']) ? intval($args['sale_id']) : 0; $units = isset($args['units']) ? $args['units'] : 0; $units = max(1, intval($units)); $billed = !empty($args['billed']); $rx = !empty($args['rx']); $del = !empty($args['del']); $fee = isset($args['fee']) ? (0 + $args['fee']) : 0; $pricelevel = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel; $warehouse_id = isset($args['warehouse_id']) ? $args['warehouse_id'] : ''; $drow = sqlQuery("SELECT name, related_code FROM drugs WHERE drug_id = ?", array($drug_id) ); $code_text = $drow['name']; // If no warehouse ID passed, use the logged-in user's default. if ($this->got_warehouses && $warehouse_id === '') $warehouse_id = $this->default_warehouse; // If fee is not provided, get it from the prices table. // It is assumed in this case that units will match what is in the product template. if (!isset($args['fee'])) { $query = "SELECT pr_price FROM prices WHERE " . "pr_id = ? AND pr_selector = ? AND pr_level = ? " . "LIMIT 1"; $prrow = sqlQuery($query, array($drug_id, $selector, $pricelevel)); $fee = empty($prrow) ? 0 : $prrow['pr_price']; } $fee = sprintf('%01.2f', $fee); $li['fee' ] = $fee; $li['price' ] = $fee / $units; $li['pricelevel'] = $pricelevel; $li['units' ] = $units; $li['del' ] = $sale_id && $del; $li['code_text'] = $code_text; $li['warehouse'] = $warehouse_id; $li['rx' ] = $rx; $li['hidden']['drug_id'] = $drug_id; $li['hidden']['selector'] = $selector; $li['hidden']['sale_id'] = $sale_id; $li['hidden']['billed' ] = $billed; $li['hidden']['price' ] = $li['price']; // This logic is only used for family planning clinics, and then only when // the option is chosen to use or auto-generate Contraception forms. // It adds contraceptive method and effectiveness to relevant lines. if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy']) { $this->checkRelatedForContraception($drow['related_code']); if ($this->line_contra_code) { $li['hidden']['method' ] = $this->line_contra_code; $li['hidden']['methtype'] = $this->line_contra_methtype; } } // For Family Planning. ++$this->required_code_count; if ($fee != 0) $this->hasCharges = true; $this->productitems[] = $li; } // Generate rows for items already in the billing table for this encounter. // public function loadServiceItems() { $billresult = getBillingByEncounter($this->pid, $this->encounter, "*"); if ($billresult) { foreach ($billresult as $iter) { if (!$this->ALLOW_COPAYS && $iter["code_type"] == 'COPAY') continue; $justify = trim($iter['justify']); if ($justify) $justify = substr(str_replace(':', ',', $justify), 0, strlen($justify) - 1); $this->addServiceLineItem(array( 'id' => $iter['id'], 'codetype' => $iter['code_type'], 'code' => trim($iter['code']), 'modifier' => trim($iter["modifier"]), 'code_text' => trim($iter['code_text']), 'units' => $iter['units'], 'fee' => $iter['fee'], 'pricelevel' => $iter['pricelevel'], 'billed' => $iter['billed'], 'ndc_info' => $iter['ndc_info'], 'provider_id' => $iter['provider_id'], 'justify' => $justify, 'notecodes' => trim($iter['notecodes']), )); } } // echo " \n"; // debugging } // Generate rows for items already in the drug_sales table for this encounter. // public function loadProductItems() { $query = "SELECT ds.*, di.warehouse_id FROM drug_sales AS ds, drug_inventory AS di WHERE " . "ds.pid = ? AND ds.encounter = ? AND di.inventory_id = ds.inventory_id " . "ORDER BY ds.sale_id"; $sres = sqlStatement($query, array($this->pid, $this->encounter)); while ($srow = sqlFetchArray($sres)) { $this->addProductLineItem(array( 'drug_id' => $srow['drug_id'], 'selector' => $srow['selector'], 'sale_id' => $srow['sale_id'], 'rx' => !empty($srow['prescription_id']), 'units' => $srow['quantity'], 'fee' => $srow['fee'], 'pricelevel' => $srow['pricelevel'], 'billed' => $srow['billed'], 'warehouse_id' => $srow['warehouse_id'], )); } } // Check for insufficient product inventory levels. // Returns an error message if any product items cannot be filled. // You must call this before save(). // public function checkInventory(&$prod) { $alertmsg = ''; $insufficient = 0; $expiredlots = false; if (is_array($prod)) foreach ($prod as $iter) { if (!empty($iter['billed'])) continue; $drug_id = $iter['drug_id']; $sale_id = empty($iter['sale_id']) ? 0 : intval($iter['sale_id']); // present only if already saved $units = empty($iter['units']) ? 1 : intval($iter['units']); $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse']; // Deleting always works. if (!empty($iter['del'])) continue; // If the item is already in the database... if ($sale_id) { $query = "SELECT ds.quantity, ds.inventory_id, di.on_hand, di.warehouse_id " . "FROM drug_sales AS ds " . "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " . "WHERE ds.sale_id = ?"; $dirow = sqlQuery($query, array($sale_id)); // There's no inventory ID when this is a non-dispensible product (i.e. no inventory). if (!empty($dirow['inventory_id'])) { if ($warehouse_id && $warehouse_id != $dirow['warehouse_id']) { // Changing warehouse so check inventory in the new warehouse. // Nothing is updated by this call. if (!sellDrug($drug_id, $units, 0, $this->pid, $this->encounter, 0, $this->visit_date, '', $warehouse_id, true, $expiredlots)) { $insufficient = $drug_id; } } else { if (($dirow['on_hand'] + $dirow['quantity'] - $units) < 0) { $insufficient = $drug_id; } } } } // Otherwise it's a new item... else { // This only checks for sufficient inventory, nothing is updated. if (!sellDrug($drug_id, $units, 0, $this->pid, $this->encounter, 0, $this->visit_date, '', $warehouse_id, true, $expiredlots)) { $insufficient = $drug_id; } } } // end for if ($insufficient) { $drow = sqlQuery("SELECT name FROM drugs WHERE drug_id = ?", array($insufficient)); $alertmsg = xl('Insufficient inventory for product') . ' "' . $drow['name'] . '".'; if ($expiredlots) $alertmsg .= " " . xl('Check expiration dates.'); } return $alertmsg; } // Save posted data to the database. $bill and $prod are the incoming arrays of line items, with // key names corresponding to those generated by addServiceLineItem() and addProductLineItem(). // public function save(&$bill, &$prod, $main_provid=NULL, $main_supid=NULL, $default_warehouse=NULL, $mark_as_closed=false) { global $code_types; if (isset($main_provid) && $main_supid == $main_provid) $main_supid = 0; $copay_update = FALSE; $update_session_id = ''; $ct0 = ''; // takes the code type of the first fee type code type entry from the fee sheet, against which the copay is posted $cod0 = ''; // takes the code of the first fee type code type entry from the fee sheet, against which the copay is posted $mod0 = ''; // takes the modifier of the first fee type code type entry from the fee sheet, against which the copay is posted if (is_array($bill)) foreach ($bill as $iter) { // Skip disabled (billed) line items. if (!empty($iter['billed'])) continue; $id = $iter['id']; $code_type = $iter['code_type']; $code = $iter['code']; $del = !empty($iter['del']); $units = empty($iter['units']) ? 1 : intval($iter['units']); $price = empty($iter['price']) ? 0 : (0 + trim($iter['price'])); $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel']; $modifier = empty($iter['mod']) ? '' : trim($iter['mod']); $justify = empty($iter['justify' ]) ? '' : trim($iter['justify']); $notecodes = empty($iter['notecodes']) ? '' : trim($iter['notecodes']); $provid = empty($iter['provid' ]) ? 0 : intval($iter['provid']); $fee = sprintf('%01.2f', $price * $units); if(!$cod0 && $code_types[$code_type]['fee'] == 1) { $mod0 = $modifier; $cod0 = $code; $ct0 = $code_type; } if ($code_type == 'COPAY') { if ($fee < 0) { $fee = $fee * -1; } if (!$id) { // adding new copay from fee sheet into ar_session and ar_activity tables $session_id = idSqlStatement("INSERT INTO ar_session " . "(payer_id, user_id, pay_total, payment_type, description, patient_id, payment_method, " . "adjustment_code, post_to_date) " . "VALUES ('0',?,?,'patient','COPAY',?,'','patient_payment',now())", array($_SESSION['authId'], $fee, $this->pid)); sqlBeginTrans(); $sequence_no = sqlQuery("SELECT IFNULL(MAX(sequence_no),0) + 1 AS increment FROM ar_activity WHERE " . "pid = ? AND encounter = ?", array($this->pid, $this->encounter)); SqlStatement("INSERT INTO ar_activity (pid, encounter, sequence_no, code_type, code, modifier, " . "payer_type, post_time, post_user, session_id, " . "pay_amount, account_code) VALUES (?,?,?,?,?,?,0,now(),?,?,?,'PCP')", array($this->pid, $this->encounter, $sequence_no['increment'], $ct0, $cod0, $mod0, $_SESSION['authId'], $session_id, $fee)); sqlCommitTrans(); } else { // editing copay saved to ar_session and ar_activity $session_id = $id; $res_amount = sqlQuery("SELECT pay_amount FROM ar_activity WHERE pid=? AND encounter=? AND session_id=?", array($this->pid, $this->encounter, $session_id)); if ($fee != $res_amount['pay_amount']) { sqlStatement("UPDATE ar_session SET user_id=?,pay_total=?,modified_time=now(),post_to_date=now() WHERE session_id=?", array($_SESSION['authId'], $fee, $session_id)); sqlStatement("UPDATE ar_activity SET code_type=?, code=?, modifier=?, post_user=?, post_time=now(),". "pay_amount=?, modified_time=now() WHERE pid=? AND encounter=? AND account_code='PCP' AND session_id=?", array($ct0, $cod0, $mod0, $_SESSION['authId'], $fee, $this->pid, $this->encounter, $session_id)); } } if (!$cod0){ $copay_update = TRUE; $update_session_id = $session_id; } continue; } # Code to create justification for all codes based on first justification if ($GLOBALS['replicate_justification'] == '1') { if ($justify != '') { $autojustify = $justify; } } if (($GLOBALS['replicate_justification'] == '1') && ($justify == '') && check_is_code_type_justify($code_type)) { $justify = $autojustify; } if ($justify) $justify = str_replace(',', ':', $justify) . ':'; $auth = "1"; $ndc_info = ''; if (!empty($iter['ndcnum'])) { $ndc_info = 'N4' . trim($iter['ndcnum']) . ' ' . $iter['ndcuom'] . trim($iter['ndcqty']); } // If the item is already in the database... if ($id) { if ($del) { $this->logFSMessage(xl('Service deleted')); deleteBilling($id); } else { $tmp = sqlQuery("SELECT * FROM billing WHERE id = ? AND (billed = 0 or billed is NULL) AND activity = 1", array($id)); if (!empty($tmp)) { $tmparr = array('code' => $code, 'authorized' => $auth); if (isset($iter['units' ])) $tmparr['units' ] = $units; if (isset($iter['price' ])) $tmparr['fee' ] = $fee; if (isset($iter['pricelevel'])) $tmparr['pricelevel'] = $pricelevel; if (isset($iter['mod' ])) $tmparr['modifier' ] = $modifier; if (isset($iter['provid' ])) $tmparr['provider_id'] = $provid; if (isset($iter['ndcnum' ])) $tmparr['ndc_info' ] = $ndc_info; if (isset($iter['justify' ])) $tmparr['justify' ] = $justify; if (isset($iter['notecodes'])) $tmparr['notecodes' ] = $notecodes; foreach ($tmparr AS $key => $value) { if ($tmp[$key] != $value) { if ('fee' == $key) $this->logFSMessage(xl('Price changed')); if ('units' == $key) $this->logFSMessage(xl('Quantity changed')); if ('provider_id' == $key) $this->logFSMessage(xl('Service provider changed')); sqlStatement("UPDATE billing SET `$key` = ? WHERE id = ?", array($value, $id)); } } } } } // Otherwise it's a new item... else if (!$del) { $this->logFSMessage(xl('Service added')); $code_text = lookup_code_descriptions($code_type.":".$code); addBilling($this->encounter, $code_type, $code, $code_text, $this->pid, $auth, $provid, $modifier, $units, $fee, $ndc_info, $justify, 0, $notecodes, $pricelevel); } } // end for // if modifier is not inserted during loop update the record using the first // non-empty modifier and code if($copay_update == TRUE && $update_session_id != '' && $mod0 != '') { sqlStatement("UPDATE ar_activity SET code_type = ?, code = ?, modifier = ?". " WHERE pid = ? AND encounter = ? AND account_code = 'PCP' AND session_id = ?", array($ct0, $cod0, $mod0, $this->pid, $this->encounter, $update_session_id)); } // Doing similarly to the above but for products. if (is_array($prod)) foreach ($prod as $iter) { // Skip disabled (billed) line items. if (!empty($iter['billed'])) continue; $drug_id = $iter['drug_id']; $selector = empty($iter['selector']) ? '' : $iter['selector']; $sale_id = $iter['sale_id']; // present only if already saved $units = max(1, intval(trim($iter['units']))); $price = empty($iter['price']) ? 0 : (0 + trim($iter['price'])); $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel']; $fee = sprintf('%01.2f', $price * $units); $del = !empty($iter['del']); $rxid = 0; $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse']; $somechange = false; // If the item is already in the database... if ($sale_id) { $tmprow = sqlQuery("SELECT ds.prescription_id, ds.quantity, ds.inventory_id, ds.fee, " . "ds.sale_date, di.warehouse_id " . "FROM drug_sales AS ds " . "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " . "WHERE ds.sale_id = ?", array($sale_id)); $rxid = 0 + $tmprow['prescription_id']; if ($del) { if (!empty($tmprow)) { // Delete this sale and reverse its inventory update. $this->logFSMessage(xl('Product deleted')); sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id)); if (!empty($tmprow['inventory_id'])) { sqlStatement("UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?", array($tmprow['quantity'], $tmprow['inventory_id'])); } } if ($rxid) { sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid)); } } else { // Modify the sale and adjust inventory accordingly. if (!empty($tmprow)) { foreach (array( 'quantity' => $units, 'fee' => $fee, 'pricelevel' => $pricelevel, 'selector' => $selector, 'sale_date' => $this->visit_date, ) AS $key => $value) { if ($tmprow[$key] != $value) { $somechange = true; if ('fee' == $key) $this->logFSMessage(xl('Price changed')); if ('pricelevel' == $key) $this->logFSMessage(xl('Price level changed')); if ('selector' == $key) $this->logFSMessage(xl('Template selector changed')); if ('quantity' == $key) $this->logFSMessage(xl('Quantity changed')); sqlStatement("UPDATE drug_sales SET `$key` = ? WHERE sale_id = ?", array($value, $sale_id)); if ($key == 'quantity' && $tmprow['inventory_id']) { sqlStatement("UPDATE drug_inventory SET on_hand = on_hand - ? WHERE inventory_id = ?", array($units - $tmprow['quantity'], $tmprow['inventory_id'])); } } } if ($tmprow['inventory_id'] && $warehouse_id && $warehouse_id != $tmprow['warehouse_id']) { // Changing warehouse. Requires deleting and re-adding the sale. // Not setting $somechange because this alone does not affect a prescription. $this->logFSMessage(xl('Warehouse changed')); sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id)); sqlStatement("UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?", array($units, $tmprow['inventory_id'])); $tmpnull = null; $sale_id = sellDrug($drug_id, $units, $fee, $this->pid, $this->encounter, (empty($iter['rx']) ? 0 : $rxid), $this->visit_date, '', $warehouse_id, false, $tmpnull, $pricelevel, $selector); } } // Delete Rx if $rxid and flag not set. if ($GLOBALS['gbl_auto_create_rx'] && $rxid && empty($iter['rx'])) { sqlStatement("UPDATE drug_sales SET prescription_id = 0 WHERE sale_id = ?", array($sale_id)); sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid)); } } } // Otherwise it's a new item... else if (! $del) { $somechange = true; $this->logFSMessage(xl('Product added')); $tmpnull = null; $sale_id = sellDrug($drug_id, $units, $fee, $this->pid, $this->encounter, 0, $this->visit_date, '', $warehouse_id, false, $tmpnull, $pricelevel, $selector); if (!$sale_id) die(xlt("Insufficient inventory for product ID") . " \"" . text($drug_id) . "\"."); } // If a prescription applies, create or update it. if (!empty($iter['rx']) && !$del && ($somechange || empty($rxid))) { // If an active rx already exists for this drug and date we will // replace it, otherwise we'll make a new one. if (empty($rxid)) $rxid = ''; // Get default drug attributes; prefer the template with the matching selector. $drow = sqlQuery("SELECT dt.*, " . "d.name, d.form, d.size, d.unit, d.route, d.substitute " . "FROM drugs AS d, drug_templates AS dt WHERE " . "d.drug_id = ? AND dt.drug_id = d.drug_id " . "ORDER BY (dt.selector = ?) DESC, dt.quantity, dt.dosage, dt.selector LIMIT 1", array($drug_id, $selector)); if (!empty($drow)) { $rxobj = new Prescription($rxid); $rxobj->set_patient_id($this->pid); $rxobj->set_provider_id(isset($main_provid) ? $main_provid : $this->provider_id); $rxobj->set_drug_id($drug_id); $rxobj->set_quantity($units); $rxobj->set_per_refill($units); $rxobj->set_start_date_y(substr($this->visit_date,0,4)); $rxobj->set_start_date_m(substr($this->visit_date,5,2)); $rxobj->set_start_date_d(substr($this->visit_date,8,2)); $rxobj->set_date_added($this->visit_date); // Remaining attributes are the drug and template defaults. $rxobj->set_drug($drow['name']); $rxobj->set_unit($drow['unit']); $rxobj->set_dosage($drow['dosage']); $rxobj->set_form($drow['form']); $rxobj->set_refills($drow['refills']); $rxobj->set_size($drow['size']); $rxobj->set_route($drow['route']); $rxobj->set_interval($drow['period']); $rxobj->set_substitute($drow['substitute']); // $rxobj->persist(); // Set drug_sales.prescription_id to $rxobj->get_id(). $oldrxid = $rxid; $rxid = 0 + $rxobj->get_id(); if ($rxid != $oldrxid) { sqlStatement("UPDATE drug_sales SET prescription_id = ? WHERE sale_id = ?", array($rxid, $sale_id)); } } } } // end for // Set default and/or supervising provider for the encounter. if (isset($main_provid) && $main_provid != $this->provider_id) { $this->logFSMessage(xl('Default provider changed')); sqlStatement("UPDATE form_encounter SET provider_id = ? WHERE pid = ? AND encounter = ?", array($main_provid, $this->pid, $this->encounter)); $this->provider_id = $main_provid; } if (isset($main_supid) && $main_supid != $this->supervisor_id) { sqlStatement("UPDATE form_encounter SET supervisor_id = ? WHERE pid = ? AND encounter = ?", array($main_supid, $this->pid, $this->encounter)); $this->supervisor_id = $main_supid; } // Save-and-Close is currently specific to Family Planning but might be more // generally useful. It provides the ability to mark an encounter as billed // directly from the Fee Sheet, if there are no charges. if ($mark_as_closed) { $tmp1 = sqlQuery("SELECT SUM(ABS(fee)) AS sum FROM drug_sales WHERE " . "pid = ? AND encounter = ? AND billed = 0", array($this->pid, $this->encounter)); $tmp2 = sqlQuery("SELECT SUM(ABS(fee)) AS sum FROM billing WHERE " . "pid = ? AND encounter = ? AND billed = 0 AND activity = 1", array($this->pid, $this->encounter)); if ($tmp1['sum'] + $tmp2['sum'] == 0) { sqlStatement("update drug_sales SET billed = 1 WHERE " . "pid = ? AND encounter = ? AND billed = 0", array($this->pid, $this->encounter)); sqlStatement("UPDATE billing SET billed = 1, bill_date = NOW() WHERE " . "pid = ? AND encounter = ? AND billed = 0 AND activity = 1", array($this->pid, $this->encounter)); } else { // Would be good to display an error message here... they clicked // Save and Close but the close could not be done. However the // framework does not provide an easy way to do that. } } } // Call this after save() for Family Planning implementations. // It checks the contraception form, or makes a new one if $newmauser is set. // Returns 0 unless user intervention is required to fix a missing or incorrect form, // and in that case the return value is an existing form ID, or -1 if none. // Returns FALSE if user intervention is required to fix a missing or incorrect form. // public function doContraceptionForm($ippfconmeth=NULL, $newmauser=NULL, $main_provid=0) { if (!empty($ippfconmeth)) { $csrow = sqlQuery("SELECT f.form_id, ld.field_value FROM forms AS f " . "LEFT JOIN lbf_data AS ld ON ld.form_id = f.form_id AND ld.field_id = 'newmethod' " . "WHERE " . "f.pid = ? AND f.encounter = ? AND " . "f.formdir = 'LBFccicon' AND f.deleted = 0 " . "ORDER BY f.form_id DESC LIMIT 1", array($this->pid, $this->encounter)); if (isset($newmauser)) { // pastmodern is 0 iff new to modern contraception $pastmodern = $newmauser == '2' ? 0 : 1; if ($newmauser == '2') $newmauser = '1'; // Add contraception form but only if it does not already exist // (if it does, must be 2 users working on the visit concurrently). if (empty($csrow)) { $newid = $this->insert_lbf_item(0, 'newmauser', $newmauser); $this->insert_lbf_item($newid, 'newmethod', "IPPFCM:$ippfconmeth"); $this->insert_lbf_item($newid, 'pastmodern', $pastmodern); // Do we care about a service-specific provider here? $this->insert_lbf_item($newid, 'provider', $main_provid); addForm($this->encounter, 'Contraception', $newid, 'LBFccicon', $this->pid, $GLOBALS['userauthorized']); } } else if (empty($csrow) || $csrow['field_value'] != "IPPFCM:$ippfconmeth") { // Contraceptive method does not match what is in an existing Contraception // form for this visit, or there is no such form. User intervention is needed. return empty($csrow) ? -1 : intval($csrow['form_id']); } } return 0; } // Get price level from patient demographics. // public function getPriceLevel() { return $this->patient_pricelevel; } // Update price level in patient demographics if it's changed. // public function updatePriceLevel($pricelevel) { if (!empty($pricelevel)) { if ($this->patient_pricelevel != $pricelevel) { $this->logFSMessage(xl('Price level changed')); sqlStatement("UPDATE patient_data SET pricelevel = ? WHERE pid = ?", array($pricelevel, $this->pid)); $this->patient_pricelevel = $pricelevel; } } } // Create JSON string representing code type, code and selector. // This can be a checkbox value for parsing when the checkbox is clicked. // As a side effect note if the code is already selected in the Fee Sheet. // public function genCodeSelectorValue($codes) { global $code_types; list($codetype, $code, $selector) = explode(':', $codes); if ($codetype == 'PROD') { $crow = sqlQuery("SELECT sale_id " . "FROM drug_sales WHERE pid = ? AND encounter = ? AND drug_id = ? " . "LIMIT 1", array($this->pid, $this->encounter, $code)); $this->code_is_in_fee_sheet = !empty($crow['sale_id']); $cbarray = array($codetype, $code, $selector); } else { $crow = sqlQuery("SELECT c.id AS code_id, b.id " . "FROM codes AS c " . "LEFT JOIN billing AS b ON b.pid = ? AND b.encounter = ? AND b.code_type = ? AND b.code = c.code AND b.activity = 1 " . "WHERE c.code_type = ? AND c.code = ? LIMIT 1", array($this->pid, $this->encounter, $codetype, $code_types[$codetype]['id'], $code)); $this->code_is_in_fee_sheet = !empty($crow['id']); $cbarray = array($codetype, $code); } $cbval = json_encode($cbarray); return $cbval; } }