Welcome to FabLabKasse’s documentation!¶
Contents:
A Brief History of Cash-Acceptance¶
Prelude¶
The FAU FabLab is a universitarian grassroot FabLab at the Univeristy of Erlangen-Nuremberg in Germany, run by volunteers but with partial initial funding through the university. Students and any interested party can come by and make use of the FabLab in their free time or for class work and research. Since the upkeep of machines and tools, as well as the needed materials are not financed by the university, users have to pay for usage.
First Act¶
With a total of 1291 individual products for sale, you can imagine how messy it gets to handle and supervise finances and operations. We started with hand-labeling all products and trust users to pay the right amount into an opened cash box. But how could we know if somebody would steal money from the lab?
Second Act¶
Next, we added a handwritten cash journal and asked users to write down the paid amounts. But with this came extra work, since I had to copy that paper over to an excel sheet and check the sums, roughly twice a week. Obviously people made mistakes and payed a little more or less, but it always evened out over a couple of days and we did not notice any theft. But with 1.000+ products, how do we keep track of what is being sold, you might ask. We did not. You might also ask, did you not get tired of typewriting endless lists of numbers and counting cash? For sure I did!
Therefore, we started implementing a touchscreen-based sales-terminal, which would replace the handwritten lists and know about all the 1.000+ products. It took some time, lots of python code and caffeinated drinks, but finally we had it. No more typing, but still all the counting of coins and bills.
Third Act¶
As time progressed, we were quite happy by all the automated tabulation and statistics. Until we noticed that 100 Euros were missing… We never figured it out, so we must assume that it was stolen out of the open cash box. What to do?
Fourth Act¶
We already had the basic software at hand, but we needed hardware. Hardware to discourage people from stealing. Since weaponizing our cash box would not have been in line with the safety aspects of the Fab Charter nor the UN Declaration of Human Rights, we needed to take a look at less drastic measures. With a FabLab at hand, we began designing and building a FabATM, or for long-word-loving-germans: ein Besucherabkassiermaschinenautomat. For real: it is called kassenterminal, which means payment terminal.
Sixth Act¶
We have built an open-soure point-of-sale terminal, which has been in everyday operation for almost a year. It counts coins, bills, returns change, prints official receipts and is completely capable of self-service. It tracks sales and will soon accept electronic payments and tell us what to restock. No more theft has been detected and endless hours of counting coins and typing numbers have been abolished.
If you are interested in not reenacting our story, have a look at the following GitHub-projects, which contain all the information necessary to build your own:
- Software: https://github.com/fau-fablab/FabLabKasse
- Wooden case: https://github.com/fau-fablab/kassenautomat.CAD
- Interface circuit board: https://github.com/fau-fablab/kassenautomat.mdb-interface
The cash-devices we used (for counting, verifying and returning bills and coins) are connected via an industry-standard interface and can be replaced with other such devices. They can also be ignored, if the use of an open cash box or a drop-in-only cash box (without change) is to be used. The software is already equipped for this. Have fun and keep your bean counters and bank accounts happy by using and helping in developing our automated cash system!
Sequel¶
At our university there is a class in which mobile apps are developed, called MAD (mobile application development). MAD developed for us an app that works together with the kassenterminal. You can select products that are stored in our ERP and then send them to the kassenterminal in a checkout process.
The app is released as iOS-Version, as Android-version and as an HTML-app. In the background works a server-software that handles the aggregation of the product database and the transfer to the kassenterminal.
And since you managed to read until here, have a picture of the kassenterminal as a reward:

