feat: expose suffix and valedictory in user admin and esign (#6814)
[openemr.git] / library / FeeSheet.class.php
blob0219282147ff94b4d9b12e3d047b60cb98542c18
1 <?php
3 /**
4 * library/FeeSheet.class.php
6 * Base class for implementations of the Fee Sheet.
7 * This should not include UI but may be extended by a class that does.
9 * LICENSE: This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version 3
12 * of the License, or (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see
19 * http://www.gnu.org/licenses/licenses.html#GPL .
21 * @package OpenEMR
22 * @license https://www.gnu.org/licenses/licenses.html#GPL GNU GPL V3+
23 * @author Rod Roark <rod@sunsetsystems.com>
24 * @link http://www.open-emr.org
27 require_once(dirname(__FILE__) . "/../interface/globals.php");
28 require_once(dirname(__FILE__) . "/../custom/code_types.inc.php");
29 require_once(dirname(__FILE__) . "/../interface/drugs/drugs.inc.php");
30 require_once(dirname(__FILE__) . "/options.inc.php");
31 require_once(dirname(__FILE__) . "/appointment_status.inc.php");
32 require_once(dirname(__FILE__) . "/forms.inc.php");
34 use OpenEMR\Billing\BillingUtilities;
35 use OpenEMR\Common\Acl\AclMain;
36 use OpenEMR\Common\Logging\EventAuditLogger;
38 // For logging checksums set this to true.
39 define('CHECKSUM_LOGGING', true);
41 // require_once(dirname(__FILE__) . "/api.inc.php");
42 // require_once(dirname(__FILE__) . "/forms.inc.php");
44 class FeeSheet
46 public $pid; // patient id
47 public $encounter; // encounter id
48 public $got_warehouses = false; // if there is more than 1 warehouse
49 public $default_warehouse = ''; // logged-in user's default warehouse
50 public $visit_date = ''; // YYYY-MM-DD date of this visit
51 public $match_services_to_products = false; // For IPPF
52 public $patient_age = 0; // Age in years as of the visit date
53 public $patient_male = 0; // 1 if male
54 public $patient_pricelevel = ''; // From patient_data.pricelevel
55 public $provider_id = 0;
56 public $supervisor_id = 0;
57 public $code_is_in_fee_sheet = false; // Set by genCodeSelectorValue()
58 public $payer_id;
60 // Possible units of measure for NDC drug quantities.
61 public $ndc_uom_choices = array(
62 'ML' => 'ML',
63 'GR' => 'Grams',
64 'ME' => 'Milligrams',
65 'F2' => 'I.U.',
66 'UN' => 'Units'
69 // Set by checkRelatedForContraception():
70 public $line_contra_code = '';
71 public $line_contra_cyp = 0;
72 public $line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult
74 // Array of line items generated by addServiceLineItem().
75 // Each element is an array of line item attributes.
76 public $serviceitems = array();
78 // Array of line items generated by addProductLineItem().
79 // Each element is an array of line item attributes.
80 public $productitems = array();
82 // Indicates if any line item has a fee.
83 public $hasCharges = false;
85 // Indicates if any clinical services or products are in the fee sheet.
86 public $required_code_count = 0;
88 // These variables are used to compute the initial consult service with highest CYP (IPPF).
89 public $contraception_code = '';
90 public $contraception_cyp = 0;
92 public $ALLOW_COPAYS = false;
94 function __construct($pid = 0, $encounter = 0)
96 if (empty($pid)) {
97 $pid = $GLOBALS['pid'];
100 if (empty($encounter)) {
101 $encounter = $GLOBALS['encounter'];
104 $this->pid = $pid;
105 $this->encounter = $encounter;
106 // get provider field for pid's primary insurance from insurance_data to be added to billing row table as payer_id
107 $primary_insurance = getInsuranceData($this->pid);
108 $this->payer_id = $primary_insurance['provider'];
110 // IPPF doesn't want any payments to be made or displayed in the Fee Sheet.
111 $this->ALLOW_COPAYS = empty($GLOBALS['ippf_specific']);
113 // Get the user's default warehouse and an indicator if there's a choice of warehouses.
114 $wrow = sqlQuery("SELECT count(*) AS count FROM list_options WHERE list_id = 'warehouse' AND activity = 1");
115 $this->got_warehouses = $wrow['count'] > 1;
116 $wrow = sqlQuery(
117 "SELECT default_warehouse FROM users WHERE username = ?",
118 array($_SESSION['authUser'])
120 $this->default_warehouse = empty($wrow['default_warehouse']) ? '' : $wrow['default_warehouse'];
122 // Get some info about this visit.
123 $visit_row = sqlQuery("SELECT fe.date, fe.provider_id, fe.supervisor_id, " .
124 "opc.pc_catname, fac.extra_validation " .
125 "FROM form_encounter AS fe " .
126 "LEFT JOIN openemr_postcalendar_categories AS opc ON opc.pc_catid = fe.pc_catid " .
127 "LEFT JOIN facility AS fac ON fac.id = fe.facility_id " .
128 "WHERE fe.pid = ? AND fe.encounter = ? LIMIT 1", array($this->pid, $this->encounter));
129 $this->visit_date = substr($visit_row['date'], 0, 10);
130 $this->provider_id = $visit_row['provider_id'];
131 if (empty($this->provider_id)) {
132 $this->provider_id = $this->findProvider();
135 $this->supervisor_id = $visit_row['supervisor_id'];
136 // This flag is specific to IPPF validation at form submit time. It indicates
137 // that most contraceptive services and products should match up on the fee sheet.
138 $this->match_services_to_products = $GLOBALS['ippf_specific'] &&
139 !empty($visit_row['extra_validation']);
141 // Get some information about the patient.
142 $patientrow = getPatientData($this->pid, "DOB, sex, pricelevel");
143 $this->patient_age = $this->getAge($patientrow['DOB'], $this->visit_date);
144 $this->patient_male = strtoupper(substr($patientrow['sex'], 0, 1)) == 'M' ? 1 : 0;
145 $this->patient_pricelevel = $patientrow['pricelevel'];
148 // Convert numeric code type to the alpha version.
150 public static function alphaCodeType($id)
152 global $code_types;
153 foreach ($code_types as $key => $value) {
154 if ($value['id'] == $id) {
155 return $key;
159 return '';
162 // Compute age in years given a DOB and "as of" date.
164 public static function getAge($dob, $asof = '')
166 if (empty($asof)) {
167 $asof = date('Y-m-d');
170 $a1 = explode('-', substr($dob, 0, 10));
171 $a2 = explode('-', substr($asof, 0, 10));
172 $age = $a2[0] - $a1[0];
173 if ($a2[1] < $a1[1] || ($a2[1] == $a1[1] && $a2[2] < $a1[2])) {
174 --$age;
177 return $age;
180 // Gets the provider from the encounter, logged-in user or patient demographics.
181 // Adapted from work by Terry Hill.
183 public function findProvider()
185 $find_provider = sqlQuery(
186 "SELECT provider_id FROM form_encounter " .
187 "WHERE pid = ? AND encounter = ? ORDER BY id DESC LIMIT 1",
188 array($this->pid, $this->encounter)
190 $providerid = $find_provider['provider_id'];
191 if (!$providerid) {
192 $get_authorized = $_SESSION['userauthorized'];
193 if ($get_authorized == 1) {
194 $providerid = $_SESSION['authUserID'];
198 if (!$providerid) {
199 $find_provider = sqlQuery("SELECT providerID FROM patient_data " .
200 "WHERE pid = ?", array($this->pid));
201 $providerid = $find_provider['providerID'];
204 return intval($providerid);
207 // Close the designated visit, making sure it has no charges.
209 public static function closeVisit($pid, $encounter)
211 $tmp1 = sqlQuery(
212 "SELECT SUM(ABS(fee)) AS sum FROM drug_sales WHERE " .
213 "pid = ? AND encounter = ? AND billed = 0",
214 array($pid, $encounter)
216 $tmp2 = sqlQuery(
217 "SELECT SUM(ABS(fee)) AS sum FROM billing WHERE " .
218 "pid = ? AND encounter = ? AND billed = 0 AND activity = 1 AND code_type != 'TAX'",
219 array($pid, $encounter)
221 if ($tmp1['sum'] + $tmp2['sum'] == 0) {
222 $this_bill_date = date('Y-m-d H:i:s');
223 sqlStatement(
224 "update drug_sales SET billed = 1, bill_date = ? WHERE " .
225 "pid = ? AND encounter = ? AND billed = 0",
226 array($this_bill_date, $pid, $encounter)
228 // Unbilled tax would have been recomputed at checkout and is invalid here.
229 sqlStatement(
230 "UPDATE billing SET activity = 0 WHERE " .
231 "pid = ? AND encounter = ? AND code_type = 'TAX' AND activity = 1 AND billed = 0",
232 array($pid, $encounter)
234 sqlStatement(
235 "UPDATE billing SET billed = 1, bill_date = ? WHERE " .
236 "pid = ? AND encounter = ? AND billed = 0 AND activity = 1",
237 array($this_bill_date, $pid, $encounter)
239 // Generate and set invoice reference number if appropriate.
240 $tmprow = sqlQuery(
241 "SELECT invoice_refno FROM form_encounter WHERE pid = ? AND encounter = ?",
242 array($pid, $encounter)
244 if (empty($tmprow['invoice_refno'])) { // not empty means there was a previous checkout
245 $refno = updateInvoiceRefNumber();
246 if ($refno) { // means user has an invoice refno pool
247 sqlStatement(
248 "UPDATE form_encounter SET invoice_refno = ? WHERE pid = ? AND encounter = ?",
249 array($refno, $pid, $encounter)
253 } else {
254 return xl('Cannot close because there are charges. Check out instead.');
256 return '';
259 public static function getBasicUnits($drug_id, $selector)
261 $basic_units = 1;
262 $tmp = sqlQuery(
263 "SELECT quantity FROM drug_templates WHERE drug_id = ? AND selector = ?",
264 array($drug_id, $selector)
266 if (!empty($tmp['quantity'])) {
267 $basic_units = 0 + $tmp['quantity'];
268 if (!$basic_units) {
269 $basic_units = 1;
272 return $basic_units;
275 // Log a message that is easy for the Re-Opened Visits Report to interpret.
276 // The optional $logarr contains these line item attributes:
277 // Code Type
278 // Code
279 // Selector (for code type "PROD")
280 // Price Level
281 // Fee
282 // Units
283 // Provider
284 // Warehouse
286 public function logFSMessage($action, $newvalue = '', $logarr = null)
288 $user_notes = $this->encounter;
289 if (is_array($logarr)) {
290 array_unshift($logarr, $newvalue);
291 foreach ($logarr as $tmp) {
292 $user_notes .= '|' . str_replace('|', '', $tmp);
296 EventAuditLogger::instance()->newEvent(
297 'fee-sheet',
298 $_SESSION['authUser'],
299 $_SESSION['authProvider'],
301 $action,
302 $this->pid,
303 $user_notes
307 // Compute a current checksum of this encounter's Fee Sheet data from the database.
309 public function visitChecksum($saved = false)
311 $rowb = sqlQuery(
312 "SELECT BIT_XOR(CRC32(CONCAT_WS(',', " .
313 "id, code, modifier, units, fee, authorized, provider_id, ndc_info, justify, billed" .
314 "))) AS checksum FROM billing WHERE " .
315 "pid = ? AND encounter = ? AND activity = 1",
316 array($this->pid, $this->encounter)
318 $rowp = sqlQuery(
319 "SELECT BIT_XOR(CRC32(CONCAT_WS(',', " .
320 "sale_id, inventory_id, prescription_id, quantity, fee, sale_date, billed" .
321 "))) AS checksum FROM drug_sales WHERE " .
322 "pid = ? AND encounter = ?",
323 array($this->pid, $this->encounter)
325 $rowv = sqlQuery(
326 "SELECT BIT_XOR(CRC32(CONCAT_WS(',', " .
327 "id, date, provider_id, supervisor_id, last_level_billed, last_level_closed" .
328 "))) AS checksum FROM form_encounter WHERE " .
329 "pid = ? AND encounter = ?",
330 array($this->pid, $this->encounter)
332 $ret = intval($rowb['checksum']) ^ intval($rowp['checksum']) ^ intval($rowv['checksum']);
333 if (CHECKSUM_LOGGING) {
334 $comment = "Checksum = '$ret'";
335 $comment .= ", Saved = " . ($saved ? "true" : "false");
336 EventAuditLogger::instance()->newEvent("checksum", $_SESSION['authUser'], $_SESSION['authProvider'], 1, $comment, $this->pid);
338 return $ret;
341 // IPPF-specific; get contraception attributes of the related codes.
343 public function checkRelatedForContraception($related_code, $is_initial_consult = false)
345 $this->line_contra_code = '';
346 $this->line_contra_cyp = 0;
347 $this->line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult
348 if (!empty($related_code)) {
349 $relcodes = explode(';', $related_code);
350 foreach ($relcodes as $relstring) {
351 if ($relstring === '') {
352 continue;
355 list($reltype, $relcode) = explode(':', $relstring);
356 if ($reltype !== 'IPPFCM') {
357 continue;
360 $methtype = $is_initial_consult ? 2 : 1;
361 $tmprow = sqlQuery(
362 "SELECT cyp_factor FROM codes WHERE " .
363 "code_type = '32' AND code = ? ORDER BY active DESC, id LIMIT 1",
364 array($relcode)
366 $cyp = 0 + $tmprow['cyp_factor'];
367 if ($cyp > $this->line_contra_cyp) {
368 $this->line_contra_cyp = $cyp;
369 // Note this is an IPPFCM code, not an IPPF2 code.
370 $this->line_contra_code = $relcode;
371 $this->line_contra_methtype = $methtype;
377 /******************************************************************
378 // Insert a row into the lbf_data table. Returns a new form ID if that is not provided.
379 // This is only needed for auto-creating Contraception forms.
381 public function insert_lbf_item($form_id, $field_id, $field_value)
383 if ($form_id) {
384 sqlStatement("INSERT INTO lbf_data (form_id, field_id, field_value) " .
385 "VALUES (?, ?, ?)", array($form_id, $field_id, $field_value));
386 } else {
387 $form_id = sqlInsert("INSERT INTO lbf_data (field_id, field_value) " .
388 "VALUES (?, ?)", array($field_id, $field_value));
391 return $form_id;
393 ******************************************************************/
395 // Insert a row into the shared_attributes table.
396 // This is only needed for auto-creating Contraception forms.
398 public function insert_lbf_item($field_id, $field_value)
400 sqlInsert(
401 "INSERT INTO shared_attributes (pid, encounter, last_update, user_id, field_id, field_value) " .
402 "VALUES (?, ?, 'NOW()', ?, ?, ?)",
403 array($this->pid, $this->encounter, $_SESSION['authId'], $field_id, $field_value)
407 // Create an array of data for a particular billing table item that is useful
408 // for building a user interface form row. $args is an array containing:
409 // codetype
410 // code
411 // modifier
412 // ndc_info
413 // auth
414 // del
415 // units
416 // fee
417 // id
418 // billed
419 // code_text
420 // justify
421 // provider_id
422 // notecodes
423 // pricelevel
424 public function addServiceLineItem($args)
426 global $code_types;
428 // echo "<!-- \n"; // debugging
429 // print_r($args); // debugging
430 // echo "--> \n"; // debugging
432 $li = array();
433 $li['hidden'] = array();
435 $codetype = $args['codetype'];
436 $code = $args['code'];
437 $revenue_code = isset($args['revenue_code']) ? $args['revenue_code'] : '';
438 $modifier = isset($args['modifier']) ? $args['modifier'] : '';
439 $code_text = isset($args['code_text']) ? $args['code_text'] : '';
440 $units = intval(isset($args['units']) ? $args['units'] : 0);
441 $billed = !empty($args['billed']);
442 $auth = !empty($args['auth']);
443 $id = isset($args['id']) ? intval($args['id']) : 0;
444 $ndc_info = isset($args['ndc_info']) ? $args['ndc_info'] : '';
445 $provider_id = isset($args['provider_id']) ? intval($args['provider_id']) : 0;
446 $justify = isset($args['justify']) ? $args['justify'] : '';
447 $notecodes = isset($args['notecodes']) ? $args['notecodes'] : '';
448 $fee = isset($args['fee']) ? (0 + $args['fee']) : 0;
449 // Price level should be unset only if adding a new line item.
450 $pricelevel = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel;
451 $del = !empty($args['del']);
453 // If using line item billing and user wishes to default to a selected provider, then do so.
454 if (!empty($GLOBALS['default_fee_sheet_line_item_provider']) && !empty($GLOBALS['support_fee_sheet_line_item_provider'])) {
455 if ($provider_id == 0) {
456 $provider_id = (int) $this->findProvider();
460 if ($codetype == 'COPAY') {
461 if (!$code_text) {
462 $code_text = 'Cash';
465 if ($fee > 0) {
466 $fee = 0 - $fee;
470 // Get the matching entry from the codes table.
471 $sqlArray = array();
472 $query = "SELECT id, units, code_text, revenue_code FROM codes WHERE " .
473 "code_type = ? AND code = ?";
474 array_push($sqlArray, ($code_types[$codetype]['id'] ?? ''), $code);
475 if ($modifier) {
476 $query .= " AND modifier = ?";
477 array_push($sqlArray, $modifier);
478 } else {
479 $query .= " AND (modifier IS NULL OR modifier = '')";
481 $query .= " ORDER BY active DESC, id LIMIT 1";
482 $result = sqlQuery($query, $sqlArray);
483 $codes_id = $result['id'] ?? null;
484 $revenue_code = $revenue_code ? $revenue_code : ($result['revenue_code'] ?? null);
485 if (!$code_text) {
486 $code_text = $result['code_text'] ?? null;
487 if (empty($units) && !empty($result)) {
488 $units = intval($result['units']);
491 if (!isset($args['fee'])) {
492 // Fees come from the prices table now.
493 $query = "SELECT pr_price, lo.option_id AS pr_level, lo.notes FROM list_options lo " .
494 " LEFT OUTER JOIN prices p ON lo.option_id=p.pr_level AND pr_id = ? AND pr_selector = '' " .
495 " WHERE lo.list_id='pricelevel' " .
496 "ORDER BY seq";
497 // echo "\n<!-- $query -->\n"; // debugging
499 $prdefault = null;
500 $prrow = null;
501 $prrecordset = sqlStatement($query, array($codes_id));
502 while ($row = sqlFetchArray($prrecordset)) {
503 if (empty($prdefault)) {
504 $prdefault = $row;
507 if ($row['pr_level'] === $pricelevel) {
508 $prrow = $row;
512 $fee = 0;
513 if (!empty($prrow)) {
514 $fee = $prrow['pr_price'];
516 // if percent-based pricing is enabled...
517 if ($GLOBALS['enable_percent_pricing']) {
518 // if this price level is a percentage, calculate price from default price
519 if (!empty($prrow['notes']) && strpos($prrow['notes'], '%') > -1 && !empty($prdefault)) {
520 $percent = intval(str_replace('%', '', $prrow['notes']));
521 if ($percent > 0) {
522 $fee = $prdefault['pr_price'] * ((100 - $percent) / 100);
530 if (!$units) {
531 $units = 1;
533 $fee = sprintf('%01.2f', $fee);
535 $li['hidden']['code_type'] = $codetype;
536 $li['hidden']['code'] = $code;
537 $li['hidden']['revenue_code'] = $revenue_code;
538 $li['hidden']['mod'] = $modifier;
539 $li['hidden']['billed'] = $billed;
540 $li['hidden']['id'] = $id;
541 $li['hidden']['codes_id'] = $codes_id;
543 // This logic is only used for family planning clinics, and then only when
544 // the option is chosen to use or auto-generate Contraception forms.
545 // It adds contraceptive method and effectiveness to relevant lines.
546 if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy'] && $codetype == 'MA') {
547 $codesrow = sqlQuery(
548 "SELECT related_code, cyp_factor FROM codes WHERE " .
549 "code_type = ? AND code = ? ORDER BY active DESC, id LIMIT 1",
550 array($code_types[$codetype]['id'], $code)
552 $this->checkRelatedForContraception($codesrow['related_code'], $codesrow['cyp_factor']);
553 if ($this->line_contra_code) {
554 $li['hidden']['method' ] = $this->line_contra_code;
555 $li['hidden']['cyp' ] = $this->line_contra_cyp;
556 $li['hidden']['methtype'] = $this->line_contra_methtype;
557 // contraception_code is only concerned with initial consults.
558 if ($this->line_contra_cyp > $this->contraception_cyp && $this->line_contra_methtype == 2) {
559 $this->contraception_cyp = $this->line_contra_cyp;
560 $this->contraception_code = $this->line_contra_code;
565 if ($codetype == 'COPAY') {
566 $li['codetype'] = xl($codetype);
567 if ($ndc_info) {
568 $li['codetype'] .= " ($ndc_info)";
570 $ndc_info = '';
571 } else {
572 $li['codetype'] = $codetype;
575 $li['code' ] = $codetype == 'COPAY' ? '' : $code;
576 $li['revenue_code' ] = $revenue_code;
577 $li['mod' ] = $modifier;
578 $li['fee' ] = $fee;
579 $li['price' ] = $fee / $units;
580 $li['pricelevel'] = $pricelevel;
581 $li['units' ] = $units;
582 $li['provid' ] = $provider_id;
583 $li['justify' ] = $justify;
584 $li['notecodes'] = $notecodes;
585 $li['del' ] = $id && $del;
586 $li['code_text'] = $code_text;
587 $li['auth' ] = $auth;
589 $li['hidden']['price'] = $li['price'];
591 // If NDC info exists or may be required, add stuff for it.
592 if ($codetype == 'HCPCS' && !$billed) {
593 if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndc_info, $tmp)) {
594 $ndcnum = $tmp[1];
595 $ndcuom = $tmp[2];
596 $ndcqty = $tmp[3];
597 } else {
598 $ndcnum = $ndc_info;
599 $ndcuom = "UN";
600 $ndcqty = "1";
603 $li['ndcnum' ] = $ndcnum;
604 $li['ndcuom' ] = $ndcuom;
605 $li['ndcqty' ] = $ndcqty;
606 } elseif ($ndc_info) {
607 $li['ndc_info' ] = $ndc_info;
610 // For Family Planning.
611 if ($codetype == 'MA') {
612 ++$this->required_code_count;
615 if ($fee != 0) {
616 $this->hasCharges = true;
619 $this->serviceitems[] = $li;
622 // Create an array of data for a particular drug_sales table item that is useful
623 // for building a user interface form row. $args is an array containing:
624 // drug_id
625 // selector
626 // sale_id
627 // rx (boolean)
628 // del (boolean)
629 // units
630 // fee
631 // billed
632 // warehouse_id
633 // pricelevel
635 // Pass true for the 2nd argument if the data is coming from the database;
636 // if from user input, make it false.
638 public function addProductLineItem($args, $convert_units = true)
640 global $code_types;
642 $li = array();
643 $li['hidden'] = array();
645 $drug_id = $args['drug_id'];
646 $selector = isset($args['selector']) ? $args['selector'] : '';
647 $sale_id = isset($args['sale_id']) ? intval($args['sale_id']) : 0;
648 $units = intval(isset($args['units']) ? $args['units'] : 0);
649 if (!$units) {
650 $units = 1;
652 $billed = !empty($args['billed']);
653 $rx = !empty($args['rx']);
654 $del = !empty($args['del']);
655 $fee = isset($args['fee']) ? (0 + $args['fee']) : 0;
656 $pricelevel = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel;
657 $warehouse_id = isset($args['warehouse_id']) ? $args['warehouse_id'] : '';
659 if ($convert_units) {
660 // "Basic Units" is the quantity from the product template and is the number of
661 // inventory items in the package that the template represents.
662 // Units seen by the user should be inventory units divided by template quantity.
663 $units = $units / $this->getBasicUnits($drug_id, $selector);
666 $drow = sqlQuery("SELECT name, related_code FROM drugs WHERE drug_id = ?", array($drug_id));
667 $code_text = $drow['name'];
669 // If no warehouse ID passed, use the logged-in user's default.
670 if ($this->got_warehouses && $warehouse_id === '') {
671 $warehouse_id = $this->default_warehouse;
674 // If fee is not provided, get it from the prices table.
675 // It is assumed in this case that units will match what is in the product template.
676 if (!isset($args['fee'])) {
677 $query = "SELECT pr_price, lo.option_id AS pr_level, lo.notes FROM list_options lo " .
678 " LEFT OUTER JOIN prices p ON lo.option_id=p.pr_level AND pr_id = ? AND pr_selector = ? " .
679 " WHERE lo.list_id='pricelevel' " .
680 "ORDER BY seq";
682 $prdefault = null;
683 $prrow = null;
684 $prrecordset = sqlStatement($query, array($drug_id, $selector));
685 while ($row = sqlFetchArray($prrecordset)) {
686 if (empty($prdefault)) {
687 $prdefault = $row;
690 if ($row['pr_level'] === $pricelevel) {
691 $prrow = $row;
695 $fee = 0;
696 if (!empty($prrow)) {
697 $fee = $prrow['pr_price'];
699 // if percent-based pricing is enabled...
700 if ($GLOBALS['enable_percent_pricing']) {
701 // if this price level is a percentage, calculate price from default price
702 if (!empty($prrow['notes']) && strpos($prrow['notes'], '%') > -1 && !empty($prdefault)) {
703 $percent = intval(str_replace('%', '', $prrow['notes']));
704 if ($percent > 0) {
705 $fee = $prdefault['pr_price'] * ((100 - $percent) / 100);
712 $fee = sprintf('%01.2f', $fee);
714 $li['fee' ] = $fee;
715 $li['price' ] = $fee / $units;
716 $li['pricelevel'] = $pricelevel;
717 $li['units' ] = $units;
718 $li['del' ] = $sale_id && $del;
719 $li['code_text'] = $code_text;
720 $li['warehouse'] = $warehouse_id;
721 $li['rx' ] = $rx;
723 $li['hidden']['drug_id'] = $drug_id;
724 $li['hidden']['selector'] = $selector;
725 $li['hidden']['sale_id'] = $sale_id;
726 $li['hidden']['billed' ] = $billed;
727 $li['hidden']['price' ] = $li['price'];
729 // This logic is only used for family planning clinics, and then only when
730 // the option is chosen to use or auto-generate Contraception forms.
731 // It adds contraceptive method and effectiveness to relevant lines.
732 if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy']) {
733 $this->checkRelatedForContraception($drow['related_code']);
734 if ($this->line_contra_code) {
735 $li['hidden']['method' ] = $this->line_contra_code;
736 $li['hidden']['methtype'] = $this->line_contra_methtype;
740 // For Family Planning.
741 ++$this->required_code_count;
742 if ($fee != 0) {
743 $this->hasCharges = true;
746 $this->productitems[] = $li;
749 // Generate rows for items already in the billing table for this encounter.
751 public function loadServiceItems()
753 $billresult = BillingUtilities::getBillingByEncounter($this->pid, $this->encounter, "*");
754 if ($billresult) {
755 foreach ($billresult as $iter) {
756 if (!$this->ALLOW_COPAYS && $iter["code_type"] == 'COPAY') {
757 continue;
760 $justify = trim($iter['justify']);
761 if ($justify) {
762 $justify = substr(str_replace(':', ',', $justify), 0, strlen($justify) - 1);
765 $this->addServiceLineItem(array(
766 'id' => $iter['id'],
767 'codetype' => $iter['code_type'],
768 'code' => trim($iter['code']),
769 'revenue_code' => trim($iter["revenue_code"]),
770 'modifier' => trim($iter["modifier"]),
771 'code_text' => trim($iter['code_text']),
772 'units' => $iter['units'],
773 'fee' => $iter['fee'],
774 'pricelevel' => $iter['pricelevel'],
775 'billed' => $iter['billed'],
776 'ndc_info' => $iter['ndc_info'],
777 'provider_id' => $iter['provider_id'],
778 'justify' => $justify,
779 'notecodes' => trim($iter['notecodes']),
784 // echo "<!-- \n"; // debugging
785 // print_r($this->serviceitems); // debugging
786 // echo "--> \n"; // debugging
789 // Generate rows for items already in the drug_sales table for this encounter.
791 public function loadProductItems()
793 $query = "SELECT ds.*, di.warehouse_id FROM drug_sales AS ds, drug_inventory AS di WHERE " .
794 "ds.pid = ? AND ds.encounter = ? AND di.inventory_id = ds.inventory_id " .
795 "ORDER BY ds.sale_id";
796 $sres = sqlStatement($query, array($this->pid, $this->encounter));
797 while ($srow = sqlFetchArray($sres)) {
798 $this->addProductLineItem(
799 array(
800 'drug_id' => $srow['drug_id'],
801 'selector' => $srow['selector'],
802 'sale_id' => $srow['sale_id'],
803 'rx' => !empty($srow['prescription_id']),
804 'units' => $srow['quantity'],
805 'fee' => $srow['fee'],
806 'pricelevel' => $srow['pricelevel'],
807 'billed' => $srow['billed'],
808 'warehouse_id' => $srow['warehouse_id'],
810 true
815 // Check for insufficient product inventory levels.
816 // Returns an error message if any product items cannot be filled.
817 // You must call this before save().
819 public function checkInventory(&$prod)
821 $alertmsg = '';
822 $insufficient = 0;
823 $expiredlots = false;
824 if (is_array($prod)) {
825 foreach ($prod as $iter) {
826 if (!empty($iter['billed'])) {
827 continue;
830 $drug_id = $iter['drug_id'];
831 $sale_id = empty($iter['sale_id']) ? 0 : intval($iter['sale_id']); // present only if already saved
832 $units = empty($iter['units']) ? 1 : intval($iter['units']);
833 $selector = empty($iter['selector']) ? '' : $iter['selector'];
834 $inv_units = $units * $this->getBasicUnits($drug_id, $selector);
835 $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse'];
837 // Deleting always works.
838 if (!empty($iter['del'])) {
839 continue;
842 // If the item is already in the database...
843 if ($sale_id) {
844 $query = "SELECT ds.quantity, ds.inventory_id, di.on_hand, di.warehouse_id " .
845 "FROM drug_sales AS ds " .
846 "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " .
847 "WHERE ds.sale_id = ?";
848 $dirow = sqlQuery($query, array($sale_id));
849 // There's no inventory ID when this is a non-dispensible product (i.e. no inventory).
850 if (!empty($dirow['inventory_id'])) {
851 if ($warehouse_id && $warehouse_id != $dirow['warehouse_id']) {
852 // Changing warehouse so check inventory in the new warehouse.
853 // Nothing is updated by this call.
854 if (
855 !sellDrug(
856 $drug_id,
857 $inv_units,
859 $this->pid,
860 $this->encounter,
862 $this->visit_date,
864 $warehouse_id,
865 true,
866 $expiredlots
869 $insufficient = $drug_id;
871 } else {
872 if (($dirow['on_hand'] + $dirow['quantity'] - $inv_units) < 0) {
873 $insufficient = $drug_id;
877 } else { // Otherwise it's a new item...
878 // This only checks for sufficient inventory, nothing is updated.
879 if (
880 !sellDrug(
881 $drug_id,
882 $inv_units,
884 $this->pid,
885 $this->encounter,
887 $this->visit_date,
889 $warehouse_id,
890 true,
891 $expiredlots
894 $insufficient = $drug_id;
897 } // end for
900 if ($insufficient) {
901 $drow = sqlQuery("SELECT name FROM drugs WHERE drug_id = ?", array($insufficient));
902 $alertmsg = xl('Insufficient inventory for product') . ' "' . $drow['name'] . '".';
903 if ($expiredlots) {
904 $alertmsg .= " " . xl('Check expiration dates.');
908 return $alertmsg;
911 // Save posted data to the database. $bill and $prod are the incoming arrays of line items, with
912 // key names corresponding to those generated by addServiceLineItem() and addProductLineItem().
914 public function save(
915 &$bill,
916 &$prod,
917 $main_provid = null,
918 $main_supid = null,
919 $default_warehouse = null,
920 $mark_as_closed = false
922 global $code_types;
924 if (isset($main_provid) && $main_supid == $main_provid) {
925 $main_supid = 0;
928 $copay_update = false;
929 $update_session_id = '';
930 $ct0 = ''; // takes the code type of the first fee type code type entry from the fee sheet, against which the copay is posted
931 $cod0 = ''; // takes the code of the first fee type code type entry from the fee sheet, against which the copay is posted
932 $mod0 = ''; // takes the modifier of the first fee type code type entry from the fee sheet, against which the copay is posted
934 if (is_array($bill)) {
935 foreach ($bill as $iter) {
936 // Skip disabled (billed) line items.
937 if (!empty($iter['billed'])) {
938 continue;
941 $id = $iter['id'] ?? null;
942 $code_type = $iter['code_type'];
943 $code = $iter['code'];
944 $del = !empty($iter['del']);
945 $units = empty($iter['units']) ? 1 : intval($iter['units']);
946 $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel'];
947 $revenue_code = empty($iter['revenue_code']) ? '' : trim($iter['revenue_code']);
948 $modifier = empty($iter['mod']) ? '' : trim($iter['mod']);
949 $justify = empty($iter['justify' ]) ? '' : trim($iter['justify']);
950 $notecodes = empty($iter['notecodes']) ? '' : trim($iter['notecodes']);
951 $provid = empty($iter['provid' ]) ? 0 : intval($iter['provid']);
953 $price = 0;
954 if (!empty($iter['price'])) {
955 if ($code_type == 'COPAY' || $this->pricesAuthorized()) {
956 $price = 0 + trim($iter['price'] ?? 0);
957 } else {
958 $price = $this->getPrice($pricelevel, $code_type, $code);
962 $fee = sprintf('%01.2f', $price * $units);
964 if (!$cod0 && ($code_types[$code_type]['fee'] ?? null) == 1) {
965 $mod0 = $modifier;
966 $cod0 = $code;
967 $ct0 = $code_type;
970 if ($code_type == 'COPAY') {
971 if ($fee < 0) {
972 $fee = $fee * -1;
974 if ($id) {
975 // editing copay in ar_session
976 $session_id = $id;
977 $res_amount = sqlQuery(
978 "SELECT pay_amount FROM ar_activity WHERE pid = ? AND encounter = ? AND session_id = ?",
979 array($this->pid, $this->encounter, $session_id)
981 if ($fee != $res_amount['pay_amount']) {
982 sqlStatement(
983 "UPDATE ar_session SET user_id = ?, pay_total = ?, modified_time = now(), " .
984 "post_to_date = now() WHERE session_id = ?",
985 array($_SESSION['authUserID'], $fee, $session_id)
988 // deleting old copay
989 sqlStatement(
990 "UPDATE ar_activity SET deleted = NOW() WHERE " .
991 "deleted IS NULL AND pid = ? AND encounter = ? AND account_code = 'PCP' AND session_id = ?",
992 array($this->pid, $this->encounter, $id)
994 } else {
995 // adding new copay from fee sheet into ar_session
996 $session_id = sqlInsert(
997 "INSERT INTO ar_session " .
998 "(payer_id, user_id, pay_total, payment_type, description, patient_id, payment_method, " .
999 "adjustment_code, post_to_date) " .
1000 "VALUES ('0',?,?,'patient','COPAY',?,'','patient_payment',now())",
1001 array($_SESSION['authUserID'], $fee, $this->pid)
1004 // adding new or changed copay from fee sheet into ar_activity
1005 sqlBeginTrans();
1006 $sequence_no = sqlQuery("SELECT IFNULL(MAX(sequence_no),0) + 1 AS increment FROM ar_activity WHERE " .
1007 "pid = ? AND encounter = ?", array($this->pid, $this->encounter));
1008 SqlStatement(
1009 "INSERT INTO ar_activity (pid, encounter, sequence_no, code_type, code, modifier, " .
1010 "payer_type, post_time, post_user, session_id, pay_amount, account_code) " .
1011 "VALUES (?,?,?,?,?,?,0,now(),?,?,?,'PCP')",
1012 array($this->pid, $this->encounter, $sequence_no['increment'], $ct0, $cod0, $mod0,
1013 $_SESSION['authUserID'], $session_id, $fee)
1015 sqlCommitTrans();
1017 if (!$cod0) {
1018 $copay_update = true;
1019 $update_session_id = $session_id;
1022 continue;
1025 # Code to create justification for all codes based on first justification
1026 if ($GLOBALS['replicate_justification'] == '1') {
1027 if ($justify != '') {
1028 $autojustify = $justify;
1032 if (($GLOBALS['replicate_justification'] == '1') && ($justify == '') && check_is_code_type_justify($code_type)) {
1033 $justify = $autojustify;
1036 if ($justify) {
1037 $justify = str_replace(',', ':', $justify) . ':';
1040 $auth = "1";
1042 $ndc_info = '';
1043 if (!empty($iter['ndcnum'])) {
1044 $ndc_info = 'N4' . trim($iter['ndcnum']) . ' ' . $iter['ndcuom'] .
1045 trim($iter['ndcqty']);
1048 // If the item is already in the database...
1049 if ($id) {
1050 // Get existing values from database.
1051 $logarr = null;
1052 $tmp = sqlQuery(
1053 "SELECT * FROM billing WHERE id = ? AND billed = 0 AND activity = 1",
1054 array($id)
1056 if (!empty($tmp)) {
1057 $logarr = array(
1058 $tmp['code_type'],
1059 $tmp['code'],
1061 $tmp['pricelevel'],
1062 $tmp['fee'],
1063 $tmp['units'],
1064 $tmp['provider_id'],
1069 if ($del) {
1070 $this->logFSMessage(xl('Item deleted'), '', $logarr);
1071 BillingUtilities::deleteBilling($id);
1072 } else {
1073 if (!empty($tmp)) {
1074 $tmparr = array('code' => $code, 'authorized' => $auth);
1075 if (isset($iter['units' ])) {
1076 $tmparr['units' ] = $units;
1079 if (isset($iter['price' ])) {
1080 $tmparr['fee' ] = $fee;
1083 if (isset($iter['pricelevel'])) {
1084 $tmparr['pricelevel'] = $pricelevel;
1087 if (isset($iter['mod' ])) {
1088 $tmparr['modifier' ] = $modifier;
1091 if (isset($iter['provid' ])) {
1092 $tmparr['provider_id'] = $provid;
1095 if (isset($iter['ndcnum' ])) {
1096 $tmparr['ndc_info' ] = $ndc_info;
1099 if (isset($iter['justify' ])) {
1100 $tmparr['justify' ] = $justify;
1103 if (isset($iter['notecodes'])) {
1104 $tmparr['notecodes' ] = $notecodes;
1107 if (isset($iter['revenue_code'])) {
1108 $tmparr['revenue_code'] = $revenue_code;
1111 foreach ($tmparr as $key => $value) {
1112 if ($tmp[$key] != $value) {
1113 if ('fee' == $key) {
1114 $this->logFSMessage(xl('Price changed'), $value, $logarr);
1117 if ('units' == $key) {
1118 $this->logFSMessage(xl('Quantity changed'), $value, $logarr);
1121 if ('provider_id' == $key) {
1122 $this->logFSMessage(xl('Service provider changed'), $value, $logarr);
1125 sqlStatement("UPDATE billing SET `$key` = ? WHERE id = ?", array($value, $id));
1130 } elseif (!$del) { // Otherwise it's a new item...
1131 $logarr = array($code_type, $code, '', $pricelevel, $fee, $units, $provid, '');
1132 $this->logFSMessage(xl('Item added'), '', $logarr);
1133 $code_text = lookup_code_descriptions($code_type . ":" . $code . ":" . $modifier);
1134 BillingUtilities::addBilling(
1135 $this->encounter,
1136 $code_type,
1137 $code,
1138 $code_text,
1139 $this->pid,
1140 $auth,
1141 $provid,
1142 $modifier,
1143 $units,
1144 $fee,
1145 $ndc_info,
1146 $justify,
1148 $notecodes,
1149 $pricelevel,
1150 $revenue_code,
1151 $this->payer_id
1154 } // end for
1157 // if modifier is not inserted during loop update the record using the first
1158 // non-empty modifier and code
1159 if ($copay_update == true && $update_session_id != '' && $mod0 != '') {
1160 sqlStatement(
1161 "UPDATE ar_activity SET code_type = ?, code = ?, modifier = ?" .
1162 " WHERE deleted IS NULL AND pid = ? AND encounter = ? AND account_code = 'PCP' AND session_id = ?",
1163 array($ct0, $cod0, $mod0, $this->pid, $this->encounter, $update_session_id)
1167 // Doing similarly to the above but for products.
1168 if (is_array($prod)) {
1169 foreach ($prod as $iter) {
1170 // Skip disabled (billed) line items.
1171 if (!empty($iter['billed'])) {
1172 continue;
1175 $drug_id = $iter['drug_id'];
1176 $selector = empty($iter['selector']) ? '' : $iter['selector'];
1177 $sale_id = $iter['sale_id']; // present only if already saved
1178 $units = intval(trim($iter['units']));
1179 if (!$units) {
1180 $units = 1;
1182 $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel'];
1183 $del = !empty($iter['del']);
1184 $rxid = 0;
1185 $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse'];
1186 $somechange = false;
1188 $price = 0;
1189 if (!empty($iter['price'])) {
1190 if ($iter['price'] != 'X') {
1191 // The price from the form is good.
1192 $price = 0 + trim($iter['price']);
1193 } else {
1194 // Otherwise get its price for the given price level and product.
1195 $price = $this->getPrice($pricelevel, 'PROD', $drug_id, $selector);
1199 $fee = sprintf('%01.2f', $price * $units);
1201 // $units is the user view, multipliers of Basic Units.
1202 // Need to compute inventory units for the save logic below.
1203 $inv_units = $units * $this->getBasicUnits($drug_id, $selector);
1205 // If the item is already in the database...
1206 if ($sale_id) {
1207 // Get existing values from database.
1208 $tmprow = sqlQuery(
1209 "SELECT ds.prescription_id, ds.quantity, ds.inventory_id, ds.fee, " .
1210 "ds.sale_date, ds.drug_id, ds.selector, ds.pricelevel, di.warehouse_id " .
1211 "FROM drug_sales AS ds " .
1212 "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " .
1213 "WHERE ds.sale_id = ?",
1214 array($sale_id)
1216 $rxid = (int) $tmprow['prescription_id'];
1217 $logarr = null;
1218 if (!empty($tmprow)) {
1219 $logarr = array(
1220 'PROD',
1221 $tmprow['drug_id'],
1222 $tmprow['selector'],
1223 $tmprow['pricelevel'],
1224 $tmprow['fee'],
1225 $tmprow['quantity'],
1227 $tmprow['warehouse_id']
1231 if ($del) {
1232 if (!empty($tmprow)) {
1233 // Delete this sale and reverse its inventory update.
1234 $this->logFSMessage(xl('Item deleted'), '', $logarr);
1235 sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id));
1236 if (!empty($tmprow['inventory_id'])) {
1237 sqlStatement(
1238 "UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?",
1239 array($tmprow['quantity'], $tmprow['inventory_id'])
1244 if ($rxid) {
1245 sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid));
1247 } else {
1248 // Modify the sale and adjust inventory accordingly.
1249 if (!empty($tmprow)) {
1250 foreach (
1251 array(
1252 'quantity' => $inv_units,
1253 'fee' => $fee,
1254 'pricelevel' => $pricelevel,
1255 'selector' => $selector,
1256 'sale_date' => $this->visit_date,
1257 ) as $key => $value
1259 if ($tmprow[$key] != $value) {
1260 $somechange = true;
1261 if ('fee' == $key) {
1262 $this->logFSMessage(xl('Price changed'), $value, $logarr);
1265 if ('pricelevel' == $key) {
1266 $this->logFSMessage(xl('Price level changed'), $value, $logarr);
1269 if ('selector' == $key) {
1270 $this->logFSMessage(xl('Template selector changed'), $value, $logarr);
1273 if ('quantity' == $key) {
1274 $this->logFSMessage(xl('Quantity changed'), $value, $logarr);
1277 sqlStatement(
1278 "UPDATE drug_sales SET `$key` = ? WHERE sale_id = ?",
1279 array($value, $sale_id)
1281 if ($key == 'quantity' && $tmprow['inventory_id']) {
1282 sqlStatement(
1283 "UPDATE drug_inventory SET on_hand = on_hand - ? WHERE inventory_id = ?",
1284 array($inv_units - $tmprow['quantity'], $tmprow['inventory_id'])
1290 if ($tmprow['inventory_id'] && $warehouse_id && $warehouse_id != $tmprow['warehouse_id']) {
1291 // Changing warehouse. Requires deleting and re-adding the sale.
1292 // Not setting $somechange because this alone does not affect a prescription.
1293 $this->logFSMessage(xl('Warehouse changed'), $warehouse_id, $logarr);
1294 sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id));
1295 sqlStatement(
1296 "UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?",
1297 array($inv_units, $tmprow['inventory_id'])
1299 $tmpnull = null;
1300 $sale_id = sellDrug(
1301 $drug_id,
1302 $inv_units,
1303 $fee,
1304 $this->pid,
1305 $this->encounter,
1306 (empty($iter['rx']) ? 0 : $rxid),
1307 $this->visit_date,
1309 $warehouse_id,
1310 false,
1311 $tmpnull,
1312 $pricelevel,
1313 $selector
1318 // Delete Rx if $rxid and flag not set.
1319 if ($GLOBALS['gbl_auto_create_rx'] && $rxid && empty($iter['rx'])) {
1320 sqlStatement("UPDATE drug_sales SET prescription_id = 0 WHERE sale_id = ?", array($sale_id));
1321 sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid));
1324 } elseif (! $del) { // Otherwise it's a new item...
1325 $somechange = true;
1326 $logarr = array('PROD', $drug_id, $selector, $pricelevel, $fee, $units, '', $warehouse_id);
1327 $this->logFSMessage(xl('Item added'), '', $logarr);
1328 $tmpnull = null;
1329 $sale_id = sellDrug(
1330 $drug_id,
1331 $inv_units,
1332 $fee,
1333 $this->pid,
1334 $this->encounter,
1336 $this->visit_date,
1338 $warehouse_id,
1339 false,
1340 $tmpnull,
1341 $pricelevel,
1342 $selector
1344 if (!$sale_id) {
1345 die(xlt("Insufficient inventory for product ID") . " \"" . text($drug_id) . "\".");
1349 // If a prescription applies, create or update it.
1350 if (!empty($iter['rx']) && !$del && ($somechange || empty($rxid))) {
1351 // If an active rx already exists for this drug and date we will
1352 // replace it, otherwise we'll make a new one.
1353 if (empty($rxid)) {
1354 $rxid = '';
1357 // Get default drug attributes; prefer the template with the matching selector.
1358 $drow = sqlQuery(
1359 "SELECT dt.*, " .
1360 "d.name, d.form, d.size, d.unit, d.route, d.substitute " .
1361 "FROM drugs AS d, drug_templates AS dt WHERE " .
1362 "d.drug_id = ? AND dt.drug_id = d.drug_id " .
1363 "ORDER BY (dt.selector = ?) DESC, dt.quantity, dt.dosage, dt.selector LIMIT 1",
1364 array($drug_id, $selector)
1366 if (!empty($drow)) {
1367 $rxobj = new Prescription($rxid);
1368 $rxobj->set_patient_id($this->pid);
1369 $rxobj->set_provider_id(isset($main_provid) ? $main_provid : $this->provider_id);
1370 $rxobj->set_drug_id($drug_id);
1371 $rxobj->set_quantity($inv_units);
1372 $rxobj->set_per_refill($inv_units);
1373 $rxobj->set_start_date_y(substr($this->visit_date, 0, 4));
1374 $rxobj->set_start_date_m(substr($this->visit_date, 5, 2));
1375 $rxobj->set_start_date_d(substr($this->visit_date, 8, 2));
1376 $rxobj->set_date_added($this->visit_date);
1377 // Remaining attributes are the drug and template defaults.
1378 $rxobj->set_drug($drow['name']);
1379 $rxobj->set_unit($drow['unit']);
1380 $rxobj->set_dosage($drow['dosage']);
1381 $rxobj->set_form($drow['form']);
1382 $rxobj->set_refills($drow['refills']);
1383 $rxobj->set_size($drow['size']);
1384 $rxobj->set_route($drow['route']);
1385 $rxobj->set_interval($drow['period']);
1386 $rxobj->set_substitute($drow['substitute']);
1388 $rxobj->persist();
1389 // Set drug_sales.prescription_id to $rxobj->get_id().
1390 $oldrxid = $rxid;
1391 $rxid = (int) $rxobj->get_id();
1392 if ($rxid != $oldrxid) {
1393 sqlStatement(
1394 "UPDATE drug_sales SET prescription_id = ? WHERE sale_id = ?",
1395 array($rxid, $sale_id)
1400 } // end for
1403 // Set default and/or supervising provider for the encounter.
1404 if (isset($main_provid) && $main_provid != $this->provider_id) {
1405 $this->logFSMessage(xl('Default provider changed'));
1406 sqlStatement(
1407 "UPDATE form_encounter SET provider_id = ? WHERE pid = ? AND encounter = ?",
1408 array($main_provid, $this->pid, $this->encounter)
1410 $this->provider_id = $main_provid;
1413 if (isset($main_supid) && $main_supid != $this->supervisor_id) {
1414 sqlStatement(
1415 "UPDATE form_encounter SET supervisor_id = ? WHERE pid = ? AND encounter = ?",
1416 array($main_supid, $this->pid, $this->encounter)
1418 $this->supervisor_id = $main_supid;
1421 // Save-and-Close is currently specific to Family Planning but might be more
1422 // generally useful. It provides the ability to mark an encounter as billed
1423 // directly from the Fee Sheet, if there are no charges.
1424 if ($mark_as_closed) {
1425 $this->closeVisit($this->pid, $this->encounter);
1429 // Call this after save() for Family Planning implementations.
1430 // It checks the contraception data, or auto-creates it if $newmauser is set.
1431 // Returns 0 unless user intervention is required to fix missing or incorrect data,
1432 // and in that case the return value is a LBFcontra form_id or -1 if none.
1433 // -1 could be returned if a non-LBFcontra form type recorded data that needs fixing.
1435 public function doContraceptionForm($ippfconmeth = null, $newmauser = null, $main_provid = 0)
1437 if (!empty($ippfconmeth)) {
1438 /**********************************************************
1439 $csrow = sqlQuery(
1440 "SELECT f.form_id, ld.field_value FROM forms AS f " .
1441 "LEFT JOIN lbf_data AS ld ON ld.form_id = f.form_id AND ld.field_id = 'newmethod' " .
1442 "WHERE " .
1443 "f.pid = ? AND f.encounter = ? AND " .
1444 "f.formdir = 'LBFccicon' AND f.deleted = 0 " .
1445 "ORDER BY f.form_id DESC LIMIT 1",
1446 array($this->pid, $this->encounter)
1448 **********************************************************/
1449 $csrow = sqlQuery(
1450 "SELECT f.form_id, d3.field_value " .
1451 "FROM form_encounter AS fe " .
1452 " JOIN shared_attributes AS d1 ON d1.pid = fe.pid AND d1.encounter = fe.encounter AND d1.field_id = 'cgen_newmauser' " .
1453 "LEFT JOIN shared_attributes AS d3 ON d3.pid = fe.pid AND d3.encounter = fe.encounter AND d3.field_id = 'cgen_MethAdopt' " .
1454 "LEFT JOIN forms AS f ON f.pid = fe.pid AND f.encounter = fe.encounter AND f.formdir = 'LBFcontra' AND f.deleted = 0 " .
1455 "WHERE fe.pid = ? AND fe.encounter = ? " .
1456 "ORDER BY fe.date DESC, fe.encounter DESC LIMIT 1",
1457 array($this->pid, $this->encounter)
1460 if (isset($newmauser)) {
1461 // pastmodern is 0 iff new to modern contraception
1462 $pastmodern = $newmauser == '2' ? 0 : 1;
1463 if ($newmauser == '2') {
1464 $newmauser = '1';
1466 /******************************************************
1467 // Add contraception form but only if it does not already exist
1468 // (if it does, must be 2 users working on the visit concurrently).
1469 if (empty($csrow)) {
1470 $newid = $this->insert_lbf_item(0, 'newmauser', $newmauser);
1471 $this->insert_lbf_item($newid, 'newmethod', "IPPFCM:$ippfconmeth");
1472 $this->insert_lbf_item($newid, 'pastmodern', $pastmodern);
1473 // Do we care about a service-specific provider here?
1474 $this->insert_lbf_item($newid, 'provider', $main_provid);
1475 addForm($this->encounter, 'Contraception', $newid, 'LBFccicon', $this->pid, $GLOBALS['userauthorized']);
1477 ******************************************************/
1478 // Auto-add contraception visit data if it does not already exist.
1479 if (empty($csrow)) {
1480 // Creating a new form. Get the new form_id by inserting and deleting a dummy row.
1481 // The form is required so there is a proper way to delete the data.
1482 $newid = sqlInsert(
1483 "INSERT INTO lbf_data " .
1484 "( field_id, field_value ) VALUES ( '', '' )"
1486 sqlStatement(
1487 "DELETE FROM lbf_data WHERE form_id = ? AND field_id = ''",
1488 array($newid)
1490 addForm($this->encounter, 'Contraception Summary', $newid, 'LBFcontra', $this->pid, $GLOBALS['userauthorized']);
1491 // Now add the needed visit fields.
1492 $this->insert_lbf_item('cgen_newmauser', $newmauser);
1493 $this->insert_lbf_item('cgen_MethAdopt', "IPPFCM:$ippfconmeth");
1494 $this->insert_lbf_item('cgen_PastModern', $pastmodern);
1495 $this->insert_lbf_item('cgen_provider', $main_provid);
1497 } elseif (empty($csrow) || $csrow['field_value'] != "IPPFCM:$ippfconmeth") {
1498 // Contraceptive method does not match what is in an existing Contraception
1499 // form for this visit, or there is no such form. User intervention is needed.
1500 return empty($csrow['form_id']) ? -1 : intval($csrow['form_id']);
1504 return 0;
1507 // Get price level from patient demographics.
1509 public function getPriceLevel()
1511 return $this->patient_pricelevel;
1514 // Update price level in patient demographics if it's changed.
1516 public function updatePriceLevel($pricelevel)
1518 if (!empty($pricelevel)) {
1519 if ($this->patient_pricelevel != $pricelevel) {
1520 $this->logFSMessage(xl('Price level changed'));
1521 sqlStatement(
1522 "UPDATE patient_data SET pricelevel = ? WHERE pid = ?",
1523 array($pricelevel, $this->pid)
1525 $this->patient_pricelevel = $pricelevel;
1530 // Create JSON string representing code type, code and selector.
1531 // This can be a checkbox value for parsing when the checkbox is clicked.
1532 // As a side effect note if the code is already selected in the Fee Sheet.
1534 public function genCodeSelectorValue($codes)
1536 global $code_types;
1537 list($codetype, $code, $selector) = explode(':', $codes);
1538 if ($codetype == 'PROD') {
1539 $crow = sqlQuery(
1540 "SELECT sale_id " .
1541 "FROM drug_sales WHERE pid = ? AND encounter = ? AND drug_id = ? " .
1542 "LIMIT 1",
1543 array($this->pid, $this->encounter, $code)
1545 $this->code_is_in_fee_sheet = !empty($crow['sale_id']);
1546 $cbarray = array($codetype, $code, $selector);
1547 } else {
1548 $crow = sqlQuery(
1549 "SELECT c.id AS code_id, b.id " .
1550 "FROM codes AS c " .
1551 "LEFT JOIN billing AS b ON b.pid = ? AND b.encounter = ? AND b.code_type = ? AND b.code = c.code AND b.activity = 1 " .
1552 "WHERE c.code_type = ? AND c.code = ? ORDER BY c.active DESC, c.id LIMIT 1",
1553 array($this->pid, $this->encounter, $codetype, $code_types[$codetype]['id'], $code)
1555 $this->code_is_in_fee_sheet = !empty($crow['id']);
1556 $cbarray = array($codetype, $code);
1559 $cbval = json_encode($cbarray);
1560 return $cbval;
1563 // Get price for a charge item and its price level.
1565 public function getPrice($pr_level, $codetype, $code, $selector = '')
1567 global $code_types;
1568 if ($codetype == 'PROD') {
1569 $pr_id = $code;
1570 } else {
1571 $crow = sqlQuery(
1572 "SELECT id FROM codes WHERE code_type = ? AND code = ? " .
1573 "ORDER BY active DESC, modifier, id LIMIT 1",
1574 array($code_types[$codetype]['id'], $code)
1576 $pr_id = $crow['id'];
1577 $selector = '';
1579 $prow = sqlQuery(
1580 "SELECT pr_price FROM prices WHERE " .
1581 "pr_id = ? AND pr_selector = ? AND pr_level = ?",
1582 array($pr_id, $selector, $pr_level)
1584 return empty($prow['pr_price']) ? 0 : $prow['pr_price'];
1587 // Determine if the current user is allowed to see prices.
1589 public function pricesAuthorized()
1591 return AclMain::aclCheckCore('acct', 'disc') || acl_check('acct', 'bill');