Add basic customer lookup
This commit is contained in:
		
							parent
							
								
									545f115dc8
								
							
						
					
					
						commit
						8f937d040a
					
				
					 4 changed files with 803 additions and 56 deletions
				
			
		|  | @ -43,6 +43,12 @@ def main(page: ft.Page): | |||
|     page.window_full_screen = True | ||||
|     # page.vertical_alignment = ft.MainAxisAlignment.CENTER | ||||
| 
 | ||||
|     # global defaults for button/text styles etc. | ||||
|     page.data = { | ||||
|         'default_button_height_pos': 100, | ||||
|         'default_button_height_dlg': 80, | ||||
|     } | ||||
| 
 | ||||
|     # nb. track current user, txn etc. | ||||
|     page.shared = {} | ||||
| 
 | ||||
|  | @ -53,6 +59,8 @@ def main(page: ft.Page): | |||
|             with open(path, 'rt') as f: | ||||
|                 page.shared = json.loads(f.read()) | ||||
|         page.shared.pop('txn_display', None) # TODO | ||||
|         page.shared.pop('cust_uuid', None) # TODO | ||||
|         page.shared.pop('cust_display', None) # TODO | ||||
|         if page.shared and page.shared.get('user_uuid'): | ||||
|             handler = app.get_batch_handler('pos') | ||||
|             session = app.make_session() | ||||
|  | @ -61,6 +69,13 @@ def main(page: ft.Page): | |||
|             batch = handler.get_current_batch(user, create=False) | ||||
|             if batch: | ||||
|                 page.shared['txn_display'] = batch.id_str | ||||
|                 page.shared['cust_uuid'] = batch.customer_uuid | ||||
|                 if batch.customer: | ||||
|                     key = app.get_customer_key_field() | ||||
|                     value = getattr(batch.customer, key) | ||||
|                     page.shared['cust_display'] = str(value or '') or None | ||||
|                 else: | ||||
|                     page.shared['cust_display'] = None | ||||
|             session.close() | ||||
| 
 | ||||
|     def clean_exit(): | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ class WuttaHeader(WuttaControl): | |||
| 
 | ||||
|     def build(self): | ||||
|         self.txn_display = ft.Text("Txn: N", weight=ft.FontWeight.BOLD, size=20) | ||||
|         self.cust_display = ft.Text("Cust: N", weight=ft.FontWeight.BOLD, size=20) | ||||
|         self.user_display = ft.Text("User: N", weight=ft.FontWeight.BOLD, size=20) | ||||
|         self.logout_button = ft.FilledButton("Logout", on_click=self.logout_click, visible=False) | ||||
|         self.logout_divider = ft.VerticalDivider(visible=False) | ||||
|  | @ -41,7 +42,7 @@ class WuttaHeader(WuttaControl): | |||
|         controls = [ | ||||
|             self.txn_display, | ||||
|             ft.VerticalDivider(), | ||||
|             ft.Text(f"Cust: N", weight=ft.FontWeight.BOLD, size=20), | ||||
|             self.cust_display, | ||||
|             ft.VerticalDivider(), | ||||
|             WuttaTimestamp(self.config, expand=True, | ||||
|                            weight=ft.FontWeight.BOLD, size=20), | ||||
|  | @ -56,6 +57,7 @@ class WuttaHeader(WuttaControl): | |||
| 
 | ||||
|     def did_mount(self): | ||||
|         self.update_txn_display() | ||||
|         self.update_cust_display() | ||||
|         self.update_user_display() | ||||
|         self.update() | ||||
| 
 | ||||
|  | @ -67,6 +69,14 @@ class WuttaHeader(WuttaControl): | |||
| 
 | ||||
|         self.txn_display.value = f"Txn: {txn_display}" | ||||
| 
 | ||||
|     def update_cust_display(self): | ||||
|         cust_display = "N" | ||||
| 
 | ||||
|         if self.page and self.page.shared and self.page.shared.get('cust_display'): | ||||
|             cust_display = self.page.shared['cust_display'] | ||||
| 
 | ||||
|         self.cust_display.value = f"Cust: {cust_display}" | ||||
| 
 | ||||
|     def update_user_display(self): | ||||
|         user_display = "N" | ||||
| 
 | ||||
|  | @ -84,5 +94,6 @@ class WuttaHeader(WuttaControl): | |||
|             'user_uuid': None, | ||||
|             'user_display': None, | ||||
|             'txn_display': None, | ||||
|             'cust_display': None, | ||||
|         }) | ||||
|         self.page.go('/login') | ||||
|  |  | |||
							
								
								
									
										179
									
								
								wuttapos/controls/keyboard.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								wuttapos/controls/keyboard.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,179 @@ | |||
