wuttapos/wuttapos/views/pos.py
Lance Edgar 9340e0d1bc Add button for "Adjust Price"
so far no constraints: any user, any item, any price
2023-10-05 20:04:29 -05:00

1327 lines
51 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
from wuttapos.controls.tenkey import WuttaTenkeyMenu
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()
if row.row_type == self.enum.POS_ROW_TYPE_BADPRICE:
self.show_snackbar(f"Product has invalid price: {row.item_entry}")
else:
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 tenkey_char(self, key):
if key == '@':
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()
else: # normal char
self.main_input.value = f"{self.main_input.value or ''}{key}"
self.main_input.focus()
self.page.update()
def tenkey_enter(self, e):
self.main_submit()
def tenkey_up_click(self, e):
# 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()
def tenkey_up_longpress(self, e):
self.items.scroll_to(delta=-500, duration=100)
self.page.update()
def tenkey_down_click(self, e):
# 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()
def tenkey_down_longpress(self, e):
self.items.scroll_to(delta=500, duration=100)
self.page.update()
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 backspace_click(e):
if self.main_input.value:
self.main_input.value = self.main_input.value[:-1]
self.main_input.focus()
self.page.update()
def clear_entry_click(e):
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.clear_item_selection()
self.main_input.focus()
self.page.update()
self.tenkey_menu = WuttaTenkeyMenu(self.config,
on_char=self.tenkey_char,
on_enter=self.tenkey_enter,
on_up_click=self.tenkey_up_click,
on_up_longpress=self.tenkey_up_longpress,
on_down_click=self.tenkey_down_click,
on_down_longpress=self.tenkey_down_longpress)
meta_button_height = self.default_button_size
meta_button_width = meta_button_height * 2
meta_font_size = self.default_font_size
def meta_button(text, font_size=None, bgcolor='blue', data=None, on_click=None):
return self.make_button(text,
font_size=font_size or meta_font_size,
height=meta_button_height,
width=meta_button_width,
bgcolor=bgcolor,
data=data,
on_click=on_click)
self.meta_menu = ft.Container(
content=ft.Column(
[
ft.Row(
[
meta_button("CUST", bgcolor='blue', on_click=self.customer_click),
meta_button("VOID", bgcolor='red', on_click=self.void_click),
],
spacing=0,
),
ft.Row(
[
meta_button("ITEM", bgcolor='blue', on_click=self.item_click),
meta_button("MGR", bgcolor='yellow', on_click=self.not_supported),
],
spacing=0,
),
ft.Row(
[
meta_button("Adjust\nPrice", font_size=30, bgcolor='yellow',
on_click=self.adjust_price_click),
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 = self.default_button_size
context_button_width = context_button_height * 2
context_font_size = self.default_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,
self.make_button("", font_size=40, bgcolor='green',
height=70, width=70,
on_click=backspace_click),
self.make_button("CE", font_size=40, bgcolor='green',
height=70, width=70,
on_click=clear_entry_click),
],
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.replace('\n', ' ')
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 adjust_price_click(self, e):
if not len(self.items.controls):
self.show_snackbar("There are no line items", update=False)
self.reset()
return
if not self.selected_item:
self.show_snackbar("Must first select a line item", update=False)
self.main_input.focus()
self.page.update()
return
def cancel(e):
dlg.open = False
self.main_input.focus()
self.page.update()
def clear(e):
price_override.value = ''
price_override.focus()
self.page.update()
def tenkey_char(key):
price_override.value = f"{price_override.value or ''}{key}"
self.page.update()
def confirm(e):
dlg.open = False
try:
price = decimal.Decimal(price_override.value)
except decimal.InvalidOperation:
self.show_snackbar(f"Price is not valid: {price_override.value}", update=False)
self.main_input.focus()
self.page.update()
return
session = self.app.make_session()
user = self.get_current_user(session)
handler = self.get_batch_handler()
row = self.selected_item.data['row']
row = session.get(row.__class__, row.uuid)
new_row = handler.override_price(row, user, price)
session.commit()
# update screen to reflect new balance
batch = row.batch
self.txn_total.value = self.app.render_currency(batch.get_balance())
# update item display
self.selected_item.data['row'] = row
self.selected_item.content.row = row
self.selected_item.content.refresh()
self.items.update()
session.expunge_all()
session.close()
self.clear_item_selection()
self.reset()
price_override = ft.TextField(value=self.main_input.value,
text_size=32,
text_style=ft.TextStyle(weight=ft.FontWeight.BOLD),
autofocus=True,
on_submit=confirm)
dlg = ft.AlertDialog(
modal=True,
title=ft.Text("Adjust Price"),
content=ft.Container(
ft.Column(
[
ft.Divider(),
ft.Row(
[
ft.Text("Reg Price:",
size=32, weight=ft.FontWeight.BOLD),
ft.Text(self.app.render_currency(self.selected_item.data['row'].reg_price),
size=32, weight=ft.FontWeight.BOLD),
],
),
ft.Row(),
ft.Row(
[
ft.Text("Txn Price:",
size=32, weight=ft.FontWeight.BOLD),
ft.Text(self.app.render_currency(self.selected_item.data['row'].txn_price),
size=32, weight=ft.FontWeight.BOLD),
],
),
ft.Row(),
ft.Row(),
ft.Row(
[
ft.Text("New Price:",
size=32, weight=ft.FontWeight.BOLD),
ft.VerticalDivider(),
ft.Text("$", size=32, weight=ft.FontWeight.BOLD),
price_override,
],
),
ft.Row(),
ft.Row(),
ft.Row(
[
WuttaTenkeyMenu(self.config, simple=True,
on_char=tenkey_char,
on_enter=confirm),
self.make_button("Clear",
height=self.default_button_size * 0.8,
width=self.default_button_size * 1.2,
on_click=clear),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
vertical_alignment=ft.CrossAxisAlignment.START,
),
],
),
height=700,
width=550,
),
actions=[
self.make_button("Cancel",
height=self.default_button_size * 0.8,
width=self.default_button_size * 1.2,
on_click=cancel),
self.make_button("Confirm",
bgcolor='blue',
height=self.default_button_size * 0.8,
width=self.default_button_size * 1.2,
on_click=confirm),
],
actions_alignment=ft.MainAxisAlignment.END,
)
self.page.dialog = dlg
dlg.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(5, 5, 5, 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.clear_item_selection()
self.selected_item = item
self.selected_item.bgcolor = 'blue'
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.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()
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_item_selection(self):
if self.selected_item:
self.selected_item.bgcolor = 'white'
self.selected_item.content.refresh()
self.selected_item = None
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()