From 0aa9dfd230b4294a90f6efaf5858c5a86f875a23 Mon Sep 17 00:00:00 2001 From: Stephen Early Date: Mon, 22 Jan 2024 18:46:27 +0000 Subject: [PATCH] Support running without a receipt printer or cash drawer If no printer is configured, prevent the user from doing things that require printing. If no cash drawer is configured, prevent the user from taking cash. There are payment method and general settings to override these restrictions. --- quicktill/card.py | 9 ++++-- quicktill/cash.py | 9 +++++- quicktill/managetill.py | 6 +++- quicktill/printer.py | 31 +++++++++++-------- quicktill/register.py | 42 +++++++++++++++++++------ quicktill/session.py | 68 +++++++++++++++++++++++++++++------------ quicktill/stocklines.py | 15 +++++++-- quicktill/till.py | 16 +++++----- 8 files changed, 141 insertions(+), 55 deletions(-) diff --git a/quicktill/card.py b/quicktill/card.py index 8a5a153..fcee175 100644 --- a/quicktill/card.py +++ b/quicktill/card.py @@ -176,8 +176,8 @@ def enter(self): r.append(pt.driver._cashback_method.driver.add_payment( trans, f"{pt.description} cashback", zero - cashback)) - if cashback > zero or pt.driver._kickout: - printer.kickout() + if (cashback > zero or pt.driver._kickout) and tillconfig.cash_drawer: + printer.kickout(tillconfig.cash_drawer) td.s.flush() self.reg.add_payments(self.transid, r) @@ -366,6 +366,11 @@ def start_payment(self, reg, transid, amount, outstanding): f"the till later."], title=f"{self.paytype} transactions not allowed") return + if self._kickout and not tillconfig.cash_drawer: + ui.infopopup(["This till doesn't have a cash drawer for you " + "to put the card receipt in. Use a different till " + "to take the card payment."], title="Error") + return if amount < zero: if amount < outstanding: ui.infopopup( diff --git a/quicktill/cash.py b/quicktill/cash.py index 6929bfa..9fb3d1b 100644 --- a/quicktill/cash.py +++ b/quicktill/cash.py @@ -50,6 +50,7 @@ def read_config(self): self._change_description = c.get('change_description', 'Change') self._drawers = c.get('drawers', 1) self._countup = c.get('countup', _default_countup) + self._require_cash_drawer = c.get('require-cash-drawer', True) self._total_fields = [ (f"Tray {t + 1}", ui.validate_float, self._countup) for t in range(self._drawers)] @@ -67,6 +68,11 @@ def add_payment(self, transaction, description, amount): return payment.pline(p) def start_payment(self, reg, transid, amount, outstanding): + if self._require_cash_drawer and not tillconfig.cash_drawer: + ui.infopopup(["This till doesn't have a cash drawer. Use " + "a till with a cash drawer to take a cash payment."], + title="Error") + return trans = td.s.query(Transaction).get(transid) description = self.paytype.description if amount < zero: @@ -93,7 +99,8 @@ def start_payment(self, reg, transid, amount, outstanding): r = [payment.pline(p)] if c: r.append(payment.pline(c)) - printer.kickout() + if tillconfig.cash_drawer: + printer.kickout(tillconfig.cash_drawer) reg.add_payments(transid, r) @user.permission_required("cancel-cash-payment", "Cancel a cash payment") diff --git a/quicktill/managetill.py b/quicktill/managetill.py index f13ecfd..ad6b54a 100644 --- a/quicktill/managetill.py +++ b/quicktill/managetill.py @@ -19,6 +19,10 @@ class receiptprint(user.permission_checked, ui.dismisspopup): 'Print any receipt given the transaction number') def __init__(self): + if not tillconfig.receipt_printer: + ui.infopopup(["This till does not have a receipt printer."], + title="Error") + return super().__init__(5, 30, title="Receipt print", dismiss=keyboard.K_CLEAR, colour=ui.colour_input) @@ -46,7 +50,7 @@ def enter(self): user.log(f"Printed {trans.state} transaction {trans.logref} " f"from transaction number") with ui.exception_guard("printing the receipt", title="Printer error"): - printer.print_receipt(rn) + printer.print_receipt(tillconfig.receipt_printer, rn) @user.permission_required('version', 'See version information') diff --git a/quicktill/printer.py b/quicktill/printer.py index 5dacddf..67baa9b 100644 --- a/quicktill/printer.py +++ b/quicktill/printer.py @@ -18,13 +18,13 @@ # This should be the case if called during a keypress! If being used # in any other context, use with td.orm_session(): around the call. -def print_receipt(transid): +def print_receipt(printer, transid): trans = td.s.query(Transaction).get(transid) if trans is None: return if not trans.lines: return - with tillconfig.receipt_printer as d: + with printer as d: d.printline(f"\t{tillconfig.pubname}", emph=1) for i in tillconfig.pubaddr().splitlines(): d.printline(f"\t{i}", colour=1) @@ -104,8 +104,11 @@ def print_receipt(transid): d.printline(f"\t{ui.formatdate(trans.session.date)}") -def print_sessioncountup(s): - with tillconfig.receipt_printer as d: +def print_sessioncountup(printer, sessionid): + s = td.s.query(Session).get(sessionid) + if s is None: + return + with printer as d: d.printline(f"\t{tillconfig.pubname}", emph=1) d.printline(f"\tSession {s.id}", colour=1) d.printline(f"\t{ui.formatdate(s.date)}", colour=1) @@ -141,17 +144,19 @@ def print_sessioncountup(s): d.printline("management menu option 1,3.") -def print_sessiontotals(session_id): +def print_sessiontotals(printer, sessionid): """Print a session totals report given a Session id. """ - s = td.s.query(Session).get(session_id) + s = td.s.query(Session).get(sessionid) + if s is None: + return printtime = ui.formattime(now()) depts = s.dept_totals # Let's use the payment type as the dict key till_totals = dict(s.payment_totals) actual_totals = dict((x.paytype, x.amount) for x in s.actual_totals) - with tillconfig.receipt_printer as d: + with printer as d: d.printline(f"\t{tillconfig.pubname}", emph=1) d.printline(f"\tSession {s.id}", colour=1) d.printline(f"\t{ui.formatdate(s.date)}", colour=1) @@ -199,13 +204,13 @@ def print_sessiontotals(session_id): d.printline(f"\tPrinted {printtime}") -def print_deferred_payment_wrapper(trans, paytype, amount, user_name): +def print_deferred_payment_wrapper(printer, trans, paytype, amount, user_name): """Print a wrapper for a deferred payment Print a wrapper for money (cash, etc.) to be set aside to use towards paying a part-paid transaction in a future session. """ - with tillconfig.receipt_printer as d: + with printer as d: for i in range(4): d.printline(f"\t{tillconfig.pubname}", emph=1) d.printline(f"\tDeferred transaction {trans.id}", emph=1) @@ -282,7 +287,7 @@ def stocklabel_print(p, sl): stock_label(d, sd) -def print_restock_list(rl): +def print_restock_list(printer, rl): """ Print a list of (stockline,stockmovement) tuples. A stockmovement tuple is (stockitem,fetchqty,newdisplayqty,qtyremain). @@ -290,7 +295,7 @@ def print_restock_list(rl): We can't assume that any of these objects are in the current session. """ - with tillconfig.receipt_printer as d: + with printer as d: d.printline(f"\t{tillconfig.pubname}", emph=1) d.printline("\tRe-stock list") d.printline(f"\tPrinted {ui.formattime(now())}") @@ -313,7 +318,7 @@ def print_restock_list(rl): d.printline("\tEnd of list") -def kickout(): +def kickout(drawer): """Kick out the cash drawer. Returns True if successful. @@ -321,7 +326,7 @@ def kickout(): with ui.exception_guard("kicking out the cash drawer", title="Printer error"): try: - tillconfig.cash_drawer.kickout() + drawer.kickout() except pdrivers.PrinterError as e: ui.infopopup([f"Could not kick out the cash drawer: {e.desc}"], title="Printer problem") diff --git a/quicktill/register.py b/quicktill/register.py index fc68adb..04168a7 100644 --- a/quicktill/register.py +++ b/quicktill/register.py @@ -1855,13 +1855,16 @@ def cashkey(self): if self.user.may('nosale'): if self.hook("nosale"): return - if printer.kickout(): - ui.toast("No Sale has been recorded.") - # Finally! We're not lying any more! - user.log("No Sale") - if lock_after_nosale(): - self.locked = True - self._redraw() + if tillconfig.cash_drawer: + if printer.kickout(tillconfig.cash_drawer): + ui.toast("No Sale has been recorded.") + # Finally! We're not lying any more! + user.log("No Sale") + else: + ui.toast("No Sale") + if lock_after_nosale(): + self.locked = True + self._redraw() else: ui.infopopup(["You don't have permission to use " "the No Sale function."], title="No Sale") @@ -2185,12 +2188,16 @@ def printkey(self): "if you have its number using the option under " "'Manage Till'."], title="Error") return + if not tillconfig.receipt_printer: + ui.infopopup(["This till does not have a receipt printer."], + title="Error") + return log.info("Register: printing transaction %d", trans.id) user.log(f"Printed {trans.state} transaction {trans.logref} " f"from register") ui.toast("The receipt is being printed.") with ui.exception_guard("printing the receipt", title="Printer error"): - printer.print_receipt(trans.id) + printer.print_receipt(tillconfig.receipt_printer, trans.id) def cancelkey(self): """The cancel key was pressed. @@ -2414,7 +2421,10 @@ def canceltrans(self): self.transid = None td.s.flush() if payments > zero: - printer.kickout() + # XXX this is looking at all payments, not just those that + # go in the cash drawer. + if tillconfig.cash_drawer: + printer.kickout(tillconfig.cash_drawer) refundtext = f"{tillconfig.fc(payments)} had already been "\ f"put in the cash drawer." else: @@ -2761,6 +2771,17 @@ def defertrans(self): # cash to use towards paying it if nds != zero: + if not tillconfig.receipt_printer: + ui.infopopup( + ["The till needs to print a ticket to wrap the " + "money for this part-paid transaction, but it " + "doesn't have a receipt printer.", + "", + "Use a till with a receipt printer to defer " + "this transaction."], + title="Error - no printer") + td.s.rollback() + return user.log(f"Deferred transaction {trans.logref} which was " f"part-paid by {tillconfig.fc(nds)}") defer_pm.driver.add_payment(ptrans, "Deferred", zero - nds) @@ -2774,8 +2795,9 @@ def defertrans(self): f"the customer.") try: printer.print_deferred_payment_wrapper( + tillconfig.receipt_printer, trans, defer_pm, nds, self.user.fullname) - printer.kickout() + printer.kickout(tillconfig.cash_drawer) except Exception as e: ui.infopopup( ["There was an error printing the deferred payment " diff --git a/quicktill/session.py b/quicktill/session.py index d24882c..c8aa376 100644 --- a/quicktill/session.py +++ b/quicktill/session.py @@ -19,6 +19,11 @@ description="Should session totals be printed after they have been " "confirmed?") +sessioncountup_print = config.BooleanConfigItem( + 'core:sessioncountup_print', True, display_name="Print countup sheets?", + description="Should a counting-up sheet be printed when a session " + "is closed?") + def trans_restore(): """Restore deferred transactions @@ -75,7 +80,8 @@ def key_enter(self): log.info("Started session number %d", sc.id) user.log(f"Started session {sc.logref}") payment.notify_session_start(sc) - printer.kickout() + if tillconfig.cash_drawer: + printer.kickout(tillconfig.cash_drawer) if deferred: deferred = [ "", @@ -103,6 +109,13 @@ def start(): def checkendsession(): + if sessioncountup_print() and not tillconfig.receipt_printer: + log.info("End session: no receipt printer") + ui.infopopup(["This till does not have a receipt printer. Use " + "a different till to close the session and print " + "the counting-up sheet."], + title="Error") + return sc = Session.current(td.s) if sc is None: log.info("End session: no session in progress") @@ -124,12 +137,13 @@ def confirmendsession(): r = checkendsession() if not r: return - # Check that the printer has paper before ending the session - pp = tillconfig.receipt_printer.offline() - if pp: - ui.infopopup(["Could not end the session: there is a problem with " - f"the printer: {pp}"], title="Printer problem") - return + if sessioncountup_print(): + # Check that the printer has paper before ending the session + pp = tillconfig.receipt_printer.offline() + if pp: + ui.infopopup(["Could not end the session: there is a problem with " + f"the printer: {pp}"], title="Printer problem") + return r.endtime = datetime.datetime.now() log.info("End of session %d confirmed.", r.id) user.log(f"Ended session {r.logref}") @@ -139,11 +153,13 @@ def confirmendsession(): "actual amounts using management option 1, 3."], title="Session Ended", colour=ui.colour_info, dismiss=keyboard.K_CASH) - ui.toast("Printing the countup sheet.") - with ui.exception_guard("printing the session countup sheet", - title="Printer error"): - printer.print_sessioncountup(r) - printer.kickout() + if sessioncountup_print(): + ui.toast("Printing the countup sheet.") + with ui.exception_guard("printing the session countup sheet", + title="Printer error"): + printer.print_sessioncountup(tillconfig.receipt_printer, r.id) + if tillconfig.cash_drawer: + printer.kickout(tillconfig.cash_drawer) managestock.stock_purge_internal(source="session end") payment.notify_session_end(r) @@ -249,6 +265,12 @@ class record(ui.dismisspopup): ffw = 13 def __init__(self, sessionid): + if sessiontotal_print() and not tillconfig.receipt_printer: + ui.infopopup(["This till does not have a receipt printer " + "to print the session totals after they have " + "been recorded. Use a till that has a printer."], + title="Error") + return log.info("Record session takings popup: session %d", sessionid) self.sessionid = sessionid s = td.s.query(Session).get(sessionid) @@ -429,17 +451,24 @@ def finish(self): self.dismiss() for i in SessionHooks.instances: i.postRecordSessionTakings(session.id) - if sessiontotal_print(): + if sessiontotal_print() and tillconfig.receipt_printer: ui.toast("Printing the confirmed session totals.") with ui.exception_guard("printing the confirmed session totals", title="Printer error"): - printer.print_sessiontotals(session.id) + printer.print_sessiontotals( + tillconfig.receipt_printer, session.id) else: ui.toast(f"Totals for session {session.id} confirmed.") @user.permission_required('record-takings', "Record takings for a session") def recordtakings(): + if sessiontotal_print() and not tillconfig.receipt_printer: + ui.infopopup(["This till does not have a receipt printer " + "to print the session totals after they have " + "been recorded. Use a till that has a printer."], + title="Error") + return m = sessionlist(record, unpaidonly=True, closedonly=True) if len(m) == 0: log.info("Record takings: no sessions available") @@ -505,11 +534,12 @@ def totalpopup(sessionid): dept.id, dept.description, tillconfig.fc(total))) dt = dt + total l.append(ui.tableformatter(" l pr ")("Total", tillconfig.fc(dt))) - l.append("") - l.append(" Press Print for a hard copy ") - keymap = { - keyboard.K_PRINT: (printer.print_sessiontotals, (s.id,), False), - } + keymap = {} + if tillconfig.receipt_printer: + l.append("") + l.append(" Press Print for a hard copy ") + keymap[keyboard.K_PRINT] = (printer.print_sessiontotals, ( + tillconfig.receipt_printer, s.id), False) ui.listpopup(l, title=f"Session number {s.id}", colour=ui.colour_info, keymap=keymap, diff --git a/quicktill/stocklines.py b/quicktill/stocklines.py index 5ef6596..c0ef0a5 100644 --- a/quicktill/stocklines.py +++ b/quicktill/stocklines.py @@ -1,6 +1,7 @@ import logging from . import keyboard, ui, td, printer, user, linekeys, modifiers from . import stocktype +from . import tillconfig from .models import Department, StockLine, KeyboardBinding from .models import StockType, StockLineTypeLog from sqlalchemy.sql import select @@ -23,7 +24,12 @@ def restock_list(stockline_list): ui.infopopup(["There is no stock to be put on display."], title="Stock movement") return - printer.print_restock_list(sl) + if not tillconfig.receipt_printer: + ui.infopopup(["This till does not have a receipt printer. " + "Use a till with a receipt printer to print out " + "the restock list."], title="Error") + return + printer.print_restock_list(tillconfig.receipt_printer, sl) user.log("Printed restock list") ui.infopopup( ["The list of stock to be put on display has been printed.", "", @@ -135,7 +141,12 @@ def return_stock(stockline): "this line."], title="Remove stock") return restock = [(stockline, rsl)] - printer.print_restock_list(restock) + if not tillconfig.receipt_printer: + ui.infopopup(["This till has no receipt printer. Use a till " + "with a printer to print the list of stock to " + "be taken off display."], title="Error") + return + printer.print_restock_list(tillconfig.receipt_printer, restock) ui.infopopup( ["The list of stock to be taken off display has been printed.", "", "Press Cash/Enter to " diff --git a/quicktill/till.py b/quicktill/till.py index 0ae4f8b..5da6117 100644 --- a/quicktill/till.py +++ b/quicktill/till.py @@ -22,7 +22,6 @@ from types import ModuleType from . import ui from . import td -from . import printer from . import lockscreen from . import tillconfig from . import user @@ -569,14 +568,15 @@ def main(): # Process the configuration for opt, val in config.items(): if opt == 'printer': - if args.disable_printer: + if val and args.disable_printer: tillconfig.receipt_printer = pdrivers.nullprinter( name="disabled-printer") else: tillconfig.receipt_printer = val - lockscreen.CheckPrinter("Receipt printer", val) + if val: + lockscreen.CheckPrinter("Receipt printer", val) elif opt == "cash_drawer": - if args.disable_printer: + if val and args.disable_printer: tillconfig.cash_drawer = pdrivers.nullprinter( name="disabled-cash-drawer") else: @@ -620,9 +620,11 @@ def main(): else: log.warning("Unknown configuration option '%s'", opt) - if tillconfig.receipt_printer is None: - log.info("no printer configured: using nullprinter()") - tillconfig.receipt_printer = pdrivers.nullprinter() + if tillconfig.receipt_printer: + # Check that the receipt printer driver is of an appropriate type + if tillconfig.receipt_printer.canvastype != "receipt": + print("Invalid receipt printer configuration") + sys.exit(1) if tillconfig.cash_drawer is None: tillconfig.cash_drawer = tillconfig.receipt_printer