| # -*- 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 - keyboard control | ||||
| """ | ||||
| 
 | ||||
| import flet as ft | ||||
| 
 | ||||
| from .base import WuttaControl | ||||
| 
 | ||||
| 
 | ||||
| class WuttaKeyboard(WuttaControl): | ||||
| 
 | ||||
|     default_font_size = 20 | ||||
|     default_button_size = 80 | ||||
| 
 | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.on_keypress = kwargs.pop('on_keypress', None) | ||||
| 
 | ||||
|         super().__init__(*args, **kwargs) | ||||
| 
 | ||||
|         self.caps_lock = False | ||||
|         self.caps_map = dict([(k, k.upper()) | ||||
|                               for k in 'abcdefghijklmnopqrstuvwxyz']) | ||||
| 
 | ||||
|         self.shift = False | ||||
|         self.shift_map = { | ||||
|             '`': '~', | ||||
|             '1': '!', | ||||
|             '2': '@', | ||||
|             '3': '#', | ||||
|             '4': '$', | ||||
|             '5': '%', | ||||
|             '6': '^', | ||||
|             '7': '&', | ||||
|             '8': '*', | ||||
|             '9': '(', | ||||
|             '0': ')', | ||||
|             '-': '_', | ||||
|             '=': '+', | ||||
|             '[': '{', | ||||
|             ']': '}', | ||||
|             '\\': '|', | ||||
|             ';': ':', | ||||
|             "'": '"', | ||||
|             ',': '<', | ||||
|             '.': '>', | ||||
|             '/': '?', | ||||
|         } | ||||
| 
 | ||||
|     def update_caps_lock(self, caps_lock): | ||||
|         self.caps_lock = caps_lock | ||||
| 
 | ||||
|         if self.caps_lock: | ||||
|             self.caps_key.bgcolor = 'blue' | ||||
|         else: | ||||
|             self.caps_key.bgcolor = None | ||||
| 
 | ||||
|         for key, button in self.keys.items(): | ||||
|             if key in self.caps_map: | ||||
|                 if self.caps_lock: | ||||
|                     button.content.value = self.caps_map[key] | ||||
|                 else: | ||||
|                     button.content.value = key | ||||
| 
 | ||||
|         self.update() | ||||
| 
 | ||||
|     def update_shift(self, shift): | ||||
|         self.shift = shift | ||||
| 
 | ||||
|         if self.shift: | ||||
|             self.shift_key.bgcolor = 'blue' | ||||
|         else: | ||||
|             self.shift_key.bgcolor = None | ||||
| 
 | ||||
|         for key, button in self.keys.items(): | ||||
|             if key in self.caps_map: | ||||
|                 if self.shift: | ||||
|                     button.content.value = self.caps_map[key] | ||||
|                 else: | ||||
|                     button.content.value = key | ||||
|             elif key in self.shift_map: | ||||
|                 if self.shift: | ||||
|                     button.content.value = self.shift_map[key] | ||||
|                 else: | ||||
|                     button.content.value = key | ||||
| 
 | ||||
|         self.update() | ||||
| 
 | ||||
|     def simple_keypress(self, e): | ||||
| 
 | ||||
|         # maybe inform parent | ||||
|         if self.on_keypress: | ||||
|             key = e.control.content.value | ||||
| 
 | ||||
|             # avoid callback for certain keys | ||||
|             if key not in ('CAPS', 'SHIFT'): | ||||
| 
 | ||||
|                 # translate certain keys | ||||
|                 if key == 'SPACE': | ||||
|                     key = ' ' | ||||
| 
 | ||||
|                 # let 'em know | ||||
|                 self.on_keypress(key) | ||||
| 
 | ||||
|         # turn off shift key if set | ||||
|         if self.shift: | ||||
|             self.update_shift(False) | ||||
| 
 | ||||
|     def build(self): | ||||
|         self.keys = {} | ||||
| 
 | ||||
|         def make_key(key, data=None, on_click=self.simple_keypress, | ||||
|                      width=self.default_button_size, | ||||
|                      bgcolor=None): | ||||
|             button = ft.Container(content=ft.Text(key, size=self.default_font_size, | ||||
|                                                   weight=ft.FontWeight.BOLD), | ||||
|                                   data=data or key, | ||||
|                                   height=self.default_button_size, | ||||
|                                   width=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.keys[key] = button | ||||
|             return button | ||||
| 
 | ||||
|         def caps_click(e): | ||||
|             self.update_caps_lock(not self.caps_lock) | ||||
| 
 | ||||
|         self.caps_key = make_key('CAPS', on_click=caps_click) | ||||
|         if self.caps_lock: | ||||
|             self.caps_key.bgcolor = 'blue' | ||||
| 
 | ||||
|         def shift_click(e): | ||||
|             self.update_shift(not self.shift) | ||||
| 
 | ||||
|         self.shift_key = make_key('SHIFT', on_click=shift_click) | ||||
|         if self.shift: | ||||
|             self.shift_key.bgcolor = 'blue' | ||||
| 
 | ||||
|         rows = [ | ||||
|             [make_key(k) for k in "`1234567890-="] + [make_key('⌫', bgcolor='yellow')], | ||||
|             [make_key(k) for k in "qwertyuiop[]\\"], | ||||
|             [self.caps_key] + [make_key(k) for k in "asdfghjkl;'"] + [make_key('⏎', bgcolor='blue')], | ||||
|             [self.shift_key] + [make_key(k) for k in "zxcvbnm,./"], | ||||
|             [make_key('SPACE', width=self.default_button_size * 5)], | ||||
|         ] | ||||
| 
 | ||||
|         rows = [ft.Row(controls, alignment=ft.MainAxisAlignment.CENTER) | ||||
|                 for controls in rows] | ||||
| 
 | ||||
|         return ft.Container( | ||||
|             content=ft.Column( | ||||
|                 rows, | ||||
|             ), | ||||
|         ) | ||||
|  | @ -24,9 +24,16 @@ | |||
| WuttaPOS - POS view | ||||
| """ | ||||
| 
 | ||||