(Unfortunately without banana for scale. However, the folder is DIN A4.)
API reference¶
Subpackages¶
UI
: User Interface components and dialogs¶
Subpackages¶
uic_generated
: autogenerated UI code¶
Submodules¶
FabLabKasse.UI.ClientDialogCode module¶
FabLabKasse.UI.CheckCartAfterImportDialogCode module¶
- ..automodule:: FabLabKasse.UI.CheckCartAfterImportDialogCode
members: undoc-members: show-inheritance:
FabLabKasse.UI.GUIHelper module¶
FabLabKasse.UI.KeyboardDialogCode module¶
FabLabKasse.UI.LoadFromMobileAppDialogCode module¶
FabLabKasse.UI.MyQLineEdit module¶
FabLabKasse.UI.CartTableView module¶
FabLabKasse.UI.PaymentMethodDialogCode module¶
FabLabKasse.UI.PayupCashDialogCode module¶
FabLabKasse.UI.PayupManualDialogCode module¶
FabLabKasse.UI.compile_all module¶
Module contents¶
cash_payment
¶
Infrastructure for accepting real coins and banknotes, including logging.
Subpackages¶
FabLabKasse.cashPayment.client package¶
Client for accessing a cash device driver.
-
class
FabLabKasse.cashPayment.client.PaymentDeviceClient.
PaymentDeviceClient
(cmd, options)[source]¶ Bases:
object
Client for accessing a cash device driver. It starts a new python process (“server”) for the specified device driver. It uses non-blocking communication and talks to the server process using stdin/stdout.
-
accept
(maximumPayin)[source]¶ accept up to maximumPayin money, until stopAccepting() is called
poll() must be called before other actions are taken
-
canAccept
()[source]¶ does the device support accept commands?
(If this function has not returned True/False once before, it may only be called while no operation is in progress and will raise an Exception otherwise. )
return values and usage:
- None: please call the function again later. The answer has not yet
been received from the device.
No other actions (dispense/accept/possibleDispense) may be called until
a non-None value was returned!
call poll() repeatedly until
canAccept() != None
- True/False: Does (not) support accepting. (Now the answer is cached and may the function may be called again
- always)
Return type: boolean | None - None: please call the function again later. The answer has not yet
been received from the device.
No other actions (dispense/accept/possibleDispense) may be called until
a non-None value was returned!
call poll() repeatedly until
-
dispense
(amount)[source]¶ Dispense up to the requested amount of money (as much as possible)
- Wait until hasStopped() is true, then retrieve the paid out value with getFinalAmountAndReset()
- An intermediate value (as a progess report) can be retrieved with getCurrentAmount, but the operation cannot
- be aborted.
- If you want to make sure that enough is available, see possibleDispense()
-
empty
()[source]¶ start service-mode emptying
The implementation of this modes is device specific:
- If the device has an inaccessible storage, it should move the contents to the cashbox so that it can be taken out for counting.
- If available, manual payout buttons are enabled.
usage:
- call empty()
- sleep, do something else, whatever you want…
- call poll() at least once before the next step:
- as soon as you want to stop, call stopEmptying()
- call hasStopped() until it returns True
- then call getFinalAmountAndReset()
-
getCurrentAmount
()[source]¶ how much has currently been paid in? (value is not always up-to-date, but will not be higher than the actual value)
-
getFinalAmountAndReset
()[source]¶ call this as soon as hasStopped() is true. this returns the final amount paid in/out (negative for payout)
-
poll
()[source]¶ update internal status
call this regularly
Raise: Exception if the device crashed - do not try to recover from this exception, or the result of any following calls will be undefined
-
possibleDispense
()[source]¶ how much can be paid out?
(function may only be called while no operation is in progress, will raise Exception otherwise)
return value:
None
: request in progress, please call the function again until it does not return None. No other actions (dispense/accept/canPayout) may be called until a non-None value was returned! Call poll() repeatedly until possibleDispense()!=None.- [maximumAmount, remainingAmount]: This one non-None response is not cached, another call will send return None again and send a new query to the device
- maximumAmount (int): the device has enough money to pay out any amount up to maximumAmount
- remainingAmount (int): How much money could be remaining at worst, if canBePaid==True? This is usually a per-device constant. remainingAmount will be == 0 for a small-coins dispenser that includes 1ct.
Important
it can be still possible to payout more, but not any value above maximumAmount!
For example a banknote dispenser filled with 2*10€ and 5*100€ bills will return:
possibleDispense() == [2999, 999]
which means “can payout any value in 0…29,99€ with an unpaid rest of <= 9,99€”But it can still fulfill a request of exactly 500€!
Return type: None | [int, int]
-
-
class
FabLabKasse.cashPayment.client.PaymentDevicesManager.
PaymentDevicesManager
(cfg)[source]¶ Bases:
object
-
canPayout
()[source]¶ returns values [totalMaximumRequest, totalRemaining]:
every requested amount <= totalMaximumRequest can be paid out, with an unpaid rest <= totalRemaining (the return value is only a conservative estimate, not the theoretical optimum)
Please warn the user if totalMaximumRequest is too low for the possible change
if this function returns None, the value is still being fetched. In this case, sleep some time, then call poll() and then call the function again.
-
getFinalAmount
()[source]¶ if stopped, return the final amount and reset to idle state
else, return None
-
Server (device driver)¶
-
class
FabLabKasse.cashPayment.server.NV11.NV11Device.
ESSPDevice
(port, presharedKey=81985526925837671, slaveID=0)[source]¶ Bases:
object
low layer eSSP protocol - implements the network layer and all communication-related commands
-
class
Helper
[source]¶ Bases:
object
-
CRC
= <FabLabKasse.cashPayment.server.NV11.crc_algorithms.Crc object>¶
-
-
class
Response
(data)[source]¶ Bases:
object
status + data
-
statusStrings
= {-1: 'decoded response contains no data', 126: 'Encrypted Data', 240: 'OK', 242: 'Command not known', 243: 'Wrong number of parameters', 244: 'Param out of range', 245: 'Command cannot be processed at this time, possibly busy or not correctly configured', 246: 'Software error', 248: 'Command failure', 250: 'Encryption key not set'}¶
-
-
class
-
class
FabLabKasse.cashPayment.server.NV11.NV11Device.
NV11Device
(port, presharedKey=81985526925837671, slaveID=0)[source]¶ Bases:
FabLabKasse.cashPayment.server.NV11.NV11Device.ESSPDevice
Interface client for Innovative Technology NV11 banknote validator/changer with eSSP Protocol
-
getPayoutValues
()[source]¶ get values of notes on payout stack. The last one of these is on top of the stack and will be paid out by the payout-command.
-
setEnabledChannels
(enabledChannels=None, upTo=0)[source]¶ enable cash input for certain channels (denominations). Use either of the two parameters. If both are used, the result will be combined with logical
or
.Parameters: - enabledChannels (list[int]) – IDs of channels to explicitly enable, even if their denominaion is
below the value of
upTo
. - upTo (int) – maximum allowed denomination, or 0
- enabledChannels (list[int]) – IDs of channels to explicitly enable, even if their denominaion is
below the value of
-
setRouteToPayout
(values)[source]¶ route all notes in the given list of values to the payout-store. others will be directly put to the cashbox and are not availble for return.
-
CRC algorithms implemented in Python. If you want to study the Python implementation of the CRC routines, then this is a good place to start from.
The algorithms Bit by Bit, Bit by Bit Fast and Table-Driven are implemented.
This module can also be used as a library from within Python.
This is an example use of the different algorithms:
>>> from crc_algorithms import Crc
>>>
>>> crc = Crc(width=16, poly=0x8005,
... reflect_in=True, xor_in=0x0000,
... reflect_out=True, xor_out=0x0000)
>>> print("0x%x" % crc.bit_by_bit("123456789"))
>>> print("0x%x" % crc.bit_by_bit_fast("123456789"))
>>> print("0x%x" % crc.table_driven("123456789"))
-
class
FabLabKasse.cashPayment.server.NV11.crc_algorithms.
Crc
(width, poly, reflect_in, xor_in, reflect_out, xor_out, table_idx_width=None)[source]¶ Bases:
object
A base class for CRC routines.
-
bit_by_bit
(in_data)[source]¶ Classic simple and slow CRC implementation. This function iterates bit by bit over the augmented input message and returns the calculated CRC value at the end.
-
bit_by_bit_fast
(in_data)[source]¶ This is a slightly modified version of the bit-by-bit algorithm: it does not need to loop over the augmented bits, i.e. the Width 0-bits which are appended to the input message in the bit-by-bit algorithm.
-
helper for stack-based banknote payout systems. see BanknoteStackHelper
-
class
FabLabKasse.cashPayment.server.helpers.banknote_stack_helper.
BanknoteStackHelper
(accepted_rest)[source]¶ Bases:
object
helper class for stack-based banknote payout systems. Such a system has a stack of banknotes from which the top one can be
- either paid out to the client (action “payout”)
- or be irrevocably put away into a cashbox (action “stack”), from where it cannot be retrieved again for payout.
From the programmer’s point of view, this stack is a list of banknotes, from which only the last one (stack.pop()) can be accessed.
This class makes the relevant decisions whether to pay out or stack away the current note. It also offers a matching implementation for
FabLabKasse.cashPayment.server.CashServer.getCanPayout()
Parameters: accepted_rest – see FabLabKasse.cashPayment.server.cashServer.CashServer.getCanPayout()
-
class
FabLabKasse.cashPayment.server.helpers.banknote_stack_helper.
BanknoteStackHelperTest
(methodName='runTest')[source]¶ Bases:
unittest.case.TestCase
Tests the banknote stack helper class
-
class
FabLabKasse.cashPayment.server.helpers.banknote_stack_helper.
BanknoteStackHelperTester
(accepted_rest)[source]¶ Bases:
FabLabKasse.cashPayment.server.helpers.banknote_stack_helper.BanknoteStackHelper
unittest methods for BanknoteStackHelper
-
classmethod
get_random_payout_parameters
(random_generator, payout_stack=None, requested_payout=None)[source]¶ determine parameters for payout_stack and requested_payout
Parameters: random_generator (random.Random) – RNG instance for calculating pseudorandom test parameters
-
unittest_payout
(random_generator)[source]¶ test one random set of parameters for BanknoteStackHelper.can_payout(), BanknoteStackHelper.get_next_payout_action()
Parameters: random_generator (random.Random) – RNG instance for calculating pseudorandom test parameters Return type: None Raise: AssertionError if the test failed
-
unittest_payout_forced_stacking
(random_generator)[source]¶ test one random set of parameters for BanknoteStackHelper._forced_stacking_is_helpful()
Parameters: random_generator (random.Random) – RNG instance for calculating pseudorandom test parameters Return type: None Raise: AssertionError if the test failed
-
classmethod
helper functions for multi-tube coin dispensers
-
FabLabKasse.cashPayment.server.helpers.coin_payout_helper.
get_possible_payout
(coins, max_number_of_coins=15)[source]¶ get possible amount of payout and remaining rest
This implementation returns a lower bound. This means that the theoretical maximum possible amount can be higher (and will be often).
Parameters: - coins (list[(int, int)]) – list of tuples
(value, count)
, sorted descending by value. The function still works if a value occurs twice (e.g. if you have a dispenser with two separate tubes for 1€ coins). - max_number_of_coins (int) –
approximate upper limit for the number of coins - this limits the reporting of the maximum possible amount of payout, so that a device won’t say it is able to pay out 50€, if that is only possible with 500x 0,10€ coins.
Please note that if you limit this too much, the user will be warned that there is not enough change money. Some amount is necessary, especially in cooperation with other devices (e.g. banknotes + coins).
Returns: as defined by
FabLabKasse.cashPayment.server.getCanPayout()
- coins (list[(int, int)]) – list of tuples
tests for coin_payout_helper
-
class
FabLabKasse.cashPayment.server.helpers.test_coin_payout_helper.
CoinPayoutHelperTestcase
(methodName='runTest')[source]¶ Bases:
unittest.case.TestCase
Tests for
coin_payout_helper
-
test_get_possible_payout
(coins=HypothesisProvided(value=st_coins()), requested_fraction=HypothesisProvided(value=floats(min_value=0, max_value=1)), coin_limit_fraction=HypothesisProvided(value=floats(min_value=0, max_value=1)))[source]¶ test
coin_payout_helper.get_possible_payout()
for a given state of available coins
-
-
FabLabKasse.cashPayment.server.helpers.test_coin_payout_helper.
simulate_payout
(coins, requested)[source]¶ get payout amount for a very simple simulated payout strategy (‘greedy strategy’, just pay out largest coins first, without ‘coin splitting’ to get rid of smaller ones)
Parameters: - coins (list[(int, int)]) – see
coin_payout_helper.get_possible_payout()
- requested (int) – requested amount
- coins (list[(int, int)]) – see
-
exception
FabLabKasse.cashPayment.server.mdbCash.mdb.
InterfaceHardwareError
[source]¶ Bases:
exceptions.Exception
-
class
FabLabKasse.cashPayment.server.mdbCash.mdb.
MdbCashDevice
(port, addr=1, extensionConfig=None)[source]¶ Bases:
object
-
ACK
= 0¶
-
BUSY
= 'busy'¶
-
CMD_COIN_TYPE
= 4¶
-
CMD_DISPENSE
= 5¶
-
CMD_EXPANSION
= 7¶
-
CMD_POLL
= 3¶
-
CMD_RESET
= 0¶
-
CMD_SETUP
= 1¶
-
CMD_TUBE_STATUS
= 2¶
-
ERROR
= 'error'¶
-
IGNORE
= 'ignore'¶
-
JUST_RESET
= 'just reset'¶
-
NAK
= 255¶
-
RET
= 170¶
-
WARNING
= 'warning'¶
-
extensionCmd
(data)[source]¶ in addition to the MDB commands, the interface hardware provides extension commands for other features (LEDs, hopper, …). Failure on these commands is not tolerated.
-
poll
(wasJustReset=False)[source]¶ get events from device. :param wasJustReset: set this to True at the first poll after the RESET command
-
setLEDs
(leds)[source]¶ set RGB-LED color via extension command, if it is enabled in the extensionConfig. :param leds: list of two LED color values. color value: RR GG BB in hex plus a mode of N (normal) or special modes B (blink) or T (timeout: switch off after 20 sec) e.g. “00FF00N” = green normal, “FF0000B” = red blink, “0000FFT” = blue with timeout (will switch off after 20sec or the next command)
-
statusEvents
= {1: ['Escrow request', 'ignore'], 2: ['Payout Busy', 'busy'], 3: ['valid coin did not get to the place where credit is given', 'warning'], 4: ['Defective Tube Sensor', 'warning'], 5: ['Double Arrival', 'ignore'], 6: ['Acceptor unplugged', 'error'], 7: ['Tube jam', 'warning'], 8: ['ROM checksum error', 'error'], 9: ['coin routing error', 'error'], 10: ['Busy', 'busy'], 11: ['Was just reset', 'just reset'], 12: ['Coin jam', 'warning'], 13: ['Possible credited coin removal', 'warning']}¶
-
Submodules¶
cashState
: logging¶
This module also has a command-line interface that can be accessed as ./cash
from the FabLabKasse directory.
FabLabKasse.cashPayment.listSerialPorts module¶
Module contents¶
Libraries¶
FabLabKasse.libs
Subpackages¶
FabLabKasse.libs.escpos package¶
ESC/POS Commands (Constants)
@author: Manuel F Martinez <manpaz@bashlinux.com> @organization: Bashlinux @copyright: Copyright (c) 2012 Bashlinux @license: GPL
ESC/POS Exceptions classes
-
exception
FabLabKasse.libs.escpos.exceptions.
Error
(msg, status=None)[source]¶ Bases:
exceptions.Exception
Base class for ESC/POS errors
@author: Manuel F Martinez <manpaz@bashlinux.com> @organization: Bashlinux @copyright: Copyright (c) 2012 Bashlinux @license: GPL
-
class
FabLabKasse.libs.escpos.printer.
File
(devfile='/dev/usb/lp0')[source]¶ Bases:
FabLabKasse.libs.escpos.escpos.Escpos
Define Generic file printer
-
class
FabLabKasse.libs.escpos.printer.
Network
(host, port=9100)[source]¶ Bases:
FabLabKasse.libs.escpos.escpos.Escpos
Define Network printer
FabLabKasse.libs.flickcharm package¶
FabLabKasse.libs.process_helper package¶
-
class
FabLabKasse.libs.process_helper.asyncproc.
Process
(*params, **kwparams)[source]¶ Bases:
object
Manager for an asynchronous process. The process will be run in the background, and its standard output and standard error will be collected asynchronously.
Since the collection of output happens asynchronously (handled by threads), the process won’t block even if it outputs large amounts of data and you do not call Process.read*().
Similarly, it is possible to send data to the standard input of the process using the write() method, and the caller of write() won’t block even if the process does not drain its input.
On the other hand, this can consume large amounts of memory, potentially even exhausting all memory available.
Parameters are identical to subprocess.Popen(), except that stdin, stdout and stderr default to subprocess.PIPE instead of to None. Note that if you set stdout or stderr to anything but PIPE, the Process object won’t collect that output, and the read*() methods will always return empty strings. Also, setting stdin to something other than PIPE will make the write() method raise an exception.
-
kill
(signal)[source]¶ Send a signal to the process. Raises OSError, with errno set to ECHILD, if the process is no longer running.
-
pid
()[source]¶ Return the process id of the process. Note that if the process has died (and successfully been waited for), that process id may have been re-used by the operating system.
-
readboth
()[source]¶ Read data written by the process to its standard output and error. Return value is a two-tuple ( stdout-data, stderr-data ).
WARNING! The name of this method is ugly, and may change in future versions!
-
terminate
(graceperiod=1)[source]¶ Terminate the process, with escalating force as needed. First try gently, but increase the force if it doesn’t respond to persuassion. The levels tried are, in order:
- close the standard input of the process, so it gets an EOF.
- send SIGTERM to the process.
- send SIGKILL to the process.
terminate() waits up to GRACEPERIOD seconds (default 1) before escalating the level of force. As there are three levels, a total of (3-1)*GRACEPERIOD is allowed before the process is SIGKILL:ed. GRACEPERIOD must be an integer, and must be at least 1.
If the process was started with stdin not set to PIPE, the first level (closing stdin) is skipped.
-
wait
(flags=0)[source]¶ Return the process’ termination status.
If bitmask parameter ‘flags’ contains os.WNOHANG, wait() will return None if the process hasn’t terminated. Otherwise it will wait until the process dies.
It is permitted to call wait() several times, even after it has succeeded; the Process instance will remember the exit status from the first successful call, and return that on subsequent calls.
-
-
FabLabKasse.libs.process_helper.asyncproc.
with_timeout
(timeout, func, *args, **kwargs)[source]¶ Call a function, allowing it only to take a certain amount of time.
param timeout: The time, in seconds, the function is allowed to spend. This must be an integer, due to limitations in the SIGALRM handling. param func: The function to call. param *args: Non-keyword arguments to pass to func. param **kwargs: Keyword arguments to pass to func. Upon successful completion, with_timeout() returns the return value from func. If a timeout occurs, the Timeout exception will be raised.
If an alarm is pending when with_timeout() is called, with_timeout() tries to restore that alarm as well as possible, and call the SIGALRM signal handler if it would have expired during the execution of func. This may cause that signal handler to be executed later than it would normally do. In particular, calling with_timeout() from within a with_timeout() call with a shorter timeout, won’t interrupt the inner call. I.e., with_timeout(5, with_timeout, 60, time.sleep, 120)` won’t interrupt the time.sleep() call until after 60 seconds.
-
FabLabKasse.libs.process_helper.nonblockingProcess.
demo
()[source]¶ small example that communicates with bc, the commandline calculator. Note that readline() never blocks!
FabLabKasse.libs.pxss package¶
-
class
FabLabKasse.libs.pxss.pxss.
Display
[source]¶ Bases:
_ctypes.Structure
-
bitmap_bit_order
¶ Structure/Union member
-
bitmap_pad
¶ Structure/Union member
-
bitmap_unit
¶ Structure/Union member
-
byte_order
¶ Structure/Union member
-
db
¶ Structure/Union member
-
default_screen
¶ Structure/Union member
-
display_name
¶ Structure/Union member
-
ext_data
¶ Structure/Union member
-
fd
¶ Structure/Union member
-
last_request_read
¶ Structure/Union member
-
max_request_size
¶ Structure/Union member
-
nformats
¶ Structure/Union member
-
nscreens
¶ Structure/Union member
-
pixmap_format
¶ Structure/Union member
-
private1
¶ Structure/Union member
-
private10
¶ Structure/Union member
-
private11
¶ Structure/Union member
-
private12
¶ Structure/Union member
-
private13
¶ Structure/Union member
-
private14
¶ Structure/Union member
-
private15
¶ Structure/Union member
-
private2
¶ Structure/Union member
-
private3
¶ Structure/Union member
-
private4
¶ Structure/Union member
-
private5
¶ Structure/Union member
-
private6
¶ Structure/Union member
-
private8
¶ Structure/Union member
-
private9
¶ Structure/Union member
-
proto_major_version
¶ Structure/Union member
-
proto_minor_version
¶ Structure/Union member
-
qlen
¶ Structure/Union member
-
release
¶ Structure/Union member
-
request
¶ Structure/Union member
-
resource_alloc
¶ Structure/Union member
-
screens
¶ Structure/Union member
-
vendor
¶ Structure/Union member
-
-
class
FabLabKasse.libs.pxss.pxss.
IdleTracker
(when_idle_wait=5000, when_disabled_wait=120000, idle_threshold=60000)[source]¶ Keeps track of idle times, screensaver state, and tells you when you to querying it for the next idle time. All times are in milliseconds. IdleTracker indicates a change in state when your idle time exceeds a certain threshold. See also XSSTracker.
-
check_idle
()[source]¶ suggested_time_till_next_check and idle_time is in milliseconds.
state_change is one of:
- None - No change in state
- “idle” - user is idle (idle time is greater than idle threshold)
- “unidle” - user is not idle (idle time is less than idle threshold)
- “disabled” - idle time not available
Note that “disabled” will be returned every time there is an error. :returns: tuple (state_change, suggested_time_till_next_check, idle_time)
-
-
class
FabLabKasse.libs.pxss.pxss.
Screen
[source]¶ Bases:
_ctypes.Structure
-
backing_store
¶ Structure/Union member
-
black_pixel
¶ Structure/Union member
-
cmap
¶ Structure/Union member
-
default_gc
¶ Structure/Union member
-
depths
¶ Structure/Union member
-
display
¶ Structure/Union member
-
ext_data
¶ Structure/Union member
-
height
¶ Structure/Union member
-
mheight
¶ Structure/Union member
-
min_maps
¶ Structure/Union member
-
mwidth
¶ Structure/Union member
-
ndepths
¶ Structure/Union member
-
root
¶ Structure/Union member
-
root_depth
¶ Structure/Union member
-
root_input_mask
¶ Structure/Union member
-
root_visual
¶ Structure/Union member
-
save_unders
¶ Structure/Union member
-
white_pixel
¶ Structure/Union member
-
width
¶ Structure/Union member
-
-
class
FabLabKasse.libs.pxss.pxss.
XSSTracker
(when_idle_wait=5000, when_disabled_wait=120000)[source]¶ Keeps track of idle times, screensaver state, and tells you when you to querying it for the next idle time. All times are in milliseconds. XSSTracker indicates a change in state when your screensaver activates. See also IdleTracker.
-
check_idle
()[source]¶ suggested_time_till_next_check and idle_time is in milliseconds.
state_change is one of:
- None - No change in state
- “idle” - screensaver has turned on since user is now idle
- “unidle” - screensaver has turned off since user is no longer idle
- “disabled” - screensaver is disabled or extension not present
Note that if the screensaver is disabled, it will return “disabled” every time. :returns: tuple (state_change, suggested_time_till_next_check, idle_time)
-
Submodules¶
FabLabKasse.libs.random_lists module¶
randomly built lists with randomness taken from random.choice()
Warning
not cryptographically secure!
-
FabLabKasse.libs.random_lists.
random_choice_list
(random_generator, possible_elements, number_of_elements)[source]¶ return a random list with len(list)==number_of_elements, list[i] in possible_elements (duplicates are possible)
Parameters: - random_generator (random.Random) – RNG instance
- possible_elements (list) – list elements to choose from
- number_of_elements (int) – length of resulting list
-
FabLabKasse.libs.random_lists.
random_integer_list
(random_generator, integer_range, number_of_elements)[source]¶ return a list of length number_of_elements with elements in the range integer_range[0] <= element <= integer_range[1]
Parameters: - random_generator (random.Random) – RNG instance
- int) integer_range ((int,) – range (min, max) – ends are included
- number_of_elements (int) – length of resulting list
Module contents¶
FabLabKasse.scripts package¶
Submodules¶
FabLabKasse.scripts.logWatchAndCleanup module¶
a cronjob for cleaning up and gzipping old logfiles
- report errors and warnings in the newest logfiles (the current one, foo.log, and the archive files foo.log.2012-12-31 created after the last run of this script [i.e. the non-gzipped ones])
- gzip old logfiles and remove too old ones
run this script by starting logWatchAndCleanup.sh which lives in the same directory
# it is recommended to run this script before midnight, because the logs wrap over at midnight and might change in the middle of running this script
Module contents¶
shopping
: backend for articles, payment etc.¶
Subpackages¶
FabLabKasse.shopping.backend package¶
-
class
FabLabKasse.shopping.backend.legacy_offline_kassenbuch_tools.importProdukteOERP.
cache
(f)[source]¶ Bases:
object
-
FabLabKasse.shopping.backend.legacy_offline_kassenbuch_tools.importProdukteOERP.
importProdukteOERP
(data, oerp, cfg)[source]¶
abstract implementations of shopping and clients
base class and interface definition for specific implementations (see other files in this folder)
-
class
FabLabKasse.shopping.backend.abstract.
AbstractClient
(client_id=None, name='')[source]¶ Bases:
object
a client that can pay by pin
-
class
FabLabKasse.shopping.backend.abstract.
AbstractShoppingBackend
(cfg)[source]¶ Bases:
object
manages products, categories and orders (cart)
-
add_order_line
(prod_id, qty, comment=None)[source]¶ add product to cart
if not all values are allowed,
qty
is rounded up to the next possible amount.The user should only be asked for a comment by the GUI if
self.product_requires_text_entry(prod_id) == True
Parameters: - prod_id (int) – product
- qty (Decimal) – amount of product
- comment ((basestring, None)) – textual comment from the user, or None.
Raise: ProductNotFound
-
get_category_path
(current_category)[source]¶ return the category path from the root to the current category, excluding the root category
[child_of_root, …, parent_of_current, current_category]
Return type: list(Category)
-
get_current_order
()[source]¶ get selected order (or return 0 if switching between multiple orders is not supported)
-
get_current_total
()[source]¶ Returns: total sum of current order Return type: Decimal Note: The internal rounding must be consistent, which is needed by :class: FabLabKasse.shopping.payment_methods. That means that x,xx5 € must always be rounded up or always down. “Fair rounding” like Decimal.ROUND_HALF_EVEN is not allowed.
For example:
- add article costing 1,015 € -> get_current_total == x
- add article costing 0,990 € -> get_current_total == x + 0,99
This would not be true with the fair strategy “round second digit to even value if the third one is exactly 5” (1,02€ and 2,00€).
-
get_products
(current_category)[source]¶ return products in current category
Return type: list(Product)
-
get_subcategories
(current_category)[source]¶ return list(Category) of subclasses of the given category-id.
-
pay_order
(method)[source]¶ store payment of current order to database :param method: payment method object, whose type is used to determine where the order should be stored in the database method.amount_paid - method.amount_returned is how much money was gained by this sale, must be equal to self.get_current_total()
-
pay_order_on_client
(client)[source]¶ charge the order on client’s account
Parameters: client – AbstractClient Raises: DebtLimitExceeded when the client’s debt limit would be exceeded
-
print_receipt
(order_id)[source]¶ print the receipt for a given, already paid order_id
The receipt data must be stored in the backend, because for accountability reasons all receipt texts need to be stored anyway.
-
product_requires_text_entry
(prod_id)[source]¶ when adding prod_id, should the user be asked for a text entry for entering comments like his name?
-
static
round_money
(value)[source]¶ rounds money in Decimal representation to 2 places
Main purpose is shopping.backend.abstract.AbstractShoppingBackend.get_current_total(), since round() does behave weird. But maybe there are other applications too.
Parameters: value (float | Decimal) – an amount of money to be rounded Returns: money, rounded to 2 digits Return type: Decimal >>> AbstractShoppingBackend.round_money(Decimal('0.005')) Decimal('0.01') >>> AbstractShoppingBackend.round_money(Decimal('0.004')) Decimal('0.00')
-
search_from_text
(searchstr)[source]¶ search searchstr in products and categories :return: tuple (list of categories, products for table)
Return type: list(Product)
-
search_product_from_code
(code)[source]¶ search via barcode, PLU or similar unique-ID entry. code may be any string
Returns: product id Raises: ProductNotFound() if nothing found
-
-
class
FabLabKasse.shopping.backend.abstract.
AbstractShoppingBackendTest
(methodName='runTest')[source]¶ Bases:
unittest.case.TestCase
test the AbstractShoppingBackend class
TODO extend this test
-
class
FabLabKasse.shopping.backend.abstract.
Category
(categ_id, name, parent_id=None)[source]¶ Bases:
object
represents a category of Products
-
exception
FabLabKasse.shopping.backend.abstract.
DebtLimitExceeded
[source]¶ Bases:
exceptions.Exception
exception raised by pay_order_on_client: order not paid because the debt limit would have been exceeded
-
class
FabLabKasse.shopping.backend.abstract.
OrderLine
(order_line_id, qty, unit, name, price_per_unit, price_subtotal, delete_if_zero_qty=True)[source]¶ Bases:
object
one order line (roughly equal to a product in a shopping cart, although there may be multiple entries for one product)
Parameters: - id – id of order-line, must be unique and non-changing inside one Order() (if None: autogenerate id)
- qty (Decimal) – amount (“unlimited” number of digits is okay)
- unit (unicode) – product unit of sale
- name (unicode) – product name
- price_per_unit (Decimal) – price for one unit
- price_subtotal (Decimal) – price for
qty
*unit
of this product - delete_if_zero_qty (boolean) –
if the qty is zero and the user starts adding something else, then remove this line
[ usually True, set to False for products that also may as comment limes costing nothing ]
-
exception
FabLabKasse.shopping.backend.abstract.
PrinterError
[source]¶ Bases:
exceptions.Exception
cannot print receipt
-
class
FabLabKasse.shopping.backend.abstract.
Product
(prod_id, name, price, unit, location, categ_id=None, qty_rounding=0, text_entry_required=False)[source]¶ Bases:
object
simple representation for a product
Parameters: - prod_id (int) – numeric unique product ID
- categ_id (int | None) –
category ID of product, or None if the product is not directly visible
TODO hide these products from search, or a more explicit solution
- name (unicode) – Name of product
- location (unicode) – Location of product (shown to the user)
- unit (unicode) – Unit of sale for this product (e.g. piece, kilogram)
- price (Decimal) – price for one unit of this product
- qty_rounding (int | Decimal) –
Product can only be bought in multiples of this quantity, user (GUI) input will be rounded/truncated to the next multiple of this.
Set to 0 so that the product can be bought in arbitrarily small quantities.
example: you cannot buy half a t-shirt, so you set qty_rounding = 1
handling this is responsibility of the shopping backend
-
exception
FabLabKasse.shopping.backend.abstract.
ProductNotFound
[source]¶ Bases:
exceptions.Exception
requested product not found
-
FabLabKasse.shopping.backend.abstract.
float_to_decimal
(number, digits)[source]¶ convert float to decimal with rounding and strict error tolerances
If the given number cannot be represented as decimal with an error within 1/1000 of the last digit,
ValueError
is raised.Parameters: - number (float | Decimal) – a float that is nearly equal to a decimal number
- digits (int) – number of decimal places of the resulting value (max. 9)
Raise: ValueError
>>> float_to_decimal(1.424, 3) Decimal('1.424') >>> float_to_decimal(0.7, 1) Decimal('0.7')
-
FabLabKasse.shopping.backend.abstract.
format_money
(amount)[source]¶ format float as money string
You should best use Decimal as input. TODO: make moneysign interchangeable
Parameters: amount (float|Decimal) – amount of money Returns: amount formatted as string with Euro-Sign Return type: unicode >>> format_money(1.23) u'1,23 \u20ac' >>> format_money(3.741) u'3,741 \u20ac' >>> format_money(42.4242) u'42,424 \u20ac' >>> format_money(5.8899) u'5,89 \u20ac' >>> format_money(Decimal('1.23')) u'1,23 \u20ac' >>> format_money(Decimal('3.741')) u'3,741 \u20ac' >>> format_money(Decimal('42.4242')) u'42,424 \u20ac' >>> format_money(Decimal('5.8899')) u'5,89 \u20ac'
-
class
FabLabKasse.shopping.backend.oerp.
Client
(client_id=None, name='')[source]¶ Bases:
FabLabKasse.shopping.backend.abstract.AbstractClient
oerp implementation of AbstractClient. do not instantiate this yourself, but please rather use Client.from_oerp or ShoppingBackend.list_clients
-
class
FabLabKasse.shopping.backend.oerp.
ShoppingBackend
(cfg)[source]¶ Bases:
FabLabKasse.shopping.backend.abstract.AbstractShoppingBackend
OpenERP implementation of AbstractShoppingBackend
-
add_order_line
(prod_id, qty, comment=None)[source]¶ add product to cart
if not all values are allowed,
qty
is rounded up to the next possible amount.The user should only be asked for a comment by the GUI if
self.product_requires_text_entry(prod_id) == True
Parameters: - prod_id (int) – product
- qty (Decimal) – amount of product
- comment ((basestring, None)) – textual comment from the user, or None.
Raise: ProductNotFound
-
get_category_path
(current_category)[source]¶ return the category path from the root to the current category, excluding the root category
[child_of_root, …, parent_of_current, current_category]
Return type: list(Category)
-
get_current_order
()[source]¶ get selected order (or return 0 if switching between multiple orders is not supported)
-
get_current_total
()[source]¶ Returns: total sum of current order Return type: Decimal Note: The internal rounding must be consistent, which is needed by :class: FabLabKasse.shopping.payment_methods. That means that x,xx5 € must always be rounded up or always down. “Fair rounding” like Decimal.ROUND_HALF_EVEN is not allowed.
For example:
- add article costing 1,015 € -> get_current_total == x
- add article costing 0,990 € -> get_current_total == x + 0,99
This would not be true with the fair strategy “round second digit to even value if the third one is exactly 5” (1,02€ and 2,00€).
-
get_products
(current_category)[source]¶ return products in current category
Return type: list(Product)
-
get_subcategories
(current_category)[source]¶ return list(Category) of subclasses of the given category-id.
-
pay_order
(method)[source]¶ store payment of current order to database :param method: payment method object, whose type is used to determine where the order should be stored in the database method.amount_paid - method.amount_returned is how much money was gained by this sale, must be equal to self.get_current_total()
-
search_from_text
(searchstr)[source]¶ search searchstr in products and categories :return: tuple (list of categories, products for table)
Return type: list(Product)
-
search_product_from_code
(code)[source]¶ search via barcode, PLU or similar unique-ID entry. code may be any string
Returns: product id Raises: ProductNotFound() if nothing found
-
cart_from_app
- Loading carts via smartphone app¶
This module supports using your smartphone to enter (or scan) which products you would like to pay and transfer the cart (list of products and amounts) to the terminal.
The smartphone app https://github.com/FAU-Inf2/fablab-android has a java-based backend server https://github.com/FAU-Inf2/fablab-server which is used for communication by both the smartphone and the terminal.
- You can use the HTML-based simulator as a replacement for FabLabKasse and also as a reference implementation:
my-server.com/checkoutDummy/
Fetch a random number from the appserver (e.g. 12345)
HTTP GET server/checkout/createCode
- show a QR code including this number to the user
(this is to guard against DOS or collisions with other people also sending a cart at the same time)
User has his cart in the app, and pushes “send to cashdesk”, scans code
app sends cart to server, authenticating with the random number
cashdesk polls for a cart:
HTTP GET server/checkout/12345
- -> If a cart was sent, the response is a JSON object containing the cart
ask for payment, process payment
When finished or aborted, send back the status to the server to notify the application:
- success:
HTTP POST server/checkout/paid/12345
- aborted:
HTTP POST server/checkout/canceled/12345
Submodules¶
payment_methods
: select and handle a payment method¶
gui¶
Main window
scriptHelper: various utilities¶
Module contents¶
Files that are here for legacy reasons¶
Some parts of FabLabKasse.shopping.backend.legacy_offline_kassenbuch
are in this folder for historical reasons
Kassenbuch¶
this file is here for legacy reasons
produkt¶
this file is here for legacy reasons
-
class
FabLabKasse.produkt.
Produkt
(plu, name, basiseinheit, basispreis, verkaufseinheiten=None, input_mode='DECIMAL')[source]¶ Bases:
object
-
add_verkaufseinheit
(verkaufseinheit, preis, basismenge=None, input_mode='DECIMAL')[source]¶ Fügt eine neue Verkaufseinheit zum Produkt hinzu.
Parameters: - verkaufseinheit (basestr) – ist ein string, welcher die Einheit beschreibt, z.B. “Platte (600x300mm)”
- preis – ist der Preis für _eine_ solche Einheit
- basismenge – (optional) ein Umrechnungsfaktor: eine Basisheinheit mal Basismenge entspricht einer Verkaufseinheit
- input_mode – (optional) kann DECIMAL, INTEGER oder MINUTES sein. Ändert nichts an dem gespeicherten Wert, dieser ist immer Decimal.
-
Code structure overview¶
run
is the launcher, it startsFabLabKasse.gui
- the rest of the code is in folder FabLabKasse
- kassenbuch.py (currently still german) accounting CLI for legacy_offline_kassenbuch shopping backend
- produkt.py is directly in FabLabKasse-folder for legacy reasons
- cashPayment: automated cash payment - coin and banknote acceptors. see README_cashPayment.md
- client: interface towards the GUI that connects with the device drivers
- PaymentDevicesManager: manages all (multiple) payment devices, used by the GUI (as self.cashPayment)
- PaymentDeviceClient: one device used by PaymentDevicesManager, launches a ‘server’ process and communicates with it via the protocol specified in cashPayment/protocol.txt
- server: device drivers. they run as a standalone process
- cashServer: abstract base class
- exampleServer: simulated hardware for first tests
- nv11, mdbCoinChanger: real hardware
- protocol.txt: specification of how client (GUI) and server (individual device-driver process) communicate
- cashState: database backend + CLI for accounting cash (individual pieces of money) inside the devices. This accounting is for auditing and error-finding purposes and therefore separate from the shopping backend, which has its own accounting (that does not look at individual coins, but just at sums). management tool can be started as ./cash from the main directory
- listSerialPorts: tool to list all ports that can be found with pyserial, useful for configuration of all (usb-)serial connecting device drivers
- client: interface towards the GUI that connects with the device drivers
- shopping:
- backend: backends that provide connection to a webshop, ERP system, database etc and manages products, categories, carts and financial accounting (storage of payments)
- abstract: abstract base class
- offline_base: abstract base class for backends that read products only once at the start and keep the cart in memory; as opposed to a always-online system that has its whole state somewhere in the cloud
- dummy: has some fake products, just silently accepts all payments without storing them somewhere
- oerp: OpenERP / odoo implementation, still needs testing.
- legacy_offline_kassenbuch: backend with product importing from a python script, SQLite based double-entry bookkeeping, contains many german database field names and is therefore marked as legacy. With some re-writing it would make a decent SQLite backend. Has a management CLI kassenbuch.py in the main folder.
- payment_methods: different methods of payment like manual cash entry, automatic cash in+output, charge on client account, …
- backend: backends that provide connection to a webshop, ERP system, database etc and manages products, categories, carts and financial accounting (storage of payments)
- libs: some helping libraries
- produkte: empty directory for local caching of product data (TODO rename)
- scripts: some helping cronjobs - TODO