wuttapos/wuttapos/views/pos.py
2023-09-23 12:00:34 -05:00

395 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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 flet as ft
from .base import WuttaView
class POSView(WuttaView):
"""
Main POS view for WuttaPOS
"""
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()
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':
print('TODO: handle enter')
elif value == '': # backspace
if self.main_input.value:
self.main_input.value = self.main_input.value[:-1]
self.page.update()
elif value == '':
pass # TODO
elif value == '':
pass # TODO
else:
self.main_input.value = f"{self.main_input.value or ''}{value}"
self.page.update()
# TODO: should be configurable?
tenkey_button_size = 100
tenkey_font_size = 40
def tenkey_button(text, width=tenkey_button_size):
return ft.Container(content=ft.Text(text, size=tenkey_font_size,
weight=ft.FontWeight.BOLD),
height=tenkey_button_size,
width=width,
on_click=tenkey_click,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor='green')
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(""),
],
spacing=0,
),
ft.Row(
[
tenkey_button("7"),
tenkey_button("8"),
tenkey_button("9"),
tenkey_button(""),
],
spacing=0,
),
ft.Row(
[
tenkey_button("0"),
tenkey_button("00"),
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='yellow'):
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=meta_click,
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"),
meta_button("VOID", bgcolor='red', on_click=self.void_click),
],
spacing=0,
),
ft.Row(
[
meta_button("ITEM"),
meta_button("CUST"),
],
spacing=0,
),
ft.Row(
[
meta_button("TODO"),
meta_button("TODO"),
],
spacing=0,
),
ft.Row(
[
meta_button("TODO"),
meta_button("TODO"),
],
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)
return [
self.build_header(),
ft.Row(
[self.main_input],
alignment=ft.MainAxisAlignment.CENTER,
),
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 did_mount(self):
model = self.model
session = self.app.make_session()
handler = self.app.get_batch_handler('pos')
user = session.get(model.User, self.page.shared['user_uuid'])
batch = handler.get_current_batch(user, create=True)
self.page.shared['txn_display'] = batch.id_str
self.items.controls.clear()
for row in batch.active_rows():
self.add_row_item(row)
self.txn_total.value = self.app.render_currency(batch.sales_total)
session.commit()
session.close()
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(pretty_price),
],
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.app.get_batch_handler('pos')
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()
# make new batch
batch = handler.get_current_batch(user, create=True)
# commit changes
session.commit()
session.close()
# reset txn display
self.items.controls.clear()
self.txn_total.value = None
self.page.shared['txn_display'] = batch.id_str
self.header.update_txn_display()
# TODO: not clear why must call update() for header too?
self.header.update()
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.TextButton("Yes", on_click=confirm),
ft.TextButton("No", 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.app.get_batch_handler('pos')
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)
# make new batch
batch = handler.get_current_batch(user, create=True)
session.commit()
session.close()
# reset txn display
self.items.controls.clear()
self.txn_total.value = None
self.page.shared['txn_display'] = batch.id_str
self.header.update_txn_display()
# TODO: not clear why must call update() for header too?
self.header.update()
self.main_input.focus()
self.page.update()
def main_submit(self, e):
handler = self.app.get_batch_handler('pos')
session = self.app.make_session()
model = self.model
user = session.get(model.User, self.page.shared['user_uuid'])
batch = handler.get_current_batch(user)
value = self.main_input.value
row = handler.process_entry(batch, value)
if row:
self.add_row_item(row)
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.page.update()