| import decimal | ||||
| import logging | ||||
| import time | ||||
| 
 | ||||
| import flet as ft | ||||
| 
 | ||||
| from .base import WuttaView | ||||
| from wuttapos.controls.keyboard import WuttaKeyboard | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class POSView(WuttaView): | ||||
|  | @ -34,6 +41,415 @@ 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 self.app.get_batch_handler('pos') | ||||
| 
 | ||||
|     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, | ||||
|  | @ -41,14 +457,17 @@ class POSView(WuttaView): | |||
|                                        text_style=ft.TextStyle(weight=ft.FontWeight.BOLD), | ||||
|                                        autofocus=True) | ||||
| 
 | ||||
|         self.items = ft.ListView() | ||||
|         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.Container( | ||||
|                     content=self.items, | ||||
|                     padding=ft.padding.only(10, 0, 10, 0)), | ||||
|                 ft.Row([self.txn_total], | ||||
|                        alignment=ft.MainAxisAlignment.END), | ||||
|             ], | ||||
|  | @ -59,37 +478,99 @@ class POSView(WuttaView): | |||
|             value = e.control.content.value | ||||
| 
 | ||||
|             if value == 'ENTER': | ||||
|                 print('TODO: handle enter') | ||||
|                 self.main_submit() | ||||
| 
 | ||||
