From 7317996c3449bb71bc3c9dc77e365d02d9eae9eb Mon Sep 17 00:00:00 2001 From: Rod Roark Date: Thu, 4 Aug 2016 10:40:06 -0700 Subject: [PATCH] Bring in Fee Sheet improvements from recent IPPF work. This refactors much of the Fee Sheet into a new pair of classes: FeeSheet to contain the "business logic" and FeeSheetHtml which extends it and adds functions to facilitate generation of HTML. Other improvements include: o Line item price levels. A new column at the line item level that appears if you have more than one price level to choose from. o Button to Add More Items, which is a step towards supporting multiple checkouts which will be in a following commit. o Button to re-open the encounter. o Improved logging. o Some fixes related to translation. o New feature with checkboxes instead of drop-lists for adding new items. o Corresponding updates to drugs.inc.php and billing.inc that will be used later. o Fixed failure of the review feature due to knockout library being moved. --- interface/drugs/drugs.inc.php | 125 +- .../fee_sheet/code_choice/css/code_choices.css | 64 + .../code_choice/initialize_code_choice.php | 27 + .../forms/fee_sheet/code_choice/js/view_model.js | 123 + .../code_choice/templates/code_choices.php | 50 + interface/forms/fee_sheet/new.php | 2691 ++++++++++---------- .../forms/fee_sheet/review/initialize_review.js | 8 +- .../forms/fee_sheet/review/initialize_review.php | 5 +- .../forms/fee_sheet/review/js/fee_sheet_core.js | 20 +- library/FeeSheet.class.php | 1013 ++++++++ library/FeeSheetHtml.class.php | 365 +++ library/appointment_status.inc.php | 38 + library/billing.inc | 189 +- sql/4_2_2-to-4_3_1_upgrade.sql | 79 +- sql/database.sql | 33 +- version.php | 2 +- 16 files changed, 3374 insertions(+), 1458 deletions(-) create mode 100644 interface/forms/fee_sheet/code_choice/css/code_choices.css create mode 100644 interface/forms/fee_sheet/code_choice/initialize_code_choice.php create mode 100644 interface/forms/fee_sheet/code_choice/js/view_model.js create mode 100644 interface/forms/fee_sheet/code_choice/templates/code_choices.php rewrite interface/forms/fee_sheet/new.php (65%) create mode 100644 library/FeeSheet.class.php create mode 100644 library/FeeSheetHtml.class.php create mode 100644 library/appointment_status.inc.php diff --git a/interface/drugs/drugs.inc.php b/interface/drugs/drugs.inc.php index 225f08207..9dc33dbf1 100644 --- a/interface/drugs/drugs.inc.php +++ b/interface/drugs/drugs.inc.php @@ -1,5 +1,5 @@ +// Copyright (C) 2006-2015 Rod Roark // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License @@ -12,6 +12,11 @@ // These lists are based on the constants found in the // openemr/library/classes/Prescription.class.php file. +// Decision was made in June 2013 that a sale line item in the Fee Sheet may +// come only from the specified warehouse. Set this to false if the decision +// is reversed. +$GLOBALS['SELL_FROM_ONE_WAREHOUSE'] = true; + $substitute_array = array('', xl('Allowed'), xl('Not Allowed')); function send_drug_email($subject, $body) { @@ -35,7 +40,8 @@ function send_drug_email($subject, $body) { } function sellDrug($drug_id, $quantity, $fee, $patient_id=0, $encounter_id=0, - $prescription_id=0, $sale_date='', $user='', $default_warehouse='') { + $prescription_id=0, $sale_date='', $user='', $default_warehouse='', + $testonly=false, &$expiredlots=null, $pricelevel='', $selector='') { if (empty($patient_id)) $patient_id = $GLOBALS['pid']; if (empty($sale_date)) $sale_date = date('Y-m-d'); @@ -50,10 +56,22 @@ function sellDrug($drug_id, $quantity, $fee, $patient_id=0, $encounter_id=0, } // Get relevant options for this product. - $rowdrug = sqlQuery("SELECT allow_combining, reorder_point, name " . + $rowdrug = sqlQuery("SELECT allow_combining, reorder_point, name, dispensable " . "FROM drugs WHERE drug_id = ?", array($drug_id)); $allow_combining = $rowdrug['allow_combining']; - + $dispensable = $rowdrug['dispensable']; + + if (!$dispensable) { + // Non-dispensable is a much simpler case and does not touch inventory. + if ($testonly) return true; + $sale_id = sqlInsert("INSERT INTO drug_sales ( " . + "drug_id, inventory_id, prescription_id, pid, encounter, user, " . + "sale_date, quantity, fee ) VALUES ( " . + "?, 0, ?, ?, ?, ?, ?, ?, ?)", + array($drug_id, $prescription_id, $patient_id, $encounter_id, $user, $sale_date, $quantity, $fee)); + return $sale_id; + } + // Combining is never allowed for prescriptions and will not work with // dispense_drug.php. if ($prescription_id) $allow_combining = 0; @@ -63,6 +81,7 @@ function sellDrug($drug_id, $quantity, $fee, $patient_id=0, $encounter_id=0, $qty_left = $quantity; $bad_lot_list = ''; $total_on_hand = 0; + $gotexpired = false; // If the user has a default warehouse, sort those lots first. $orderby = ($default_warehouse === '') ? @@ -70,21 +89,28 @@ function sellDrug($drug_id, $quantity, $fee, $patient_id=0, $encounter_id=0, $orderby .= "lo.seq, di.expiration, di.lot_number, di.inventory_id"; // Retrieve lots in order of expiration date within warehouse preference. - $res = sqlStatement("SELECT di.*, lo.option_id, lo.seq " . + $query = "SELECT di.*, lo.option_id, lo.seq " . "FROM drug_inventory AS di " . "LEFT JOIN list_options AS lo ON lo.list_id = 'warehouse' AND " . "lo.option_id = di.warehouse_id " . "WHERE " . - "di.drug_id = ? AND di.destroy_date IS NULL " . - "ORDER BY $orderby", array($drug_id)); + "di.drug_id = ? AND di.destroy_date IS NULL "; + $sqlarr = array($drug_id); + if ($GLOBALS['SELL_FROM_ONE_WAREHOUSE'] && $default_warehouse) { + $query .= "AND di.warehouse_id = ? "; + $sqlarr[] = $default_warehouse; + } + $query .= "ORDER BY $orderby"; + $res = sqlStatement($query, $sqlarr); // First pass. Pick out lots to be used in filling this order, figure out // if there is enough quantity on hand and check for lots to be destroyed. while ($row = sqlFetchArray($res)) { - // Warehouses with seq > 99 are not available. - $seq = empty($row['seq']) ? 0 : $row['seq'] + 0; - if ($seq > 99) continue; - + if ($row['warehouse_id'] != $default_warehouse) { + // Warehouses with seq > 99 are not available. + $seq = empty($row['seq']) ? 0 : $row['seq'] + 0; + if ($seq > 99) continue; + } $on_hand = $row['on_hand']; $expired = (!empty($row['expiration']) && $row['expiration'] <= $sale_date); if ($expired || $on_hand < $quantity) { @@ -93,7 +119,10 @@ function sellDrug($drug_id, $quantity, $fee, $patient_id=0, $encounter_id=0, if ($bad_lot_list) $bad_lot_list .= ', '; $bad_lot_list .= $tmp; } - if ($expired) continue; + if ($expired) { + $gotexpired = true; + continue; + } /***************************************************************** // Note the first row in case total quantity is insufficient and we are @@ -109,6 +138,14 @@ function sellDrug($drug_id, $quantity, $fee, $patient_id=0, $encounter_id=0, } } + if ($expiredlots !== null) $expiredlots = $gotexpired; + + if ($testonly) { + // Just testing inventory, so return true if OK, false if insufficient. + // $qty_left, if positive, is the amount requested that could not be allocated. + return $qty_left <= 0; + } + if ($bad_lot_list) { send_drug_email("Possible lot destruction needed", "The following lot(s) are expired or were too small to fill the " . @@ -165,19 +202,75 @@ function sellDrug($drug_id, $quantity, $fee, $patient_id=0, $encounter_id=0, "on_hand = on_hand - ? " . "WHERE inventory_id = ?", array($thisqty,$inventory_id)); $sale_id = sqlInsert("INSERT INTO drug_sales ( " . - "drug_id, inventory_id, prescription_id, pid, encounter, user, sale_date, quantity, fee ) " . - "VALUES (?,?,?,?,?,?,?,?,?)", array($drug_id,$inventory_id,$prescription_id,$patient_id,$encounter_id,$user,$sale_date,$thisqty,$thisfee)); + "drug_id, inventory_id, prescription_id, pid, encounter, user, sale_date, quantity, fee, pricelevel, selector ) " . + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + array($drug_id, $inventory_id, $prescription_id, $patient_id, $encounter_id, $user, + $sale_date, $thisqty, $thisfee, $pricelevel, $selector)); + + // If this sale exhausted the lot then auto-destroy it if that is wanted. + if ($row['on_hand'] == $thisqty && !empty($GLOBALS['gbl_auto_destroy_lots'])) { + sqlStatement("UPDATE drug_inventory SET " . + "destroy_date = ?, destroy_method = ?, destroy_witness = ?, destroy_notes = ? " . + "WHERE drug_id = ? AND inventory_id = ?", + array($sale_date, xl('Automatic from sale'), $user, "sale_id = $sale_id", + $drug_id, $inventory_id)); + } } + /******************************************************************* // If appropriate, generate email to notify that re-order is due. if (($total_on_hand - $quantity) <= $rowdrug['reorder_point']) { send_drug_email("Product re-order required", "Product '" . $rowdrug['name'] . "' has reached its reorder point.\n"); } - + // TBD: If the above is un-commented, fix it to handle the case of + // $GLOBALS['gbl_min_max_months'] being true. + *******************************************************************/ + // If combining is allowed then $sale_id will be just the last inserted ID, // and it serves only to indicate that everything worked. Otherwise there // can be only one inserted row and this is its ID. return $sale_id; } -?> + +// Determine if facility and warehouse restrictions are applicable for this user. +function isUserRestricted($userid=0) { + if (!$userid) $userid = $_SESSION['authId']; + $countrow = sqlQuery("SELECT count(*) AS count FROM users_facility WHERE " . + "tablename = 'users' AND table_id = ?", array($userid)); + return !empty($countrow['count']); +} + +// Check if the user has access to the given facility. +// Do not call this if user is not restricted! +function isFacilityAllowed($facid, $userid=0) { + if (!$userid) $userid = $_SESSION['authId']; + $countrow = sqlQuery("SELECT count(*) AS count FROM users_facility WHERE " . + "tablename = 'users' AND table_id = ? AND facility_id = ?", + array($userid, $facid)); + if (empty($countrow['count'])) { + $countrow = sqlQuery("SELECT count(*) AS count FROM users WHERE " . + "id = ? AND facility_id = ?", + array($userid, $facid)); + return !empty($countrow['count']); + } + return true; +} + +// Check if the user has access to the given warehouse within the given facility. +// Do not call this if user is not restricted! +function isWarehouseAllowed($facid, $whid, $userid=0) { + if (!$userid) $userid = $_SESSION['authId']; + $countrow = sqlQuery("SELECT count(*) AS count FROM users_facility WHERE " . + "tablename = 'users' AND table_id = ? AND facility_id = ? AND " . + "(warehouse_id = ? OR warehouse_id = '')", + array($userid, $facid, $whid)); + if (empty($countrow['count'])) { + $countrow = sqlQuery("SELECT count(*) AS count FROM users WHERE " . + "id = ? AND default_warehouse = ?", + array($userid, $whid)); + return !empty($countrow['count']); + } + return true; +} +?> \ No newline at end of file diff --git a/interface/forms/fee_sheet/code_choice/css/code_choices.css b/interface/forms/fee_sheet/code_choice/css/code_choices.css new file mode 100644 index 000000000..6c4616a99 --- /dev/null +++ b/interface/forms/fee_sheet/code_choice/css/code_choices.css @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2014 Kevin Yeh and OEMR + * + * LICENSE: This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see ;. + * + * @package OpenEMR + * @author Kevin Yeh + * @link http://www.open-emr.org + */ + +div.code-choices +{ + font-size: 10pt; + position:relative; + z-index: 0; +} +div.category-display +{ + float:left; + width: 33%; + cursor: pointer; +} + +div.category-display > button +{ + width: 25em; + font-weight: bold; + text-align: left; +} + +div.category-display > button:hover +{ + color: blue; +} +.active-category +{ + position: absolute; + z-index: 10; + background-color: white; + width: 99%; + border: 1px black solid; + padding: 4pt; +} + +div.code-choice +{ + float:left; + width: 33%; + cursor: pointer; + text-align: left; +} + +div.code-choice:hover{ + background-color: yellow; +} \ No newline at end of file diff --git a/interface/forms/fee_sheet/code_choice/initialize_code_choice.php b/interface/forms/fee_sheet/code_choice/initialize_code_choice.php new file mode 100644 index 000000000..d52dbe5d0 --- /dev/null +++ b/interface/forms/fee_sheet/code_choice/initialize_code_choice.php @@ -0,0 +1,27 @@ + and OEMR + * + * LICENSE: This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see ;. + * + * @package OpenEMR + * @author Kevin Yeh + * @link http://www.open-emr.org + */ + +require_once("templates/code_choices.php"); +?> + + + + + diff --git a/interface/forms/fee_sheet/code_choice/js/view_model.js b/interface/forms/fee_sheet/code_choice/js/view_model.js new file mode 100644 index 000000000..b52d4586d --- /dev/null +++ b/interface/forms/fee_sheet/code_choice/js/view_model.js @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2014 Kevin Yeh and OEMR + * + * LICENSE: This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see ;. + * + * @package OpenEMR + * @author Kevin Yeh + * @link http://www.open-emr.org + */ + +function toggle_code(data,event) +{ + data.selected(!data.selected()); +} + +function codes_ok(data,event) +{ + codes_choices_vm.show_choices(false); + var f = document.forms[0]; + var choices=codes_choices_vm.active_category().codes(); + for (var i = 0; i < choices.length; ++i) { + if (choices[i].selected()) { + if (f.newcodes.value) f.newcodes.value += '~'; + f.newcodes.value += choices[i].value(); + choices[i].selected(false); + } + } + if (f.newcodes.value) { + // top.restoreSession(); + // f.submit(); + // This supports the option to immediately save: + codeselect(null); + } + return false; +} + +function codes_cancel(data,event) +{ + codes_choices_vm.show_choices(false); + return false; +} + +//Events +function set_active_category(data,event) +{ + codes_choices_vm.active_category(data); + codes_choices_vm.show_choices(true); +} + +//End Events +var codes_choices_vm={ + categories : ko.observableArray(), + active_category:ko.observable(false), + show_choices: ko.observable(false) +}; + +function code_category(title) +{ + var self=this; + this.title=ko.observable(title); + this.codes=ko.observableArray(); + return this; +} + +function code_choice(description,value) +{ + var self=this; + this.description=ko.observable(description); + this.value=ko.observable(value); + this.selected=ko.observable(false); + return this; +} + +function populate_vm_categories(idx,elem) +{ + var jqElem=$(elem); + jqElem.hide(); + jqElem.parent().parent().hide(); // select is child of a td and a tr. + var title=jqElem.find("option[value='']").text(); + + var category=new code_category(title); + codes_choices_vm.categories().push(category); + + var choices=jqElem.find("option:not([value=''])"); + choices.each(function(idx,elem) + { + var jqChoice=$(elem); + var description=jqChoice.text(); + var value=jqChoice.attr("value"); + var choice=new code_choice(description,value); + category.codes().push(choice); + } + ); +} + +function analyze_codes() +{ + var code_table=$("table[width='95%']"); + var categories=code_table.find("td[width='50%'] > select"); + categories.each(populate_vm_categories); + add_code_template(code_table); +} + +function add_code_template(elem) +{ + var template=$("
"); + template.attr("data-bind","template: {name: 'code-choice-options'}"); + template.addClass("code-choices"); + elem.before(template); + ko.applyBindings(codes_choices_vm,template.get(0)); + codes_choices_vm.active_category(codes_choices_vm.categories()[1]); +} + +analyze_codes(); diff --git a/interface/forms/fee_sheet/code_choice/templates/code_choices.php b/interface/forms/fee_sheet/code_choice/templates/code_choices.php new file mode 100644 index 000000000..a41ccdb92 --- /dev/null +++ b/interface/forms/fee_sheet/code_choice/templates/code_choices.php @@ -0,0 +1,50 @@ + and OEMR + * + * LICENSE: This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see ;. + * + * @package OpenEMR + * @author Kevin Yeh + * @link http://www.open-emr.org + */ +?> + + + diff --git a/interface/forms/fee_sheet/new.php b/interface/forms/fee_sheet/new.php dissimilarity index 65% index a7dbf5975..0ed9306d5 100644 --- a/interface/forms/fee_sheet/new.php +++ b/interface/forms/fee_sheet/new.php @@ -1,1407 +1,1284 @@ - -* -* LICENSE: This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public License -* as published by the Free Software Foundation; either version 3 -* of the License, or (at your option) any later version. -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* You should have received a copy of the GNU General Public License -* along with this program. If not, see ;. -* -* @package OpenEMR -* @author Rod Roark -* @author Terry Hill -* @link http://www.open-emr.org -*/ - -$fake_register_globals=false; -$sanitize_all_escapes=true; - -require_once("../../globals.php"); -require_once("$srcdir/acl.inc"); -require_once("$srcdir/api.inc"); -require_once("codes.php"); -require_once("../../../custom/code_types.inc.php"); -require_once("../../drugs/drugs.inc.php"); -require_once("$srcdir/formatting.inc.php"); -require_once("$srcdir/options.inc.php"); -require_once("$srcdir/formdata.inc.php"); -require_once("$srcdir/log.inc"); - -// For logging checksums set this to true. -define('CHECKSUM_LOGGING', true); - -// Some table cells will not be displayed unless insurance billing is used. -$usbillstyle = $GLOBALS['ippf_specific'] ? " style='display:none'" : ""; - -// This may be an error message or warning that pops up when the form is loaded. -$alertmsg = ''; - -function alphaCodeType($id) { - global $code_types; - foreach ($code_types as $key => $value) { - if ($value['id'] == $id) return $key; - } - return ''; -} - -// Helper function for creating drop-lists. -function endFSCategory() { - global $i, $last_category, $FEE_SHEET_COLUMNS; - if (! $last_category) return; - echo " \n"; - echo " \n"; - if ($i >= $FEE_SHEET_COLUMNS) { - echo " \n"; - $i = 0; - } -} - -// Generate JavaScript to build the array of diagnoses. -function genDiagJS($code_type, $code) { - global $code_types; - if ($code_types[$code_type]['diag']) { - echo "diags.push('" . attr($code_type) . "|" . attr($code) . "');\n"; - } -} - -// For IPPF only. Returns 0 = none, 1 = nonsurgical, 2 = surgical. -// -function contraceptionClass($code_type, $code) { - global $code_types; - if (!$GLOBALS['ippf_specific']) return 0; - $contra = 0; - // Get the related service codes. - $codesrow = sqlQuery("SELECT related_code FROM codes WHERE " . - "code_type = ? " . - " AND code = ? LIMIT 1", array($code_types[$code_type]['id'],$code) ); - if (!empty($codesrow['related_code']) && $code_type == 'MA') { - $relcodes = explode(';', $codesrow['related_code']); - foreach ($relcodes as $relstring) { - if ($relstring === '') continue; - list($reltype, $relcode) = explode(':', $relstring); - if ($reltype !== 'IPPF') continue; - if (preg_match('/^11....110/' , $relcode)) $contra |= 1; - else if (preg_match('/^11....999/' , $relcode)) $contra |= 1; - else if (preg_match('/^112152010/' , $relcode)) $contra |= 1; - else if (preg_match('/^11317[1-2]111/', $relcode)) $contra |= 1; - else if (preg_match('/^12118[1-2].13/', $relcode)) $contra |= 2; - else if (preg_match('/^12118[1-2]999/', $relcode)) $contra |= 2; - } - } - return $contra; -} -# gets the provider from the encounter file , or from the logged on user or from the patient file -function findProvider() { - global $encounter, $pid; - $find_provider = sqlQuery("SELECT provider_id FROM form_encounter " . - "WHERE pid = ? AND encounter = ? " . - "ORDER BY id DESC LIMIT 1", array($pid,$encounter) ); - $providerid = $find_provider['provider_id']; - if($providerid == 0) { - $get_authorized = $_SESSION['userauthorized']; - if($get_authorized ==1) { - $providerid = $_SESSION[authUserID]; - } - } - if($providerid == 0) { - $find_provider = sqlQuery("SELECT providerID FROM patient_data " . - "WHERE pid = ? ", array($pid) ); - $providerid = $find_provider['providerID']; - } - return $providerid; -} - -// This writes a billing line item to the output page. -// -function echoLine($lino, $codetype, $code, $modifier, $ndc_info='', - $auth = TRUE, $del = FALSE, $units = NULL, $fee = NULL, $id = NULL, - $billed = FALSE, $code_text = NULL, $justify = NULL, $provider_id = 0, $notecodes='') -{ - global $code_types, $ndc_applies, $ndc_uom_choices, $justinit, $pid; - global $contraception, $usbillstyle, $hasCharges; - - // If using line item billing and user wishes to default to a selected provider, then do so. - if($GLOBALS['default_fee_sheet_line_item_provider'] == 1 && $GLOBALS['support_fee_sheet_line_item_provider'] ==1 ) { - if ($provider_id == 0) { - $provider_id = 0 + findProvider(); - } - } - - if ($codetype == 'COPAY') { - if (!$code_text) $code_text = 'Cash'; - if ($fee > 0) $fee = 0 - $fee; - } - if (! $code_text) { - $sqlArray = array(); - $query = "select id, units, code_text from codes where code_type = ? " . - " and " . - "code = ? and "; - array_push($sqlArray,$code_types[$codetype]['id'],$code); - if ($modifier) { - $query .= "modifier = ?"; - array_push($sqlArray,$modifier); - } else { - $query .= "(modifier is null or modifier = '')"; - } - $result = sqlQuery($query, $sqlArray); - $code_text = $result['code_text']; - if (empty($units)) $units = max(1, intval($result['units'])); - if (!isset($fee)) { - // Fees come from the prices table now. - $query = "SELECT prices.pr_price " . - "FROM patient_data, prices WHERE " . - "patient_data.pid = ? AND " . - "prices.pr_id = ? AND " . - "prices.pr_selector = '' AND " . - "prices.pr_level = patient_data.pricelevel " . - "LIMIT 1"; - echo "\n\n"; // debugging - $prrow = sqlQuery($query, array($pid,$result['id']) ); - $fee = empty($prrow) ? 0 : $prrow['pr_price']; - } - } - $fee = sprintf('%01.2f', $fee); - if (empty($units)) $units = 1; - $units = max(1, intval($units)); - // We put unit price on the screen, not the total line item fee. - $price = $fee / $units; - $strike1 = ($id && $del) ? "" : ""; - $strike2 = ($id && $del) ? "" : ""; - echo " \n"; - echo " $strike1" . - ($codetype == 'COPAY' ? xl($codetype) : $codetype) . $strike2; - //if the line to ouput is copay, show the date here passed as $ndc_info, - //since this variable is not applicable in the case of copay. - if($codetype == 'COPAY'){ - echo "(".htmlspecialchars($ndc_info).")"; - $ndc_info = ''; - } - if ($id) { - echo ""; - } - echo ""; - echo ""; - echo ""; - echo "\n"; - if ($codetype != 'COPAY') { - echo " $strike1" . text($code) . "$strike2\n"; - } else { - echo "  \n"; - } - if ($billed) { - if (modifiers_are_used(true)) { - echo " $strike1" . text($modifier) . "$strike2" . - "\n"; - } - if (fees_are_used()) { - echo " " . text(oeFormatMoney($price)) . "\n"; - if ($codetype != 'COPAY') { - echo " " . text($units) . "\n"; - } else { - echo "  \n"; - } - } - if (justifiers_are_used()) { - echo " " . text($justify) . "\n"; - } - - // Show provider for this line (if using line item billing). - if($GLOBALS['support_fee_sheet_line_item_provider'] ==1) { - echo " "; - } - else - { - echo " "; - } - genProviderSelect('', '-- '.xl("Default").' --', $provider_id, true); - echo "\n"; - - if ($code_types[$codetype]['claim'] && !$code_types[$codetype]['diag']) { - echo " " . - htmlspecialchars($notecodes, ENT_NOQUOTES) . "\n"; - } - else { - echo " \n"; - } - echo " \n"; - echo " \n"; - } - else { // not billed - if (modifiers_are_used(true)) { - if ($codetype != 'COPAY' && ($code_types[$codetype]['mod'] || $modifier)) { - echo " \n"; - } else { - echo "  \n"; - } - } - if (fees_are_used()) { - if ($codetype == 'COPAY' || $code_types[$codetype]['fee'] || $fee != 0) { - echo " " . - "\n"; - echo " "; - if ($codetype != 'COPAY') { - echo ""; - } else { - echo ""; - } - echo "\n"; - } else { - echo "  \n"; - echo "  \n"; - } - } - if (justifiers_are_used()) { - if ($code_types[$codetype]['just'] || $justify) { - echo " "; - echo ""; - echo "\n"; - $justinit .= "setJustify(f['bill[".attr($lino)."][justify]']);\n"; - } else { - echo "  \n"; - } - } - - // Show provider for this line (if using line item billing) - if($GLOBALS['support_fee_sheet_line_item_provider'] ==1) { - echo " "; - } - else - { - echo " "; - } - genProviderSelect("bill[$lino][provid]", '-- '.xl("Default").' --', $provider_id); - echo "\n"; - - if ($code_types[$codetype]['claim'] && !$code_types[$codetype]['diag']) { - echo " \n"; - } - else { - echo " \n"; - } - echo " \n"; - echo " \n"; - } - - echo " $strike1" . text($code_text) . "$strike2\n"; - echo " \n"; - - // If NDC info exists or may be required, add a line for it. - if ($codetype == 'HCPCS' && $ndc_applies && !$billed) { - $ndcnum = ''; $ndcuom = ''; $ndcqty = ''; - if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndc_info, $tmp)) { - $ndcnum = $tmp[1]; $ndcuom = $tmp[2]; $ndcqty = $tmp[3]; - } - echo " \n"; - echo "  \n"; - echo "  NDC: "; - echo ""; - echo "  Qty: "; - echo ""; - echo " "; - echo ""; - echo "\n"; - echo " \n"; - } - else if ($ndc_info) { - echo " \n"; - echo "  \n"; - echo "  " . xlt("NDC Data") . ": " . text($ndc_info) . "\n"; - echo " \n"; - } - - // For IPPF. Track contraceptive services. - if (!$del) $contraception |= contraceptionClass($codetype, $code); - - if ($fee != 0) $hasCharges = true; -} - -// This writes a product (drug_sales) line item to the output page. -// -function echoProdLine($lino, $drug_id, $del = FALSE, $units = NULL, - $fee = NULL, $sale_id = 0, $billed = FALSE) -{ - global $code_types, $ndc_applies, $pid, $usbillstyle, $hasCharges; - - $drow = sqlQuery("SELECT name FROM drugs WHERE drug_id = ?", array($drug_id) ); - $code_text = $drow['name']; - - $fee = sprintf('%01.2f', $fee); - if (empty($units)) $units = 1; - $units = max(1, intval($units)); - // We put unit price on the screen, not the total line item fee. - $price = $fee / $units; - $strike1 = ($sale_id && $del) ? "" : ""; - $strike2 = ($sale_id && $del) ? "" : ""; - echo " \n"; - echo " {$strike1}" . xlt("Product") . "$strike2"; - echo ""; - echo ""; - echo ""; - echo "\n"; - echo " $strike1" . text($drug_id) . "$strike2\n"; - if (modifiers_are_used(true)) { - echo "  \n"; - } - if ($billed) { - if (fees_are_used()) { - echo " " . text(oeFormatMoney($price)) . "\n"; - echo " " . text($units) . "\n"; - } - if (justifiers_are_used()) { - echo "  \n"; // justify - } - echo "  \n"; // provider - echo "  \n"; // note codes - echo "  \n"; // auth - echo " \n"; - } else { - if (fees_are_used()) { - echo " " . - "\n"; - echo " "; - echo ""; - echo "\n"; - } - if (justifiers_are_used()) { - echo "  \n"; // justify - } - echo "  \n"; // provider - echo "  \n"; // note codes - echo "  \n"; // auth - echo " \n"; - } - - echo " $strike1" . text($code_text) . "$strike2\n"; - echo " \n"; - - if ($fee != 0) $hasCharges = true; -} - -// Build a drop-down list of providers. This includes users who -// have the word "provider" anywhere in their "additional info" -// field, so that we can define providers (for billing purposes) -// who do not appear in the calendar. -// -function genProviderSelect($selname, $toptext, $default=0, $disabled=false) { - $query = "SELECT id, lname, fname FROM users WHERE " . - "( authorized = 1 OR info LIKE '%provider%' ) AND username != '' " . - "AND active = 1 AND ( info IS NULL OR info NOT LIKE '%Inactive%' ) " . - "ORDER BY lname, fname"; - $res = sqlStatement($query); - echo " \n"; -} - -// Compute a current checksum of Fee Sheet data from the database. -// -function visitChecksum($pid, $encounter, $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($pid, $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($pid, $encounter)); - $ret = intval($rowb['checksum']) ^ intval($rowp['checksum']); - if (CHECKSUM_LOGGING) { - $comment = "Checksum = '$ret'"; - $comment .= ", AJAX = " . (empty($_POST['running_as_ajax']) ? "false" : "true"); - $comment .= ", Save = " . (empty($_POST['bn_save']) ? "false" : "true"); - $comment .= ", Saved = " . ($saved ? "true" : "false"); - newEvent("checksum", $_SESSION['authUser'], $_SESSION['authProvider'], 1, $comment, $pid); - } - return $ret; -} - -// This is just for IPPF, to indicate if the visit includes contraceptive services. -$contraception = 0; - -// Possible units of measure for NDC drug quantities. -// -$ndc_uom_choices = array( - 'ML' => 'ML', - 'GR' => 'Grams', - 'ME' => 'Milligrams', - 'F2' => 'I.U.', - 'UN' => 'Units' -); - -// $FEE_SHEET_COLUMNS should be defined in codes.php. -if (empty($FEE_SHEET_COLUMNS)) $FEE_SHEET_COLUMNS = 2; - -$returnurl = $GLOBALS['concurrent_layout'] ? 'encounter_top.php' : 'patient_encounter.php'; - -// Update price level in patient demographics. -if (!empty($_POST['pricelevel'])) { - sqlStatement("UPDATE patient_data SET pricelevel = ? WHERE pid = ?", array($_POST['pricelevel'],$pid) ); -} - -// Get some info about this visit. -$visit_row = sqlQuery("SELECT fe.date, opc.pc_catname " . - "FROM form_encounter AS fe " . - "LEFT JOIN openemr_postcalendar_categories AS opc ON opc.pc_catid = fe.pc_catid " . - "WHERE fe.pid = ? AND fe.encounter = ? LIMIT 1", array($pid,$encounter) ); -$visit_date = substr($visit_row['date'], 0, 10); - -$current_checksum = visitChecksum($pid, $encounter); -// It's important to look for a checksum mismatch even if we're just refreshing -// the display, otherwise the error goes undetected on a refresh-then-save. -if (isset($_POST['form_checksum'])) { - if ($_POST['form_checksum'] != $current_checksum) { - $alertmsg = xl('Someone else has just changed this visit. Please cancel this page and try again.'); - if (CHECKSUM_LOGGING) { - $comment = "CHECKSUM ERROR, expecting '{$_POST['form_checksum']}'"; - newEvent("checksum", $_SESSION['authUser'], $_SESSION['authProvider'], 1, $comment, $pid); - } - } -} - -// If Save or Save-and-Close was clicked, save the new and modified billing -// lines; then if no error, redirect to $returnurl. -// -if (!$alertmsg && ($_POST['bn_save'] || $_POST['bn_save_close'])) { - $main_provid = 0 + $_POST['ProviderID']; - $main_supid = 0 + $_POST['SupervisorID']; - if ($main_supid == $main_provid) $main_supid = 0; - $default_warehouse = $_POST['default_warehouse']; - - $bill = $_POST['bill']; - $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 - for ($lino = 1; $bill["$lino"]['code_type']; ++$lino) { - $iter = $bill["$lino"]; - $code_type = $iter['code_type']; - $code = $iter['code']; - $del = $iter['del']; - - // Skip disabled (billed) line items. - if ($iter['billed']) continue; - - $id = $iter['id']; - $modifier = trim($iter['mod']); - if( !($cod0) && ($code_types[$code_type]['fee'] == 1) ){ - $mod0 = $modifier; - $cod0 = $code; - $ct0 = $code_type; - } - $units = max(1, intval(trim($iter['units']))); - $fee = sprintf('%01.2f',(0 + trim($iter['price'])) * $units); - - if($code_type == 'COPAY'){ - if($id == ''){ - //adding new copay from fee sheet into ar_session and ar_activity tables - if($fee < 0){ - $fee = $fee * -1; - } - $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,$pid)); - - sqlBeginTrans(); - $sequence_no = sqlQuery( "SELECT IFNULL(MAX(sequence_no),0) + 1 AS increment FROM ar_activity WHERE pid = ? AND encounter = ?", array($pid, $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($pid,$encounter,$sequence_no['increment'],$ct0,$cod0,$mod0,$_SESSION['authId'],$session_id,$fee)); - sqlCommitTrans(); - }else{ - //editing copay saved to ar_session and ar_activity - if($fee < 0){ - $fee = $fee * -1; - } - $session_id = $id; - $res_amount = sqlQuery("SELECT pay_amount FROM ar_activity WHERE pid=? AND encounter=? AND session_id=?", - array($pid,$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,$pid,$encounter,$session_id)); - } - } - if(!$cod0){ - $copay_update = TRUE; - $update_session_id = $session_id; - } - continue; - } - $justify = trim($iter['justify']); - # 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; - } - - $notecodes = trim($iter['notecodes']); - if ($justify) $justify = str_replace(',', ':', $justify) . ':'; - // $auth = $iter['auth'] ? "1" : "0"; - $auth = "1"; - $provid = 0 + $iter['provid']; - - $ndc_info = ''; - if ($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) { - deleteBilling($id); - } - else { - // authorizeBilling($id, $auth); - sqlQuery("UPDATE billing SET code = ?, " . - "units = ?, fee = ?, modifier = ?, " . - "authorized = ?, provider_id = ?, " . - "ndc_info = ?, justify = ?, notecodes = ? " . - "WHERE " . - "id = ? AND billed = 0 AND activity = 1", array($code,$units,$fee,$modifier,$auth,$provid,$ndc_info,$justify,$notecodes,$id) ); - } - } - - // Otherwise it's a new item... - else if (! $del) { - $code_text = lookup_code_descriptions($code_type.":".$code); - addBilling($encounter, $code_type, $code, $code_text, $pid, $auth, - $provid, $modifier, $units, $fee, $ndc_info, $justify, 0, $notecodes); - } - } // 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,$pid,$encounter,$update_session_id)); - } - - // Doing similarly to the above but for products. - $prod = $_POST['prod']; - for ($lino = 1; $prod["$lino"]['drug_id']; ++$lino) { - $iter = $prod["$lino"]; - - if (!empty($iter['billed'])) continue; - - $drug_id = $iter['drug_id']; - $sale_id = $iter['sale_id']; // present only if already saved - $units = max(1, intval(trim($iter['units']))); - $fee = sprintf('%01.2f',(0 + trim($iter['price'])) * $units); - $del = $iter['del']; - - // If the item is already in the database... - if ($sale_id) { - if ($del) { - // Zero out this sale and reverse its inventory update. We bring in - // drug_sales twice so that the original quantity can be referenced - // unambiguously. - sqlStatement("UPDATE drug_sales AS dsr, drug_sales AS ds, " . - "drug_inventory AS di " . - "SET di.on_hand = di.on_hand + dsr.quantity, " . - "ds.quantity = 0, ds.fee = 0 WHERE " . - "dsr.sale_id = ? AND ds.sale_id = dsr.sale_id AND " . - "di.inventory_id = ds.inventory_id", array($sale_id) ); - // And delete the sale for good measure. - sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id) ); - } - else { - // Modify the sale and adjust inventory accordingly. - $query = "UPDATE drug_sales AS dsr, drug_sales AS ds, " . - "drug_inventory AS di " . - "SET di.on_hand = di.on_hand + dsr.quantity - " . add_escape_custom($units) . ", " . - "ds.quantity = ?, ds.fee = ?, " . - "ds.sale_date = ? WHERE " . - "dsr.sale_id = ? AND ds.sale_id = dsr.sale_id AND " . - "di.inventory_id = ds.inventory_id"; - sqlStatement($query, array($units,$fee,$visit_date,$sale_id) ); - } - } - - // Otherwise it's a new item... - else if (! $del) { - $sale_id = sellDrug($drug_id, $units, $fee, $pid, $encounter, 0, - $visit_date, '', $default_warehouse); - if (!$sale_id) die(xlt("Insufficient inventory for product ID") . " \"" . text($drug_id) . "\"."); - } - } // end for - - // Set the main/default service provider in the new-encounter form. - /******************************************************************* - sqlStatement("UPDATE forms, users SET forms.user = users.username WHERE " . - "forms.pid = '$pid' AND forms.encounter = '$encounter' AND " . - "forms.formdir = 'newpatient' AND users.id = '$provid'"); - *******************************************************************/ - sqlStatement("UPDATE form_encounter SET provider_id = ?, " . - "supervisor_id = ? WHERE " . - "pid = ? AND encounter = ?", array($main_provid,$main_supid,$pid,$encounter) ); - - // Save-and-Close is currently IPPF-specific 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 ($_POST['bn_save_close']) { - $tmp1 = sqlQuery("SELECT SUM(ABS(fee)) AS sum FROM drug_sales WHERE " . - "pid = ? AND encounter = ?", array($pid,$encounter) ); - $tmp2 = sqlQuery("SELECT SUM(ABS(fee)) AS sum FROM billing WHERE " . - "pid = ? AND encounter = ? AND billed = 0 AND " . - "activity = 1", array($pid,$encounter) ); - if ($tmp1['sum'] + $tmp2['sum'] == 0) { - sqlStatement("update drug_sales SET billed = 1 WHERE " . - "pid = ? AND encounter = ? AND billed = 0", array($pid,$encounter)); - sqlStatement("UPDATE billing SET billed = 1, bill_date = NOW() WHERE " . - "pid = ? AND encounter = ? AND billed = 0 AND " . - "activity = 1", array($pid,$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. - } - } - - // More IPPF stuff. - if (!empty($_POST['contrastart'])) { - $contrastart = $_POST['contrastart']; - sqlStatement("UPDATE patient_data SET contrastart = ?" . - " WHERE pid = ?", array($contrastart,$pid) ); - } - - // Note: Taxes are computed at checkout time (in pos_checkout.php which - // also posts to SL). Currently taxes with insurance claims make no sense, - // so for now we'll ignore tax computation in the insurance billing logic. - - if ($_POST['running_as_ajax']) { - // In the case of running as an AJAX handler, we need to return this same - // form with an updated checksum to properly support the invoking logic. - // See review/js/fee_sheet_core.js for that logic. - $current_checksum = visitChecksum($pid, $encounter, true); - // Also remove form data for the newly entered lines so they are not - // duplicated from the database. - unset($_POST['bill']); - unset($_POST['prod']); - } - else { - formHeader("Redirecting...."); - formJump(); - formFooter(); - exit; - } -} - -$billresult = getBillingByEncounter($pid, $encounter, "*"); -?> - - - - - - - - - -
-
- - -
- -" . xlt("This encounter has been billed. If you need to change it, it must be re-opened.") . "

