What’s for Sale?
Discover the Power of Koha’s Point of Sale System
Barbara Glassford Johnson, Library Manager Mary Woodward, Circulation Supervisor
Bedford Public Library, TX
2024 koha-US Conference
Round Rock, TX – August 13, 2024
2
What is Point of Sale (POS)?
Point of Sale (aka POS)
For selling items not connected to a patron account: used books, tote bags, flash drives, event tickets, etc.
If desired, integrates with patron-connected payments for lost items, damage, etc.
Point of Sale overview
3
Bedford Public Library
Our Challenge
Our Solution
Setup & Configuration
Sales & Refunds
Reconciling & Reports
Customizations
Opportunities
4
Our Library
Single location
40,516 sq. ft.
Open 7 days/week for 57 hours
Cardholders
35,400
Collection
113,425 items
Annual circulation
422,876
Visits per year
200,000+
730 programs
29,344 attendees
Library staff
20.85 FTE
5
Our Information Desk
Central Information Desk
staffed with two employees
24 staff members
assigned to work the desk
Staff changes hourly
Payments taken for three entities – Library, Friends and Foundation
6
Ancient register from 2009
7
Our Challenge
Old
technology
No support
No manual
One shared register on back counter
Missing receipts wasted tons of register tape
Credit card swipe stopped working
Different devices for cash vs credit cards
On its death bed - needed a quick solution
8
The Integrated Solution
9
The Finance Dept. Solution
10
The Koha Solution
Setup and Configuration
11
12
System preferences
13
POS enabled
14
Configure cash registers
15
Set a default register (optional)
16
Configure items for purchase
17
Debit types
18
Payment types
Making Sales
19
20
New setup
Physical cash drawer
Press to open or use key
Separate credit card swipes
Receipt printers
Pin pad for debit cards
21
POS enabled
22
Select items to add to sale
Click the +Add button make a sale
1. Click button to make a sale
23
2. Double-click in cells to edit cost and quantity
Totals are auto-calculated
3. Enter amount patron gives you
4. Change is auto-calculated
5. Select payment type
6. Click confirm
24
Confirmation & receipts
25
POS connected to patron accounts
26
Manual invoice on patron account
27
Lost book payment on patron account
Click the +Add button make a sale
28
Lost book payment
29
POS transaction history
Giving Refunds
30
Refunds�
Go to Transaction History
Find transaction that needs to be refunded
Click on ‘Issue Refund’ button
Enter refund amount
Select original transaction type
Create new transaction for correct amount
31
Daily Reconciliation
32
33
Daily Cash Register Reconciliation
Go to POS > Cash Summary
Print daily cash register reconciliation report
Count cash, checks and credit/debit in the drawer
Verify amounts in drawer match daily report and Cash Summary page
Fix mistakes
Click on the 'Reconcile drawer' link
Confirm that you are removing the amount of cash listed
Click the 'Confirm' button to zero out the register
34
35
36
Types of errors
37
Date/Time problems
Missed credit card transaction entered into POS the next morning
Last reconciliation
Cash summary page has a link to the last reconciliation
Gives totals by debit type and payment type
Only viewable after cash register has been reconciled
38
Enhanced reconciliation modal
Bug 37530
Ability to view detailed information prior to reconciliation
Ability to detect and fix mistakes prior to reconciling
Totals by debit type
Could use in place of a custom report
39
Edit POS transactions - button
Bug 36029
Add edit button to transaction history
Controlled by a permission
Launches a modal
40
Edit POS transactions - modal
Bug 36029
Create a modal for editing POS transactions
Add edit button for each transaction
Change Debit types to a dropdown
Allow editing of debit type, date, amount, payment type, cash register
41
Reports
42
Built-in cash register report�
Go to Reports module > Statistics wizards > Cash register
Gives dates, amounts, debit types, etc.
Doesn’t include what was sold, what entity it was for, how it was paid, etc.
43
44
Built-in cash register report
45
Reports process
46
Account offsets report
SELECT *
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.id)
WHERE DATE(c.date) BETWEEN <<Collected BETWEEN (yyyy-mm-dd)|date>> AND <<and (yyyyy-mm-dd)|date>>
47
Reports
Tables needed:
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.id)
48
Daily cash reconciliation report
Daily cash register reconciliation report
Provides total for each fund
Broken down by payment type (cash, credit/debt, check)
Daily cash register reconciliation�
Lists totals for each debit type
Breaks payments down by payment type
Gives subtotals by payment type
Includes refunds
Calculates net totals by payment type
Calculates the grand total
49
50
SQL for daily report
SELECT to_char(c.timestamp, 'yyyy-mm-dd') AS 'Date', cash_registers.name AS 'Register', d.debit_type_code AS 'Fund',
CONCAT('<p style="text-align:right!important; margin-top:0px!important;">',FORMAT(abs(sum(o.amount)),2),'</p>') AS 'Amount',
c.payment_type AS 'Payment Type'
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.id)
WHERE DATE(c.date) BETWEEN <<Collected BETWEEN (yyyy-mm-dd)|date>> AND <<and (yyyyy-mm-dd)|date>>
AND d.debit_type_code != ''
AND c.payment_type = 'CASH'
GROUP BY d.debit_type_code ASC
UNION ALL
51
Daily report – cash sales
CONCAT('<p style="text-align:right!important; font-weight: bold; margin-top:0px!important;">',FORMAT(ABS(sum(o.amount)),2),'</p>') AS 'Amount',
'<h5 style="color: black !important; text-align: left; padding: 6px; margin-top: 0px !important; margin-bottom: 0px !important; line-height: .65 !important; padding-left: 0px; font-size: 15px !important; font-weight: bold;">Total Cash Sales</h5>' AS 'Payment Type'
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.id)
WHERE DATE(c.date) BETWEEN <<Collected BETWEEN (yyyy-mm-dd)|date>> AND <<and (yyyyy-mm-dd)|date>>
AND d.debit_type_code != ''
AND c.payment_type = 'CASH'
UNION ALL
52
Daily report – cash refunds
SELECT '', '', '', CONCAT('<p style="color:#9e0000 !important; font-weight:700 !important; text-align:right; height: 5px !important; margin-top: 0px !important;">',FORMAT(-ABS(sum(c.amount)),2),'</p>') AS 'Amount', CONCAT('<b>','Total Cash Refunds','</b>') AS 'Payment Type'
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.name)
WHERE DATE(c.date) BETWEEN <<Refunded BETWEEN (yyyy-mm-dd)|date>> AND <<and (yyyyy-mm-dd)|date>>
AND c.credit_type_code != 'WRITEOFF' AND c.credit_type_code != 'CANCELLATION’ AND c.credit_type_code != 'PURCHASE'
AND c.credit_type_code != 'PAYMENT’ AND c.credit_type_code != 'CREDIT'
AND d.payment_type = 'CASH’ AND o.type = 'APPLY’ AND o.amount != '0.00'
UNION ALL
53
Daily report – net sales
SELECT '', '', '', CONCAT('<p style="text-align:right!important; font-weight: bold; margin-top:0px!important;">',(
(SELECT FORMAT(ABS(sum(o.amount)),2) as sales
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.id)
WHERE DATE(c.date) BETWEEN <<Collected BETWEEN (yyyy-mm-dd)|date>> AND <<and (yyyyy-mm-dd)|date>>
AND d.debit_type_code != ''AND c.payment_type = 'CASH')
+
(SELECT FORMAT(-ABS(sum(c.amount)),2) as refunds
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.name)
WHERE DATE(c.date) BETWEEN <<Refunded BETWEEN (yyyy-mm-dd)|date>> AND <<and (yyyyy-mm-dd)|date>>
AND c.credit_type_code != 'WRITEOFF' AND c.credit_type_code != 'CANCELLATION’ AND c.credit_type_code != 'PURCHASE’ AND c.credit_type_code != 'PAYMENT'
AND c.credit_type_code != 'CREDIT’ AND d.payment_type = 'CASH’ AND o.type = 'APPLY'
AND o.amount != '0.00')) ,'</b>') AS 'Amount', CONCAT('<b>','Net Cash (cash in drawer)','</b>') AS 'Payment Type'
54
Monthly cash reconciliation report
Monthly cash register reconciliation report
Provides total for each fund
Broken down by entity (Library, Friends, Foundation)
Monthly cash register reconciliation�
Totals for each fund broken down by entity
Totals are for all payment types
Grand totals by entity
Allows for proper disbursement of funds to City, Friends and Foundation
55
56
SQL for monthly report
SELECT to_char(c.timestamp, 'yyyy-mm') AS 'Month', 'Library' AS 'Entity',
d.debit_type_code AS 'Fund', CONCAT('<p style="text-align:right!important; margin-top:0px!important;">',FORMAT(abs(sum(o.amount)),2),'</p>') AS 'Total amount'
FROM account_offsets o
LEFT JOIN accountlines d ON (o.debit_id = d.accountlines_id and d.debit_type_code is not null)
LEFT JOIN accountlines c ON (o.credit_id = c.accountlines_id and c.credit_type_code is not null)
LEFT JOIN cash_registers ON (c.register_id = cash_registers.id)
WHERE c.date BETWEEN CAST(CONCAT(<<Collected between|date>>, ' ', '09:00:00') AS DATETIME) AND CAST(CONCAT(<<and|date>>, ' ', '08:59:59') AS DATETIME)
AND d.debit_type_code LIKE 'LI30%’ AND c.credit_type_code != 'WRITEOFF’ AND d.status IS NULL
OR c.date BETWEEN CAST(CONCAT(<<Collected between|date>>, ' ', '09:00:00') AS DATETIME) AND CAST(CONCAT(<<and|date>>, ' ', '08:59:59') AS DATETIME)
AND d.debit_type_code LIKE 'Printing/Copier Fees’ AND c.credit_type_code != 'WRITEOFF’ AND d.status IS NULL
GROUP BY d.debit_type_code asc
57
Want the SQL?
CSS and jQuery
58
59
Confusing terminology
Cashup = Reconcile/Reconciliation
Bankable = Cash in drawer
Income (cash) = Payment/Sales (cash)
Outgoing (cash) = Refunds (cash)
60
Improved terminology
61
jQuery terminology changes
//BEGIN Rename Cashup buttons in POS
$('button.cashup_individual.btn.btn-xs.btn-default:contains("Record cashup")').text("Reconcile drawer");
$('button.cashup_all.btn.btn-xs.btn-default:contains("Cashup all")').text("Reconcile all");
$('#registers > thead > tr > th:nth-child(3):contains("Last cashup")').text("Last reconciliation");
$('#pos_cashup.btn.btn-default:contains("Record cashup")').text("Reconcile drawer");
$('h2:contains("Cashup history")').text("Drawer reconciliation history");
$('button.cashup_all.btn.btn-default:contains("Cashup all")').text("Reconcile all");
//BEGIN Rename Cash Summary column headers on registers.pl
$('#registers > thead > tr > th:nth-child(5):contains("Bankable")').text("Cash in drawer");
$('#registers > thead > tr > th:nth-child(6):contains("Income (cash)")').text("Payments/Sales (cash)");
$('#registers > thead > tr > th:nth-child(7):contains("Outgoing (cash)")').text("Refunds (cash)*");
62
Make ‘Issue refund’ easier to locate
button.btn.btn-default.btn-xs.pos_refund {
background-color: green;
color: white;
}
Debit type descriptions�
Used a naming scheme to sort debit types by description
By default, the table sorts by debit type code
Table can’t be configured
Used jQuery to sort by the description
Places most used items at the top and reduces scrolling
63
64
Sort by description as a default
/*BEGIN Sort POS items for purchases by description asc as default */
if (window.location.href.indexOf("pay.pl") > -1) {
$('#invoices').DataTable({
"order": [[ 2, "asc" ]],
"columnDefs" : [{"targets":2}],
});
}
65
Sales - money left-aligned
66
CSS to right-align money for sales
/*POS - right-align all money and quantities in 'items for purchase' and 'this sale' area on pos/pay.pl*/
#sale > tbody > tr.odd > td.editable, #sale > tbody > tr.even > td.editable,
#sale > tbody > tr.odd > td:nth-child(4), #sale > tbody > tr.even > td:nth-child(4),
#sale > tfoot > tr > td:nth-child(2),
#sale > tbody > tr.odd > td.editable_int, #sale > tbody > tr.even > td.editable_int,
#invoices > tbody > tr > td:nth-child(3) {
text-align: right;
}
67
Sales - right-aligned money & quantities
Transaction history - money left-aligned
68
69
Right-align money – transaction history
/*POS - right-align all money on transaction history page at register.pl*/
#sales > tbody > tr > td:nth-child(3),
#sales > tfoot > tr > td:nth-child(2),
#past_sales > tbody > tr > td:nth-child(3),
#past_sales > tfoot > tr > td:nth-child(2),
#table_cashups > tbody > tr > td:nth-child(3) {
text-align: right;
}
Transaction history - money right-aligned
70
Opportunities
71
72
Wishlist
73
More POS bugs
Thank You!