|             elif value == '⌫':  # backspace | ||||
|                 if self.main_input.value: | ||||
|                     self.main_input.value = self.main_input.value[:-1] | ||||
|                     self.page.update() | ||||
|                 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 == '↑': | ||||
|                 pass            # TODO | ||||
|                 self.items.scroll_to(delta=-50, duration=250) | ||||
|                 self.page.update() | ||||
| 
 | ||||
|             elif value == '↓': | ||||
|                 pass            # TODO | ||||
|                 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() | ||||
| 
 | ||||
|         # TODO: should be configurable? | ||||
|         tenkey_button_size = 100 | ||||
|         tenkey_font_size = 40 | ||||
|         def up_long_press(e): | ||||
|             self.items.scroll_to(delta=-500, duration=250) | ||||
|             self.page.update() | ||||
| 
 | ||||
|         def tenkey_button(text, width=tenkey_button_size): | ||||
|         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_long_press=None, | ||||
|                           ): | ||||
|             return ft.Container(content=ft.Text(text, size=tenkey_font_size, | ||||
|                                                 weight=ft.FontWeight.BOLD), | ||||
|                                 height=tenkey_button_size, | ||||
|                                 height=height, | ||||
|                                 width=width, | ||||
|                                 on_click=tenkey_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='green') | ||||
|                                 bgcolor=bgcolor) | ||||
| 
 | ||||
|         self.tenkey_menu = ft.Container( | ||||
|             content=ft.Column( | ||||
|  | @ -99,7 +580,7 @@ class POSView(WuttaView): | |||
|                             tenkey_button("1"), | ||||
|                             tenkey_button("2"), | ||||
|                             tenkey_button("3"), | ||||
|                             tenkey_button("⌫"), | ||||
|                             tenkey_button("@"), | ||||
|                         ], | ||||
|                         spacing=0, | ||||
|                     ), | ||||
|  | @ -108,7 +589,7 @@ class POSView(WuttaView): | |||
|                             tenkey_button("4"), | ||||
|                             tenkey_button("5"), | ||||
|                             tenkey_button("6"), | ||||
|                             tenkey_button("↑"), | ||||
|                             tenkey_button("↑", on_long_press=up_long_press), | ||||
|                         ], | ||||
|                         spacing=0, | ||||
|                     ), | ||||
|  | @ -117,14 +598,15 @@ class POSView(WuttaView): | |||
|                             tenkey_button("7"), | ||||
|                             tenkey_button("8"), | ||||
|                             tenkey_button("9"), | ||||
|                             tenkey_button("↓"), | ||||
|                             tenkey_button("↓", on_long_press=down_long_press), | ||||
|                         ], | ||||
|                         spacing=0, | ||||
|                     ), | ||||
|                     ft.Row( | ||||
|                         [ | ||||
|                             tenkey_button("0"), | ||||
|                             tenkey_button("00"), | ||||
|                             # tenkey_button("00"), | ||||
|                             tenkey_button("."), | ||||
|                             tenkey_button("ENTER", width=tenkey_button_size * 2), | ||||
|                         ], | ||||
|                         alignment=ft.MainAxisAlignment.SPACE_BETWEEN, | ||||
|  | @ -139,12 +621,11 @@ class POSView(WuttaView): | |||
|         meta_button_width = meta_button_height * 2 | ||||
|         meta_font_size = tenkey_font_size | ||||
| 
 | ||||
|         def meta_button(text, on_click=None, bgcolor='yellow'): | ||||
|         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=meta_click, | ||||
|                                 on_click=on_click, | ||||
|                                 alignment=ft.alignment.center, | ||||
|                                 border=ft.border.all(1, 'black'), | ||||
|  | @ -156,29 +637,29 @@ class POSView(WuttaView): | |||
|                 [ | ||||
|                     ft.Row( | ||||
|                         [ | ||||
|                             meta_button("MGR"), | ||||
|                             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"), | ||||
|                             meta_button("CUST"), | ||||
|                             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"), | ||||
|                             meta_button("TODO"), | ||||
|                             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"), | ||||
|                             meta_button("TODO"), | ||||
|                             meta_button("TODO", bgcolor='blue', on_click=self.not_supported), | ||||
|                             meta_button("TODO", bgcolor='blue', on_click=self.not_supported), | ||||
|                         ], | ||||
|                         spacing=0, | ||||
|                     ), | ||||
|  | @ -217,11 +698,18 @@ class POSView(WuttaView): | |||
|             ), | ||||
|             expand=0) | ||||
| 
 | ||||