\n"; -} -else { // the encounter is not yet billed -?> - - -\n" : ""; - echo " \n" : ""; - echo " \n"; - if ($i >= $FEE_SHEET_COLUMNS) { - echo " \n"; - $i = 0; - } -} - -// Create one more drop-list, for Products. -if ($GLOBALS['sell_non_drug_products']) { - ++$i; - echo ($i <= 1) ? " \n" : ""; - echo " \n"; - if ($i >= $FEE_SHEET_COLUMNS) { - echo " \n"; - $i = 0; - } -} - -$search_type = $default_search_type; -if ($_POST['search_type']) $search_type = $_POST['search_type']; - -$ndc_applies = true; // Assume all payers require NDC info. - -echo $i ? " \n \n" : ""; -echo " \n"; -echo " \n"; -echo " \n"; -?> - -
\n"; - echo "
\n"; - echo " \n"; - echo "
\n"; - echo " \n"; - echo "
\n"; - -// If Search was clicked, do it and write the list of results here. -// There's no limit on the number of results! -// -$numrows = 0; -if ($_POST['bn_search'] && $_POST['search_term']) { - $res = main_code_set_search($search_type,$_POST['search_term']); - if (!empty($res)) { - $numrows = sqlNumRows($res); - } -} - -echo " \n"; -echo "
- -

