Add support for suspend/resume txn
This commit is contained in:
parent
52c35ed85c
commit
e7ecd88e64
|
@ -40,10 +40,12 @@ class WuttaLookup(WuttaControl):
|
|||
long_scroll_delta = 500
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.show_search = kwargs.pop('show_search', True)
|
||||
self.initial_search = kwargs.pop('initial_search', None)
|
||||
self.allow_empty_query = kwargs.pop('allow_empty_query', False)
|
||||
self.on_select = kwargs.pop('on_select', None)
|
||||
self.on_cancel = kwargs.pop('on_cancel', None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# track current selection
|
||||
|
@ -52,11 +54,6 @@ class WuttaLookup(WuttaControl):
|
|||
|
||||
def build(self):
|
||||
|
||||
self.searchbox = ft.TextField("", text_size=self.font_size * 0.8,
|
||||
on_submit=self.lookup,
|
||||
autofocus=True,
|
||||
expand=True)
|
||||
|
||||
self.search_results = ft.DataTable(
|
||||
columns=[ft.DataColumn(self.make_cell_text(text))
|
||||
for text in self.get_results_columns()],
|
||||
|
@ -99,67 +96,75 @@ class WuttaLookup(WuttaControl):
|
|||
scroll=ft.ScrollMode.AUTO,
|
||||
)
|
||||
|
||||
return ft.Column(
|
||||
[
|
||||
controls = []
|
||||
|
||||
if self.show_search:
|
||||
|
||||
self.searchbox = ft.TextField("", text_size=self.font_size * 0.8,
|
||||
on_submit=self.lookup,
|
||||
autofocus=True,
|
||||
expand=True)
|
||||
|
||||
controls.extend([
|
||||
ft.Row(
|
||||
[
|
||||
ft.Text("SEARCH FOR:"),
|
||||
self.searchbox,
|
||||
ft.Container(
|
||||
content=ft.Text("Lookup", size=self.font_size * 0.8),
|
||||
alignment=ft.alignment.center,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
border=ft.border.all(1, 'black'),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
on_click=self.lookup,
|
||||
bgcolor='blue',
|
||||
),
|
||||
ft.Container(
|
||||
content=ft.Text("Reset", size=self.font_size * 0.8),
|
||||
alignment=ft.alignment.center,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
border=ft.border.all(1, 'black'),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
on_click=self.reset,
|
||||
bgcolor='yellow',
|
||||
),
|
||||
ft.Container(
|
||||
content=ft.Text("Cancel", size=self.font_size * 0.8),
|
||||
alignment=ft.alignment.center,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
border=ft.border.all(1, 'black'),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
on_click=self.cancel,
|
||||
),
|
||||
self.make_button("Lookup",
|
||||
font_size=self.font_size * 0.8,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
on_click=self.lookup,
|
||||
bgcolor='blue'),
|
||||
self.make_button("Reset",
|
||||
font_size=self.font_size * 0.8,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
on_click=self.reset,
|
||||
bgcolor='yellow'),
|
||||
],
|
||||
),
|
||||
ft.Divider(),
|
||||
WuttaKeyboard(self.config, on_keypress=self.keypress,
|
||||
on_long_backspace=self.long_backspace),
|
||||
ft.Divider(),
|
||||
ft.Row(
|
||||
[
|
||||
self.search_results_wrapper,
|
||||
ft.VerticalDivider(),
|
||||
ft.Column(
|
||||
[
|
||||
self.select_button,
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
self.up_button,
|
||||
self.down_button,
|
||||
],
|
||||
),
|
||||
],
|
||||
vertical_alignment=ft.CrossAxisAlignment.START,
|
||||
),
|
||||
],
|
||||
])
|
||||
|
||||
controls.extend([
|
||||
ft.Divider(),
|
||||
ft.Row(
|
||||
[
|
||||
self.search_results_wrapper,
|
||||
ft.VerticalDivider(),
|
||||
ft.Column(
|
||||
[
|
||||
self.select_button,
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
self.up_button,
|
||||
self.down_button,
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
ft.Row(),
|
||||
self.make_button("Cancel",
|
||||
font_size=self.font_size * 0.8,
|
||||
height=self.default_button_height_dlg * 0.8,
|
||||
width=self.default_button_height_dlg * 1.3,
|
||||
on_click=self.cancel),
|
||||
],
|
||||
),
|
||||
],
|
||||
vertical_alignment=ft.CrossAxisAlignment.START,
|
||||
),
|
||||
])
|
||||
|
||||
return ft.Container(
|
||||
content=ft.Column(controls=controls),
|
||||
height=None if self.show_search else 600,
|
||||
)
|
||||
|
||||
def get_results_columns(self):
|
||||
|
@ -167,7 +172,8 @@ class WuttaLookup(WuttaControl):
|
|||
|
||||
def did_mount(self):
|
||||
if self.initial_search is not None:
|
||||
self.searchbox.value = self.initial_search
|
||||
if self.show_search:
|
||||
self.searchbox.value = self.initial_search
|
||||
self.initial_search = None # only do it once
|
||||
self.update()
|
||||
self.lookup()
|
||||
|
@ -205,11 +211,15 @@ class WuttaLookup(WuttaControl):
|
|||
return obj
|
||||
|
||||
def lookup(self, e=None):
|
||||
entry = self.searchbox.value
|
||||
if not entry and not self.allow_empty_query:
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
return
|
||||
|
||||
if self.show_search:
|
||||
entry = self.searchbox.value
|
||||
if not entry and not self.allow_empty_query:
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
return
|
||||
else:
|
||||
entry = None
|
||||
|
||||
session = self.app.make_session()
|
||||
results = self.get_results(session, entry)
|
||||
|
@ -231,21 +241,27 @@ class WuttaLookup(WuttaControl):
|
|||
self.no_results.visible = False
|
||||
|
||||
else:
|
||||
self.no_results.value = f"NO RESULTS FOR: {entry}"
|
||||
if self.show_search:
|
||||
self.no_results.value = f"NO RESULTS FOR: {entry}"
|
||||
else:
|
||||
self.no_results.value = "NO RESULTS FOUND"
|
||||
self.no_results.visible = True
|
||||
|
||||
self.searchbox.focus()
|
||||
if self.show_search:
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
|
||||
def reset(self, e):
|
||||
self.searchbox.value = ""
|
||||
if self.show_search:
|
||||
self.searchbox.value = ""
|
||||
self.search_results.rows.clear()
|
||||
self.no_results.visible = False
|
||||
self.selected_uuid = None
|
||||
self.selected_datarow = None
|
||||
self.select_button.disabled = True
|
||||
self.select_button.bgcolor = self.disabled_bgcolor
|
||||
self.searchbox.focus()
|
||||
if self.show_search:
|
||||
self.searchbox.focus()
|
||||
self.update()
|
||||
|
||||
def set_selection(self, row):
|
||||
|
|
91
wuttapos/controls/txnlookup.py
Normal file
91
wuttapos/controls/txnlookup.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaPOS -- Pythonic Point of Sale System
|
||||
# Copyright © 2023 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaPOS.
|
||||
#
|
||||
# WuttaPOS is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaPOS - transaction lookup control
|
||||
"""
|
||||
|
||||
from .lookup import WuttaLookup
|
||||
|
||||
|
||||
class WuttaTransactionLookup(WuttaLookup):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# nb. this forces first query
|
||||
kwargs.setdefault('initial_search', True)
|
||||
|
||||
# TODO: how to deal with 'modes'
|
||||
self.mode = kwargs.pop('mode', None)
|
||||
if not self.mode:
|
||||
raise ValueError("must specify mode")
|
||||
if self.mode != 'resume':
|
||||
raise ValueError("only 'resume' mode is supported")
|
||||
|
||||
kwargs.setdefault('show_search', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_results_columns(self):
|
||||
return [
|
||||
"Date/Time",
|
||||
"Terminal",
|
||||
"Txn ID",
|
||||
"Cashier",
|
||||
"Customer",
|
||||
"Balance",
|
||||
]
|
||||
|
||||
def get_results(self, session, entry):
|
||||
model = self.app.model
|
||||
|
||||
# TODO: how to deal with 'modes'
|
||||
assert self.mode == 'resume'
|
||||
query = session.query(model.POSBatch)\
|
||||
.filter(model.POSBatch.status_code == model.POSBatch.STATUS_SUSPENDED)\
|
||||
.filter(model.POSBatch.executed == None)\
|
||||
.order_by(model.POSBatch.created.desc())
|
||||
|
||||
transactions = []
|
||||
for batch in query:
|
||||
# TODO: should use 'suspended' timestamp instead here?
|
||||
dt = self.app.localtime(batch.created, from_utc=True)
|
||||
transactions.append({
|
||||
'uuid': batch.uuid,
|
||||
'datetime': self.app.render_datetime(dt),
|
||||
'terminal': batch.terminal_id,
|
||||
'txnid': batch.id_str,
|
||||
'cashier': str(batch.cashier or ''),
|
||||
'customer': str(batch.customer or ''),
|
||||
'balance': self.app.render_currency(batch.get_balance()),
|
||||
})
|
||||
return transactions
|
||||
|
||||
def make_result_row(self, txn):
|
||||
return [
|
||||
txn['datetime'],
|
||||
txn['terminal'],
|
||||
txn['txnid'],
|
||||
txn['cashier'],
|
||||
txn['customer'],
|
||||
txn['balance'],
|
||||
]
|
|
@ -35,6 +35,7 @@ from wuttapos.controls.loginform import WuttaLoginForm
|
|||
from wuttapos.controls.custlookup import WuttaCustomerLookup
|
||||
from wuttapos.controls.itemlookup import WuttaProductLookup
|
||||
from wuttapos.controls.deptlookup import WuttaDepartmentLookup
|
||||
from wuttapos.controls.txnlookup import WuttaTransactionLookup
|
||||
from wuttapos.controls.txnitem import WuttaTxnItem
|
||||
from wuttapos.controls.tenkey import WuttaTenkeyMenu
|
||||
|
||||
|
@ -708,6 +709,21 @@ class POSView(WuttaView):
|
|||
),
|
||||
expand=0)
|
||||
|
||||
self.suspend_menu = ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Row(
|
||||
[
|
||||
self.make_suspend_button(),
|
||||
self.make_resume_button(),
|
||||
],
|
||||
spacing=0,
|
||||
),
|
||||
],
|
||||
spacing=0,
|
||||
),
|
||||
expand=0)
|
||||
|
||||
self.set_quantity = ft.Text(value=None, data=None, weight=ft.FontWeight.BOLD, size=40)
|
||||
|
||||
return [
|
||||
|
@ -748,7 +764,12 @@ class POSView(WuttaView):
|
|||
self.meta_menu,
|
||||
],
|
||||
),
|
||||
self.context_menu,
|
||||
ft.Row(
|
||||
[
|
||||
self.context_menu,
|
||||
self.suspend_menu,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -796,6 +817,124 @@ class POSView(WuttaView):
|
|||
'tender_name': "Check"}
|
||||
return self.make_tender_button(check, **kwargs)
|
||||
|
||||
def make_suspend_button(self, **kwargs):
|
||||
return self.make_button("SUSPEND", bgcolor='purple',
|
||||
font_size=self.default_font_size,
|
||||
height=self.default_button_size,
|
||||
width=self.default_button_size * 2,
|
||||
on_click=self.suspend_click)
|
||||
|
||||
def suspend_click(self, e):
|
||||
session = self.app.make_session()
|
||||
batch = self.get_current_batch(session, create=False)
|
||||
session.close()
|
||||
|
||||
# nothing to suspend if no batch
|
||||
if not batch:
|
||||
self.show_snackbar("NO TRANSACTION", bgcolor='yellow')
|
||||
self.reset()
|
||||
return
|
||||
|
||||
def confirm(e):
|
||||
dlg.open = False
|
||||
self.page.update()
|
||||
|
||||
# nb. do this just in case we must show login dialog
|
||||
# cf. https://github.com/flet-dev/flet/issues/1670
|
||||
time.sleep(0.1)
|
||||
|
||||
self.authorized_action('pos.suspend', self.suspend_transaction,
|
||||
message="Suspend Transaction")
|
||||
|
||||
def cancel(e):
|
||||
dlg.open = False
|
||||
self.reset()
|
||||
|
||||
# prompt to suspend
|
||||
dlg = ft.AlertDialog(
|
||||
title=ft.Text("Confirm SUSPEND"),
|
||||
content=ft.Text("Really SUSPEND transaction?"),
|
||||
actions=[
|
||||
self.make_button(f"Yes, SUSPEND", font_size=self.default_font_size,
|
||||
height=self.default_button_size,
|
||||
width=self.default_button_size * 3,
|
||||
bgcolor='yellow',
|
||||
on_click=confirm),
|
||||
self.make_button("Cancel", font_size=self.default_font_size,
|
||||
height=self.default_button_size,
|
||||
width=self.default_button_size * 2.5,
|
||||
on_click=cancel),
|
||||
])
|
||||
|
||||
self.page.dialog = dlg
|
||||
dlg.open = True
|
||||
self.page.update()
|
||||
|
||||
def suspend_transaction(self, user):
|
||||
session = self.app.make_session()
|
||||
batch = self.get_current_batch(session)
|
||||
user = session.get(user.__class__, user.uuid)
|
||||
handler = self.get_batch_handler()
|
||||
|
||||
handler.suspend_transaction(batch, user)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
self.clear_all()
|
||||
self.reset()
|
||||
|
||||
def make_resume_button(self, **kwargs):
|
||||
return self.make_button("RESUME", bgcolor='purple',
|
||||
font_size=self.default_font_size,
|
||||
height=self.default_button_size,
|
||||
width=self.default_button_size * 2,
|
||||
on_click=self.resume_click)
|
||||
|
||||
def resume_click(self, e):
|
||||
session = self.app.make_session()
|
||||
batch = self.get_current_batch(session, create=False)
|
||||
session.close()
|
||||
|
||||
# can't resume if txn in progress
|
||||
if batch:
|
||||
self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor='yellow')
|
||||
self.reset()
|
||||
return
|
||||
|
||||
def select(uuid):
|
||||
session = self.app.make_session()
|
||||
user = self.get_current_user(session)
|
||||
handler = self.get_batch_handler()
|
||||
|
||||
# TODO: this would need to work differently if suspended
|
||||
# txns are kept in a central server DB
|
||||
batch = session.get(self.app.model.POSBatch, uuid)
|
||||
|
||||
batch = handler.resume_transaction(batch, user)
|
||||
session.commit()
|
||||
|
||||
session.refresh(batch)
|
||||
self.load_batch(batch)
|
||||
session.close()
|
||||
|
||||
dlg.open = False
|
||||
self.reset()
|
||||
|
||||
def cancel(e):
|
||||
dlg.open = False
|
||||
self.reset()
|
||||
|
||||
# prompt to choose txn
|
||||
dlg = ft.AlertDialog(
|
||||
title=ft.Text("Resume Transaction"),
|
||||
content=WuttaTransactionLookup(self.config, mode='resume',
|
||||
on_select=select, on_cancel=cancel),
|
||||
)
|
||||
|
||||
self.page.dialog = dlg
|
||||
dlg.open = True
|
||||
self.page.update()
|
||||
|
||||
def get_current_user(self, session):
|
||||
uuid = self.page.session.get('user_uuid')
|
||||
if uuid:
|
||||
|
@ -819,30 +958,36 @@ class POSView(WuttaView):
|
|||
session = self.app.make_session()
|
||||
batch = self.get_current_batch(session, create=False)
|
||||
if batch:
|
||||
|
||||
handler = self.get_batch_handler()
|
||||
self.page.session.set('txn_display', handler.get_screen_txn_display(batch))
|
||||
self.page.session.set('cust_uuid', batch.customer_uuid)
|
||||
self.page.session.set('cust_display', handler.get_screen_cust_display(batch=batch))
|
||||
|
||||
self.items.controls.clear()
|
||||
for row in batch.active_rows():
|
||||
self.add_row_item(row)
|
||||
self.items.scroll_to(offset=-1, duration=100)
|
||||
|
||||
self.refresh_totals(batch)
|
||||
|
||||
self.load_batch(batch)
|
||||
else:
|
||||
self.page.session.set('txn_display', None)
|
||||
self.page.session.set('cust_uuid', None)
|
||||
self.page.session.set('cust_display', None)
|
||||
self.informed_refresh()
|
||||
|
||||
self.informed_refresh()
|
||||
|
||||
session.commit()
|
||||
# TODO: i think commit() was for when it auto-created the
|
||||
# batch, so that can go away now..right?
|
||||
#session.commit()
|
||||
session.close()
|
||||
self.page.update()
|
||||
|
||||
def load_batch(self, batch):
|
||||
"""
|
||||
Load the given batch as the current transaction.
|
||||
"""
|
||||
handler = self.get_batch_handler()
|
||||
self.page.session.set('txn_display', handler.get_screen_txn_display(batch))
|
||||
self.page.session.set('cust_uuid', batch.customer_uuid)
|
||||
self.page.session.set('cust_display', handler.get_screen_cust_display(batch=batch))
|
||||
|
||||
self.items.controls.clear()
|
||||
for row in batch.active_rows():
|
||||
self.add_row_item(row)
|
||||
self.items.scroll_to(offset=-1, duration=100)
|
||||
|
||||
self.refresh_totals(batch)
|
||||
self.informed_refresh()
|
||||
|
||||
def not_supported(self, e=None, feature=None):
|
||||
|
||||
# test error handler
|
||||
|
|
Loading…
Reference in a new issue