Add basic mechanism for manager override, use for void txn

This commit is contained in:
Lance Edgar 2023-10-06 09:18:07 -05:00
parent 21f848cc82
commit 61a06ec5de
4 changed files with 168 additions and 186 deletions

View file

@ -87,7 +87,7 @@ class WuttaLoginForm(WuttaControl):
ft.Row(), ft.Row(),
ft.Row(), ft.Row(),
WuttaKeyboard(self.config, on_keypress=self.keyboard_keypress, WuttaKeyboard(self.config, on_keypress=self.keyboard_keypress,
on_long_backspace=self.long_backspace), on_long_backspace=self.keyboard_long_backspace),
] ]
return ft.Column(controls=controls, return ft.Column(controls=controls,
@ -245,15 +245,12 @@ class WuttaLoginForm(WuttaControl):
else: else:
if self.on_authz_failure: if self.on_authz_failure:
self.on_authz_failure(user, user_display) self.on_authz_failure(user, user_display)
self.clear_login() self.clear_login()
else: else:
if self.on_login_failure: if self.on_login_failure:
self.on_login_failure(e) self.on_login_failure(e)
self.clear_login()
self.password.focus()
self.update()
return False return False

View file

@ -118,7 +118,6 @@ class WuttaTenkeyMenu(WuttaControl):
self, self,
text, text,
font_size=None, font_size=None,
bgcolor='green',
height=None, height=None,
width=None, width=None,
on_click=None, on_click=None,
@ -133,7 +132,7 @@ class WuttaTenkeyMenu(WuttaControl):
if not on_click: if not on_click:
on_click = self.tenkey_click on_click = self.tenkey_click
return self.make_button(text, font_size=font_size, bgcolor=bgcolor, return self.make_button(text, font_size=font_size, bgcolor='green',
height=height, width=width, height=height, width=width,
on_click=on_click, on_click=on_click,
on_long_press=on_long_press) on_long_press=on_long_press)

View file

@ -89,7 +89,7 @@ class WuttaView(ft.View):
def show_snackbar(self, text, bgcolor='yellow'): def show_snackbar(self, text, bgcolor='yellow'):
self.page.snack_bar = ft.SnackBar(ft.Text(text, color='black', self.page.snack_bar = ft.SnackBar(ft.Text(text, color='black',
size=20, size=40,
weight=ft.FontWeight.BOLD), weight=ft.FontWeight.BOLD),
bgcolor=bgcolor, bgcolor=bgcolor,
duration=1500) duration=1500)

View file

@ -31,6 +31,7 @@ import time
import flet as ft import flet as ft
from .base import WuttaView from .base import WuttaView
from wuttapos.controls.loginform import WuttaLoginForm
from wuttapos.controls.custlookup import WuttaCustomerLookup from wuttapos.controls.custlookup import WuttaCustomerLookup
from wuttapos.controls.itemlookup import WuttaProductLookup from wuttapos.controls.itemlookup import WuttaProductLookup
from wuttapos.controls.txnitem import WuttaTxnItem from wuttapos.controls.txnitem import WuttaTxnItem
@ -98,13 +99,7 @@ class POSView(WuttaView):
self.page.session.set('cust_display', handler.get_screen_cust_display(customer=customer)) self.page.session.set('cust_display', handler.get_screen_cust_display(customer=customer))
self.informed_refresh() self.informed_refresh()
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER SET: {customer}", self.show_snackbar(f"CUSTOMER SET: {customer}", bgcolor='green')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='green',
duration=1500)
self.page.snack_bar.open = True
def item_click(self, e): def item_click(self, e):
@ -156,13 +151,7 @@ class POSView(WuttaView):
if record_badscan: if record_badscan:
handler.record_badscan(batch, entry, quantity=quantity, user=user) handler.record_badscan(batch, entry, quantity=quantity, user=user)
self.page.snack_bar = ft.SnackBar(ft.Text(f"PRODUCT NOT FOUND: {entry}", self.show_snackbar(f"PRODUCT NOT FOUND: {entry}", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
session.commit() session.commit()
session.close() session.close()
@ -228,13 +217,7 @@ class POSView(WuttaView):
customer = clientele.locate_customer_for_entry(session, entry) customer = clientele.locate_customer_for_entry(session, entry)
if not customer: if not customer:
session.close() session.close()
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER NOT FOUND: {entry}", self.show_snackbar(f"CUSTOMER NOT FOUND: {entry}", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update() self.page.update()
return return
@ -401,13 +384,7 @@ class POSView(WuttaView):
self.informed_refresh() self.informed_refresh()
dlg.open = False dlg.open = False
self.page.snack_bar = ft.SnackBar(ft.Text("CUSTOMER REMOVED", self.show_snackbar("CUSTOMER REMOVED", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset() self.reset()
def cancel(e): def cancel(e):
@ -454,13 +431,7 @@ class POSView(WuttaView):
self.reset() self.reset()
else: # customer not found else: # customer not found
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER NOT FOUND: {entry}", self.show_snackbar(f"CUSTOMER NOT FOUND: {entry}", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
# TODO: should use reset() here? # TODO: should use reset() here?
self.main_input.focus() self.main_input.focus()
self.page.update() self.page.update()
@ -494,13 +465,7 @@ class POSView(WuttaView):
if self.set_quantity.data is not None: if self.set_quantity.data is not None:
quantity = self.set_quantity.data quantity = self.set_quantity.data
self.page.snack_bar = ft.SnackBar(ft.Text(f"QUANTITY ALREADY SET: {quantity}", self.show_snackbar(f"QUANTITY ALREADY SET: {quantity}", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
else: else:
try: try:
@ -516,13 +481,7 @@ class POSView(WuttaView):
self.main_input.focus() self.main_input.focus()
else: else:
self.page.snack_bar = ft.SnackBar(ft.Text(f"INVALID @ QUANTITY: {quantity}", self.show_snackbar(f"INVALID @ QUANTITY: {quantity}", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update() self.page.update()
@ -854,12 +813,7 @@ class POSView(WuttaView):
feature = e.control.content.value.replace('\n', ' ') feature = e.control.content.value.replace('\n', ' ')
if feature: if feature:
text += f": {feature}" text += f": {feature}"
self.page.snack_bar = ft.SnackBar(ft.Text(text, color='black', self.show_snackbar(text, bgcolor='yellow')
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update() self.page.update()
def adjust_price_click(self, e): def adjust_price_click(self, e):
@ -1033,65 +987,78 @@ class POSView(WuttaView):
self.selected_item.bgcolor = 'blue' self.selected_item.bgcolor = 'blue'
self.page.update() self.page.update()
def authorized_action(self, perm, action, cancel=None, message=None):
auth = self.app.get_auth_handler()
# current user is assumed if they have the perm
session = self.app.make_session()
user = self.get_current_user(session)
has_perm = auth.has_permission(session, user, perm)
session.expunge(user)
session.close()
if has_perm:
action(user)
return
# otherwise must prompt for different user credentials...
def login_cancel(e):
dlg.open = False
if cancel:
cancel()
self.reset()
def login_failure(e):
self.show_snackbar("Login failed", bgcolor='yellow')
self.page.update()
def authz_failure(user, user_display):
self.show_snackbar(f"User does not have permission: {user_display}",
bgcolor='yellow')
self.page.update()
def login_success(user, user_display):
dlg.open = False
self.page.update()
# # nb. just in case?
# # cf. https://github.com/flet-dev/flet/issues/1670
# time.sleep(0.1)
action(user)
self.reset()
title = "Manager Override"
if message:
title = f"{title} - {message}"
dlg = ft.AlertDialog(
modal=True,
title=ft.Text(title),
content=ft.Container(
ft.Column(
[
ft.Divider(),
WuttaLoginForm(self.config,
perm_required=perm,
on_login_success=login_success,
on_login_failure=login_failure,
on_authz_failure=authz_failure),
],
),
height=600,
),
actions=[
self.make_button("Cancel", on_click=login_cancel,
height=80, width=120),
],
)
self.page.dialog = dlg
dlg.open = True
self.page.update()
def void_click(self, e): def void_click(self, e):
def confirm(e):
dlg.open = False
session = self.app.make_session()
handler = self.get_batch_handler()
user = self.get_current_user(session)
batch = handler.get_current_batch(user, create=True)
if self.selected_item:
# void line
row = self.selected_item.data['row']
if row.void:
# cannot void an already void line
self.page.snack_bar = ft.SnackBar(ft.Text("LINE ALREADY VOID",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
elif row.row_type != self.enum.POS_ROW_TYPE_SELL:
# cannot void line unless of type 'sell'
self.page.snack_bar = ft.SnackBar(ft.Text("LINE DOES NOT ALLOW VOID",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
else:
# okay, void the line
row = session.get(row.__class__, row.uuid)
handler.void_row(row, user)
self.selected_item.data['row'] = row
self.selected_item.content.row = row
self.selected_item.content.refresh()
self.clear_item_selection()
# update screen to reflect new balance
self.txn_total.value = self.app.render_currency(batch.get_balance())
else:
# void txn
handler.void_batch(batch, user)
self.clear_all()
session.commit()
session.close()
self.reset()
def cancel(e):
dlg.open = False
self.reset()
session = self.app.make_session() session = self.app.make_session()
batch = self.get_current_batch(session, create=False) batch = self.get_current_batch(session, create=False)
session.close() session.close()
@ -1102,36 +1069,89 @@ class POSView(WuttaView):
self.reset() self.reset()
return return
def confirm(e):
dlg.open = False
self.page.update()
if self.selected_item:
session = self.app.make_session()
handler = self.get_batch_handler()
user = self.get_current_user(session)
# void line
row = self.selected_item.data['row']
if row.void:
# cannot void an already void line
self.show_snackbar("LINE ALREADY VOID", bgcolor='yellow')
elif row.row_type != self.enum.POS_ROW_TYPE_SELL:
# cannot void line unless of type 'sell'
self.show_snackbar("LINE DOES NOT ALLOW VOID", bgcolor='yellow')
else:
# okay, void the line
row = session.get(row.__class__, row.uuid)
handler.void_row(row, user)
session.commit()
# refresh display
self.selected_item.data['row'] = row
self.selected_item.content.row = row
self.selected_item.content.refresh()
self.clear_item_selection()
self.txn_total.value = self.app.render_currency(row.batch.get_balance())
session.close()
self.reset()
else: # void txn
# 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.void_txn', self.void_transaction,
message="Void Transaction")
def cancel(e):
dlg.open = False
self.reset()
# prompt to void something
target = 'LINE' if self.selected_item else 'TXN'
dlg = ft.AlertDialog( dlg = ft.AlertDialog(
title=ft.Text("Confirm VOID"), title=ft.Text("Confirm VOID"),
content=ft.Text("Really VOID this transaction?"), content=ft.Text(f"Really VOID {target}?"),
actions=[ actions=[
ft.Container(content=ft.Text("Yes, VOID", self.make_button(f"VOID {target}", font_size=self.default_font_size,
size=self.default_font_size, height=self.default_button_size,
color='black', width=self.default_button_size * 2.5,
weight=ft.FontWeight.BOLD), bgcolor='red',
height=self.default_button_size, on_click=confirm),
width=self.default_button_size * 2.5, self.make_button("Cancel", font_size=self.default_font_size,
alignment=ft.alignment.center, height=self.default_button_size,
bgcolor='red', width=self.default_button_size * 2.5,
border=ft.border.all(1, 'black'), on_click=cancel),
border_radius=ft.border_radius.all(5),
on_click=confirm),
ft.Container(content=ft.Text("Cancel",
size=self.default_font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=cancel),
]) ])
self.page.dialog = dlg self.page.dialog = dlg
dlg.open = True dlg.open = True
self.page.update() self.page.update()
def void_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.void_batch(batch, user)
session.commit()
session.close()
self.clear_all()
self.reset()
def tender_click(self, e): def tender_click(self, e):
session = self.app.make_session() session = self.app.make_session()
handler = self.get_batch_handler() handler = self.get_batch_handler()
@ -1143,39 +1163,21 @@ class POSView(WuttaView):
# nothing to do if no transaction # nothing to do if no transaction
if not batch: if not batch:
session.close() session.close()
self.page.snack_bar = ft.SnackBar(ft.Text("NO TRANSACTION", self.show_snackbar("NO TRANSACTION", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset() self.reset()
return return
# nothing to do if zero sales # nothing to do if zero sales
if not batch.get_balance(): if not batch.get_balance():
session.close() session.close()
self.page.snack_bar = ft.SnackBar(ft.Text("NO SALES", self.show_snackbar("NO SALES", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset() self.reset()
return return
# nothing to do if no amount provided # nothing to do if no amount provided
if not self.main_input.value: if not self.main_input.value:
session.close() session.close()
self.page.snack_bar = ft.SnackBar(ft.Text("MUST SPECIFY AMOUNT", self.show_snackbar("MUST SPECIFY AMOUNT", bgcolor='yellow')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset() self.reset()
return return
@ -1184,26 +1186,16 @@ class POSView(WuttaView):
amount = decimal.Decimal(self.main_input.value) amount = decimal.Decimal(self.main_input.value)
except: except:
session.close() session.close()
self.page.snack_bar = ft.SnackBar(ft.Text(f"AMOUNT NOT VALID: {self.main_input.value}", self.show_snackbar(f"AMOUNT NOT VALID: {self.main_input.value}",
color='black', bgcolor='yellow')
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset() self.reset()
return return
# do nothing if @ quantity present # do nothing if @ quantity present
if self.set_quantity.data: if self.set_quantity.data:
session.close() session.close()
self.page.snack_bar = ft.SnackBar(ft.Text(f"QUANTITY NOT ALLOWED FOR TENDER: {self.set_quantity.value}", self.show_snackbar(f"QUANTITY NOT ALLOWED FOR TENDER: {self.set_quantity.value}",
color='black', bgcolor='yellow')
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset() self.reset()
return return
@ -1223,13 +1215,7 @@ class POSView(WuttaView):
session.rollback() session.rollback()
log.exception("failed to apply tender '%s' for %s in batch %s", log.exception("failed to apply tender '%s' for %s in batch %s",
code, amount, batch.id_str) code, amount, batch.id_str)
self.page.snack_bar = ft.SnackBar(ft.Text(f"ERROR: {error}", self.show_snackbar(f"ERROR: {error}", bgcolor='red')
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='red',
duration=1500)
self.page.snack_bar.open = True
else: else:
session.commit() session.commit()
@ -1257,10 +1243,10 @@ class POSView(WuttaView):
ft.Container( ft.Container(
ft.Column( ft.Column(
[ [
ft.Text("Change Back", size=24, ft.Text("Change Due", size=24,
weight=ft.FontWeight.BOLD), weight=ft.FontWeight.BOLD),
ft.Divider(), ft.Divider(),
ft.Text("Please give customer change back:", ft.Text("Please give customer their change:",
size=20), size=20),
ft.Text(self.app.render_currency(last_row.tender_total), ft.Text(self.app.render_currency(last_row.tender_total),
size=32, weight=ft.FontWeight.BOLD), size=32, weight=ft.FontWeight.BOLD),