- - - - - - - - - -
- ' - onclick="copayselect()" />      - -   - - $value) { - if (!empty($value['nofs'])) continue; - $nofs_code_types[$key] = $value; - } - $size_select = (count($nofs_code_types) < 5) ? count($nofs_code_types) : 5; -?> - - -   - -   - - '> -
-

-

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - $iter) { - if ($iter["id"]) continue; // skip if it came from the database - if ($iter["del"]) continue; // skip if Delete was checked - $ndc_info = ''; - if ($iter['ndcnum']) { - $ndc_info = 'N4' . trim($iter['ndcnum']) . ' ' . $iter['ndcuom'] . - trim($iter['ndcqty']); - } - // $fee = 0 + trim($iter['fee']); - $units = max(1, intval(trim($iter['units']))); - $fee = sprintf('%01.2f',(0 + trim($iter['price'])) * $units); - //the date is passed as $ndc_info, since this variable is not applicable in the case of copay. - $ndc_info = ''; - if ($iter['code_type'] == 'COPAY'){ - $ndc_info = date("Y-m-d"); - if($fee > 0) - $fee = 0 - $fee; - } - echoLine(++$bill_lino, $iter["code_type"], $iter["code"], trim($iter["mod"]), - $ndc_info, $iter["auth"], $iter["del"], $units, - $fee, NULL, FALSE, NULL, $iter["justify"], 0 + $iter['provid'], - $iter['notecodes']); - } -} - -// Generate lines for items already in the drug_sales table for this encounter. -// -$query = "SELECT * FROM drug_sales WHERE " . - "pid = ? AND encounter = ? " . - "ORDER BY sale_id"; -$sres = sqlStatement($query, array($pid,$encounter) ); -$prod_lino = 0; -while ($srow = sqlFetchArray($sres)) { - ++$prod_lino; - $pline = $_POST['prod']["$prod_lino"]; - $del = $pline['del']; // preserve Delete if checked - $sale_id = $srow['sale_id']; - $drug_id = $srow['drug_id']; - $units = $srow['quantity']; - $fee = $srow['fee']; - $billed = $srow['billed']; - // Also preserve other items from the form, if present and unbilled. - if ($pline['sale_id'] && !$srow['billed']) { - // $units = trim($pline['units']); - // $fee = trim($pline['fee']); - $units = max(1, intval(trim($pline['units']))); - $fee = sprintf('%01.2f',(0 + trim($pline['price'])) * $units); - } - echoProdLine($prod_lino, $drug_id, $del, $units, $fee, $sale_id, $billed); -} - -// Echo new product items from this form here, but omit any line -// whose Delete checkbox is checked. -// -if ($_POST['prod']) { - foreach ($_POST['prod'] as $key => $iter) { - if ($iter["sale_id"]) continue; // skip if it came from the database - if ($iter["del"]) continue; // skip if Delete was checked - // $fee = 0 + trim($iter['fee']); - $units = max(1, intval(trim($iter['units']))); - $fee = sprintf('%01.2f',(0 + trim($iter['price'])) * $units); - echoProdLine(++$prod_lino, $iter['drug_id'], FALSE, $units, $fee); - } -} - -// If new billing code(s) were
 > - - >>
-

- -
-  - -\n"; -echo xlt('Providers') . ":  "; - -echo "  " . xlt('Rendering') . "\n"; -genProviderSelect('ProviderID', '-- '.xl("Please Select").' --', $encounter_provid, $isBilled); - -if (!$GLOBALS['ippf_specific']) { - echo "  " . xlt('Supervising') . "\n"; - genProviderSelect('SupervisorID', '-- '.xl("N/A").' --', $encounter_supid, $isBilled); -} - -echo "\n"; -?> - -

-  - - 0"); -if ($trow['count'] && $contraception && !$isBilled) { - $date1 = substr($visit_row['date'], 0, 10); - // If admission or surgical, then force contrastart. - if ($contraception > 1 || - strpos(strtolower($visit_row['pc_catname']), 'admission') !== false) - { - echo " \n"; - } - else { - // echo "\n"; // debugging - $trow = sqlQuery("SELECT contrastart " . - "FROM patient_data WHERE " . - "pid = ? LIMIT 1", array($pid) ); - if (empty($trow['contrastart']) || substr($trow['contrastart'], 0, 4) == '0000') { - $date0 = date('Y-m-d', strtotime($date1) - (60 * 60 * 24)); - echo " \n"; - echo "     \n"; - } - } -} - -// If there is a choice of warehouses, allow override of user default. -if ($prod_lino > 0) { // if any products are in this form - $trow = sqlQuery("SELECT count(*) AS count FROM list_options WHERE list_id = 'warehouse'"); - if ($trow['count'] > 1) { - $trow = sqlQuery("SELECT default_warehouse FROM users WHERE username = ?", array($_SESSION['authUser']) ); - echo " " . xlt('Warehouse') . ":\n"; - echo generate_select_list('default_warehouse', 'warehouse', - $trow['default_warehouse'], ''); - echo "     \n"; - } -} - -// Allow the patient price level to be fixed here. -$plres = sqlStatement("SELECT option_id, title FROM list_options " . - "WHERE list_id = 'pricelevel' ORDER BY seq"); -if (true) { - $trow = sqlQuery("SELECT pricelevel FROM patient_data WHERE " . - "pid = ? LIMIT 1", array($pid) ); - $pricelevel = $trow['pricelevel']; - echo " " . xlt('Price Level') . ":\n"; - echo " \n"; -} -?> - -      - - -' /> -  - -' /> -  - -'> -  - - - - - -' - onclick="top.restoreSession();location=''" /> - - -


- -

- - -
- -
- - - - + +* +* LICENSE: This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 3 +* of the License, or (at your option) any later version. +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* You should have received a copy of the GNU General Public License +* along with this program. If not, see ;. +* +* @package OpenEMR +* @author Rod Roark +* @author Terry Hill +* @link http://www.open-emr.org +*/ + +$fake_register_globals = false; +$sanitize_all_escapes = true; + +require_once("../../globals.php"); +require_once("$srcdir/FeeSheetHtml.class.php"); +require_once("codes.php"); + +// Some table cells will not be displayed unless insurance billing is used. +$usbillstyle = $GLOBALS['ippf_specific'] ? " style='display:none'" : ""; +$justifystyle = justifiers_are_used() ? "" : " style='display:none'"; + +$liprovstyle = (isset($GLOBALS['support_fee_sheet_line_item_provider']) && + $GLOBALS['support_fee_sheet_line_item_provider'] != 1) ? " style='display:none'" : ""; + +// This flag comes from the LBFmsivd form and perhaps later others. +$rapid_data_entry = empty($_GET['rde']) ? 0 : 1; + +// This comes from the Add More Items button, or is preserved from its previous value. +$add_more_items = (empty($_GET['addmore']) && empty($_POST['bn_addmore'])) ? 0 : 1; + +$alertmsg = ''; + +// Determine if more than one price level is in use. +$tmp = sqlQuery("SELECT COUNT(*) AS count FROM list_options where list_id = 'pricelevel'"); +$price_levels_are_used = $tmp['count'] > 1; + +// Format a money amount with decimals but no other decoration. +// Second argument is used when extra precision is required. +function formatMoneyNumber($value, $extradecimals=0) { + return sprintf('%01.' . ($GLOBALS['currency_decimals'] + $extradecimals) . 'f', $value); +} + +// Helper function for creating drop-lists. +function endFSCategory() { + global $i, $last_category, $FEE_SHEET_COLUMNS; + if (! $last_category) return; + echo " \n"; + echo " \n"; + if ($i >= $FEE_SHEET_COLUMNS) { + echo " \n"; + $i = 0; + } +} + +// Generate JavaScript to build the array of diagnoses. +function genDiagJS($code_type, $code) { + global $code_types; + if ($code_types[$code_type]['diag']) { + echo "diags.push('" . attr($code_type) . "|" . attr($code) . "');\n"; + } +} + +// Write all service lines to the web form. +// +function echoServiceLines() { + global $code_types, $justinit, $usbillstyle, $liprovstyle, $justifystyle, $fs, $price_levels_are_used; + + foreach ($fs->serviceitems as $lino => $li) { + $id = $li['hidden']['id']; + $codetype = $li['hidden']['code_type']; + $code = $li['hidden']['code']; + $modifier = $li['hidden']['mod']; + $billed = $li['hidden']['billed']; + $ndc_info = isset($li['ndc_info']) ? $li['ndc_info'] : ''; + $pricelevel = $li['pricelevel']; + $justify = $li['justify']; + + $strike1 = $li['del'] ? "" : ""; + $strike2 = $li['del'] ? "" : ""; + + echo " \n"; + + echo " $strike1" . + ($codetype == 'COPAY' ? xl($codetype) : $codetype) . $strike2; + // if the line to ouput is copay, show the date here passed as $ndc_info, + // since this variable is not applicable in the case of copay. + if ($codetype == 'COPAY') { + echo "(" . text($ndc_info) . ")"; + $ndc_info = ''; + } + if ($id) { + echo ""; + } + echo ""; + echo ""; + echo ""; + if (isset($li['hidden']['method'])) { + echo ""; + echo ""; + echo ""; + } + echo "\n"; + + if ($codetype != 'COPAY') { + echo " $strike1" . text($code) . "$strike2\n"; + } else { + echo "  \n"; + } + + echo " $strike1" . text($li['code_text']) . "$strike2\n"; + + if ($billed) { + + if (modifiers_are_used(true)) { + echo " $strike1" . text($modifier) . "$strike2" . + "\n"; + } + + if (fees_are_used()) { + if ($price_levels_are_used) { + // Show price level for this line. + echo " "; + echo $fs->genPriceLevelSelect('', ' ', $li['hidden']['codes_id'], '', $pricelevel, true); + echo "\n"; + } + echo " " . text(oeFormatMoney($li['price'])) . "\n"; + if ($codetype != 'COPAY') { + echo " " . text($li['units']) . "\n"; + } else { + echo "  \n"; + } + echo " $justify\n"; + } + + // Show provider for this line. + echo " "; + echo $fs->genProviderSelect('', '-- ' .xl("Default"). ' --', $li['provid'], true); + echo "\n"; + + if ($code_types[$codetype]['claim'] && !$code_types[$codetype]['diag']) { + echo " " . + text($li['notecodes']) . "\n"; + } + else { + echo " \n"; + } + + echo " \n"; + + if ($GLOBALS['gbl_auto_create_rx']) { + echo "  \n"; + } + + echo " \n"; + } + + else { // not billed + + if (modifiers_are_used(true)) { + if ($codetype != 'COPAY' && ($code_types[$codetype]['mod'] || $modifier)) { + echo " \n"; + } else { + echo "  \n"; + } + } + if (fees_are_used()) { + if ($codetype == 'COPAY' || $code_types[$codetype]['fee'] || $fee != 0) { + if ($price_levels_are_used) { + echo " "; + echo $fs->genPriceLevelSelect("bill[$lino][pricelevel]", ' ', $li['hidden']['codes_id'], '', $pricelevel); + echo "\n"; + } + echo " " . + "\n"; + + echo " "; + if ($codetype != 'COPAY') { + echo ""; + } else { + echo ""; + } + echo "\n"; + + if ($code_types[$codetype]['just'] || $li['justify']) { + echo " "; + echo ""; + echo "\n"; + $justinit .= "setJustify(f['bill[" . attr($lino) . "][justify]']);\n"; + } else { + echo "  \n"; + } + } else { + if ($price_levels_are_used) echo "  \n"; + echo "  \n"; + echo "  \n"; + echo "  \n"; // justify + } + } + + // Provider drop-list for this line. + echo " "; + echo $fs->genProviderSelect("bill[$lino][provid]", '-- '.xl("Default").' --', $li['provid']); + echo "\n"; + + if ($code_types[$codetype]['claim'] && !$code_types[$codetype]['diag']) { + echo " \n"; + } + else { + echo " \n"; + } + + echo " \n"; + + if ($GLOBALS['gbl_auto_create_rx']) { + echo "  \n"; // KHY: May need to confirm proper location of this cell + } + + echo " \n"; + } + + echo " \n"; + + // If NDC info exists or may be required, add a line for it. + if (isset($li['ndcnum'])) { + echo " \n"; + echo "  \n"; + echo "  NDC: "; + echo ""; + echo "  Qty: "; + echo ""; + echo " "; + echo ""; + echo "\n"; + echo " \n"; + } + else if (!empty($li['ndc_info'])) { + echo " \n"; + echo "  \n"; + echo "  " . xlt("NDC Data") . ": " . text($li['ndc_info']) . "\n"; + echo " \n"; + } + } +} + +// Write all product lines to the web form. +// +function echoProductLines() { + global $code_types, $usbillstyle, $liprovstyle, $justifystyle, $fs, $price_levels_are_used; + + foreach ($fs->productitems as $lino => $li) { + $drug_id = $li['hidden']['drug_id']; + $selector = $li['hidden']['selector']; + $sale_id = $li['hidden']['sale_id']; + $billed = $li['hidden']['billed']; + $fee = $li['fee']; + $price = $li['price']; + $pricelevel = $li['pricelevel']; + $units = $li['units']; + $del = $li['del']; + $warehouse_id = $li['warehouse']; + $rx = $li['rx']; + + $strike1 = ($sale_id && $del) ? "" : ""; + $strike2 = ($sale_id && $del) ? "" : ""; + + echo " \n"; + echo " {$strike1}" . xlt("Product") . "$strike2"; + echo ""; + echo ""; + echo ""; + echo ""; + if (isset($li['hidden']['method'])) { + echo ""; + echo ""; + } + echo "\n"; + + echo " $strike1" . text($drug_id) . "$strike2\n"; + + echo " $strike1" . text($li['code_text']) . "$strike2\n"; + + if (modifiers_are_used(true)) { + echo "  \n"; + } + + if ($billed) { + if (fees_are_used()) { + if ($price_levels_are_used) { + echo " "; + echo $fs->genPriceLevelSelect('', ' ', $drug_id, $selector, $pricelevel, true); + echo "\n"; + } + echo " " . text(oeFormatMoney($price)) . "\n"; + echo " " . text($units) . "\n"; + } + if (justifiers_are_used()) { // KHY Evaluate proper position/usage of if justifiers + echo "  \n"; // justify + } + // Show warehouse for this line. + echo " "; + echo $fs->genWarehouseSelect('', ' ', $warehouse_id, true, $drug_id, $sale_id > 0); + echo "\n"; + // + echo "  \n"; // note codes + echo "  \n"; // auth + if ($GLOBALS['gbl_auto_create_rx']) { + echo " \n"; + } + echo " \n"; + } + + else { // not billed + if (fees_are_used()) { + if ($price_levels_are_used) { + echo " "; + echo $fs->genPriceLevelSelect("prod[$lino][pricelevel]", ' ', $drug_id, $selector, $pricelevel); + echo "\n"; + } + echo " " . + "\n"; + echo " "; + echo ""; + echo "\n"; + } + if (justifiers_are_used()) { + echo "  \n"; // justify + } + // Generate warehouse selector if there is a choice of warehouses. + echo " "; + echo $fs->genWarehouseSelect("prod[$lino][warehouse]", ' ', $warehouse_id, false, $drug_id, $sale_id > 0); + echo "\n"; + // + echo "  \n"; // note codes + echo "  \n"; // auth + if ($GLOBALS['gbl_auto_create_rx']) { + echo " " . + "\n"; + } + echo " \n"; + } + + echo " \n"; + } +} + +$fs = new FeeSheetHtml(); + +// $FEE_SHEET_COLUMNS should be defined in codes.php. +if (empty($FEE_SHEET_COLUMNS)) $FEE_SHEET_COLUMNS = 2; + +// Update price level in patient demographics if it's changed. +if (!empty($_POST['pricelevel'])) { + $fs->updatePriceLevel($_POST['pricelevel']); +} + +$current_checksum = $fs->visitChecksum(); +// It's important to look for a checksum mismatch even if we're just refreshing +// the display, otherwise the error goes undetected on a refresh-then-save. +if (isset($_POST['form_checksum'])) { + if ($_POST['form_checksum'] != $current_checksum) { + $alertmsg = xl('Someone else has just changed this visit. Please cancel this page and try again.'); + $comment = "CHECKSUM ERROR, expecting '{$_POST['form_checksum']}'"; + newEvent("checksum", $_SESSION['authUser'], $_SESSION['authProvider'], 1, $comment, $pid); + } +} + +if (!$alertmsg && ($_POST['bn_save'] || $_POST['bn_save_close'])) { + $alertmsg = $fs->checkInventory($_POST['prod']); +} + +// If Save or Save-and-Close was clicked, save the new and modified billing +// lines; then if no error, redirect to $GLOBALS['form_exit_url']. +// +if (!$alertmsg && ($_POST['bn_save'] || $_POST['bn_save_close'])) { + $main_provid = 0 + $_POST['ProviderID']; + $main_supid = 0 + $_POST['SupervisorID']; + + $fs->save($_POST['bill'], $_POST['prod'], $main_provid, $main_supid, + $_POST['default_warehouse'], $_POST['bn_save_close']); + + // Note: Taxes are computed at checkout time (in pos_checkout.php which + // also posts to SL). Currently taxes with insurance claims make no sense, + // so for now we'll ignore tax computation in the insurance billing logic. + + if ($_POST['running_as_ajax']) { + // In the case of running as an AJAX handler, we need to return this same + // form with an updated checksum to properly support the invoking logic. + // See review/js/fee_sheet_core.js for that logic. + $current_checksum = $fs->visitChecksum(true); + // Also remove form data for the newly entered lines so they are not + // duplicated from the database. + unset($_POST['bill']); + unset($_POST['prod']); + } + else { // not running as ajax + // If appropriate, update the status of the related appointment to + // "In exam room". + updateAppointmentStatus($fs->pid, $fs->visit_date, '<'); + + // More Family Planning stuff. + if (isset($_POST['ippfconmeth'])) { + $tmp_form_id = $fs->doContraceptionForm($_POST['ippfconmeth'], $_POST['newmauser'], $main_provid); + if ($tmp_form_id) { + // Contraceptive method does not match what is in an existing Contraception + // form for this visit, or there is no such form. Open the form. + formJump("{$GLOBALS['rootdir']}/patient_file/encounter/view_form.php" . + "?formname=LBFccicon&id=" . ($tmp_form_id < 0 ? 0 : $tmp_form_id)); + formFooter(); + exit; + } + } + + if ($rapid_data_entry || ($_POST['bn_save_close'] && $_POST['form_has_charges'])) { + // In rapid data entry mode or if "Save and Checkout" was clicked, + // we go directly to the Checkout page. + formJump("{$GLOBALS['rootdir']}/patient_file/pos_checkout.php?framed=1" . + "&ptid={$fs->pid}&enid={$fs->encounter}&rde=$rapid_data_entry"); + } + else { + // Otherwise return to the normal encounter summary frameset. + formHeader("Redirecting...."); + formJump(); + } + formFooter(); + exit; + } // end not running as ajax +} // end save or save-and-close + +// Handle reopen request. In that case no other changes will be saved. +// If there was a checkout this will undo it. +if (!$alertmsg && $_POST['bn_reopen']) { + doVoid($fs->pid, $fs->encounter, true); + $current_checksum = $fs->visitChecksum(); + // Remove the line items so they are refreshed from the database on redisplay. + unset($_POST['bill']); + unset($_POST['prod']); +} + +$billresult = getBillingByEncounter($fs->pid, $fs->encounter, "*"); +?> + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +pid, $fs->encounter); +if ($isBilled) { + echo "

" . + xlt("This encounter has been billed. To make changes, re-open it or select Add More Items.") . + "

\n"; +} +else { // the encounter is not yet billed +?> + + +\n" : ""; + echo " \n" : ""; + echo " \n"; + if ($i >= $FEE_SHEET_COLUMNS) { + echo " \n"; + $i = 0; + } +} + +// Create one more drop-list, for Products. +if ($GLOBALS['sell_non_drug_products']) { + ++$i; + echo ($i <= 1) ? " \n" : ""; + echo " \n"; + if ($i >= $FEE_SHEET_COLUMNS) { + echo " \n"; + $i = 0; + } +} + +$search_type = $default_search_type; +if ($_POST['search_type']) $search_type = $_POST['search_type']; + +$ndc_applies = true; // Assume all payers require NDC info. + +echo $i ? " \n \n" : ""; +echo " \n"; +echo " \n"; +echo " \n"; +?> + +
\n"; + echo "
\n"; + echo " \n"; + echo "
\n"; + echo " \n"; + echo "
\n"; + +// If Search was clicked, do it and write the list of results here. +// There's no limit on the number of results! +// +$numrows = 0; +if ($_POST['bn_search'] && $_POST['search_term']) { + $res = main_code_set_search($search_type,$_POST['search_term']); + if (!empty($res)) { + $numrows = sqlNumRows($res); + } +} + +echo " \n"; +echo "
+ +

