Tool check-out/locker integration

Hi there! I was wondering if there are already implementations for hand-tool checkout/check in. I’d assume each tool would need an RFID tag attached to it, but if there’s already a similar implementation for a similar purpose, I’d much more easily be able to modify it to our purposes.

If not, what do you think would be the best practice for an expandable system that would work off yours?

Hi,

We at the Happylab also thought long and hard about how we could make a rental solution with Fabman. We solved it like this:

  • We have created our own equipment category “Rental Items”.
  • Each rental item is created as equipment and assigned to the “Rental Items” category. For a better overview, we have put the rental items in a separate space.
  • The billing information is stored in the metadata of the equipment.
  • Rental fees are billed automatically via a webhook. Details on this below.

Example and Process

Here is an example of the rental item called “Universal Profile Cutter Set” (I think it’s self-explanatory what the individual parameters of the metadata mean):

Rental

To rent an item, proceed as follows:

  • Open the equipment details and click on “Track activity”.

  • The current time is already entered as the start of borrowing (and can be changed if necessary). Then confirm with “Save”.

  • In the overview you can see which items are or were on loan and when.

Return

  • Simply select the article bar in the overview and click on “edit”.

image

  • Select “Set end date”. The current time is automatically suggested and can be changed if necessary. Complete the return with “Save changes”.

  • You can check the billing immediately on the affected user’s charges tab.

Billing via Webhook

We have created a webhook with the following parameters:

  • URL: https:///ChargeRental.php?active=1&resourcecategory=&secret=
  • Descriptive label: Equipment Rental
  • Kinds of events: Activity Log

Source Code in PHP (feel free to use it and adapt it to your needs, no support, no warranty):

if ($_GET['secret'] != $token) {
$token = "<PutYourTokenHere>";
    print "Webhook not executed due to wrong token (url?secret=token)\n";
    exit;
}

if (isset($_GET['active'])) {
    if ($_GET['active'] == "0" or strtolower($_GET['active']) == "false") {
        print "Webhook is not active.\n";
        exit;
    }
}

// only charge for eqipments of the right resource category        
if (isset($_GET['resourcecategory'])) {
    $resourcecategory = $_GET['resourcecategory'];
} else {
    echo "resourcecategory has to be set like: ?resourcecategory=237\n";
    exit;
}

$obj = json_decode(file_get_contents("php://input"));

var_dump($obj);	
echo "\n\n";

$APIurl = "https://fabman.io/api/v1/";

if ($obj->{'details'}->{'resource'}->{'type'} != $resourcecategory) {
    echo "resourcecategory is ".$obj->{'details'}->{'resource'}->{'type'}." but should be $resourcecategory\n";
    echo "Resource is not a rental item.\n";
    exit;
} 

if (!isset($obj->{'details'}->{'log'}->{'stopType'})) {
	echo "stopType not set.\n";
    exit;
}

// read accounting parameter from metadata
$metadata = $obj->{'details'}->{'resource'}->{'metadata'};
var_dump($metadata);

echo ($obj->{'details'}->{'log'}->{'createdAt'}."\n");
$start = new DateTime($obj->{'details'}->{'log'}->{'createdAt'}, new DateTimeZone("UTC"));
$start->setTimezone(new DateTimeZone("Europe/Vienna"));

echo ($obj->{'details'}->{'log'}->{'stoppedAt'}."\n");
$stop = new DateTime($obj->{'details'}->{'log'}->{'stoppedAt'}, new DateTimeZone("UTC"));
$stop->setTimezone(new DateTimeZone("Europe/Vienna"));

$duration = strtotime($obj->{'details'}->{'log'}->{'stoppedAt'}) - strtotime($obj->{'details'}->{'log'}->{'createdAt'});
echo ("Dauer: $duration\n");

$accountingUnits = ceil($duration / $metadata->{'AccountingUnit'}->{'Seconds'});
echo ("accountingUnits: $accountingUnits\n");

$price = $metadata->{'Pricing'}->{'PricePerTransaction'} + $accountingUnits * $metadata->{'Pricing'}->{'PricePerUnit'};

if (isset($metadata->{'DisplayUnit'})) {       
    $unitName = $metadata->{'DisplayUnit'}->{'Name'};
    $units = round($accountingUnits * $metadata->{'AccountingUnit'}->{'Seconds'} / $metadata->{'DisplayUnit'}->{'Seconds'},4);
    $unitPrice = $metadata->{'Pricing'}->{'PricePerUnit'} * $metadata->{'DisplayUnit'}->{'Seconds'} / $metadata->{'AccountingUnit'}->{'Seconds'};
} else {
    $unitName = $metadata->{'AccountingUnit'}->{'Name'};
    $units = $accountingUnits;
    $unitPrice = $metadata->{'Pricing'}->{'PricePerUnit'};
} 

