wuttapos/wuttapos/views/pos.py

1232 lines
47 KiB
Python

# -*- 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 - POS view
"""
import decimal
import logging
import time
import flet as ft
from .base import WuttaView
from wuttapos.controls.custlookup import WuttaCustomerLookup
from wuttapos.controls.itemlookup import WuttaProductLookup
from wuttapos.controls.txnitem import WuttaTxnItem
log = logging.getLogger(__name__)
class POSView(WuttaView):
"""
Main POS view for WuttaPOS
"""
# TODO: should be configurable?
default_button_size = 100
default_font_size = 40
disabled_bgcolor = '#aaaaaa'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# keep a list of "informed" controls - i.e. child controls
# within this view, which need to stay abreast of global
# changes to the transaction, customer etc.
self.informed_controls = []
if hasattr(self, 'header'):
self.informed_controls.append(self.header)
def informed_refresh(self):
for control in self.informed_controls:
control.informed_refresh()
def reset(self, e=None, clear_quantity=True):
"""
This is a convenience method, meant only to clear the main
input and set focus to it. Will also update() the page.
The ``e`` arg is ignored and accepted only so this method may
be registered as an event handler, e.g. ``on_cancel``.
"""
# clear set (@) quantity
if clear_quantity:
self.set_quantity.data = None
self.set_quantity.value = None
# clear/focus main input
self.main_input.value = ''
self.main_input.focus()
self.page.update()
def set_customer(self, customer, batch=None):
session = self.app.get_session(customer)
user = self.get_current_user(session)
if not batch:
batch = self.get_current_batch(session, user=user)
handler = self.get_batch_handler()
handler.set_customer(batch, customer, user=user)
self.page.session.set('txn_display', handler.get_screen_txn_display(batch))
self.page.session.set('cust_uuid', customer.uuid)
self.page.session.set('cust_display', handler.get_screen_cust_display(customer=customer))
self.informed_refresh()
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER SET: {customer}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='green',
duration=1500)
self.page.snack_bar.open = True
def item_click(self, e):
value = self.main_input.value
if value:
if not self.attempt_add_product():
self.item_lookup(value)
else:
self.item_lookup()
def attempt_add_product(self, uuid=None, record_badscan=False):
session = self.app.make_session()
handler = self.get_batch_handler()
user = self.get_current_user(session)
batch = self.get_current_batch(session, user=user)
entry = self.main_input.value
quantity = 1
if self.set_quantity.data is not None:
quantity = self.set_quantity.data
product = None
item_entry = entry
if uuid:
product = session.get(self.model.Product, uuid)
assert product
key = self.app.get_product_key_field()
item_entry = str(getattr(product, key) or '') or uuid
row = handler.process_entry(batch, product or entry, quantity=quantity,
item_entry=item_entry, user=user)
if row:
session.commit()
self.add_row_item(row)
self.items.scroll_to(offset=-1, duration=100)
self.txn_total.value = self.app.render_currency(batch.get_balance())
self.reset()
else:
if record_badscan:
handler.record_badscan(batch, entry, quantity=quantity, user=user)
self.page.snack_bar = ft.SnackBar(ft.Text(f"PRODUCT NOT FOUND: {entry}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
session.commit()
session.close()
self.page.update()
return bool(row)
def item_lookup(self, value=None):
def select(uuid):
self.attempt_add_product(uuid=uuid)
dlg.open = False
self.reset()
def cancel(e):
dlg.open = False
self.reset(clear_quantity=False)
dlg = ft.AlertDialog(
modal=True,
title=ft.Text("Product Lookup"),
content=WuttaProductLookup(self.config, initial_search=value,
on_select=select, on_cancel=cancel),
)
self.page.dialog = dlg
dlg.open = True
self.page.update()
def customer_lookup(self, value=None):
def select(uuid):
session = self.app.make_session()
customer = session.get(self.model.Customer, uuid)
self.set_customer(customer)
session.commit()
session.close()
dlg.open = False
self.reset()
def cancel(e):
dlg.open = False
self.reset()
dlg = ft.AlertDialog(
modal=True,
title=ft.Text("Customer Lookup"),
content=WuttaCustomerLookup(self.config, initial_search=value,
on_select=select, on_cancel=cancel),
)
self.page.dialog = dlg
dlg.open = True
self.page.update()
def customer_info(self):
clientele = self.app.get_clientele_handler()
session = self.app.make_session()
entry = self.main_input.value
if entry:
different = True
customer = clientele.locate_customer_for_entry(session, entry)
if not customer:
session.close()
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER NOT FOUND: {entry}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update()
return
else:
different = False
customer = session.get(self.model.Customer, self.page.session.get('cust_uuid'))
assert customer
info = clientele.get_customer_info_markdown(customer)
session.close()
def close(e):
dlg.open = False
self.reset()
font_size = self.default_font_size * 0.8
dlg = ft.AlertDialog(
# modal=True,
title=ft.Text("Customer Info"),
content=ft.Container(ft.Column(
[
ft.Container(
content=ft.Text("NOTE: this is a DIFFERENT customer than the txn has!"),
bgcolor='yellow',
visible=different,
),
ft.Divider(),
ft.Container(
theme_mode=ft.ThemeMode.SYSTEM,
theme=ft.Theme(text_theme=ft.TextTheme(
body_medium=ft.TextStyle(
size=24,
color='black',
))),
content=ft.Markdown(info),
),
],
height=500,
width=500,
)),
actions=[
ft.Container(content=ft.Text("Close",
size=font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size * 0.8,
width=self.default_button_size * 1.2,
alignment=ft.alignment.center,
bgcolor='blue',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=close),
],
# actions_alignment=ft.MainAxisAlignment.END,
)
self.page.dialog = dlg
dlg.open = True
self.page.update()
def customer_prompt(self):
def view_info(e):
dlg.open = False
self.page.update()
# cf. https://github.com/flet-dev/flet/issues/1670
time.sleep(0.1)
self.customer_info()
def remove(e):
dlg.open = False
self.page.update()
# cf. https://github.com/flet-dev/flet/issues/1670
time.sleep(0.1)
self.remove_customer_prompt()
def replace(e):
dlg.open = False
self.page.update()
# cf. https://github.com/flet-dev/flet/issues/1670
time.sleep(0.1)
entry = self.main_input.value
if entry:
if not self.attempt_set_customer(entry):
self.customer_lookup(entry)
else:
self.customer_lookup()
def cancel(e):
dlg.open = False
self.reset()
font_size = self.default_font_size * 0.8
dlg = ft.AlertDialog(
# modal=True,
title=ft.Text("Customer Already Selected"),
content=ft.Text("What would you like to do?", size=20),
actions=[
ft.Container(content=ft.Text("Remove",
size=font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='red',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=remove),
ft.Container(content=ft.Text("Replace",
size=font_size,
color='black',
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='yellow',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=replace),
ft.Container(content=ft.Text("View Info",
size=font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='blue',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=view_info),
ft.Container(content=ft.Text("Cancel",
size=font_size,
# color='black',
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
dlg.open = True
self.page.update()
def remove_customer_prompt(self):
def remove(e):
session = self.app.make_session()
user = self.get_current_user(session)
handler = self.get_batch_handler()
batch = self.get_current_batch(session, user=user)
handler.set_customer(batch, None, user=user)
session.commit()
session.close()
self.page.session.set('cust_uuid', None)
self.page.session.set('cust_display', None)
self.informed_refresh()
dlg.open = False
self.page.snack_bar = ft.SnackBar(ft.Text("CUSTOMER REMOVED",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset()
def cancel(e):
dlg.open = False
self.reset()
font_size = self.default_font_size * 0.8
dlg = ft.AlertDialog(
title=ft.Text("Remove Customer"),
content=ft.Text("Really remove the customer from this transaction?", size=20),
actions=[
ft.Container(content=ft.Text("Yes, Remove",
size=font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='red',
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
on_click=remove),
ft.Container(content=ft.Text("Cancel",
size=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
dlg.open = True
self.page.update()
def attempt_set_customer(self, entry=None):
session = self.app.make_session()
customer = self.app.get_clientele_handler().locate_customer_for_entry(session, entry)
if customer:
self.set_customer(customer)
self.reset()
else: # customer not found
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER NOT FOUND: {entry}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
# TODO: should use reset() here?
self.main_input.focus()
self.page.update()
session.commit()
session.close()
return bool(customer)
def customer_click(self, e):
# prompt user to replace customer if already set
if self.page.session.get('cust_uuid'):
self.customer_prompt()
else:
value = self.main_input.value
if value:
# okay try to set it with given value
if not self.attempt_set_customer(value):
self.customer_lookup(value)
else:
# no value provided, so do lookup
self.customer_lookup()
def build_controls(self):
session = self.app.make_session()
handler = self.get_batch_handler()
self.tender_cash = handler.get_tender(session, 'cash')
self.tender_check = handler.get_tender(session, 'check')
session.expunge_all()
session.close()
self.main_input = ft.TextField(on_submit=self.main_submit,
text_size=24,
text_style=ft.TextStyle(weight=ft.FontWeight.BOLD),
autofocus=True)
self.selected_item = None
self.items = ft.ListView(
item_extent=50,
height=800,
)
self.txn_total = ft.Text("", size=40)
self.items_column = ft.Column(
controls=[
ft.Container(
content=self.items,
padding=ft.padding.only(10, 0, 10, 0)),
ft.Row([self.txn_total],
alignment=ft.MainAxisAlignment.END),
],
expand=1,
)
def tenkey_click(e):
value = e.control.content.value
if value == 'ENTER':
self.main_submit()
elif value == '': # backspace
if self.main_input.value:
self.main_input.value = self.main_input.value[:-1]
self.main_input.focus()
self.page.update()
elif value == 'CE': # clear entry
if self.main_input.value:
self.main_input.value = ""
elif self.set_quantity.data is not None:
self.set_quantity.data = None
self.set_quantity.value = None
elif self.selected_item:
self.selected_item.bgcolor = 'white'
self.selected_item = None
self.main_input.focus()
self.page.update()
elif value == '@':
quantity = self.main_input.value
valid = False
if self.set_quantity.data is not None:
quantity = self.set_quantity.data
self.page.snack_bar = ft.SnackBar(ft.Text(f"QUANTITY ALREADY SET: {quantity}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
else:
try:
quantity = decimal.Decimal(quantity)
valid = True
except decimal.InvalidOperation:
pass
if valid and quantity:
self.set_quantity.data = quantity
self.set_quantity.value = self.app.render_quantity(quantity) + " @ "
self.main_input.value = ""
self.main_input.focus()
else:
self.page.snack_bar = ft.SnackBar(ft.Text(f"INVALID @ QUANTITY: {quantity}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update()
elif value == '': # UP
# select previous item, if selection in progress
if self.selected_item:
i = self.items.controls.index(self.selected_item)
if i > 0:
self.items.scroll_to(delta=-50, duration=100)
self.select_txn_item(self.items.controls[i - 1])
return
self.items.scroll_to(delta=-50, duration=100)
self.page.update()
elif value == '':
# select next item, if selection in progress
if self.selected_item:
i = self.items.controls.index(self.selected_item)
if (i + 1) < len(self.items.controls):
self.items.scroll_to(delta=50, duration=100)
self.select_txn_item(self.items.controls[i + 1])
return
self.items.scroll_to(delta=50, duration=100)
self.page.update()
else:
self.main_input.value = f"{self.main_input.value or ''}{value}"
self.main_input.focus()
self.page.update()
def up_long_press(e):
self.items.scroll_to(delta=-500, duration=100)
self.page.update()
def down_long_press(e):
self.items.scroll_to(delta=500, duration=100)
self.page.update()
tenkey_button_size = self.default_button_size
tenkey_font_size = self.default_font_size
def tenkey_button(text,
bgcolor='green',
height=tenkey_button_size,
width=tenkey_button_size,
on_click=tenkey_click,
on_long_press=None,
):
return ft.Container(content=ft.Text(text, size=tenkey_font_size,
weight=ft.FontWeight.BOLD),
height=height,
width=width,
on_click=on_click,
on_long_press=on_long_press,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor=bgcolor)
self.tenkey_menu = ft.Container(
content=ft.Column(
[
ft.Row(
[
tenkey_button("1"),
tenkey_button("2"),
tenkey_button("3"),
tenkey_button("@"),
],
spacing=0,
),
ft.Row(
[
tenkey_button("4"),
tenkey_button("5"),
tenkey_button("6"),
tenkey_button("", on_long_press=up_long_press),
],
spacing=0,
),
ft.Row(
[
tenkey_button("7"),
tenkey_button("8"),
tenkey_button("9"),
tenkey_button("", on_long_press=down_long_press),
],
spacing=0,
),
ft.Row(
[
tenkey_button("0"),
# tenkey_button("00"),
tenkey_button("."),
tenkey_button("ENTER", width=tenkey_button_size * 2),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
spacing=0,
),
],
spacing=0,
),
expand=0)
meta_button_height = tenkey_button_size
meta_button_width = meta_button_height * 2
meta_font_size = tenkey_font_size
def meta_button(text, on_click=None, bgcolor='blue', data=None,
font_size=None):
return ft.Container(content=ft.Text(text, size=font_size or meta_font_size,
weight=ft.FontWeight.BOLD),
height=meta_button_height,
width=meta_button_width,
on_click=on_click,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor=bgcolor,
data=data)
self.meta_menu = ft.Container(
content=ft.Column(
[
ft.Row(
[
meta_button("ITEM", bgcolor='blue', on_click=self.item_click),
meta_button("VOID", bgcolor='red', on_click=self.void_click),
],
spacing=0,
),
ft.Row(
[
meta_button("CUST", bgcolor='blue', on_click=self.customer_click),
meta_button("MGR", bgcolor='yellow', on_click=self.not_supported),
],
spacing=0,
),
ft.Row(
[
meta_button("TODO", bgcolor='blue', on_click=self.not_supported),
meta_button("TODO", bgcolor='blue', on_click=self.not_supported),
],
spacing=0,
),
ft.Row(
[
meta_button("TODO", bgcolor='blue', on_click=self.not_supported),
meta_button("(TEST ERROR)", bgcolor='red', on_click=self.not_supported,
font_size=24, data={'error': True}),
],
spacing=0,
),
],
spacing=0,
),
expand=0)
context_button_height = tenkey_button_size
context_button_width = context_button_height * 2
context_font_size = tenkey_font_size
def context_button(text, on_click=None, data=None):
return ft.Container(content=ft.Text(text, size=context_font_size,
weight=ft.FontWeight.BOLD),
height=context_button_height,
width=context_button_width,
on_click=on_click,
data=data,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor='orange')
self.context_menu = ft.Container(
content=ft.Column(
[
ft.Row(
[
self.make_cash_button(),
self.make_check_button(),
],
spacing=0,
),
],
spacing=0,
),
expand=0)
self.set_quantity = ft.Text(value=None, data=None, weight=ft.FontWeight.BOLD, size=40)
return [
self.build_header(),
ft.Row(
[
self.make_logo_image(height=80),
ft.Row(
[
self.set_quantity,
self.main_input,
tenkey_button("", height=70, width=70),
tenkey_button("CE", height=70, width=70),
],
alignment=ft.MainAxisAlignment.CENTER,
expand=True,
),
],
),
ft.Row(),
ft.Row(),
ft.Row(),
ft.Row(
[
self.items_column,
ft.Column(
[
ft.Row(
[
self.tenkey_menu,
self.meta_menu,
],
),
self.context_menu,
],
),
],
vertical_alignment=ft.CrossAxisAlignment.START,
),
]
def make_text(self, *args, **kwargs):
kwargs.setdefault('weight', ft.FontWeight.BOLD)
kwargs.setdefault('size', 24)
return ft.Text(*args, **kwargs)
def make_tender_button(self, tender, **kwargs):
if isinstance(tender, self.model.Tender):
info = {'tender_code': tender.code,
'tender_name': tender.name}
else:
info = tender
font_size = kwargs.pop('font_size', self.default_font_size)
text = ft.Text(info['tender_name'],
size=font_size,
weight=ft.FontWeight.BOLD)
kw = {
'height': self.default_button_size,
'width': self.default_button_size * 2,
'on_click': self.tender_click,
'alignment': ft.alignment.center,
'border': ft.border.all(1, 'black'),
'border_radius': ft.border_radius.all(5),
'bgcolor': 'orange',
}
kw.update(kwargs)
kw['data'] = info
return ft.Container(content=text, **kw)
def make_cash_button(self, **kwargs):
cash = self.tender_cash or {'tender_code': 'CA',
'tender_name': "Cash"}
return self.make_tender_button(cash, **kwargs)
def make_check_button(self, **kwargs):
check = self.tender_check or {'tender_code': 'CK',
'tender_name': "Check"}
return self.make_tender_button(check, **kwargs)
def get_current_user(self, session):
uuid = self.page.session.get('user_uuid')
if uuid:
return session.get(self.model.User, uuid)
def get_current_batch(self, session, user=None, create=True):
handler = self.get_batch_handler()
if not user:
user = self.get_current_user(session)
batch, created = handler.get_current_batch(user, create=create, return_created=True)
if created:
self.page.session.set('txn_display', handler.get_screen_txn_display(batch))
self.informed_refresh()
return batch
def did_mount(self):
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.txn_total.value = self.app.render_currency(batch.get_balance())
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()
session.commit()
session.close()
self.page.update()
def not_supported(self, e=None, feature=None):
# test error handler
if e.control.data and e.control.data.get('error'):
raise RuntimeError("NOT YET SUPPORTED")
text = "NOT YET SUPPORTED"
if not feature and e:
feature = e.control.content.value
if feature:
text += f": {feature}"
self.page.snack_bar = ft.SnackBar(ft.Text(text, color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.page.update()
def add_row_item(self, row):
# TODO: row types ugh
if row.row_type not in (self.enum.POS_ROW_TYPE_SELL,
self.enum.POS_ROW_TYPE_TENDER,
self.enum.POS_ROW_TYPE_CHANGE_BACK):
return
self.items.controls.append(
ft.Container(
content=WuttaTxnItem(self.config, row),
border=ft.border.only(bottom=ft.border.BorderSide(1, 'gray')),
padding=ft.padding.only(0, 5, 0, 5),
on_click=self.list_item_click,
data={'row': row},
key=row.uuid,
))
def list_item_click(self, e):
self.select_txn_item(e.control)
def select_txn_item(self, item):
if self.selected_item:
self.selected_item.bgcolor = 'white'
item.bgcolor = 'blue'
self.selected_item = item
self.page.update()
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.mark_void()
self.selected_item.bgcolor = 'white'
self.selected_item = None
# 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()
batch = self.get_current_batch(session, create=False)
session.close()
if batch:
# prompt to void something
target = 'LINE' if self.selected_item else 'TXN'
dlg = ft.AlertDialog(
title=ft.Text("Confirm VOID"),
content=ft.Text(f"Really VOID {target}?"),
actions=[
ft.Container(content=ft.Text(f"VOID {target}",
size=self.default_font_size,
color='black',
weight=ft.FontWeight.BOLD),
height=self.default_button_size,
width=self.default_button_size * 2.5,
alignment=ft.alignment.center,
bgcolor='red',
border=ft.border.all(1, 'black'),
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),
])
else: # nothing to void
dlg = ft.AlertDialog(
title=ft.Text("No Transaction"),
content=ft.Text("You do not have a transaction open currently.", size=20),
actions=[
ft.Container(content=ft.Text("Close",
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
dlg.open = True
self.page.update()
def tender_click(self, e):
session = self.app.make_session()
handler = self.get_batch_handler()
user = self.get_current_user(session)
batch = self.get_current_batch(session, user=user, create=False)
code = e.control.data['tender_code']
tender = handler.get_tender(session, code)
# nothing to do if no transaction
if not batch:
session.close()
self.page.snack_bar = ft.SnackBar(ft.Text("NO TRANSACTION",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset()
return
# nothing to do if zero sales
if not batch.get_balance():
session.close()
self.page.snack_bar = ft.SnackBar(ft.Text("NO SALES",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset()
return
# nothing to do if no amount provided
if not self.main_input.value:
session.close()
self.page.snack_bar = ft.SnackBar(ft.Text("MUST SPECIFY AMOUNT",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset()
return
# nothing to do if amount not valid
try:
amount = decimal.Decimal(self.main_input.value)
except:
session.close()
self.page.snack_bar = ft.SnackBar(ft.Text(f"AMOUNT NOT VALID: {self.main_input.value}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset()
return
# do nothing if @ quantity present
if self.set_quantity.data:
session.close()
self.page.snack_bar = ft.SnackBar(ft.Text(f"QUANTITY NOT ALLOWED FOR TENDER: {self.set_quantity.value}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='yellow',
duration=1500)
self.page.snack_bar.open = True
self.reset()
return
# tender / execute batch
try:
# TODO: still need support for the following:
# - if balance remains, update display (i.e. show this tender)
# - if this tender overpays, how to handle change back?
# apply tender amount to batch
# nb. this *may* execute the batch!
# nb. we negate the amount supplied by user
rows = handler.apply_tender(batch, user, tender or code, -amount)
except Exception as error:
session.rollback()
log.exception("failed to apply tender '%s' for %s in batch %s",
code, amount, batch.id_str)
self.page.snack_bar = ft.SnackBar(ft.Text(f"ERROR: {error}",
color='black',
size=20,
weight=ft.FontWeight.BOLD),
bgcolor='red',
duration=1500)
self.page.snack_bar.open = True
else:
session.commit()
# update screen to reflect new items/balance
for row in rows:
self.add_row_item(row)
self.txn_total.value = self.app.render_currency(batch.get_balance())
# executed batch means txn was finalized
if batch.executed:
# look for "change back" row, if found then show alert
last_row = rows[-1]
if last_row.row_type == self.enum.POS_ROW_TYPE_CHANGE_BACK:
def close_bs(e):
# user dismissed the change back alert; clear screen
bs.open = False
bs.update()
self.clear_all()
self.page.update()
bs = ft.BottomSheet(
ft.Container(
ft.Column(
[
ft.Text("Change Back", size=24,
weight=ft.FontWeight.BOLD),
ft.Divider(),
ft.Text("Please give customer change back:",
size=20),
ft.Text(self.app.render_currency(last_row.tender_total),
size=32, weight=ft.FontWeight.BOLD),
ft.Container(
content=self.make_button("Dismiss", on_click=close_bs,
height=80, width=120),
alignment=ft.alignment.center,
expand=1,
),
],
),
bgcolor='green',
padding=20,
),
open=True,
dismissible=False,
)
# show change back alert
# nb. we do *not* clear screen yet
self.page.overlay.append(bs)
else:
# txn finalized but no change back; clear screen
self.clear_all()
finally:
session.close()
self.reset()
def clear_all(self):
self.items.controls.clear()
self.txn_total.value = None
self.page.session.set('txn_display', None)
self.page.session.set('cust_uuid', None)
self.page.session.set('cust_display', None)
self.informed_refresh()
def main_submit(self, e=None):
if self.main_input.value:
self.attempt_add_product(record_badscan=True)
self.reset()