+ + +ALLOW_COPAYS) { ?> + + + + + + + + +
+ ' + onclick="copayselect()" />      + +   + + $value) { + if (!empty($value['nofs'])) continue; + $nofs_code_types[$key] = $value; + } + $size_select = (count($nofs_code_types) < 5) ? count($nofs_code_types) : 5; +?> + $value) { + echo " " . xlt($value['label']) . " \n"; + } +?> + +   + +   + + ' + onclick='return this.clicked = true;'> +
+

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +serviceitems); + $bline = $_POST['bill']["$bill_lino"]; + $del = $bline['del']; // preserve Delete if checked + + $modifier = trim($iter["modifier"]); + $units = $iter["units"]; + $fee = $iter["fee"]; + $authorized = $iter["authorized"]; + $ndc_info = $iter["ndc_info"]; + $justify = trim($iter['justify']); + $notecodes = trim($iter['notecodes']); + if ($justify) $justify = substr(str_replace(':', ',', $justify), 0, strlen($justify) - 1); + $provider_id = $iter['provider_id']; + + // Also preserve other items from the form, if present. + if ($bline['id'] && !$iter["billed"]) { + $modifier = trim($bline['mod']); + $units = max(1, intval(trim($bline['units']))); + $fee = formatMoneyNumber((0 + trim($bline['price'])) * $units); + $authorized = $bline['auth']; + $ndc_info = ''; + if ($bline['ndcnum']) { + $ndc_info = 'N4' . trim($bline['ndcnum']) . ' ' . $bline['ndcuom'] . + trim($bline['ndcqty']); + } + $justify = $bline['justify']; + $notecodes = trim($bline['notecodes']); + $provider_id = 0 + $bline['provid']; + } + + if($iter['code_type'] == 'COPAY'){ // moved copay display to below + continue; + } + + $fs->addServiceLineItem(array( + 'codetype' => $iter['code_type'], + 'code' => trim($iter['code']), + 'modifier' => $modifier, + 'ndc_info' => $ndc_info, + 'auth' => $authorized, + 'del' => $del, + 'units' => $units, + 'pricelevel' => $iter['pricelevel'], + 'fee' => $fee, + 'id' => $iter['id'], + 'billed' => $iter['billed'], + 'code_text' => trim($iter['code_text']), + 'justify' => $justify, + 'provider_id' => $provider_id, + 'notecodes' => $notecodes, + )); + } +} + +$resMoneyGot = sqlStatement("SELECT pay_amount as PatientPay,session_id as id,date(post_time) as date ". + "FROM ar_activity where pid =? and encounter =? and payer_type=0 and account_code='PCP'", + array($fs->pid, $fs->encounter)); //new fees screen copay gives account_code='PCP' +while($rowMoneyGot = sqlFetchArray($resMoneyGot)){ + $PatientPay=$rowMoneyGot['PatientPay']*-1; + $id=$rowMoneyGot['id']; + $fs->addServiceLineItem(array( + 'codetype' => 'COPAY', + 'code' => '', + 'modifier' => '', + 'ndc_info' => $rowMoneyGot['date'], + 'auth' => 1, + 'del' => '', + 'units' => '', + 'fee' => $PatientPay, + 'id' => $id, + )); +} + +// Echo new billing items from this form here, but omit any line +// whose Delete checkbox is checked. +// +if ($_POST['bill']) { + foreach ($_POST['bill'] as $key => $iter) { + if ($iter["id"]) continue; // skip if it came from the database + if ($iter["del"]) continue; // skip if Delete was checked + $ndc_info = ''; + if ($iter['ndcnum']) { + $ndc_info = 'N4' . trim($iter['ndcnum']) . ' ' . $iter['ndcuom'] . + trim($iter['ndcqty']); + } + $units = max(1, intval(trim($iter['units']))); + $fee = formatMoneyNumber((0 + trim($iter['price'])) * $units); + //the date is passed as $ndc_info, since this variable is not applicable in the case of copay. + $ndc_info = ''; + if ($iter['code_type'] == 'COPAY'){ + $ndc_info = date("Y-m-d"); + if($fee > 0) + $fee = 0 - $fee; + } + $fs->addServiceLineItem(array( + 'codetype' => $iter['code_type'], + 'code' => trim($iter['code']), + 'modifier' => trim($iter["mod"]), + 'ndc_info' => $ndc_info, + 'auth' => $iter['auth'], + 'del' => $iter['del'], + 'units' => $units, + 'fee' => $fee, + 'justify' => $iter['justify'], + 'provider_id' => $iter['provid'], + 'notecodes' => $iter['notecodes'], + 'pricelevel' => $iter['pricelevel'], + )); + } +} + +// Generate lines for items already in the drug_sales table for this encounter. +// +$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 sale_id"; +$sres = sqlStatement($query, array($fs->pid, $fs->encounter) ); +// $prod_lino = 0; +while ($srow = sqlFetchArray($sres)) { + // ++$prod_lino; + $prod_lino = count($fs->productitems); + $pline = $_POST['prod']["$prod_lino"]; + $rx = !empty($srow['prescription_id']); + $del = $pline['del']; // preserve Delete if checked + $sale_id = $srow['sale_id']; + $drug_id = $srow['drug_id']; + $selector = $srow['selector']; + $pricelevel = $srow['pricelevel']; + $units = $srow['quantity']; + $fee = $srow['fee']; + $billed = $srow['billed']; + $warehouse_id = $srow['warehouse_id']; + // Also preserve other items from the form, if present and unbilled. + if ($pline['sale_id'] && !$srow['billed']) { + $units = max(1, intval(trim($pline['units']))); + $fee = formatMoneyNumber((0 + trim($pline['price'])) * $units); + $rx = !empty($pline['rx']); + } + $fs->addProductLineItem(array( + 'drug_id' => $drug_id, + 'selector' => $selector, + 'pricelevel' => $pricelevel, + 'rx' => $rx, + 'del' => $del, + 'units' => $units, + 'fee' => $fee, + 'sale_id' => $sale_id, + 'billed' => $billed, + 'warehouse_id' => $warehouse_id, + )); +} + +// Echo new product items from this form here, but omit any line +// whose Delete checkbox is checked. +// +if ($_POST['prod']) { + foreach ($_POST['prod'] as $key => $iter) { + if ($iter["sale_id"]) continue; // skip if it came from the database + if ($iter["del"]) continue; // skip if Delete was checked + $units = max(1, intval(trim($iter['units']))); + $fee = formatMoneyNumber((0 + trim($iter['price'])) * $units); + $rx = !empty($iter['rx']); // preserve Rx if checked + $warehouse_id = empty($iter['warehouse_id']) ? '' : $iter['warehouse_id']; + $fs->addProductLineItem(array( + 'drug_id' => $iter['drug_id'], + 'selector' => $iter['selector'], + 'pricelevel' => $iter['pricelevel'], + 'rx' => $rx, + 'units' => $units, + 'fee' => $fee, + 'warehouse_id' => $warehouse_id, + )); + } +} + +// If new billing code(s) were
  >>>>