$cha_text = $units." ".$unitName." á € ".number_format($unitPrice,2,",",".")." - ".$obj->{'details'}->{'resource'}->{'name'}." (".$start->format("Y-m-d H:i")." - ".$stop->format("Y-m-d H:i").")";
   
# delete previous charges on update
deleteAllChargesFromActivity($obj->{'details'}->{'log'}->{'id'});

# create charge
if (isset($metadata->{'Pricing'}->{'MaxPrice'})) {
    $price = min($price, $metadata->{'Pricing'}->{'MaxPrice'});
}
if ($price > 0) 
{
    $usr_fabman_id = $obj->{'details'}->{'log'}->{'member'};
    echo ("$cha_text : ".$price);
    createCharge($usr_fabman_id, substr($obj->{'details'}->{'log'}->{'stoppedAt'}, 0, 19), $cha_text, $price, false, $obj->{'details'}->{'log'}->{'id'});		
    echo " -> Charge in Fabman created.\n";
}


/* Functions */

function callAPI($method, $url, $data = false)
{
	global $APIurl;
	$url = $APIurl . $url;
	
	#echo "CALL API: $url\n";
	$path_to_fabman_cookie = "fabmancookie";
	$curl = curl_init(); 
	curl_setopt($curl, CURLOPT_COOKIEJAR, $path_to_fabman_cookie);
	curl_setopt($curl, CURLOPT_COOKIEFILE, $path_to_fabman_cookie); 
	
	// ignore SSL Zertifikat
	curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
	curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

	switch ($method)
	{
		case "POST":
			curl_setopt($curl, CURLOPT_POST, 1);

			if ($data) {
				curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
				curl_setopt($curl, CURLOPT_HTTPHEADER, array(
					'Content-Type: application/json',
					'Content-Length: ' . strlen($data))
				);                                                                                                                   
			}
			break;

		case "PUT":
			curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT");
			
			if ($data) {
				curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
				curl_setopt($curl, CURLOPT_HTTPHEADER, array(
					'Content-Type: application/json',
					'Content-Length: ' . strlen($data))
				);                                                                                                                   
			}
			break;

		case "DELETE":
			curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "DELETE");
			break;
			
		default:
			if ($data)
				$url = sprintf("%s?%s", $url, http_build_query($data));
	}

	#print ($url);
	curl_setopt($curl, CURLOPT_URL, $url);
	curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

	$result = curl_exec($curl);
	$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
	
	curl_close($curl);

	return array("http_code" => $http_code, "data" => json_decode($result));
}

function createCharge($member,$date,$description,$price,$taxPercent,$resourceLogId = NULL) {
    if (strlen($date) === 10) {
        $date .= "T00:00:00";
    }
    
	fabman_login($fabman_user,$fabman_password);
    if ($taxPercent) {
        $data = array("member" => $member, "dateTime" => $date, "description" => $description, "price" => $price, "taxPercent" => $taxPercent);            
    } else {
        $data = array("member" => $member, "dateTime" => $date, "description" => $description, "price" => $price);                        
    }    
	if ($resourceLogId != NULL) {
		$data['resourceLog'] = $resourceLogId;
	}
	$result = callAPI("POST", "charges", json_encode($data));
	return $result; // error if $result['http_code'] != 200
}

function deleteAllChargesFromActivity($resourceLog) {
    global $obj;

    $result = callAPI("GET", "charges?limit=50&resourceLog=$resourceLog&onlyUninvoiced=true");

    if ($result['http_code'] != 200) { // error if $result['http_code'] != 200
        echo "ERROR: http_code = ".$result['http_code']."\n";
        return false;
    } else {
        foreach ($result['data'] as $charge) {
            deleteCharge($charge->{'id'});
        }
    }
}
1 Like

Absolute cheers and many thanks for the quick and detailed response, this is way more perfect than I could have ever hoped for!

We have a similar situation where we want students (a university) to be able to check out specific tools/equipment (DVMs, Calipers etc.) and then return them within a specific time (our open/close hours). I would like to link these tools to a KIOSK type MAC so that the student can scan in, get the tool from our tool crib via our staff, use the tool, return it with scanning it back in via the KIOSK MAC. My first thought was to assign the MAC to each of the tools but this seems to be rather difficult to do. Any suggestions on how to do this? (we do not rent for dollars, our equipment - we are a University Maker Space, and, we also have lockers :slight_smile: that would be nice to have on Fabman the same as the tools.)

Hey @tiw,

what prevents you from using Roland’s approach without the webhook and let you staff start the activity when they hand out the tool / rent out the locker?