1025 lines
39 KiB
Python
1025 lines
39 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.keyboard import WuttaKeyboard
|
||
from wuttapos.util import get_pos_batch_handler
|
||
|
||
|
||
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 get_batch_handler(self):
|
||
return get_pos_batch_handler(self.config)
|
||
|
||
def set_customer(self, customer, batch=None):
|
||
session = self.app.get_session(customer)
|
||
if not batch:
|
||
batch = self.get_current_batch(session)
|
||
|
||
handler = self.get_batch_handler()
|
||
handler.set_customer(batch, customer)
|
||
|
||
key = self.app.get_customer_key_field()
|
||
value = getattr(customer, key)
|
||
self.page.shared['cust_uuid'] = customer.uuid
|
||
self.page.shared['cust_display'] = str(value or '')
|
||
self.header.update_cust_display()
|
||
self.header.update()
|
||
# TODO: can we assume caller will do this?
|
||
# self.page.update()
|
||
|
||
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER SET: {customer}",
|
||
color='black',
|
||
weight=ft.FontWeight.BOLD),
|
||
bgcolor='green',
|
||
duration=1500)
|
||
self.page.snack_bar.open = True
|
||
|
||
def customer_lookup(self):
|
||
font_size = self.default_font_size * 0.8
|
||
selected_customer_uuid = None
|
||
|
||
def lookup(e=None):
|
||
global selected_customer_uuid
|
||
entry = searchbox.value
|
||
|
||
session = self.app.make_session()
|
||
results = self.app.get_clientele_handler().search_customers(session, entry)
|
||
|
||
search_results.rows.clear()
|
||
selected_customer_uuid = None
|
||
select_button.disabled = True
|
||
select_button.bgcolor = self.disabled_bgcolor
|
||
|
||
if results:
|
||
for customer in results:
|
||
search_results.rows.append(ft.DataRow(
|
||
cells=[
|
||
make_cell(customer['_customer_key_']),
|
||
make_cell(customer['name']),
|
||
make_cell(customer['phone_number']),
|
||
make_cell(customer['email_address']),
|
||
],
|
||
on_select_changed=select_changed,
|
||
data={'uuid': customer['uuid']},
|
||
))
|
||
no_results.visible = False
|
||
|
||
else:
|
||
no_results.value = f"NO RESULTS FOR: {entry}"
|
||
no_results.visible = True
|
||
|
||
searchbox.value = ''
|
||
searchbox.focus()
|
||
self.page.update()
|
||
|
||
searchbox = ft.TextField("", text_size=font_size * 0.8,
|
||
on_submit=lookup,
|
||
autofocus=True,
|
||
expand=True)
|
||
|
||
def keypress(key):
|
||
if key == '⏎':
|
||
lookup()
|
||
else:
|
||
if key == '⌫':
|
||
searchbox.value = searchbox.value[:-1]
|
||
else:
|
||
searchbox.value += key
|
||
searchbox.focus()
|
||
self.page.update()
|
||
|
||
def make_cell_text(text):
|
||
return ft.Text(text, size=32)
|
||
|
||
def make_cell(text):
|
||
return ft.DataCell(make_cell_text(text))
|
||
|
||
def reset(e):
|
||
global selected_customer_uuid
|
||
searchbox.value = ""
|
||
search_results.rows.clear()
|
||
no_results.visible = False
|
||
selected_customer_uuid = None
|
||
select_button.disabled = True
|
||
select_button.bgcolor = self.disabled_bgcolor
|
||
searchbox.focus()
|
||
self.page.update()
|
||
|
||
def select_changed(e):
|
||
global selected_customer_uuid
|
||
|
||
if e.data: # selected
|
||
selected_customer_uuid = e.control.data['uuid']
|
||
select_button.disabled = False
|
||
select_button.bgcolor = 'blue'
|
||
e.control.color = ft.colors.BLUE
|
||
else:
|
||
selected_customer_uuid = None
|
||
select_button.disabled = True
|
||
select_button.bgcolor = self.disabled_bgcolor
|
||
e.control.color = None
|
||
|
||
self.page.update()
|
||
|
||
def select_customer(e):
|
||
global selected_customer_uuid
|
||
if not selected_customer_uuid:
|
||
raise RuntimeError("no customer selected?")
|
||
|
||
session = self.app.make_session()
|
||
customer = session.get(self.model.Customer, selected_customer_uuid)
|
||
self.set_customer(customer)
|
||
session.commit()
|
||
session.close()
|
||
|
||
dlg.open = False
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
def cancel(e):
|
||
dlg.open = False
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
search_results = ft.DataTable(
|
||
columns=[
|
||
ft.DataColumn(make_cell_text(self.app.get_customer_key_label())),
|
||
ft.DataColumn(make_cell_text("Name")),
|
||
ft.DataColumn(make_cell_text("Phone")),
|
||
ft.DataColumn(make_cell_text("Email")),
|
||
],
|
||
)
|
||
|
||
no_results = ft.Text("NO RESULTS", size=32, color='red',
|
||
weight=ft.FontWeight.BOLD,
|
||
visible=False)
|
||
|
||
select_button = ft.Container(
|
||
content=ft.Text("Select", size=font_size * 0.8),
|
||
alignment=ft.alignment.center,
|
||
height=self.page.data['default_button_height_dlg'] * 0.8,
|
||
width=self.page.data['default_button_height_dlg'] * 1.3,
|
||
border=ft.border.all(1, 'black'),
|
||
border_radius=ft.border_radius.all(5),
|
||
on_click=select_customer,
|
||
disabled=True,
|
||
bgcolor=self.disabled_bgcolor,
|
||
)
|
||
|
||
dlg = ft.AlertDialog(
|
||
modal=True,
|
||
title=ft.Text("Customer Lookup"),
|
||
content=ft.Column(
|
||
[
|
||
ft.Row(
|
||
[
|
||
ft.Text("SEARCH FOR:"),
|
||
searchbox,
|
||
ft.Container(
|
||
content=ft.Text("Lookup", size=font_size * 0.8),
|
||
alignment=ft.alignment.center,
|
||
height=self.page.data['default_button_height_dlg'] * 0.8,
|
||
width=self.page.data['default_button_height_dlg'] * 1.3,
|
||
border=ft.border.all(1, 'black'),
|
||
border_radius=ft.border_radius.all(5),
|
||
on_click=lookup,
|
||
bgcolor='blue',
|
||
),
|
||
ft.Container(
|
||
content=ft.Text("Reset", size=font_size * 0.8),
|
||
alignment=ft.alignment.center,
|
||
height=self.page.data['default_button_height_dlg'] * 0.8,
|
||
width=self.page.data['default_button_height_dlg'] * 1.3,
|
||
border=ft.border.all(1, 'black'),
|
||
border_radius=ft.border_radius.all(5),
|
||
on_click=reset,
|
||
bgcolor='yellow',
|
||
),
|
||
ft.Container(
|
||
content=ft.Text("Cancel", size=font_size * 0.8),
|
||
alignment=ft.alignment.center,
|
||
height=self.page.data['default_button_height_dlg'] * 0.8,
|
||
width=self.page.data['default_button_height_dlg'] * 1.3,
|
||
border=ft.border.all(1, 'black'),
|
||
border_radius=ft.border_radius.all(5),
|
||
on_click=cancel,
|
||
),
|
||
],
|
||
),
|
||
ft.Divider(),
|
||
WuttaKeyboard(self.config, on_keypress=keypress),
|
||
ft.Divider(),
|
||
ft.Row(
|
||
[
|
||
ft.Column(
|
||
[
|
||
search_results,
|
||
no_results,
|
||
],
|
||
expand=True,
|
||
),
|
||
select_button,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
)
|
||
|
||
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',
|
||
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.shared['cust_uuid'])
|
||
assert customer
|
||
|
||
info = clientele.get_customer_info_markdown(customer)
|
||
session.close()
|
||
|
||
def close(e):
|
||
dlg.open = False
|
||
self.main_input.value = ''
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
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.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 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:
|
||
self.attempt_set_customer(entry)
|
||
else:
|
||
self.customer_lookup()
|
||
|
||
def cancel(e):
|
||
dlg.open = False
|
||
self.main_input.value = ''
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
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("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("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("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 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.main_input.value = ''
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
else: # customer not found
|
||
self.page.snack_bar = ft.SnackBar(ft.Text(f"CUSTOMER NOT FOUND: {entry}",
|
||
color='black',
|
||
weight=ft.FontWeight.BOLD),
|
||
bgcolor='yellow',
|
||
duration=1500)
|
||
self.page.snack_bar.open = True
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
session.commit()
|
||
session.close()
|
||
|
||
def customer_click(self, e):
|
||
|
||
# prompt user to replace customer if already set
|
||
if self.page.shared['cust_uuid']:
|
||
self.customer_prompt()
|
||
|
||
else:
|
||
value = self.main_input.value
|
||
if value:
|
||
# okay try to set it with given value
|
||
self.attempt_set_customer(value)
|
||
|
||
else:
|
||
# no value provided, so do lookup
|
||
self.customer_lookup()
|
||
|
||
def build_controls(self):
|
||
|
||
self.main_input = ft.TextField(on_submit=self.main_submit,
|
||
text_size=24,
|
||
text_style=ft.TextStyle(weight=ft.FontWeight.BOLD),
|
||
autofocus=True)
|
||
|
||
self.items = ft.ListView(
|
||
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 feedback_click(e):
|
||
|
||
message = ft.TextField(label="Message",
|
||
multiline=True, min_lines=5,
|
||
autofocus=True)
|
||
|
||
def send_feedback(e):
|
||
self.app.send_email('pos_feedback', data={
|
||
'user_name': self.page.shared['user_display'],
|
||
'message': message.value,
|
||
})
|
||
|
||
dlg.open = False
|
||
|
||
self.page.snack_bar = ft.SnackBar(ft.Text(f"MESSAGE WAS SENT",
|
||
color='black',
|
||
weight=ft.FontWeight.BOLD),
|
||
bgcolor='green',
|
||
duration=1500)
|
||
self.page.snack_bar.open = True
|
||
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
def cancel(e):
|
||
dlg.open = False
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
button_height = self.default_button_size * 0.8
|
||
dlg = ft.AlertDialog(
|
||
modal=True,
|
||
title=ft.Text("User Feedback"),
|
||
content=ft.Container(
|
||
content=ft.Column(
|
||
[
|
||
ft.Text("Questions, suggestions, comments, complaints, etc. "
|
||
"are welcome and may be submitted below. "),
|
||
ft.Divider(),
|
||
message,
|
||
],
|
||
expand=True,
|
||
),
|
||
height=500,
|
||
),
|
||
actions=[
|
||
ft.Row(
|
||
[
|
||
ft.Container(content=ft.Text("Send Message",
|
||
size=self.default_font_size,
|
||
color='black',
|
||
weight=ft.FontWeight.BOLD),
|
||
height=button_height,
|
||
width=self.default_button_size * 3,
|
||
alignment=ft.alignment.center,
|
||
bgcolor='blue',
|
||
border=ft.border.all(1, 'black'),
|
||
border_radius=ft.border_radius.all(5),
|
||
on_click=send_feedback),
|
||
ft.Container(content=ft.Text("Cancel",
|
||
size=self.default_font_size,
|
||
weight=ft.FontWeight.BOLD),
|
||
height=button_height,
|
||
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),
|
||
],
|
||
alignment=ft.MainAxisAlignment.CENTER,
|
||
),
|
||
],
|
||
)
|
||
|
||
self.page.dialog = dlg
|
||
dlg.open = True
|
||
self.page.update()
|
||
|
||
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
|
||
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',
|
||
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',
|
||
weight=ft.FontWeight.BOLD),
|
||
bgcolor='yellow',
|
||
duration=1500)
|
||
self.page.snack_bar.open = True
|
||
|
||
self.page.update()
|
||
|
||
elif value == '↑':
|
||
self.items.scroll_to(delta=-50, duration=250)
|
||
self.page.update()
|
||
|
||
elif value == '↓':
|
||
self.items.scroll_to(delta=50, duration=250)
|
||
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=250)
|
||
self.page.update()
|
||
|
||
def down_long_press(e):
|
||
self.items.scroll_to(delta=500, duration=250)
|
||
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'):
|
||
return ft.Container(content=ft.Text(text, size=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)
|
||
|
||
self.meta_menu = ft.Container(
|
||
content=ft.Column(
|
||
[
|
||
ft.Row(
|
||
[
|
||
meta_button("MGR", bgcolor='blue', on_click=self.not_supported),
|
||
meta_button("VOID", bgcolor='red', on_click=self.void_click),
|
||
],
|
||
spacing=0,
|
||
),
|
||
ft.Row(
|
||
[
|
||
meta_button("ITEM", bgcolor='blue', on_click=self.not_supported),
|
||
meta_button("CUST", bgcolor='blue', on_click=self.customer_click),
|
||
],
|
||
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("TODO", bgcolor='blue', on_click=self.not_supported),
|
||
],
|
||
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):
|
||
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,
|
||
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(
|
||
[
|
||
context_button("CASH", on_click=self.tender_click),
|
||
context_button("CHECK", on_click=self.tender_click),
|
||
],
|
||
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(
|
||
[
|
||
tenkey_button("Feedback", height=70, width=200,
|
||
bgcolor='blue', on_click=feedback_click),
|
||
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 get_current_batch(self, session, user=None, create=True):
|
||
if not user:
|
||
user = session.get(self.model.User, self.page.shared['user_uuid'])
|
||
handler = self.get_batch_handler()
|
||
return handler.get_current_batch(user, create=create)
|
||
|
||
def did_mount(self):
|
||
session = self.app.make_session()
|
||
batch = self.get_current_batch(session)
|
||
self.page.shared['txn_display'] = batch.id_str
|
||
|
||
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.sales_total)
|
||
|
||
session.commit()
|
||
session.close()
|
||
self.page.update()
|
||
|
||
def not_supported(self, e=None, feature=None):
|
||
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',
|
||
weight=ft.FontWeight.BOLD),
|
||
bgcolor='yellow',
|
||
duration=1500)
|
||
self.page.snack_bar.open = True
|
||
self.page.update()
|
||
|
||
def add_row_item(self, row):
|
||
quantity = self.app.render_quantity(row.quantity)
|
||
pretty_price = self.app.render_currency(row.txn_price)
|
||
self.items.controls.append(
|
||
ft.Container(
|
||
content=ft.Row(
|
||
[
|
||
ft.Row([
|
||
self.make_text(f"{row.description}"),
|
||
self.make_text(f"× {quantity} @ {pretty_price}",
|
||
weight=None, italic=True, size=20),
|
||
]),
|
||
self.make_text(self.app.render_currency(row.sales_total)),
|
||
],
|
||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||
),
|
||
border=ft.border.only(bottom=ft.border.BorderSide(1, 'gray')),
|
||
padding=ft.padding.only(0, 5, 0, 5),
|
||
))
|
||
|
||
|
||
def void_click(self, e):
|
||
|
||
def confirm(e):
|
||
dlg.open = False
|
||
|
||
model = self.model
|
||
session = self.app.make_session()
|
||
handler = self.get_batch_handler()
|
||
user = session.get(model.User, self.page.shared['user_uuid'])
|
||
batch = handler.get_current_batch(user, create=True)
|
||
|
||
# void current batch
|
||
handler.void_batch(batch, user)
|
||
session.flush()
|
||
self.clear_all()
|
||
|
||
# make new batch
|
||
batch = handler.get_current_batch(user, create=True)
|
||
self.page.shared['txn_display'] = batch.id_str
|
||
|
||
session.commit()
|
||
session.close()
|
||
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
def cancel(e):
|
||
dlg.open = False
|
||
self.page.update()
|
||
|
||
dlg = ft.AlertDialog(
|
||
# modal=True,
|
||
title=ft.Text("Confirm VOID"),
|
||
content=ft.Text("Really VOID this transaction?"),
|
||
actions=[
|
||
ft.Container(content=ft.Text("Yes, VOID",
|
||
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),
|
||
])
|
||
|
||
self.page.dialog = dlg
|
||
dlg.open = True
|
||
self.page.update()
|
||
|
||
def tender_click(self, e):
|
||
model = self.model
|
||
session = self.app.make_session()
|
||
handler = self.get_batch_handler()
|
||
user = session.get(model.User, self.page.shared['user_uuid'])
|
||
batch = handler.get_current_batch(user)
|
||
|
||
# tender / execute batch
|
||
tender = e.control.content.value
|
||
handler.tender_and_execute(batch, user, tender)
|
||
self.clear_all()
|
||
|
||
# make new batch
|
||
batch = handler.get_current_batch(user, create=True)
|
||
self.page.shared['txn_display'] = batch.id_str
|
||
self.header.update_txn_display()
|
||
self.header.update()
|
||
|
||
session.commit()
|
||
session.close()
|
||
|
||
self.main_input.focus()
|
||
self.page.update()
|
||
|
||
def clear_all(self):
|
||
self.items.controls.clear()
|
||
self.txn_total.value = None
|
||
self.page.shared['txn_display'] = None
|
||
self.page.shared['cust_uuid'] = None
|
||
self.page.shared['cust_display'] = None
|
||
self.header.update_txn_display()
|
||
self.header.update_cust_display()
|
||
# TODO: not clear why must call update() for header too?
|
||
self.header.update()
|
||
|
||
def main_submit(self, e=None):
|
||
value = self.main_input.value
|
||
if not value:
|
||
self.main_input.focus()
|
||
self.items.scroll_to(offset=-1, duration=250)
|
||
self.page.update()
|
||
return
|
||
|
||
handler = self.get_batch_handler()
|
||
session = self.app.make_session()
|
||
model = self.model
|
||
|
||
user = session.get(model.User, self.page.shared['user_uuid'])
|
||
batch = handler.get_current_batch(user)
|
||
|
||
kwargs = {}
|
||
if self.set_quantity.data is not None:
|
||
kwargs['quantity'] = self.set_quantity.data
|
||
row = handler.process_entry(batch, value, **kwargs)
|
||
if row:
|
||
self.add_row_item(row)
|
||
self.items.scroll_to(offset=-1, duration=250)
|
||
self.txn_total.value = self.app.render_currency(batch.sales_total)
|
||
|
||
else:
|
||
self.page.snack_bar = ft.SnackBar(ft.Text(f"UNRECOGNIZED: {value}",
|
||
color='black',
|
||
weight=ft.FontWeight.BOLD),
|
||
bgcolor='yellow',
|
||
duration=1500)
|
||
self.page.snack_bar.open = True
|
||
|
||
session.commit()
|
||
session.close()
|
||
|
||
self.main_input.value = ""
|
||
self.main_input.focus()
|
||
self.set_quantity.data = None
|
||
self.set_quantity.value = None
|
||
self.page.update()
|