+

+ +
+  + +\n"; +echo xlt('Providers') . ":  "; + +echo "  " . xlt('Rendering') . "\n"; +echo $fs->genProviderSelect('ProviderID', '-- '.xl("Please Select").' --', $fs->provider_id, $isBilled); + +if (!$GLOBALS['ippf_specific']) { + echo "  " . xlt('Supervising') . "\n"; + echo $fs->genProviderSelect('SupervisorID', '-- '.xl("N/A").' --', $fs->supervisor_id, $isBilled); +} + +echo "\n"; + +echo "\n"; +?> + +

+  + +contraception_code && !$isBilled) { + // This will give the form save logic the associated contraceptive method. + echo "\n"; + // If needed, this generates a dropdown to ask about prior contraception. + echo $fs->generateContraceptionSelector(); +} + +// Allow the patient price level to be fixed here. +$plres = sqlStatement("SELECT option_id, title FROM list_options " . + "WHERE list_id = 'pricelevel' ORDER BY seq"); +if (true) { + $pricelevel = $fs->getPriceLevel(); + echo " " . xlt('Default Price Level') . ":\n"; + echo " \n"; +} +?> + +      + + +' + +/> + +hasCharges) { // unbilled with charges ?> +' /> + +' /> + +  + +'> +  + +hasCharges) { // billed with charges ?> +' + onclick="top.restoreSession();location='../../patient_file/pos_checkout.php?framed=1pid) . "&enc=" . urlencode($fs->encounter); ?>'" /> +  +' /> +  + +' /> +  + +' /> +  + + + + + +' + onclick="top.restoreSession();location=''" /> + +

+ +
+ + + + + + + + + + diff --git a/interface/forms/fee_sheet/review/initialize_review.js b/interface/forms/fee_sheet/review/initialize_review.js index 15e938c81..124b9367a 100644 --- a/interface/forms/fee_sheet/review/initialize_review.js +++ b/interface/forms/fee_sheet/review/initialize_review.js @@ -33,7 +33,6 @@ var display_table_selector="table[cellspacing='5']"; function add_review_button() { - var review=$(""); review.attr("value",review_tag); review.attr("data-bind","click: review_event") @@ -41,12 +40,11 @@ function add_review_button() td.append(review) var template=$("
").appendTo(td); template.attr("data-bind","template: {name: 'review-display', data: review}"); - var copay=$("td > input:first").parent(); - copay.before(td); + // This makes the Review button first in the row. + $("[name='search_term']").parent().parent().prepend(td); return td; } - function get_fee_sheet_options(level) { fee_sheet_options=[]; @@ -60,7 +58,6 @@ function get_fee_sheet_options(level) return fee_sheet_options; } - var view_model; function initialize_review() { @@ -72,4 +69,3 @@ function initialize_review() ko.applyBindings(view_model,review.get(0)); } $(document).ready(initialize_review); - diff --git a/interface/forms/fee_sheet/review/initialize_review.php b/interface/forms/fee_sheet/review/initialize_review.php index 000b3342a..fe749623a 100644 --- a/interface/forms/fee_sheet/review/initialize_review.php +++ b/interface/forms/fee_sheet/review/initialize_review.php @@ -31,6 +31,7 @@ require_once("code_check.php"); var justify_click_title=""; var fee_sheet_options=[]; var diag_code_types=; // This is a list of diagnosis code types to present for as options in the justify dialog, for now, only "internal codes" included. + var ippf_specific = ; @@ -44,13 +45,13 @@ require_once("code_check.php"); return this; } - + + - 0) { + var i = selobj ? selobj.selectedIndex : -1; + if (i) { var f = document.forms[0]; - f.newcodes.value = selobj.options[i].value; + if (selobj) f.newcodes.value = selobj.options[i].value; // Submit the newly selected code. top.restoreSession(); var form_data=$("form").serialize() + "&running_as_ajax=1"; @@ -90,7 +92,7 @@ function codeselect_and_save(selobj) // "data" here is the complete newly generated fee sheet HTML. f.newcodes.value = ""; // Clear the selection - $(selobj).find("option:selected").prop("selected",false); + if (selobj) $(selobj).find("option:selected").prop("selected",false); // We do a refresh and then save because refresh does not save the new line item. // Note the save is skipped if refresh returned an error. if (update_display_table(data)) { @@ -142,6 +144,7 @@ function justify_start(evt) var justify_model=new fee_sheet_justify_view_model(parent.attr("billing_id"),enc,pid,current_justify_choices); ko.applyBindings(justify_model,template_div.get(0)); } + function tag_justify_rows(display) { var justify_selectors=display.find("select[onchange^='setJustify']").parent(); @@ -168,11 +171,14 @@ function tag_justify_rows(display) } - function setup_core() { + // KY on 2014-01-29 commented out this setup for the IPPF version. Not sure why. + // I (Rod) made them conditional so we can share the same code. + if (!ippf_specific) { codeselect=codeselect_and_save; tag_justify_rows($(display_table_selector)); + } } setup_core(); diff --git a/library/FeeSheet.class.php b/library/FeeSheet.class.php new file mode 100644 index 000000000..f193758a7 --- /dev/null +++ b/library/FeeSheet.class.php @@ -0,0 +1,1013 @@ + + * @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"); +// require_once(dirname(__FILE__) . "/formdata.inc.php"); + +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'"); + $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 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; + } + +} diff --git a/library/FeeSheetHtml.class.php b/library/FeeSheetHtml.class.php new file mode 100644 index 000000000..461e2f828 --- /dev/null +++ b/library/FeeSheetHtml.class.php @@ -0,0 +1,365 @@ + + * @link http://www.open-emr.org + */ + +require_once(dirname(__FILE__) . "/FeeSheet.class.php"); +require_once(dirname(__FILE__) . "/api.inc"); +require_once(dirname(__FILE__) . "/formdata.inc.php"); + +class FeeSheetHtml extends FeeSheet { + + // Dynamically generated JavaScript to maintain justification codes. + public $justinit = "var f = document.forms[0];\n"; + + function __construct($pid=0, $encounter=0) { + parent::__construct($pid, $encounter); + } + + // Build a drop-down list of providers. This includes users who + // have the word "provider" anywhere in their "additional info" + // field, so that we can define providers (for billing purposes) + // who do not appear in the calendar. + // + public static function genProviderOptionList($toptext, $default=0) { + $s = ''; + // Get user's default facility, or 0 if none. + $drow = sqlQuery("SELECT facility_id FROM users where username = '" . $_SESSION['authUser'] . "'"); + $def_facility = 0 + $drow['facility_id']; + // + $sqlarr = array($def_facility); + $query = "SELECT id, lname, fname, facility_id FROM users WHERE " . + "( authorized = 1 OR info LIKE '%provider%' ) AND username != '' " . + "AND active = 1 AND ( info IS NULL OR info NOT LIKE '%Inactive%' )"; + // If restricting to providers matching user facility... + if ($GLOBALS['gbl_restrict_provider_facility']) { + $query .= " AND ( facility_id = 0 OR facility_id = ? )"; + $query .= " ORDER BY lname, fname"; + } + // If not restricting then sort the matching providers first. + else { + $query .= " ORDER BY (facility_id = ?) DESC, lname, fname"; + } + $res = sqlStatement($query, $sqlarr); + $s .= ""; + while ($row = sqlFetchArray($res)) { + $provid = $row['id']; + $s .= "\n"; + } + $s .= ""; + } + return $s; + } + + // Build a drop-down list of price levels. + // Includes the specified item's price in the "id" of each option. + // + public function genPriceLevelSelect($tagname, $toptext, $pr_id, $pr_selector='', $default='', $disabled=false) { + // echo "\n"; // debugging + $s = ""; + return $s; + } + + // If Contraception forms can be auto-created by the Fee Sheet we might need + // to ask about the client's prior contraceptive use. + // + public function generateContraceptionSelector($tagname='newmauser') { + $s = ''; + if ($GLOBALS['gbl_new_acceptor_policy'] == '1') { + $csrow = sqlQuery("SELECT COUNT(*) AS count FROM forms AS f WHERE " . + "f.pid = ? AND f.encounter = ? AND " . + "f.formdir = 'LBFccicon' AND f.deleted = 0", + array($this->pid, $this->encounter)); + // Do it only if a contraception form does not already exist for this visit. + // Otherwise assume that whoever created it knows what they were doing. + if ($csrow['count'] == 0) { + // Determine if this client ever started contraception with the MA. + // Even if only a method change, we assume they have. + $query = "SELECT f.form_id FROM forms AS f " . + "JOIN form_encounter AS fe ON fe.pid = f.pid AND fe.encounter = f.encounter " . + "WHERE f.formdir = 'LBFccicon' AND f.deleted = 0 AND f.pid = ? " . + "ORDER BY fe.date DESC LIMIT 1"; + $csrow = sqlQuery($query, array($this->pid)); + if (empty($csrow)) { + $s .= "\n"; + } + } + } + return $s; + } + + // Generate a price level drop-down defaulting to the patient's current price level. + // + public function generatePriceLevelSelector($tagname='pricelevel', $disabled=false) { + $s = ""; + return $s; + } + + // Return Javascript that defines a function to validate the line items. + // Most of this is currently IPPF-specific, but NDC codes are also validated. + // This also computes and sets the form's ippfconmeth value if appropriate. + // This does not validate form fields not related to or derived from line items. + // Do not call this javascript function if you are just refreshing the form. + // The arguments are the names of the form arrays for services and products. + // + public function jsLineItemValidation($bill='bill', $prod='prod') { + $s = " +function jsLineItemValidation(f) { + var max_contra_cyp = 0; + var max_contra_code = ''; + var required_code_count = 0; + // Loop thru the services. + for (var lino = 0; f['{$bill}['+lino+'][code_type]']; ++lino) { + var pfx = '{$bill}[' + lino + ']'; + if (f[pfx + '[del]'] && f[pfx + '[del]'].checked) continue; + if (f[pfx + '[ndcnum]'] && f[pfx + '[ndcnum]'].value) { + // Check NDC number format. + var ndcok = true; + var ndc = f[pfx + '[ndcnum]'].value; + var a = ndc.split('-'); + if (a.length != 3) { + ndcok = false; + } + else if (a[0].length < 1 || a[1].length < 1 || a[2].length < 1 || + a[0].length > 5 || a[1].length > 4 || a[2].length > 2) { + ndcok = false; + } + else { + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < a[i].length; ++j) { + var c = a[i].charAt(j); + if (c < '0' || c > '9') ndcok = false; + } + } + } + if (!ndcok) { + alert('" . xls('Format incorrect for NDC') . "\"' + ndc + + '\", " . xls('should be like nnnnn-nnnn-nn') . "'); + if (f[pfx+'[ndcnum]'].focus) f[pfx+'[ndcnum]'].focus(); + return false; + } + // Check for valid quantity. + var qty = f[pfx+'[ndcqty]'].value - 0; + if (isNaN(qty) || qty <= 0) { + alert('" . xls('Quantity for NDC') . " \"' + ndc + + '\" " . xls('is not valid (decimal fractions are OK).') . "'); + if (f[pfx+'[ndcqty]'].focus) f[pfx+'[ndcqty]'].focus(); + return false; + } + } + if (f[pfx+'[method]'] && f[pfx+'[method]'].value) { + // The following applies to contraception for family planning clinics. + var tmp_cyp = parseFloat(f[pfx+'[cyp]'].value); + var tmp_meth = f[pfx+'[method]'].value; + var tmp_methtype = parseInt(f[pfx+'[methtype]'].value); + if (tmp_cyp > max_contra_cyp && tmp_methtype == 2) { + // max_contra_* tracks max cyp for initial consults only. + max_contra_cyp = tmp_cyp; + max_contra_code = tmp_meth; + } +"; + if ($this->patient_male) { + $s .= " + var male_compatible_method = ( + // TBD: Fix hard coded dependency on IPPFCM codes here. + tmp_meth == '4450' || // male condoms + tmp_meth == '4570'); // male vasectomy + if (!male_compatible_method) { + if (!confirm('" . xls('Warning: Contraceptive method is not compatible with a male patient.') . "')) + return false; + } +"; + } // end if male patient + if ($this->patient_age < 10 || $this->patient_age > 50) { + $s .= " + if (!confirm('" . xls('Warning: Contraception for a patient under 10 or over 50.') . "')) + return false; +"; + } // end if improper age + if ($this->match_services_to_products) { + $s .= " + // Nonsurgical methods should normally include a corresponding product. + // This takes advantage of the fact that only nonsurgical methods have CYP + // less than 10, in both the old and new frameworks. + if (tmp_cyp < 10.0) { + // Was: if (tmp_meth.substring(0, 2) != '12') { + var got_prod = false; + for (var plino = 0; f['{$prod}['+plino+'][drug_id]']; ++plino) { + var ppfx = '{$prod}[' + plino + ']'; + if (f[ppfx+'[del]'] && f[ppfx+'[del]'].checked) continue; + if (f[ppfx+'[method]'] && f[ppfx+'[method]'].value) { + if (f[ppfx+'[method]'].value == tmp_meth) got_prod = true; + } + } + if (!got_prod) { + if (!confirm('" . xls('Warning: There is no product matching the contraceptive service.') . "')) + return false; + } + } +"; + } // end match services to products + $s .= " + } + ++required_code_count; + } +"; + if ($this->match_services_to_products) { + $s .= " + // The following applies to contraception for family planning clinics. + // Loop thru the products. + for (var lino = 0; f['{$prod}['+lino+'][drug_id]']; ++lino) { + var pfx = '{$prod}[' + lino + ']'; + if (f[pfx + '[del]'] && f[pfx + '[del]'].checked) continue; + if (f[pfx + '[method]'] && f[pfx + '[method]'].value) { + var tmp_meth = f[pfx + '[method]'].value; + // Contraceptive products should normally include a corresponding method. + var got_svc = false; + for (var slino = 0; f['{$bill}[' + slino + '][code_type]']; ++slino) { + var spfx = '{$bill}[' + slino + ']'; + if (f[spfx + '[del]'] && f[spfx + '[del]'].checked) continue; + if (f[spfx + '[method]'] && f[spfx + '[method]'].value) { + if (f[spfx + '[method]'].value == tmp_meth) got_svc = true; + } + } + if (!got_svc) { + if (!confirm('" . xls('Warning: There is no service matching the contraceptive product.') . "')) + return false; + } + } + ++required_code_count; + } +"; + } // end match services to products + if (isset($GLOBALS['code_types']['MA'])) { + $s .= " + if (required_code_count == 0) { + if (!confirm('" . xls('You have not entered any clinical services or products. Click Cancel to add them. Or click OK if you want to save as-is.') . "')) { + return false; + } + } +"; + } + $s .= " + // End contraception validation. + if (f.ippfconmeth) { + // Save the primary contraceptive method to its hidden form field. + f.ippfconmeth.value = max_contra_code; + } + return true; +} +"; + return $s; + } + +} diff --git a/library/appointment_status.inc.php b/library/appointment_status.inc.php new file mode 100644 index 000000000..ed21dbc11 --- /dev/null +++ b/library/appointment_status.inc.php @@ -0,0 +1,38 @@ + +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. + +// This is called to update the appointment status for a specified patient +// with an encounter on the specified date. It does nothing unless the +// feature to auto-update appointment statuses is enabled. + +// See sample code in: interface/patient_tracker/patient_tracker_status.php +// This updates the patient tracker board as well as the appointment. + +require_once(dirname(__FILE__) . '/patient_tracker.inc.php'); + +function updateAppointmentStatus($pid, $encdate, $newstatus) { + if (empty($GLOBALS['gbl_auto_update_appt_status'])) return; + $query = "SELECT pc_eid, pc_aid, pc_catid, pc_apptstatus, pc_eventDate, pc_startTime, " . + "pc_hometext, pc_facility, pc_billing_location, pc_room " . + "FROM openemr_postcalendar_events WHERE " . + "pc_pid = ? AND pc_recurrtype = 0 AND pc_eventDate = ? " . + "ORDER BY pc_startTime DESC, pc_eid DESC LIMIT 1"; + $tmp = sqlQuery($query, array($pid, $encdate)); + if (!empty($tmp['pc_eid'])) { + $appt_eid = $tmp['pc_eid']; + $appt_status = $tmp['pc_apptstatus']; + // Some tests for illogical changes. + if ($appt_status == '$') return; + if ($newstatus == '<' && $appt_status == '>') return; + $encounter = todaysEncounterCheck($pid, $tmp['pc_eventDate'], $tmp['pc_hometext'], $tmp['pc_facility'], + $tmp['pc_billing_location'], $tmp['pc_aid'], $tmp['pc_catid'],false); + manage_tracker_status($tmp['pc_eventDate'], $tmp['pc_startTime'], $appt_eid, $pid, + $_SESSION["authUser"], $newstatus, $tmp['pc_room'], $encounter); + } +} +?> diff --git a/library/billing.inc b/library/billing.inc index b48ebaee8..e3b6d4b0a 100644 --- a/library/billing.inc +++ b/library/billing.inc @@ -27,13 +27,15 @@ function getBillingByEncounter ($pid,$encounter, $cols = "code_type, code, code_ function addBilling($encounter_id, $code_type, $code, $code_text, $pid, $authorized="0", $provider, $modifier="", $units="", $fee="0.00", - $ndc_info='', $justify='', $billed=0, $notecodes='') + $ndc_info='', $justify='', $billed=0, $notecodes='', $pricelevel='') { $sql = "insert into billing (date, encounter, code_type, code, code_text, " . "pid, authorized, user, groupname, activity, billed, provider_id, " . - "modifier, units, fee, ndc_info, justify, notecodes) values (" . - "NOW(), ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?)"; - return sqlInsert($sql, array($encounter_id,$code_type,$code,$code_text,$pid,$authorized,$_SESSION['authId'],$_SESSION['authProvider'],$billed,$provider,$modifier,$units,$fee,$ndc_info,$justify,$notecodes)); + "modifier, units, fee, ndc_info, justify, notecodes, pricelevel) values (" . + "NOW(), ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + return sqlInsert($sql, array($encounter_id,$code_type,$code,$code_text,$pid,$authorized, + $_SESSION['authId'],$_SESSION['authProvider'],$billed,$provider,$modifier,$units,$fee, + $ndc_info,$justify,$notecodes,$pricelevel)); } function authorizeBilling($id, $authorized = "1") @@ -222,19 +224,35 @@ function updateClaim($newversion, $patient_id, $encounter_id, $payer_id=-1, $pay return 1; } -// Determine if anything in a visit has been billed. +// Determine if the encounter is billed. It is considered billed if it +// has at least one chargeable item, and all of them are billed. // function isEncounterBilled($pid, $encounter) { - $row = sqlQuery("SELECT count(*) AS count FROM billing WHERE " . - "pid = '$pid' AND encounter = '$encounter' AND activity = 1 AND " . - "billed = 1"); - $count = $row['count']; - if (!$count) { - $row = sqlQuery("SELECT count(*) AS count FROM drug_sales WHERE " . - "pid = '$pid' AND encounter = '$encounter' AND billed = 1"); - $count = $row['count']; + $billed = -1; // no chargeable services yet + + $bres = sqlStatement("SELECT " . + "billing.billed FROM billing, code_types WHERE " . + "billing.pid = ? AND " . + "billing.encounter = ? AND " . + "billing.activity = 1 AND " . + "code_types.ct_key = billing.code_type AND " . + "code_types.ct_fee = 1 " . + "UNION " . + "SELECT billed FROM drug_sales WHERE " . + "pid = ? AND " . + "encounter = ?", + array($pid, $encounter, $pid, $encounter)); + + while ($brow = sqlFetchArray($bres)) { + if ($brow['billed'] == 0) { + $billed = 0; + } + else { + if ($billed < 0) $billed = 1; + } } - return $count ? true : false; + + return $billed > 0; } // Get the co-pay amount that is effective on the given date. @@ -258,4 +276,147 @@ function getPatientCopay($patient_id, $encounter) { $Copay=$rowMoneyGot['PatientPay']; return $Copay*-1; } + +// Get the "next invoice reference number" from this user's pool of reference numbers. +// +function getInvoiceRefNumber() { + $trow = sqlQuery("SELECT lo.notes " . + "FROM users AS u, list_options AS lo " . + "WHERE u.username = ? AND " . + "lo.list_id = 'irnpool' AND lo.option_id = u.irnpool LIMIT 1", + array($_SESSION['authUser'])); + return empty($trow['notes']) ? '' : $trow['notes']; +} + +// Increment the "next invoice reference number" of this user's pool. +// This identifies the "digits" portion of that number and adds 1 to it. +// If it contains more than one string of digits, the last is used. +// +function updateInvoiceRefNumber() { + $irnumber = getInvoiceRefNumber(); + // Here "?" specifies a minimal match, to get the most digits possible: + if (preg_match('/^(.*?)(\d+)(\D*)$/', $irnumber, $matches)) { + $newdigs = sprintf('%0' . strlen($matches[2]) . 'd', $matches[2] + 1); + $newnumber = $matches[1] . $newdigs . $matches[3]; + sqlStatement("UPDATE users AS u, list_options AS lo " . + "SET lo.notes = ? WHERE " . + "u.username = ? AND " . + "lo.list_id = 'irnpool' AND lo.option_id = u.irnpool", + array($newnumber, $_SESSION['authUser'])); + } + return $irnumber; +} + +// Common function for voiding a receipt or checkout. When voiding a checkout you can specify +// $time as a timestamp (yyyy-mm-dd hh:mm:ss) or 'all'; default is the last checkout. +// +function doVoid($patient_id, $encounter_id, $purge=false, $time='') { + $what_voided = $purge ? 'checkout' : 'receipt'; + $date_original = ''; + $adjustments = 0; + $payments = 0; + + if (!$time) { + // Get last checkout timestamp. + $corow = sqlQuery("(SELECT bill_date FROM billing WHERE " . + "pid = ? AND encounter = ? AND activity = 1 AND bill_date IS NOT NULL) " . + "UNION " . + "(SELECT bill_date FROM drug_sales WHERE " . + "pid = ? AND encounter = ? AND bill_date IS NOT NULL) " . + "ORDER BY bill_date DESC LIMIT 1", + array($patient_id, $encounter_id, $patient_id, $encounter_id)); + if (!empty($corow['bill_date'])) { + $date_original = $corow['bill_date']; + } + } + else if ($time == 'all') { + $row = sqlQuery("SELECT SUM(pay_amount) AS payments, " . + "SUM(adj_amount) AS adjustments FROM ar_activity WHERE " . + "pid = ? AND encounter = ?", + array($patient_id, $encounter_id)); + $adjustments = $row['adjustments']; + $payments = $row['payments']; + } + else { + $date_original = $time; + } + // Get its charges and adjustments. + if ($date_original) { + $row = sqlQuery("SELECT SUM(pay_amount) AS payments, " . + "SUM(adj_amount) AS adjustments FROM ar_activity WHERE " . + "pid = ? AND encounter = ? AND post_time = ?", + array($patient_id, $encounter_id, $date_original)); + $adjustments = $row['adjustments']; + $payments = $row['payments']; + } + + // Get old invoice reference number. + $encrow = sqlQuery("SELECT invoice_refno FROM form_encounter WHERE " . + "pid = ? AND encounter = ? LIMIT 1", + array($patient_id, $encounter_id)); + $old_invoice_refno = $encrow['invoice_refno']; + // + $usingirnpools = getInvoiceRefNumber(); + // If not (undoing a checkout or using IRN pools), nothing is done. + if ($purge || $usingirnpools) { + $query = "INSERT INTO voids SET " . + "patient_id = ?, " . + "encounter_id = ?, " . + "what_voided = ?, " . + "date_voided = NOW(), " . + "user_id = ?, " . + "amount1 = ?, " . + "amount2 = ?, " . + "other_info = ?"; + $sqlarr = array($patient_id, $encounter_id, $what_voided, $_SESSION['authUserID'], $row['adjustments'], + $row['payments'], $old_invoice_refno); + if ($date_original) { + $query .= ", date_original = ?"; + $sqlarr[] = $date_original; + } + sqlStatement($query, $sqlarr); + } + if ($purge) { + // Purge means delete adjustments and payments from the last checkout + // and re-open the visit. + if ($date_original) { + sqlStatement("DELETE FROM ar_activity WHERE " . + "pid = ? AND encounter = ? AND post_time = ?", + array($patient_id, $encounter_id, $date_original)); + sqlStatement("UPDATE billing SET billed = 0, bill_date = NULL WHERE " . + "pid = ? AND encounter = ? AND activity = 1 AND " . + "bill_date IS NOT NULL AND bill_date = ?", + array($patient_id, $encounter_id, $date_original)); + sqlStatement("update drug_sales SET billed = 0, bill_date = NULL WHERE " . + "pid = ? AND encounter = ? AND " . + "bill_date IS NOT NULL AND bill_date = ?", + array($patient_id, $encounter_id, $date_original)); + } + else { + if ($time == 'all') { + sqlStatement("DELETE FROM ar_activity WHERE " . + "pid = ? AND encounter = ?", + array($patient_id, $encounter_id)); + } + sqlStatement("UPDATE billing SET billed = 0, bill_date = NULL WHERE " . + "pid = ? AND encounter = ? AND activity = 1", + array($patient_id, $encounter_id)); + sqlStatement("update drug_sales SET billed = 0, bill_date = NULL WHERE " . + "pid = ? AND encounter = ?", + array($patient_id, $encounter_id)); + } + sqlStatement("UPDATE form_encounter SET last_level_billed = 0, " . + "last_level_closed = 0, stmt_count = 0, last_stmt_date = NULL " . + "WHERE pid = ? AND encounter = ?", + array($patient_id, $encounter_id)); + } + else if ($usingirnpools) { + // Non-purge means just assign a new invoice reference number. + $new_invoice_refno = add_escape_custom(updateInvoiceRefNumber()); + sqlStatement("UPDATE form_encounter " . + "SET invoice_refno = ? " . + "WHERE pid = ? AND encounter = ?", + array($new_invoice_refno, $patient_id, $encounter_id)); + } +} ?> diff --git a/sql/4_2_2-to-4_3_1_upgrade.sql b/sql/4_2_2-to-4_3_1_upgrade.sql index 38059c670..23e1955c7 100644 --- a/sql/4_2_2-to-4_3_1_upgrade.sql +++ b/sql/4_2_2-to-4_3_1_upgrade.sql @@ -612,8 +612,6 @@ DELETE FROM `enc_category_map` where rule_enc_id = 'enc_pregnancy' and main_cat_ ALTER TABLE `documents` ADD `thumb_url` VARCHAR( 255 ) DEFAULT NULL; #EndIf - - #IfMissingColumn layout_options validation ALTER TABLE layout_options ADD COLUMN validation varchar(100) default NULL; #EndIf @@ -627,7 +625,82 @@ INSERT INTO `list_options` (`list_id`,`option_id`,`title`,`notes`,`seq`) VALUES INSERT INTO `list_options` (`list_id`,`option_id`,`title`,`notes`,`seq`) VALUES ('LBF_Validations','email','E-Mail','{\"email\":true}','40'); INSERT INTO `list_options` (`list_id`,`option_id`,`title`,`notes`,`seq`) VALUES ('LBF_Validations','url','URL','{\"url\":true}','50'); INSERT INTO `list_options` (`list_id`,`option_id`,`title`,`notes`,`seq`) VALUES ('LBF_Validations','luhn','Luhn','{"numericality": {"onlyInteger": true}, "luhn":true}','80'); - #EndIf +#IfMissingColumn facility extra_validation +ALTER TABLE facility ADD extra_validation tinyint(1) NOT NULL DEFAULT '1'; +#EndIf + +#IfMissingColumn drugs consumable +ALTER TABLE drugs + ADD consumable tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 = will not show on the fee sheet'; +#EndIf + +#IfMissingColumn billing pricelevel +ALTER TABLE `billing` ADD COLUMN `pricelevel` varchar(31) default ''; +# Fill in missing price levels where possible. Specific to IPPF but will not hurt anyone else. +UPDATE billing AS b, codes AS c, prices AS p + SET b.pricelevel = p.pr_level WHERE + b.code_type = 'MA' AND b.activity = 1 AND b.pricelevel = '' AND b.units = 1 AND b.fee > 0.00 AND + c.code_type = '12' AND c.code = b.code AND c.modifier = b.modifier AND + p.pr_id = c.id AND p.pr_selector = '' AND p.pr_price = b.fee; +#EndIf + +#IfMissingColumn drug_sales pricelevel +ALTER TABLE `drug_sales` ADD COLUMN `pricelevel` varchar(31) default ''; +#EndIf + +#IfMissingColumn drug_sales selector +ALTER TABLE `drug_sales` ADD COLUMN `selector` varchar(255) default '' comment 'references drug_templates.selector'; +# Fill in missing selector values where not ambiguous. +UPDATE drug_sales AS s, drug_templates AS t + SET s.selector = t.selector WHERE + s.pid != 0 AND s.selector = '' AND t.drug_id = s.drug_id AND + (SELECT COUNT(*) FROM drug_templates AS t2 WHERE t2.drug_id = s.drug_id) = 1; +# Fill in missing price levels where not ambiguous. +UPDATE drug_sales AS s, drug_templates AS t, prices AS p + SET s.pricelevel = p.pr_level WHERE + s.pid != 0 AND s.selector != '' AND s.pricelevel = '' AND + t.drug_id = s.drug_id AND t.selector = s.selector AND t.quantity = s.quantity AND + p.pr_id = s.drug_id AND p.pr_selector = s.selector AND p.pr_price = s.fee; +#EndIf + +#IfMissingColumn drug_sales bill_date +ALTER TABLE `drug_sales` ADD COLUMN `bill_date` datetime default NULL; +UPDATE drug_sales AS s, billing AS b SET s.bill_date = b.bill_date WHERE s.billed = 1 AND s.bill_date IS NULL AND b.pid = s.pid AND b.encounter = s.encounter AND b.bill_date IS NOT NULL AND b.activity = 1; +UPDATE drug_sales AS s, ar_activity AS a SET s.bill_date = a.post_time WHERE s.billed = 1 AND s.bill_date IS NULL AND a.pid = s.pid AND a.encounter = s.encounter; +UPDATE drug_sales AS s SET s.bill_date = s.sale_date WHERE s.billed = 1 AND s.bill_date IS NULL; +#EndIf + +#IfNotTable voids +CREATE TABLE `voids` ( + `void_id` bigint(20) NOT NULL AUTO_INCREMENT, + `patient_id` bigint(20) NOT NULL COMMENT 'references patient_data.pid', + `encounter_id` bigint(20) NOT NULL DEFAULT 0 COMMENT 'references form_encounter.encounter', + `what_voided` varchar(31) NOT NULL COMMENT 'checkout,receipt and maybe other options later', + `date_original` datetime DEFAULT NULL COMMENT 'time of original action that is now voided', + `date_voided` datetime NOT NULL COMMENT 'time of void action', + `user_id` bigint(20) NOT NULL COMMENT 'references users.id', + `amount1` decimal(12,2) NOT NULL DEFAULT 0 COMMENT 'for checkout,receipt total voided adjustments', + `amount2` decimal(12,2) NOT NULL DEFAULT 0 COMMENT 'for checkout,receipt total voided payments', + `other_info` text COMMENT 'for checkout,receipt the old invoice refno', + PRIMARY KEY (`void_id`), + KEY datevoided (date_voided), + KEY pidenc (patient_id, encounter_id) +) ENGINE=InnoDB; +#EndIf +#IfMissingColumn drugs dispensable +UPDATE drug_sales AS s, prescriptions AS p, form_encounter AS fe + SET s.prescription_id = p.id WHERE + s.pid > 0 AND + s.encounter > 0 AND + s.prescription_id = 0 AND + fe.pid = s.pid AND + fe.encounter = s.encounter AND + p.patient_id = s.pid AND + p.drug_id = s.drug_id AND + p.start_date = fe.date; +ALTER TABLE drugs + ADD dispensable tinyint(1) NOT NULL DEFAULT 1 COMMENT '0 = pharmacy elsewhere, 1 = dispensed here'; +#EndIf diff --git a/sql/database.sql b/sql/database.sql index 5e75dfc1e..e53841a7e 100644 --- a/sql/database.sql +++ b/sql/database.sql @@ -203,6 +203,7 @@ CREATE TABLE `billing` ( `ndc_info` varchar(255) default NULL, `notecodes` varchar(25) NOT NULL default '', `external_id` VARCHAR(20) DEFAULT NULL, + `pricelevel` varchar(31) default '', PRIMARY KEY (`id`), KEY `pid` (`pid`) ) ENGINE=InnoDB AUTO_INCREMENT=1 ; @@ -997,6 +998,9 @@ CREATE TABLE `drug_sales` ( `xfer_inventory_id` int(11) NOT NULL DEFAULT 0, `distributor_id` bigint(20) NOT NULL DEFAULT 0 COMMENT 'references users.id', `notes` varchar(255) NOT NULL DEFAULT '', + `bill_date` datetime default NULL, + `pricelevel` varchar(31) default '', + `selector` varchar(255) default '' comment 'references drug_templates.selector', PRIMARY KEY (`sale_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 ; @@ -1045,6 +1049,8 @@ CREATE TABLE `drugs` ( `allow_combining` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 = allow filling an order from multiple lots', `allow_multiple` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1 = allow multiple lots at one warehouse', `drug_code` varchar(25) NULL, + `consumable` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 = will not show on the fee sheet', + `dispensable` tinyint(1) NOT NULL DEFAULT 1 COMMENT '0 = pharmacy elsewhere, 1 = dispensed here', PRIMARY KEY (`drug_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 ; @@ -1225,6 +1231,7 @@ CREATE TABLE `facility` ( `color` VARCHAR(7) NOT NULL DEFAULT '', `primary_business_entity` INT(10) NOT NULL DEFAULT '0' COMMENT '0-Not Set as business entity 1-Set as business entity', `facility_code` VARCHAR(31) default NULL, + `extra_validation` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 ; @@ -1232,7 +1239,7 @@ CREATE TABLE `facility` ( -- Dumping data for table `facility` -- -INSERT INTO `facility` VALUES (3, 'Your Clinic Name Here', '000-000-0000', '000-000-0000', '', '', '', '', '', '', NULL, NULL, 1, 1, 0, NULL, '', '', '', '', '','#99FFFF','0', ''); +INSERT INTO `facility` VALUES (3, 'Your Clinic Name Here', '000-000-0000', '000-000-0000', '', '', '', '', '', '', NULL, NULL, 1, 1, 0, NULL, '', '', '', '', '','#99FFFF','0', '', '1'); -- -------------------------------------------------------- @@ -6535,6 +6542,29 @@ INSERT INTO user_settings ( setting_user, setting_label, setting_value ) VALUES -- -------------------------------------------------------- -- +-- Table structure for table `voids` +-- + +DROP TABLE IF EXISTS `voids`; +CREATE TABLE `voids` ( + `void_id` bigint(20) NOT NULL AUTO_INCREMENT, + `patient_id` bigint(20) NOT NULL COMMENT 'references patient_data.pid', + `encounter_id` bigint(20) NOT NULL DEFAULT 0 COMMENT 'references form_encounter.encounter', + `what_voided` varchar(31) NOT NULL COMMENT 'checkout,receipt and maybe other options later', + `date_original` datetime DEFAULT NULL COMMENT 'time of original action that is now voided', + `date_voided` datetime NOT NULL COMMENT 'time of void action', + `user_id` bigint(20) NOT NULL COMMENT 'references users.id', + `amount1` decimal(12,2) NOT NULL DEFAULT 0 COMMENT 'for checkout,receipt total voided adjustments', + `amount2` decimal(12,2) NOT NULL DEFAULT 0 COMMENT 'for checkout,receipt total voided payments', + `other_info` text COMMENT 'for checkout,receipt the old invoice refno', + PRIMARY KEY (`void_id`), + KEY datevoided (date_voided), + KEY pidenc (patient_id, encounter_id) +) ENGINE=InnoDB; + +-- -------------------------------------------------------- + +-- -- Table structure for table `x12_partners` -- @@ -8307,5 +8337,4 @@ CREATE TABLE calendar_external ( `source` VARCHAR(45) NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB; - -- -------------------------------------------------------- diff --git a/version.php b/version.php index 2b6ee4432..9a6dde0b9 100644 --- a/version.php +++ b/version.php @@ -17,7 +17,7 @@ $v_realpatch = '0'; // is a database change in the course of development. It is used // internally to determine when a database upgrade is needed. // -$v_database = 173; +$v_database = 174; // Access control version identifier, this is to be incremented whenever there // is a access control change in the course of development. It is used -- 2.11.4.GIT