Refactor previous name into dedicated service (#7571)
[openemr.git] / interface / drugs / drugs.inc.php
blob54d6910309264cb024e089d0764e5eca1cc8ff92
1 <?php
3 // Copyright (C) 2006-2016 Rod Roark <rod@sunsetsystems.com>
4 //
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
9 //
10 // Modified 7-2009 by BM in order to migrate using the form,
11 // unit, route, and interval lists with the
12 // functions in openemr/library/options.inc.php .
13 // These lists are based on the constants found in the
14 // openemr/library/classes/Prescription.class.php file.
16 use PHPMailer\PHPMailer\PHPMailer;
18 // Decision was made in June 2013 that a sale line item in the Fee Sheet may
19 // come only from the specified warehouse. Set this to false if the decision
20 // is reversed.
21 $GLOBALS['SELL_FROM_ONE_WAREHOUSE'] = true;
23 $substitute_array = array('', xl('Allowed'), xl('Not Allowed'));
25 function send_drug_email($subject, $body)
27 $recipient = $GLOBALS['practice_return_email_path'];
28 if (empty($recipient)) {
29 return;
32 $mail = new PHPMailer();
33 $mail->From = $recipient;
34 $mail->FromName = 'In-House Pharmacy';
35 $mail->isMail();
36 $mail->Host = "localhost";
37 $mail->Mailer = "mail";
38 $mail->Body = $body;
39 $mail->Subject = $subject;
40 $mail->AddAddress($recipient);
41 if (!$mail->Send()) {
42 error_log("There has been a mail error sending to " . errorLogEscape($recipient .
43 " " . $mail->ErrorInfo));
47 function sellDrug(
48 $drug_id,
49 $quantity,
50 $fee,
51 $patient_id = 0,
52 $encounter_id = 0,
53 $prescription_id = 0,
54 $sale_date = '',
55 $user = '',
56 $default_warehouse = '',
57 $testonly = false,
58 &$expiredlots = null,
59 $pricelevel = '',
60 $selector = ''
61 ) {
63 if (empty($patient_id)) {
64 $patient_id = $GLOBALS['pid'];
67 if (empty($sale_date)) {
68 $sale_date = date('Y-m-d');
71 if (empty($user)) {
72 $user = $_SESSION['authUser'];
75 // error_log("quantity = '$quantity'"); // debugging
77 // Sanity check.
78 if (!$testonly) {
79 $tmp = sqlQuery(
80 "SELECT count(*) AS count from form_encounter WHERE pid = ? AND encounter = ?",
81 array($patient_id, $encounter_id)
83 if (empty($tmp['count'])) {
84 die(xlt('Internal error: the referenced encounter no longer exists.') . text(" $patient_id $encounter_id"));
88 if (empty($default_warehouse)) {
89 // Get the default warehouse, if any, for the user.
90 $rowuser = sqlQuery("SELECT default_warehouse FROM users WHERE username = ?", array($user));
91 $default_warehouse = $rowuser['default_warehouse'];
94 // Get relevant options for this product.
95 $rowdrug = sqlQuery("SELECT allow_combining, reorder_point, name, dispensable " .
96 "FROM drugs WHERE drug_id = ?", array($drug_id));
97 $allow_combining = $rowdrug['allow_combining'];
98 $dispensable = $rowdrug['dispensable'];
100 if (!$dispensable) {
101 // Non-dispensable is a much simpler case and does not touch inventory.
102 if ($testonly) {
103 return true;
106 $sale_id = sqlInsert(
107 "INSERT INTO drug_sales ( " .
108 "drug_id, inventory_id, prescription_id, pid, encounter, user, " .
109 "sale_date, quantity, fee ) VALUES ( " .
110 "?, 0, ?, ?, ?, ?, ?, ?, ?)",
111 array($drug_id, $prescription_id, $patient_id, $encounter_id, $user, $sale_date, $quantity, $fee)
113 return $sale_id;
116 // Combining is never allowed for prescriptions and will not work with
117 // dispense_drug.php.
118 if ($prescription_id) {
119 $allow_combining = 0;
122 $rows = array();
123 // $firstrow = false;
124 $qty_left = $quantity;
125 $bad_lot_list = '';
126 $total_on_hand = 0;
127 $gotexpired = false;
129 // If the user has a default warehouse, sort those lots first.
130 $orderby = ($default_warehouse === '') ?
131 "" : "di.warehouse_id != '$default_warehouse', ";
132 $orderby .= "lo.seq, di.expiration, di.lot_number, di.inventory_id";
134 // Retrieve lots in order of expiration date within warehouse preference.
135 $query = "SELECT di.*, lo.option_id, lo.seq " .
136 "FROM drug_inventory AS di " .
137 "LEFT JOIN list_options AS lo ON lo.list_id = 'warehouse' AND " .
138 "lo.option_id = di.warehouse_id AND lo.activity = 1 " .
139 "WHERE " .
140 "di.drug_id = ? AND di.destroy_date IS NULL AND di.on_hand != 0 ";
141 $sqlarr = array($drug_id);
142 if ($GLOBALS['SELL_FROM_ONE_WAREHOUSE'] && $default_warehouse) {
143 $query .= "AND di.warehouse_id = ? ";
144 $sqlarr[] = $default_warehouse;
147 $query .= "ORDER BY $orderby";
148 $res = sqlStatement($query, $sqlarr);
150 // First pass. Pick out lots to be used in filling this order, figure out
151 // if there is enough quantity on hand and check for lots to be destroyed.
152 while ($row = sqlFetchArray($res)) {
153 if ($row['warehouse_id'] != $default_warehouse) {
154 // Warehouses with seq > 99 are not available.
155 $seq = empty($row['seq']) ? 0 : $row['seq'] + 0;
156 if ($seq > 99) {
157 continue;
161 $on_hand = $row['on_hand'];
162 $expired = (!empty($row['expiration']) && $row['expiration'] <= $sale_date);
163 if ($expired || $on_hand < $quantity) {
164 $tmp = $row['lot_number'];
165 if (! $tmp) {
166 $tmp = '[missing lot number]';
169 if ($bad_lot_list) {
170 $bad_lot_list .= ', ';
173 $bad_lot_list .= $tmp;
176 if ($expired) {
177 $gotexpired = true;
178 continue;
181 /*****************************************************************
182 // Note the first row in case total quantity is insufficient and we are
183 // allowed to go negative.
184 if (!$firstrow) $firstrow = $row;
185 *****************************************************************/
187 $total_on_hand += $on_hand;
189 if ($on_hand > 0 && $qty_left > 0 && ($allow_combining || $on_hand >= $qty_left)) {
190 $rows[] = $row;
191 $qty_left -= $on_hand;
195 if ($expiredlots !== null) {
196 $expiredlots = $gotexpired;
199 if ($testonly) {
200 // Just testing inventory, so return true if OK, false if insufficient.
201 // $qty_left, if positive, is the amount requested that could not be allocated.
202 return $qty_left <= 0;
205 if ($bad_lot_list) {
206 send_drug_email(
207 "Possible lot destruction needed",
208 "The following lot(s) are expired or were too small to fill the " .
209 "order for patient $patient_id: $bad_lot_list\n"
213 /*******************************************************************
214 if (empty($firstrow)) return 0; // no suitable lots exist
215 // This can happen when combining is not allowed. We will use the
216 // first row and take it negative.
217 if (empty($rows)) {
218 $rows[] = $firstrow;
219 $qty_left -= $firstrow['on_hand'];
221 *******************************************************************/
223 // The above was an experiment in permitting a negative lot quantity.
224 // We decided that was a bad idea, so now we just error out if there
225 // is not enough on hand.
226 if ($qty_left > 0) {
227 return 0;
230 $sale_id = 0;
231 $qty_final = $quantity; // remaining unallocated quantity
232 $fee_final = $fee; // remaining unallocated fee
234 // Second pass. Update the database.
235 foreach ($rows as $row) {
236 $inventory_id = $row['inventory_id'];
238 /*****************************************************************
239 $thisqty = $row['on_hand'];
240 if ($qty_left > 0) {
241 $thisqty += $qty_left;
242 $qty_left = 0;
244 else if ($thisqty > $qty_final) {
245 $thisqty = $qty_final;
247 *****************************************************************/
248 $thisqty = min($qty_final, $row['on_hand']);
250 $qty_final -= $thisqty;
252 // Compute the proportional fee for this line item. For the last line
253 // item take the remaining unallocated fee to avoid round-off error.
254 if ($qty_final) {
255 $thisfee = sprintf('%0.2f', $fee * $thisqty / $quantity);
256 } else {
257 $thisfee = sprintf('%0.2f', $fee_final);
260 $fee_final -= $thisfee;
262 // Update inventory and create the sale line item.
263 sqlStatement("UPDATE drug_inventory SET " .
264 "on_hand = on_hand - ? " .
265 "WHERE inventory_id = ?", array($thisqty,$inventory_id));
266 $sale_id = sqlInsert(
267 "INSERT INTO drug_sales ( " .
268 "drug_id, inventory_id, prescription_id, pid, encounter, user, sale_date, quantity, fee, pricelevel, selector ) " .
269 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
270 array($drug_id, $inventory_id, $prescription_id, $patient_id, $encounter_id, $user,
271 $sale_date,
272 $thisqty,
273 $thisfee,
274 $pricelevel,
275 $selector)
278 // If this sale exhausted the lot then auto-destroy it if that is wanted.
279 if ($row['on_hand'] == $thisqty && !empty($GLOBALS['gbl_auto_destroy_lots'])) {
280 sqlStatement(
281 "UPDATE drug_inventory SET " .
282 "destroy_date = ?, destroy_method = ?, destroy_witness = ?, destroy_notes = ? " .
283 "WHERE drug_id = ? AND inventory_id = ?",
284 array($sale_date, xl('Automatic from sale'), $user, "sale_id = $sale_id",
285 $drug_id,
286 $inventory_id)
291 /*******************************************************************
292 // If appropriate, generate email to notify that re-order is due.
293 if (($total_on_hand - $quantity) <= $rowdrug['reorder_point']) {
294 send_drug_email("Product re-order required",
295 "Product '" . $rowdrug['name'] . "' has reached its reorder point.\n");
297 // TBD: If the above is un-commented, fix it to handle the case of
298 // $GLOBALS['gbl_min_max_months'] being true.
299 *******************************************************************/
301 // If combining is allowed then $sale_id will be just the last inserted ID,
302 // and it serves only to indicate that everything worked. Otherwise there
303 // can be only one inserted row and this is its ID.
304 return $sale_id;
307 // Determine if facility and warehouse restrictions are applicable for this user.
308 function isUserRestricted($userid = 0)
310 if (!$userid) {
311 $userid = $_SESSION['authUserID'];
314 $countrow = sqlQuery("SELECT count(*) AS count FROM users_facility WHERE " .
315 "tablename = 'users' AND table_id = ?", array($userid));
316 return !empty($countrow['count']);
319 // Check if the user has access to the given facility.
320 // Do not call this if user is not restricted!
321 function isFacilityAllowed($facid, $userid = 0)
323 if (!$userid) {
324 $userid = $_SESSION['authUserID'];
327 $countrow = sqlQuery(
328 "SELECT count(*) AS count FROM users_facility WHERE " .
329 "tablename = 'users' AND table_id = ? AND facility_id = ?",
330 array($userid, $facid)
332 if (empty($countrow['count'])) {
333 $countrow = sqlQuery(
334 "SELECT count(*) AS count FROM users WHERE " .
335 "id = ? AND facility_id = ?",
336 array($userid, $facid)
338 return !empty($countrow['count']);
341 return true;
344 // Check if the user has access to the given warehouse within the given facility.
345 // Do not call this if user is not restricted!
346 function isWarehouseAllowed($facid, $whid, $userid = 0)
348 if (!$userid) {
349 $userid = $_SESSION['authUserID'];
352 $countrow = sqlQuery(
353 "SELECT count(*) AS count FROM users_facility WHERE " .
354 "tablename = 'users' AND table_id = ? AND facility_id = ? AND " .
355 "(warehouse_id = ? OR warehouse_id = '')",
356 array($userid, $facid, $whid)
358 if (empty($countrow['count'])) {
359 $countrow = sqlQuery(
360 "SELECT count(*) AS count FROM users WHERE " .
361 "id = ? AND default_warehouse = ?",
362 array($userid, $whid)
364 return !empty($countrow['count']);
367 return true;
370 // Determine if this product is one that we have on hand and that the user has permission for.
372 function isProductSelectable($drug_id)
374 $is_user_restricted = isUserRestricted();
375 $wfres = sqlStatement(
376 "SELECT di.warehouse_id, lo.option_value AS facid " .
377 "FROM drug_inventory AS di " .
378 "LEFT JOIN list_options AS lo ON lo.list_id = 'warehouse' AND " .
379 "lo.option_id = di.warehouse_id AND lo.activity = 1 " .
380 "WHERE di.drug_id = ? AND di.destroy_date IS NULL AND di.on_hand > 0 AND " .
381 "(di.expiration IS NULL OR di.expiration > NOW())",
382 array($drug_id)
384 while ($wfrow = sqlFetchArray($wfres)) {
385 if ($is_user_restricted && !isWarehouseAllowed($wfrow['facid'], $wfrow['warehouse_id'])) {
386 continue;
388 return true;
390 return false;