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?

Hey @raphael

Sorry for the delay in responding, I’m a different team member than the one that originally commented. The issue with Roland’s solution in our implementation is that we don’t want to give our staff any kind of admin access, which would be necessary for the “Track activity” step of the solution.

Instead, we want to let students use a single bride scanner, not connected to anything, for each tool. So, (we call the scanners MACs so that’s what I’m going to refer to them as here) one MAC would have an inventory of like 10 drills, a student could scan the MAC if a drill is available and book it so now there’s only 9 drills. Then if a student returned a drill they could scan the MAC and press return thus updating the number available. More likely we’ll have our staff scan the MAC and then hand the students the item but either way it would be a similar integration.

I’m honestly not sure if this kind of a solution is possible with the current Fabman architecture, either way I’d love to hear from you as soon as you can about it, thanks.

I’m pretty sure you could build something like this using Fabman’s API, but it’s not trivial.
@roland has further extended his rental system lately. It’s pretty fancy – but geared towards Happylab’s needs. He might be able to tell you whether it could fit what you’re looking for.