|         self.set_quantity = ft.Text(value=None, data=None, weight=ft.FontWeight.BOLD, size=40) | ||||
| 
 | ||||
|         return [ | ||||
|             self.build_header(), | ||||
| 
 | ||||
|             ft.Row( | ||||
|                 [self.main_input], | ||||
|                 [ | ||||
|                     self.set_quantity, | ||||
|                     self.main_input, | ||||
|                     tenkey_button("⌫", height=70, width=70), | ||||
|                     tenkey_button("CE", height=70, width=70), | ||||
|                 ], | ||||
|                 alignment=ft.MainAxisAlignment.CENTER, | ||||
|             ), | ||||
| 
 | ||||
|  | @ -253,22 +741,41 @@ class POSView(WuttaView): | |||
|         kwargs.setdefault('size', 24) | ||||
|         return ft.Text(*args, **kwargs) | ||||
| 
 | ||||
|     def did_mount(self): | ||||
|         model = self.model | ||||
|         session = self.app.make_session() | ||||
|     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.app.get_batch_handler('pos') | ||||
|         user = session.get(model.User, self.page.shared['user_uuid']) | ||||
|         batch = handler.get_current_batch(user, create=True) | ||||
|         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) | ||||
|  | @ -282,12 +789,14 @@ class POSView(WuttaView): | |||
|                             self.make_text(f"× {quantity} @ {pretty_price}", | ||||
|                                            weight=None, italic=True, size=20), | ||||
|                         ]), | ||||
|                         self.make_text(pretty_price), | ||||
|                         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))) | ||||
|                 padding=ft.padding.only(0, 5, 0, 5), | ||||
|             )) | ||||
| 
 | ||||
| 
 | ||||
|     def void_click(self, e): | ||||
| 
 | ||||
|  | @ -303,21 +812,16 @@ class POSView(WuttaView): | |||
|             # 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 | ||||
| 
 | ||||
|             # 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.main_input.focus() | ||||
|             self.page.update() | ||||
| 
 | ||||
|         def cancel(e): | ||||
|  | @ -325,12 +829,30 @@ class POSView(WuttaView): | |||
|             self.page.update() | ||||
| 
 | ||||
|         dlg = ft.AlertDialog( | ||||
|             modal=True, | ||||
|             # 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), | ||||
|                 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 | ||||
|  | @ -347,24 +869,39 @@ class POSView(WuttaView): | |||
|         # 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() | ||||
| 
 | ||||
|         # 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): | ||||
|     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.app.get_batch_handler('pos') | ||||
|         session = self.app.make_session() | ||||
|         model = self.model | ||||
|  | @ -372,10 +909,13 @@ class POSView(WuttaView): | |||
|         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) | ||||
|         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: | ||||
|  | @ -391,4 +931,6 @@ class POSView(WuttaView): | |||
| 
 | ||||
|         self.main_input.value = "" | ||||
|         self.main_input.focus() | ||||
|         self.set_quantity.data = None | ||||
|         self.set_quantity.value = None | ||||
|         self.page.update() | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar