yowza, whole bunch of changes
this adds most of the core schema needed to ring sales at the POS. and the POS now supports several common functions via the button menu. but i have yet to tackle many aspects / features, a lot remains to do
This commit is contained in:
parent
17a3a71532
commit
548210d645
43 changed files with 3944 additions and 425 deletions
|
|
@ -32,19 +32,65 @@ class WuttaPosAppProvider(base.AppProvider):
|
||||||
Custom :term:`app provider` for WuttaPOS.
|
Custom :term:`app provider` for WuttaPOS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
default_people_handler_spec = "wuttapos.people:PeopleHandler"
|
||||||
|
default_employment_handler_spec = "wuttapos.employment:EmploymentHandler"
|
||||||
|
default_clientele_handler_spec = "wuttapos.clientele:ClienteleHandler"
|
||||||
|
default_products_handler_spec = "wuttapos.products:ProductsHandler"
|
||||||
|
|
||||||
email_templates = ["wuttapos:email-templates"]
|
email_templates = ["wuttapos:email-templates"]
|
||||||
|
|
||||||
def get_transaction_handler(self):
|
def get_clientele_handler(self):
|
||||||
"""
|
"""
|
||||||
Get the configured :term:`transaction handler`.
|
Get the configured "clientele" :term:`handler`.
|
||||||
|
|
||||||
:rtype: :class:`~wuttapos.handler.TransactionHandler`
|
:rtype: :class:`~wuttapos.clientele.ClienteleHandler`
|
||||||
"""
|
"""
|
||||||
if "transaction" not in self.app.handlers:
|
if "clientele" not in self.app.handlers:
|
||||||
spec = self.config.get(
|
spec = self.config.get(
|
||||||
"wuttapos.transaction_handler",
|
f"{self.appname}.clientele.handler",
|
||||||
default="wuttapos.handler:TransactionHandler",
|
default=self.default_clientele_handler_spec,
|
||||||
)
|
)
|
||||||
factory = self.app.load_object(spec)
|
factory = self.app.load_object(spec)
|
||||||
self.app.handlers["transaction"] = factory(self.config)
|
self.app.handlers["clientele"] = factory(self.config)
|
||||||
return self.app.handlers["transaction"]
|
return self.app.handlers["clientele"]
|
||||||
|
|
||||||
|
def get_employment_handler(self):
|
||||||
|
"""
|
||||||
|
Get the configured "employment" :term:`handler`.
|
||||||
|
|
||||||
|
:rtype: :class:`~wuttapos.employment.EmploymentHandler`
|
||||||
|
"""
|
||||||
|
if "employment" not in self.app.handlers:
|
||||||
|
spec = self.config.get(
|
||||||
|
f"{self.appname}.employment.handler",
|
||||||
|
default=self.default_employment_handler_spec,
|
||||||
|
)
|
||||||
|
factory = self.app.load_object(spec)
|
||||||
|
self.app.handlers["employment"] = factory(self.config)
|
||||||
|
return self.app.handlers["employment"]
|
||||||
|
|
||||||
|
def get_products_handler(self):
|
||||||
|
"""
|
||||||
|
Get the configured "products" :term:`handler`.
|
||||||
|
|
||||||
|
:rtype: :class:`~wuttapos.products.ProductsHandler`
|
||||||
|
"""
|
||||||
|
if "products" not in self.app.handlers:
|
||||||
|
spec = self.config.get(
|
||||||
|
f"{self.appname}.products.handler",
|
||||||
|
default=self.default_products_handler_spec,
|
||||||
|
)
|
||||||
|
factory = self.app.load_object(spec)
|
||||||
|
self.app.handlers["products"] = factory(self.config)
|
||||||
|
return self.app.handlers["products"]
|
||||||
|
|
||||||
|
def get_employee(self, obj):
|
||||||
|
"""
|
||||||
|
Convenience method to locate a
|
||||||
|
:class:`~wuttjamaican.db.model.base.Person` for the given
|
||||||
|
object.
|
||||||
|
|
||||||
|
This delegates to the "people" handler method,
|
||||||
|
:meth:`~wuttjamaican.people.PeopleHandler.get_person()`.
|
||||||
|
"""
|
||||||
|
return self.get_employment_handler().get_employee(obj)
|
||||||
|
|
|
||||||
0
src/wuttapos/batch/__init__.py
Normal file
0
src/wuttapos/batch/__init__.py
Normal file
747
src/wuttapos/batch/pos.py
Normal file
747
src/wuttapos/batch/pos.py
Normal file
|
|
@ -0,0 +1,747 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
POS batch handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
# import decimal
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.batch import BatchHandler
|
||||||
|
|
||||||
|
from wuttapos.db.model import POSBatch
|
||||||
|
|
||||||
|
|
||||||
|
class POSBatchHandler(BatchHandler):
|
||||||
|
"""
|
||||||
|
Handler for POS batches
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = POSBatch
|
||||||
|
|
||||||
|
def get_store_id(self, require=False):
|
||||||
|
"""
|
||||||
|
Returns the ID string for store to which local terminal belongs.
|
||||||
|
"""
|
||||||
|
return self.config.get("wuttapos.store_id", require=require)
|
||||||
|
|
||||||
|
def get_terminal_id(self, require=False):
|
||||||
|
"""
|
||||||
|
Returns the ID string for current POS terminal.
|
||||||
|
"""
|
||||||
|
return self.config.get("wuttapos.terminal_id", require=require)
|
||||||
|
|
||||||
|
def init_batch(self, batch, session=None, **kwargs):
|
||||||
|
if not session:
|
||||||
|
raise ValueError(
|
||||||
|
f"must provide session to {self.__class__.__name__}init_batch()"
|
||||||
|
)
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
batch.store = (
|
||||||
|
session.query(model.Store)
|
||||||
|
.filter_by(store_id=self.get_store_id(require=True))
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
batch.terminal = (
|
||||||
|
session.query(model.Terminal)
|
||||||
|
.filter_by(terminal_id=self.get_terminal_id(require=True))
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
batch.status_code = batch.STATUS_OK
|
||||||
|
|
||||||
|
# TODO: should also filter this by terminal
|
||||||
|
def get_current_batch(
|
||||||
|
self, user, training_mode=False, create=True, return_created=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get the "current" POS batch for the given user, creating it as
|
||||||
|
needed.
|
||||||
|
|
||||||
|
:param user: Reference to active user (cashier) for the batch
|
||||||
|
/ POS transaction.
|
||||||
|
|
||||||
|
:param training_mode: Flag indicating whether the transaction
|
||||||
|
should be in training mode. The lookup will be restricted
|
||||||
|
according to the value of this flag. If a new batch is
|
||||||
|
created, it will be assigned this flag value.
|
||||||
|
|
||||||
|
:param create: Whether a new batch should be created, if no
|
||||||
|
current batch is found.
|
||||||
|
|
||||||
|
:param return_created: Indicates the return value should
|
||||||
|
include both the batch *and* a flag indicating whether the
|
||||||
|
batch was newly-created.
|
||||||
|
"""
|
||||||
|
if not user:
|
||||||
|
raise ValueError("must specify a user")
|
||||||
|
|
||||||
|
created = False
|
||||||
|
model = self.app.model
|
||||||
|
session = self.app.get_session(user)
|
||||||
|
employee = self.app.get_employee(user)
|
||||||
|
|
||||||
|
# TODO: can we assume cashier (employee) is always set /
|
||||||
|
# accurate? if so maybe stop filtering on created_by?
|
||||||
|
cashier_criteria = sa.and_(
|
||||||
|
model.POSBatch.cashier == None, model.POSBatch.created_by == user
|
||||||
|
)
|
||||||
|
if employee:
|
||||||
|
cashier_criteria = sa.or_(
|
||||||
|
model.POSBatch.cashier == employee, cashier_criteria
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
batch = (
|
||||||
|
session.query(model.POSBatch)
|
||||||
|
.filter(cashier_criteria)
|
||||||
|
.filter(model.POSBatch.status_code == model.POSBatch.STATUS_OK)
|
||||||
|
.filter(model.POSBatch.executed == None)
|
||||||
|
.filter(model.POSBatch.training_mode == training_mode)
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
except orm.exc.NoResultFound:
|
||||||
|
if not create:
|
||||||
|
if return_created:
|
||||||
|
return None, False
|
||||||
|
return None
|
||||||
|
batch = self.make_batch(
|
||||||
|
session, created_by=user, training_mode=training_mode, cashier=employee
|
||||||
|
)
|
||||||
|
session.add(batch)
|
||||||
|
session.flush()
|
||||||
|
created = True
|
||||||
|
|
||||||
|
if return_created:
|
||||||
|
return batch, created
|
||||||
|
|
||||||
|
return batch
|
||||||
|
|
||||||
|
def get_screen_txn_display(self, batch):
|
||||||
|
"""
|
||||||
|
Should return the text to be used for displaying transaction
|
||||||
|
identifier within the header of POS screen.
|
||||||
|
"""
|
||||||
|
return batch.id_str
|
||||||
|
|
||||||
|
def get_screen_cust_display(self, batch=None, customer=None):
|
||||||
|
"""
|
||||||
|
Should return the text to be used for displaying customer
|
||||||
|
identifier / name etc. within the header of POS screen.
|
||||||
|
"""
|
||||||
|
if not customer and batch:
|
||||||
|
customer = batch.customer
|
||||||
|
if customer:
|
||||||
|
return customer.customer_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
# TODO: this should account for shoppers somehow too
|
||||||
|
def set_customer(self, batch, customer, user=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Assign the customer account for POS transaction.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
if customer and batch.customer:
|
||||||
|
row_type = enum.POS_ROW_TYPE_SWAP_CUSTOMER
|
||||||
|
elif customer:
|
||||||
|
row_type = enum.POS_ROW_TYPE_SET_CUSTOMER
|
||||||
|
else:
|
||||||
|
if not batch.customer:
|
||||||
|
return
|
||||||
|
row_type = enum.POS_ROW_TYPE_DEL_CUSTOMER
|
||||||
|
|
||||||
|
batch.customer = customer
|
||||||
|
if customer:
|
||||||
|
# member = self.app.get_member(customer)
|
||||||
|
# batch.customer_is_member = bool(member and member.active)
|
||||||
|
# batch.customer_is_member = False
|
||||||
|
batch.customer_is_member = None
|
||||||
|
employee = self.app.get_employee(customer)
|
||||||
|
batch.customer_is_employee = bool(employee and employee.active)
|
||||||
|
else:
|
||||||
|
batch.customer_is_member = None
|
||||||
|
batch.customer_is_employee = None
|
||||||
|
|
||||||
|
row = self.make_row()
|
||||||
|
row.modified_by = user
|
||||||
|
row.row_type = row_type
|
||||||
|
if customer:
|
||||||
|
row.item_entry = customer.customer_id
|
||||||
|
row.description = (
|
||||||
|
f"{customer.customer_id} - {customer.name}"
|
||||||
|
if customer
|
||||||
|
else "REMOVE CUSTOMER"
|
||||||
|
)
|
||||||
|
self.add_row(batch, row)
|
||||||
|
|
||||||
|
def process_entry(self, batch, entry, quantity=1, user=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Process an "entry" value direct from POS. Most typically,
|
||||||
|
this is effectively "ringing up an item" and hence we add a
|
||||||
|
row to the batch and return the row.
|
||||||
|
"""
|
||||||
|
session = self.app.get_session(batch)
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
if isinstance(entry, model.Product):
|
||||||
|
product = entry
|
||||||
|
entry = product.uuid
|
||||||
|
else:
|
||||||
|
# TODO: maybe should only search by product key (or GPC?) etc.
|
||||||
|
product = self.app.get_products_handler().locate_product_for_entry(
|
||||||
|
session, entry
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: if product not found, should auto-record a badscan
|
||||||
|
# entry? maybe only if config says so, e.g. might be nice to
|
||||||
|
# only record badscan if entry truly came from scanner device,
|
||||||
|
# in which case only the caller would know that
|
||||||
|
if not product:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# if product.not_for_sale:
|
||||||
|
# key = self.app.get_products_handler().render_product_key(product)
|
||||||
|
# raise ValueError(f"product is not for sale: {key}")
|
||||||
|
|
||||||
|
# product located, so add item row
|
||||||
|
row = self.make_row()
|
||||||
|
row.modified_by = user
|
||||||
|
row.item_entry = kwargs.get("item_entry", entry)
|
||||||
|
row.product = product
|
||||||
|
# row.brand_name = product.brand.name if product.brand else None
|
||||||
|
row.description = product.description
|
||||||
|
# row.size = product.size
|
||||||
|
# dept = product.department
|
||||||
|
# if dept:
|
||||||
|
# row.department_number = dept.number
|
||||||
|
# row.department_name = dept.name
|
||||||
|
# row.department = product.department
|
||||||
|
# subdept = product.subdepartment
|
||||||
|
# if subdept:
|
||||||
|
# row.subdepartment_number = subdept.number
|
||||||
|
# row.subdepartment_name = subdept.name
|
||||||
|
# row.foodstamp_eligible = product.food_stampable
|
||||||
|
row.sold_by_weight = product.sold_by_weight
|
||||||
|
row.quantity = quantity
|
||||||
|
|
||||||
|
# regprice = product.regular_price
|
||||||
|
# if regprice:
|
||||||
|
# row.reg_price = regprice.price
|
||||||
|
|
||||||
|
row.reg_price = product.unit_price_reg
|
||||||
|
|
||||||
|
# curprice = product.current_price
|
||||||
|
# if curprice:
|
||||||
|
# row.cur_price = curprice.price
|
||||||
|
# row.cur_price_type = curprice.type
|
||||||
|
# row.cur_price_start = curprice.starts
|
||||||
|
# row.cur_price_end = curprice.ends
|
||||||
|
|
||||||
|
# row.txn_price = row.cur_price or row.reg_price
|
||||||
|
row.txn_price = row.reg_price
|
||||||
|
|
||||||
|
if row.txn_price:
|
||||||
|
row.sales_total = row.txn_price * row.quantity
|
||||||
|
batch.sales_total = (batch.sales_total or 0) + row.sales_total
|
||||||
|
# if row.foodstamp_eligible:
|
||||||
|
# batch.fs_total = (batch.fs_total or 0) + row.sales_total
|
||||||
|
|
||||||
|
# tax = product.tax
|
||||||
|
# if tax:
|
||||||
|
# row.tax_code = tax.code
|
||||||
|
|
||||||
|
if row.txn_price:
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_SELL
|
||||||
|
# if tax:
|
||||||
|
# self.update_tax(batch, row, tax)
|
||||||
|
else:
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_BADPRICE
|
||||||
|
|
||||||
|
self.add_row(batch, row)
|
||||||
|
session.flush()
|
||||||
|
return row
|
||||||
|
|
||||||
|
# def update_tax(self, batch, row, tax=None, tax_code=None, **kwargs):
|
||||||
|
# """
|
||||||
|
# Update the tax totals for the batch, basd on given row.
|
||||||
|
# """
|
||||||
|
# if not tax and not tax_code:
|
||||||
|
# raise ValueError("must specify either tax or tax_code")
|
||||||
|
|
||||||
|
# session = self.app.get_session(batch)
|
||||||
|
# if not tax:
|
||||||
|
# tax = self.get_tax(session, tax_code)
|
||||||
|
|
||||||
|
# btax = batch.taxes.get(tax.code)
|
||||||
|
# if not btax:
|
||||||
|
# btax = self.model.POSBatchTax()
|
||||||
|
# btax.tax = tax
|
||||||
|
# btax.tax_code = tax.code
|
||||||
|
# btax.tax_rate = tax.rate
|
||||||
|
# session.add(btax)
|
||||||
|
# btax.batch = batch
|
||||||
|
# session.flush()
|
||||||
|
|
||||||
|
# # calculate relevant sales
|
||||||
|
# rows = [r for r in batch.active_rows() if r.tax_code == tax.code and not r.void]
|
||||||
|
# sales = sum([r.sales_total for r in rows])
|
||||||
|
# # nb. must add row separately if not yet in batch
|
||||||
|
# if not row.batch and not row.batch_uuid:
|
||||||
|
# sales += row.sales_total
|
||||||
|
|
||||||
|
# # total for this tax
|
||||||
|
# before = btax.tax_total or 0
|
||||||
|
# btax.tax_total = (sales * (tax.rate / 100)).quantize(decimal.Decimal("0.02"))
|
||||||
|
# batch.tax_total = (batch.tax_total or 0) - before + btax.tax_total
|
||||||
|
|
||||||
|
# def record_badscan(self, batch, entry, quantity=1, user=None, **kwargs):
|
||||||
|
# """
|
||||||
|
# Add a row to the batch which represents a "bad scan" at POS.
|
||||||
|
# """
|
||||||
|
# row = self.make_row()
|
||||||
|
# row.user = user
|
||||||
|
# row.row_type = self.enum.POS_ROW_TYPE_BADSCAN
|
||||||
|
# row.item_entry = entry
|
||||||
|
# row.description = "BADSCAN"
|
||||||
|
# row.quantity = quantity
|
||||||
|
# self.add_row(batch, row)
|
||||||
|
# return row
|
||||||
|
|
||||||
|
def add_open_ring(self, batch, department, price, quantity=1, user=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Adds an "open ring" row to the batch.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
session = self.app.get_session(batch)
|
||||||
|
|
||||||
|
if not isinstance(department, model.Department):
|
||||||
|
department = session.get(model.Department, department)
|
||||||
|
if not department:
|
||||||
|
raise ValueError("must specify valid department")
|
||||||
|
|
||||||
|
# add row for open ring
|
||||||
|
row = self.make_row()
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_OPEN_RING
|
||||||
|
row.modified_by = user
|
||||||
|
row.item_entry = department.department_id
|
||||||
|
row.description = f"OPEN RING: {department.name}"
|
||||||
|
# row.department_number = department.number
|
||||||
|
# row.department_name = department.name
|
||||||
|
# row.department = department
|
||||||
|
# row.foodstamp_eligible = department.food_stampable
|
||||||
|
row.quantity = quantity
|
||||||
|
|
||||||
|
row.txn_price = price
|
||||||
|
if row.txn_price:
|
||||||
|
row.sales_total = row.txn_price * row.quantity
|
||||||
|
batch.sales_total = (batch.sales_total or 0) + row.sales_total
|
||||||
|
# if row.foodstamp_eligible:
|
||||||
|
# batch.fs_total = (batch.fs_total or 0) + row.sales_total
|
||||||
|
|
||||||
|
# tax = department.tax
|
||||||
|
# if tax:
|
||||||
|
# row.tax_code = tax.code
|
||||||
|
# self.update_tax(batch, row, tax)
|
||||||
|
|
||||||
|
self.add_row(batch, row)
|
||||||
|
session.flush()
|
||||||
|
return row
|
||||||
|
|
||||||
|
# def get_tax(self, session, code, **kwargs):
|
||||||
|
# """
|
||||||
|
# Return the tax record corresponding to the given code.
|
||||||
|
|
||||||
|
# :param session: Current DB session.
|
||||||
|
|
||||||
|
# :param code: Tax code to fetch.
|
||||||
|
# """
|
||||||
|
# model = self.model
|
||||||
|
# return session.query(model.Tax).filter(model.Tax.code == code).one()
|
||||||
|
|
||||||
|
# def get_tender(self, session, key, **kwargs):
|
||||||
|
# """
|
||||||
|
# Return the tender record corresponding to the given key.
|
||||||
|
|
||||||
|
# :param session: Current DB session.
|
||||||
|
|
||||||
|
# :param key: Either a tender UUID, or "true" tender code (i.e.
|
||||||
|
# :attr:`rattail.db.model.sales.Tender.code` value) or a
|
||||||
|
# "pseudo-code" for common tenders (e.g. ``'cash'``).
|
||||||
|
# """
|
||||||
|
# model = self.model
|
||||||
|
|
||||||
|
# # Tender.uuid match?
|
||||||
|
# tender = session.get(model.Tender, key)
|
||||||
|
# if tender:
|
||||||
|
# return tender
|
||||||
|
|
||||||
|
# # Tender.code match?
|
||||||
|
# try:
|
||||||
|
# return session.query(model.Tender).filter(model.Tender.code == key).one()
|
||||||
|
# except orm.exc.NoResultFound:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# # try settings, if value then recurse
|
||||||
|
# # TODO: not sure why get_vendor() only checks settings?
|
||||||
|
# # for now am assuming we should also check config file
|
||||||
|
# # key = self.app.get_setting(session, f'rattail.tender.{key}')
|
||||||
|
# key = self.config.get("rattail", f"tender.{key}")
|
||||||
|
# if key:
|
||||||
|
# return self.get_tender(session, key, **kwargs)
|
||||||
|
|
||||||
|
def refresh_row(self, row):
|
||||||
|
# TODO (?)
|
||||||
|
row.status_code = row.STATUS_OK
|
||||||
|
row.status_text = None
|
||||||
|
|
||||||
|
# # TODO: this subclass used to override the base method here, but for
|
||||||
|
# # the moment we implement the full method below, sans customization.
|
||||||
|
# def clone(self, oldbatch, created_by, **kwargs):
|
||||||
|
# newbatch = super().clone(oldbatch, created_by, **kwargs)
|
||||||
|
# session = self.app.get_session(oldbatch)
|
||||||
|
# model = self.app.model
|
||||||
|
|
||||||
|
# # tax_mapper = sa.inspect(model.POSBatchTax)
|
||||||
|
# # for oldtax in oldbatch.taxes.values():
|
||||||
|
# # newtax = model.POSBatchTax()
|
||||||
|
# # for key in tax_mapper.columns.keys():
|
||||||
|
# # if key not in ("uuid", "batch_uuid"):
|
||||||
|
# # setattr(newtax, key, getattr(oldtax, key))
|
||||||
|
# # session.add(newtax)
|
||||||
|
# # newtax.batch = newbatch
|
||||||
|
|
||||||
|
# return newbatch
|
||||||
|
|
||||||
|
# TODO: this used to be in the BatchHandler base class, and i
|
||||||
|
# guess it probably still should be, once it's vetted. also
|
||||||
|
# see the note above for custom clone() method.
|
||||||
|
def clone(self, oldbatch, created_by, progress=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Clone the given batch as a new batch, and return the new batch.
|
||||||
|
"""
|
||||||
|
session = self.app.get_session(oldbatch)
|
||||||
|
|
||||||
|
# self.setup_clone(oldbatch, progress=progress)
|
||||||
|
batch_class = self.model_class
|
||||||
|
batch_mapper = sa.inspect(batch_class)
|
||||||
|
|
||||||
|
newbatch = batch_class()
|
||||||
|
newbatch.id = self.consume_batch_id(session)
|
||||||
|
newbatch.created_by = created_by
|
||||||
|
newbatch.row_count = 0
|
||||||
|
for name in batch_mapper.columns.keys():
|
||||||
|
if name not in (
|
||||||
|
"uuid",
|
||||||
|
"id",
|
||||||
|
"created",
|
||||||
|
"created_by_uuid",
|
||||||
|
"row_count",
|
||||||
|
"executed",
|
||||||
|
"executed_by_uuid",
|
||||||
|
):
|
||||||
|
setattr(newbatch, name, getattr(oldbatch, name))
|
||||||
|
|
||||||
|
session.add(newbatch)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
row_class = newbatch.__row_class__
|
||||||
|
row_mapper = sa.inspect(row_class)
|
||||||
|
|
||||||
|
def clone_row(oldrow, i):
|
||||||
|
newrow = self.clone_row(oldrow)
|
||||||
|
self.add_row(newbatch, newrow)
|
||||||
|
|
||||||
|
self.app.progress_loop(
|
||||||
|
clone_row,
|
||||||
|
self.get_clonable_rows(oldbatch),
|
||||||
|
progress,
|
||||||
|
message="Cloning data rows for new batch",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.refresh_batch_status(newbatch)
|
||||||
|
# self.teardown_clone(newbatch, progress=progress)
|
||||||
|
return newbatch
|
||||||
|
|
||||||
|
# # TODO: this used to be defined by the base method as shown here
|
||||||
|
# def get_clonable_rows(self, batch, **kwargs):
|
||||||
|
# return batch.data_rows
|
||||||
|
|
||||||
|
# TODO: for now we are providing the only implementation; see above
|
||||||
|
def get_clonable_rows(self, batch, **kwargs):
|
||||||
|
enum = self.app.enum
|
||||||
|
# TODO: row types..ugh
|
||||||
|
return [row for row in batch.rows if row.row_type != enum.POS_ROW_TYPE_TENDER]
|
||||||
|
|
||||||
|
# # TODO: this used to be defined by the base method as shown here (i think)
|
||||||
|
def clone_row(self, oldrow):
|
||||||
|
row_class = self.model_class.__row_class__
|
||||||
|
row_mapper = sa.inspect(row_class)
|
||||||
|
newrow = row_class()
|
||||||
|
for name in row_mapper.columns.keys():
|
||||||
|
if name not in ("uuid", "batch_uuid", "sequence"):
|
||||||
|
setattr(newrow, name, getattr(oldrow, name))
|
||||||
|
return newrow
|
||||||
|
|
||||||
|
def override_price(self, row, user, txn_price, **kwargs):
|
||||||
|
"""
|
||||||
|
Override the transaction price for the given batch row.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
batch = row.batch
|
||||||
|
|
||||||
|
# update price for given row
|
||||||
|
orig_row = row
|
||||||
|
orig_txn_price = orig_row.txn_price
|
||||||
|
orig_sales_total = orig_row.sales_total
|
||||||
|
orig_row.txn_price = txn_price
|
||||||
|
orig_row.txn_price_adjusted = True
|
||||||
|
orig_row.sales_total = orig_row.quantity * orig_row.txn_price
|
||||||
|
|
||||||
|
# adjust totals
|
||||||
|
batch.sales_total = (
|
||||||
|
(batch.sales_total or 0) - orig_sales_total + orig_row.sales_total
|
||||||
|
)
|
||||||
|
# if orig_row.foodstamp_eligible:
|
||||||
|
# batch.fs_total = (
|
||||||
|
# (batch.fs_total or 0) - orig_sales_total + orig_row.sales_total
|
||||||
|
# )
|
||||||
|
# if orig_row.tax_code:
|
||||||
|
# self.update_tax(batch, orig_row, tax_code=orig_row.tax_code)
|
||||||
|
|
||||||
|
# add another row indicating who/when
|
||||||
|
row = self.make_row()
|
||||||
|
row.modified_by = user
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_ADJUST_PRICE
|
||||||
|
row.item_entry = orig_row.item_entry
|
||||||
|
row.txn_price = txn_price
|
||||||
|
row.description = (
|
||||||
|
f"ROW {orig_row.sequence} PRICE ADJUST "
|
||||||
|
f"FROM {self.app.render_currency(orig_txn_price)}"
|
||||||
|
)
|
||||||
|
self.add_row(batch, row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
def void_row(self, row, user, **kwargs):
|
||||||
|
"""
|
||||||
|
Apply "void" status to the given batch row.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
batch = row.batch
|
||||||
|
|
||||||
|
# mark given row as void
|
||||||
|
orig_row = row
|
||||||
|
orig_row.void = True
|
||||||
|
|
||||||
|
# adjust batch totals
|
||||||
|
if orig_row.sales_total:
|
||||||
|
batch.sales_total = (batch.sales_total or 0) - orig_row.sales_total
|
||||||
|
# if orig_row.foodstamp_eligible:
|
||||||
|
# batch.fs_total = (batch.fs_total or 0) - orig_row.sales_total
|
||||||
|
# if orig_row.tax_code:
|
||||||
|
# self.update_tax(batch, orig_row, tax_code=orig_row.tax_code)
|
||||||
|
|
||||||
|
# add another row indicating who/when
|
||||||
|
row = self.make_row()
|
||||||
|
row.modified_by = user
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_VOID_LINE
|
||||||
|
row.item_entry = orig_row.item_entry
|
||||||
|
row.description = f"VOID ROW {orig_row.sequence}"
|
||||||
|
self.add_row(batch, row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
def suspend_transaction(self, batch, user, **kwargs):
|
||||||
|
"""
|
||||||
|
Suspend transaction for the given POS batch.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
# add another row indicating who/when
|
||||||
|
row = self.make_row()
|
||||||
|
row.modified_by = user
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_SUSPEND
|
||||||
|
row.description = "SUSPEND TXN"
|
||||||
|
self.add_row(batch, row)
|
||||||
|
|
||||||
|
# TODO: should do something different if we have a server
|
||||||
|
# engine, i.e. central location for suspended txns
|
||||||
|
|
||||||
|
# mark batch as suspended (but not executed)
|
||||||
|
batch.status_code = batch.STATUS_SUSPENDED
|
||||||
|
|
||||||
|
def resume_transaction(self, batch, user, **kwargs):
|
||||||
|
"""
|
||||||
|
Resume transaction for the given POS batch. By default this
|
||||||
|
always creates a *new* batch.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
# TODO: should do something different if we have a server
|
||||||
|
# engine, i.e. central location for suspended txns
|
||||||
|
|
||||||
|
newbatch = self.clone(batch, user)
|
||||||
|
newbatch.cashier = self.app.get_employee(user)
|
||||||
|
newbatch.status_code = newbatch.STATUS_OK
|
||||||
|
|
||||||
|
# add another row indicating who/when
|
||||||
|
row = self.make_row()
|
||||||
|
row.modified_by = user
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_RESUME
|
||||||
|
row.description = "RESUME TXN"
|
||||||
|
self.add_row(newbatch, row)
|
||||||
|
|
||||||
|
# mark original batch as executed
|
||||||
|
batch.executed = self.app.make_utc()
|
||||||
|
batch.executed_by = user
|
||||||
|
|
||||||
|
return newbatch
|
||||||
|
|
||||||
|
def void_batch(self, batch, user, **kwargs):
|
||||||
|
"""
|
||||||
|
Void the given POS batch.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
# add another row indicating who/when
|
||||||
|
row = self.make_row()
|
||||||
|
row.modified_by = user
|
||||||
|
row.row_type = enum.POS_ROW_TYPE_VOID_TXN
|
||||||
|
row.description = "VOID TXN"
|
||||||
|
self.add_row(batch, row)
|
||||||
|
|
||||||
|
# void/execute batch
|
||||||
|
batch.void = True
|
||||||
|
batch.executed = self.app.make_utc()
|
||||||
|
batch.executed_by = user
|
||||||
|
|
||||||
|
# def apply_tender(self, batch, user, tender, amount, **kwargs):
|
||||||
|
# """
|
||||||
|
# Apply the given tender amount to the batch.
|
||||||
|
|
||||||
|
# :param tender: Reference to a
|
||||||
|
# :class:`~rattail.db.model.sales.Tender` or similar object, or dict
|
||||||
|
# with similar keys, or can be just a tender code.
|
||||||
|
|
||||||
|
# :param amount: Amount to apply. Note, this usually should be
|
||||||
|
# a *negative* number.
|
||||||
|
|
||||||
|
# :returns: List of rows which were added to the batch.
|
||||||
|
# """
|
||||||
|
# session = self.app.get_session(batch)
|
||||||
|
# model = self.model
|
||||||
|
|
||||||
|
# tender_info = tender
|
||||||
|
# tender = None
|
||||||
|
# item_entry = None
|
||||||
|
# description = None
|
||||||
|
|
||||||
|
# if isinstance(tender_info, model.Tender):
|
||||||
|
# tender = tender_info
|
||||||
|
# else:
|
||||||
|
# if isinstance(tender_info, str):
|
||||||
|
# tender_code = tender_info
|
||||||
|
# else:
|
||||||
|
# tender_code = tender_info["code"]
|
||||||
|
# item_entry = tender_code
|
||||||
|
# description = f"TENDER '{tender_code}'"
|
||||||
|
# tender = self.get_tender(session, tender_code)
|
||||||
|
|
||||||
|
# if tender:
|
||||||
|
# item_entry = tender.code
|
||||||
|
# description = tender.name
|
||||||
|
|
||||||
|
# if tender.disabled:
|
||||||
|
# # TODO: behavior here should be configurable, probably
|
||||||
|
# # needs a dedicated email etc. ..or maybe just ignore
|
||||||
|
# log.error(
|
||||||
|
# "disabled tender '%s' being applied to POS batch: %s", tender, batch
|
||||||
|
# )
|
||||||
|
|
||||||
|
# rows = []
|
||||||
|
|
||||||
|
# # add row for tender
|
||||||
|
# row = self.make_row()
|
||||||
|
# row.user = user
|
||||||
|
# row.row_type = self.enum.POS_ROW_TYPE_TENDER
|
||||||
|
# row.item_entry = item_entry
|
||||||
|
# row.description = description
|
||||||
|
# row.tender_total = amount
|
||||||
|
# row.tender = tender
|
||||||
|
# batch.tender_total = (batch.tender_total or 0) + row.tender_total
|
||||||
|
# if tender and tender.is_foodstamp:
|
||||||
|
# batch.fs_tender_total = (batch.fs_tender_total or 0) + row.tender_total
|
||||||
|
# self.add_row(batch, row)
|
||||||
|
# rows.append(row)
|
||||||
|
|
||||||
|
# # nothing more to do for now, if balance remains
|
||||||
|
# balance = batch.get_balance()
|
||||||
|
# if balance > 0:
|
||||||
|
# return rows
|
||||||
|
|
||||||
|
# # next we'll give change back
|
||||||
|
# # nb. if balance is 0, then change due is 0, but we always
|
||||||
|
# # include the change due line item even so..
|
||||||
|
|
||||||
|
# # ..but some tenders do not allow cash back
|
||||||
|
# if balance < 0:
|
||||||
|
# if hasattr(tender, "is_cash") and not tender.is_cash:
|
||||||
|
# if not tender.allow_cash_back:
|
||||||
|
# raise ValueError(
|
||||||
|
# f"tender '{tender.name}' does not allow "
|
||||||
|
# f" cash back: ${-balance:0.2f}"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # TODO: maybe should always give change as 'cash'
|
||||||
|
# if tender and tender.is_cash:
|
||||||
|
# cash = tender
|
||||||
|
# else:
|
||||||
|
# cash = self.get_tender(session, "cash")
|
||||||
|
|
||||||
|
# row = self.make_row()
|
||||||
|
# row.user = user
|
||||||
|
# row.row_type = self.enum.POS_ROW_TYPE_CHANGE_BACK
|
||||||
|
# row.item_entry = item_entry
|
||||||
|
# row.description = "CHANGE DUE"
|
||||||
|
# row.tender_total = -balance
|
||||||
|
# row.tender = cash
|
||||||
|
# batch.tender_total = (batch.tender_total or 0) + row.tender_total
|
||||||
|
# self.add_row(batch, row)
|
||||||
|
# rows.append(row)
|
||||||
|
|
||||||
|
# # all paid up, so finalize
|
||||||
|
# session.flush()
|
||||||
|
# assert batch.get_balance() == 0
|
||||||
|
# self.do_execute(batch, user, **kwargs)
|
||||||
|
# return rows
|
||||||
|
|
||||||
|
# def execute(self, batch, progress=None, **kwargs):
|
||||||
|
# # TODO
|
||||||
|
# return True
|
||||||
284
src/wuttapos/clientele.py
Normal file
284
src/wuttapos/clientele.py
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Clientele Handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
# from collections import OrderedDict
|
||||||
|
import logging
|
||||||
|
import uuid as _uuid
|
||||||
|
|
||||||
|
# import warnings
|
||||||
|
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.app import GenericHandler
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ClienteleHandler(GenericHandler):
|
||||||
|
"""
|
||||||
|
Base class and default implementation for clientele handlers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# def get_customer(self, obj):
|
||||||
|
# """
|
||||||
|
# Return the Customer associated with the given object, if any.
|
||||||
|
# """
|
||||||
|
# model = self.model
|
||||||
|
|
||||||
|
# if isinstance(obj, model.Customer):
|
||||||
|
# return obj
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# person = self.app.get_person(obj)
|
||||||
|
# if person:
|
||||||
|
# # TODO: all 3 options below are indeterminate, since it's
|
||||||
|
# # *possible* for a person to hold multiple accounts
|
||||||
|
# # etc. but not sure how to fix in a generic way? maybe
|
||||||
|
# # just everyone must override as needed
|
||||||
|
# if person.customer_accounts:
|
||||||
|
# return person.customer_accounts[0]
|
||||||
|
# for shopper in person.customer_shoppers:
|
||||||
|
# if shopper.shopper_number == 1:
|
||||||
|
# return shopper.customer
|
||||||
|
# # legacy fallback
|
||||||
|
# if person.customers:
|
||||||
|
# return person.customers[0]
|
||||||
|
|
||||||
|
# def make_customer(self, person, **kwargs):
|
||||||
|
# """
|
||||||
|
# Create and return a new customer record.
|
||||||
|
# """
|
||||||
|
# session = self.app.get_session(person)
|
||||||
|
# customer = self.model.Customer()
|
||||||
|
# customer.name = person.display_name
|
||||||
|
# customer.account_holder = person
|
||||||
|
# session.add(customer)
|
||||||
|
# session.flush()
|
||||||
|
# session.refresh(person)
|
||||||
|
# return customer
|
||||||
|
|
||||||
|
def locate_customer_for_entry(self, session, entry, **kwargs):
|
||||||
|
"""
|
||||||
|
This method aims to provide sane default logic for locating a
|
||||||
|
:class:`~rattail.db.model.customers.Customer` record for the
|
||||||
|
given "entry" value.
|
||||||
|
|
||||||
|
The default logic here will try to honor the "configured"
|
||||||
|
customer field, and prefer that when attempting the lookup.
|
||||||
|
|
||||||
|
:param session: Reference to current DB session.
|
||||||
|
|
||||||
|
:param entry: Value to use for lookup. This is most often a
|
||||||
|
simple string, but the method can handle a few others. For
|
||||||
|
instance it is common to read values from a spreadsheet,
|
||||||
|
and sometimes those come through as integers etc.
|
||||||
|
|
||||||
|
:param lookup_fields: Optional list of fields to use for
|
||||||
|
lookup. The default value is ``['uuid', '_customer_key_']``
|
||||||
|
which means to lookup by UUID as well as "customer key"
|
||||||
|
field, which is configurable. You can include any of the
|
||||||
|
following in ``lookup_fields``:
|
||||||
|
|
||||||
|
* ``uuid``
|
||||||
|
* ``_customer_key_`` - :meth:`locate_customer_for_key`
|
||||||
|
|
||||||
|
:returns: First :class:`~rattail.db.model.customers.Customer`
|
||||||
|
instance found if there was a match; otherwise ``None``.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
if not entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# figure out which fields we should match on
|
||||||
|
# TODO: let config declare default lookup_fields
|
||||||
|
lookup_fields = kwargs.get(
|
||||||
|
"lookup_fields",
|
||||||
|
[
|
||||||
|
"uuid",
|
||||||
|
"customer_id",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# try to locate customer by uuid before other, more specific key
|
||||||
|
if "uuid" in lookup_fields:
|
||||||
|
if isinstance(entry, (_uuid.UUID, str)):
|
||||||
|
customer = session.get(model.Customer, entry)
|
||||||
|
if customer:
|
||||||
|
return customer
|
||||||
|
|
||||||
|
lookups = {
|
||||||
|
"uuid": None,
|
||||||
|
"customer_id": self.locate_customer_for_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
for field in lookup_fields:
|
||||||
|
if field in lookups:
|
||||||
|
lookup = lookups[field]
|
||||||
|
if lookup:
|
||||||
|
customer = lookup(session, entry, **kwargs)
|
||||||
|
if customer:
|
||||||
|
return customer
|
||||||
|
else:
|
||||||
|
log.warning("unknown lookup field: %s", field)
|
||||||
|
|
||||||
|
def locate_customer_for_id(self, session, entry, **kwargs):
|
||||||
|
"""
|
||||||
|
Locate the customer which matches the given ID.
|
||||||
|
|
||||||
|
This will do a lookup on the
|
||||||
|
:attr:`rattail.db.model.customers.Customer.id` field only.
|
||||||
|
|
||||||
|
Note that instead of calling this method directly, you might
|
||||||
|
consider calling :meth:`locate_customer_for_key()` instead.
|
||||||
|
|
||||||
|
:param session: Current session for Rattail DB.
|
||||||
|
|
||||||
|
:param entry: Customer ID value as string.
|
||||||
|
|
||||||
|
:returns: First :class:`~rattail.db.model.customers.Customer`
|
||||||
|
instance found if there was a match; otherwise ``None``.
|
||||||
|
"""
|
||||||
|
if not entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# assume entry is string
|
||||||
|
entry = str(entry)
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
session.query(model.Customer)
|
||||||
|
.filter(model.Customer.customer_id == entry)
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
except orm.exc.NoResultFound:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search_customers(self, session, entry, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform a customer search across multiple fields, and return
|
||||||
|
results as JSON data rows.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
final_results = []
|
||||||
|
|
||||||
|
# first we'll attempt "lookup" logic..
|
||||||
|
|
||||||
|
lookup_fields = kwargs.get(
|
||||||
|
"lookup_fields",
|
||||||
|
["customer_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup_fields:
|
||||||
|
customer = self.locate_customer_for_entry(
|
||||||
|
session, entry, lookup_fields=lookup_fields
|
||||||
|
)
|
||||||
|
if customer:
|
||||||
|
final_results.append(customer)
|
||||||
|
|
||||||
|
# then we'll attempt "search" logic..
|
||||||
|
|
||||||
|
search_fields = kwargs.get(
|
||||||
|
"search_fields",
|
||||||
|
[
|
||||||
|
"name",
|
||||||
|
"email_address",
|
||||||
|
"phone_number",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
searches = {
|
||||||
|
"name": self.search_customers_for_name,
|
||||||
|
"email_address": self.search_customers_for_email_address,
|
||||||
|
"phone_number": self.search_customers_for_phone_number,
|
||||||
|
}
|
||||||
|
|
||||||
|
for field in search_fields:
|
||||||
|
if field in searches:
|
||||||
|
search = searches[field]
|
||||||
|
if search:
|
||||||
|
customers = search(session, entry, **kwargs)
|
||||||
|
final_results.extend(customers)
|
||||||
|
else:
|
||||||
|
log.warning("unknown search field: %s", field)
|
||||||
|
|
||||||
|
return [self.normalize_customer(c) for c in final_results]
|
||||||
|
|
||||||
|
def search_customers_for_name(self, session, entry, **kwargs):
|
||||||
|
model = self.app.model
|
||||||
|
entry = entry.lower()
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.query(model.Customer)
|
||||||
|
.filter(model.Customer.name.ilike(f"%{entry}%"))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
def search_customers_for_email_address(self, session, entry, **kwargs):
|
||||||
|
model = self.app.model
|
||||||
|
entry = entry.lower()
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.query(model.Customer)
|
||||||
|
.filter(model.Customer.email_address.ilike(f"%{entry}%"))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
def search_customers_for_phone_number(self, session, entry, **kwargs):
|
||||||
|
model = self.app.model
|
||||||
|
entry = entry.lower()
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.query(model.Customer)
|
||||||
|
.filter(model.Customer.phone_number.ilike(f"%{entry}%"))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
def normalize_customer(self, customer):
|
||||||
|
"""
|
||||||
|
Normalize the given customer to a JSON-serializable dict.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"uuid": customer.uuid,
|
||||||
|
"customer_id": customer.customer_id,
|
||||||
|
"name": customer.name,
|
||||||
|
"phone_number": customer.phone_number,
|
||||||
|
"email_address": customer.email_address,
|
||||||
|
"_str": str(customer),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_customer_info_markdown(self, customer, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a Markdown string containing pertinent info about a
|
||||||
|
given customer account.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
f"Customer ID: {customer.customer_id}\n\n"
|
||||||
|
f"Name: {customer.name}\n\n"
|
||||||
|
f"Phone: {customer.phone_number or ''}\n\n"
|
||||||
|
f"Email: {customer.email_address or ''}\n\n"
|
||||||
|
)
|
||||||
|
|
@ -42,6 +42,7 @@ class WuttaPosConfigExtension(WuttaConfigExtension):
|
||||||
|
|
||||||
# app model
|
# app model
|
||||||
config.setdefault(f"{config.appname}.model_spec", "wuttapos.db.model")
|
config.setdefault(f"{config.appname}.model_spec", "wuttapos.db.model")
|
||||||
|
config.setdefault(f"{config.appname}.enum_spec", "wuttapos.enum")
|
||||||
|
|
||||||
# # auth handler
|
# # auth handler
|
||||||
# config.setdefault(
|
# config.setdefault(
|
||||||
|
|
|
||||||
80
src/wuttapos/db/alembic/versions/3f548013be91_add_stores.py
Normal file
80
src/wuttapos/db/alembic/versions/3f548013be91_add_stores.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
"""add Stores
|
||||||
|
|
||||||
|
Revision ID: 3f548013be91
|
||||||
|
Revises: 7880450562d5
|
||||||
|
Create Date: 2026-01-02 10:18:20.199691
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wuttjamaican.db.util
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "3f548013be91"
|
||||||
|
down_revision: Union[str, None] = "32e965f42f0f"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
|
||||||
|
# store
|
||||||
|
op.create_table(
|
||||||
|
"store",
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("store_id", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=100), nullable=False),
|
||||||
|
sa.Column("active", sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_store")),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"store_version",
|
||||||
|
sa.Column(
|
||||||
|
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("store_id", sa.String(length=20), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("name", sa.String(length=100), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("active", sa.Boolean(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint(
|
||||||
|
"uuid", "transaction_id", name=op.f("pk_store_version")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_store_version_end_transaction_id"),
|
||||||
|
"store_version",
|
||||||
|
["end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_store_version_operation_type"),
|
||||||
|
"store_version",
|
||||||
|
["operation_type"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_store_version_transaction_id"),
|
||||||
|
"store_version",
|
||||||
|
["transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
|
||||||
|
# store
|
||||||
|
op.drop_index(op.f("ix_store_version_transaction_id"), table_name="store_version")
|
||||||
|
op.drop_index(op.f("ix_store_version_operation_type"), table_name="store_version")
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_store_version_end_transaction_id"), table_name="store_version"
|
||||||
|
)
|
||||||
|
op.drop_table("store_version")
|
||||||
|
op.drop_table("store")
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
"""add Terminals
|
||||||
|
|
||||||
|
Revision ID: 653b7d27c709
|
||||||
|
Revises: 3f548013be91
|
||||||
|
Create Date: 2026-01-02 11:08:43.118117
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wuttjamaican.db.util
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "653b7d27c709"
|
||||||
|
down_revision: Union[str, None] = "3f548013be91"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
|
||||||
|
# terminal
|
||||||
|
op.create_table(
|
||||||
|
"terminal",
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("terminal_id", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=50), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_terminal")),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"terminal_version",
|
||||||
|
sa.Column(
|
||||||
|
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"terminal_id", sa.String(length=20), autoincrement=False, nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("name", sa.String(length=50), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint(
|
||||||
|
"uuid", "transaction_id", name=op.f("pk_terminal_version")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_terminal_version_end_transaction_id"),
|
||||||
|
"terminal_version",
|
||||||
|
["end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_terminal_version_operation_type"),
|
||||||
|
"terminal_version",
|
||||||
|
["operation_type"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_terminal_version_transaction_id"),
|
||||||
|
"terminal_version",
|
||||||
|
["transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
|
||||||
|
# terminal
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_terminal_version_transaction_id"), table_name="terminal_version"
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_terminal_version_operation_type"), table_name="terminal_version"
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_terminal_version_end_transaction_id"), table_name="terminal_version"
|
||||||
|
)
|
||||||
|
op.drop_table("terminal_version")
|
||||||
|
op.drop_table("terminal")
|
||||||
80
src/wuttapos/db/alembic/versions/7067ef686eb0_add_taxes.py
Normal file
80
src/wuttapos/db/alembic/versions/7067ef686eb0_add_taxes.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
"""add Taxes
|
||||||
|
|
||||||
|
Revision ID: 7067ef686eb0
|
||||||
|
Revises: b026e4f5c5bc
|
||||||
|
Create Date: 2026-01-02 12:15:45.279817
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wuttjamaican.db.util
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "7067ef686eb0"
|
||||||
|
down_revision: Union[str, None] = "b026e4f5c5bc"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
|
||||||
|
# tax
|
||||||
|
op.create_table(
|
||||||
|
"tax",
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("tax_id", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=50), nullable=False),
|
||||||
|
sa.Column("rate", sa.Numeric(precision=7, scale=5), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_tax")),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"tax_version",
|
||||||
|
sa.Column(
|
||||||
|
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("tax_id", sa.String(length=20), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("name", sa.String(length=50), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"rate", sa.Numeric(precision=7, scale=5), autoincrement=False, nullable=True
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", "transaction_id", name=op.f("pk_tax_version")),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_tax_version_end_transaction_id"),
|
||||||
|
"tax_version",
|
||||||
|
["end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_tax_version_operation_type"),
|
||||||
|
"tax_version",
|
||||||
|
["operation_type"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_tax_version_transaction_id"),
|
||||||
|
"tax_version",
|
||||||
|
["transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
|
||||||
|
# tax
|
||||||
|
op.drop_table("batch_pos_row")
|
||||||
|
op.drop_table("batch_pos")
|
||||||
|
op.drop_index(op.f("ix_tax_version_transaction_id"), table_name="tax_version")
|
||||||
|
op.drop_index(op.f("ix_tax_version_operation_type"), table_name="tax_version")
|
||||||
|
op.drop_index(op.f("ix_tax_version_end_transaction_id"), table_name="tax_version")
|
||||||
|
op.drop_table("tax_version")
|
||||||
|
op.drop_table("tax")
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
"""add Employees
|
||||||
|
|
||||||
|
Revision ID: 8ce8b14af66d
|
||||||
|
Revises: 653b7d27c709
|
||||||
|
Create Date: 2026-01-02 11:18:07.359270
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wuttjamaican.db.util
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "8ce8b14af66d"
|
||||||
|
down_revision: Union[str, None] = "653b7d27c709"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
|
||||||
|
# employee
|
||||||
|
op.create_table(
|
||||||
|
"employee",
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("person_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
|
sa.Column("name", sa.String(length=100), nullable=False),
|
||||||
|
sa.Column("public_name", sa.String(length=100), nullable=True),
|
||||||
|
sa.Column("active", sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["person_uuid"],
|
||||||
|
["person.uuid"],
|
||||||
|
name=op.f("fk_employee_person_uuid_person"),
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_employee")),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"employee_version",
|
||||||
|
sa.Column(
|
||||||
|
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"person_uuid",
|
||||||
|
wuttjamaican.db.util.UUID(),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.Column("name", sa.String(length=100), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"public_name", sa.String(length=100), autoincrement=False, nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("active", sa.Boolean(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint(
|
||||||
|
"uuid", "transaction_id", name=op.f("pk_employee_version")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_employee_version_end_transaction_id"),
|
||||||
|
"employee_version",
|
||||||
|
["end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_employee_version_operation_type"),
|
||||||
|
"employee_version",
|
||||||
|
["operation_type"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_employee_version_transaction_id"),
|
||||||
|
"employee_version",
|
||||||
|
["transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
|
||||||
|
# employee
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_employee_version_transaction_id"), table_name="employee_version"
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_employee_version_operation_type"), table_name="employee_version"
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_employee_version_end_transaction_id"), table_name="employee_version"
|
||||||
|
)
|
||||||
|
op.drop_table("employee_version")
|
||||||
|
op.drop_table("employee")
|
||||||
92
src/wuttapos/db/alembic/versions/b026e4f5c5bc_add_tenders.py
Normal file
92
src/wuttapos/db/alembic/versions/b026e4f5c5bc_add_tenders.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""add Tenders
|
||||||
|
|
||||||
|
Revision ID: b026e4f5c5bc
|
||||||
|
Revises: 8ce8b14af66d
|
||||||
|
Create Date: 2026-01-02 11:34:27.523125
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wuttjamaican.db.util
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "b026e4f5c5bc"
|
||||||
|
down_revision: Union[str, None] = "8ce8b14af66d"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
|
||||||
|
# tender
|
||||||
|
op.create_table(
|
||||||
|
"tender",
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("tender_id", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=50), nullable=False),
|
||||||
|
sa.Column("notes", sa.Text(), nullable=True),
|
||||||
|
sa.Column("is_cash", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("is_foodstamp", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("allow_cashback", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("kick_drawer", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("active", sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_tender")),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"tender_version",
|
||||||
|
sa.Column(
|
||||||
|
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"tender_id", sa.String(length=20), autoincrement=False, nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("name", sa.String(length=50), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("notes", sa.Text(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("is_cash", sa.Boolean(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("is_foodstamp", sa.Boolean(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("allow_cashback", sa.Boolean(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("kick_drawer", sa.Boolean(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column("active", sa.Boolean(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint(
|
||||||
|
"uuid", "transaction_id", name=op.f("pk_tender_version")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_tender_version_end_transaction_id"),
|
||||||
|
"tender_version",
|
||||||
|
["end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_tender_version_operation_type"),
|
||||||
|
"tender_version",
|
||||||
|
["operation_type"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_tender_version_transaction_id"),
|
||||||
|
"tender_version",
|
||||||
|
["transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
|
||||||
|
# tender
|
||||||
|
op.drop_index(op.f("ix_tender_version_transaction_id"), table_name="tender_version")
|
||||||
|
op.drop_index(op.f("ix_tender_version_operation_type"), table_name="tender_version")
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_tender_version_end_transaction_id"), table_name="tender_version"
|
||||||
|
)
|
||||||
|
op.drop_table("tender_version")
|
||||||
|
op.drop_table("tender")
|
||||||
135
src/wuttapos/db/alembic/versions/c2b1f0983c97_add_pos_batches.py
Normal file
135
src/wuttapos/db/alembic/versions/c2b1f0983c97_add_pos_batches.py
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
"""add POS Batches
|
||||||
|
|
||||||
|
Revision ID: c2b1f0983c97
|
||||||
|
Revises: 7067ef686eb0
|
||||||
|
Create Date: 2026-01-02 13:11:37.703157
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wuttjamaican.db.util
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "c2b1f0983c97"
|
||||||
|
down_revision: Union[str, None] = "7067ef686eb0"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
|
||||||
|
# batch_pos
|
||||||
|
op.create_table(
|
||||||
|
"batch_pos",
|
||||||
|
sa.Column("store_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("terminal_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("cashier_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("customer_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
|
sa.Column("customer_is_member", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("customer_is_employee", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("sales_total", sa.Numeric(precision=9, scale=2), nullable=True),
|
||||||
|
sa.Column("fs_total", sa.Numeric(precision=9, scale=2), nullable=True),
|
||||||
|
sa.Column("tax_total", sa.Numeric(precision=9, scale=2), nullable=True),
|
||||||
|
sa.Column("fs_tender_total", sa.Numeric(precision=9, scale=2), nullable=True),
|
||||||
|
sa.Column("tender_total", sa.Numeric(precision=9, scale=2), nullable=True),
|
||||||
|
sa.Column("void", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("training_mode", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("description", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("notes", sa.Text(), nullable=True),
|
||||||
|
sa.Column("row_count", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("status_code", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("status_text", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("created", sa.DateTime(), nullable=False),
|
||||||
|
sa.Column("created_by_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("executed", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("executed_by_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["cashier_uuid"],
|
||||||
|
["employee.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_cashier_uuid_employee"),
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["created_by_uuid"],
|
||||||
|
["user.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_created_by_uuid_user"),
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["customer_uuid"],
|
||||||
|
["customer.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_customer_uuid_customer"),
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["executed_by_uuid"],
|
||||||
|
["user.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_executed_by_uuid_user"),
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["store_uuid"], ["store.uuid"], name=op.f("fk_batch_pos_store_uuid_store")
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["terminal_uuid"],
|
||||||
|
["terminal.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_terminal_uuid_terminal"),
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_batch_pos")),
|
||||||
|
)
|
||||||
|
|
||||||
|
# batch_pos_row
|
||||||
|
op.create_table(
|
||||||
|
"batch_pos_row",
|
||||||
|
sa.Column("modified_by_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("row_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("item_entry", sa.String(length=20), nullable=True),
|
||||||
|
sa.Column("product_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
|
sa.Column("description", sa.String(length=100), nullable=True),
|
||||||
|
sa.Column("foodstamp_eligible", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("sold_by_weight", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("quantity", sa.Numeric(precision=8, scale=2), nullable=True),
|
||||||
|
sa.Column("cost", sa.Numeric(precision=8, scale=3), nullable=True),
|
||||||
|
sa.Column("reg_price", sa.Numeric(precision=8, scale=3), nullable=True),
|
||||||
|
sa.Column("cur_price", sa.Numeric(precision=8, scale=3), nullable=True),
|
||||||
|
sa.Column("cur_price_type", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("cur_price_start", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("cur_price_end", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("txn_price", sa.Numeric(precision=8, scale=3), nullable=True),
|
||||||
|
sa.Column("txn_price_adjusted", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("sales_total", sa.Numeric(precision=9, scale=2), nullable=True),
|
||||||
|
sa.Column("tax_code", sa.String(length=30), nullable=True),
|
||||||
|
sa.Column("tender_total", sa.Numeric(precision=9, scale=2), nullable=True),
|
||||||
|
sa.Column("void", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("batch_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("sequence", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("status_code", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("status_text", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("modified", sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["batch_uuid"],
|
||||||
|
["batch_pos.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_row_batch_uuid_batch_pos"),
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["modified_by_uuid"],
|
||||||
|
["user.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_row_modified_by_uuid_user"),
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["product_uuid"],
|
||||||
|
["product.uuid"],
|
||||||
|
name=op.f("fk_batch_pos_row_product_uuid_product"),
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_batch_pos_row")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
|
||||||
|
# batch_pos*
|
||||||
|
op.drop_table("batch_pos_row")
|
||||||
|
op.drop_table("batch_pos")
|
||||||
|
|
@ -26,6 +26,11 @@ WuttaPOS - data model
|
||||||
|
|
||||||
from wuttjamaican.db.model import *
|
from wuttjamaican.db.model import *
|
||||||
|
|
||||||
|
from .stores import Store
|
||||||
|
from .terminals import Terminal
|
||||||
|
from .tenders import Tender
|
||||||
|
from .taxes import Tax
|
||||||
|
from .employees import Employee
|
||||||
from .customers import Customer
|
from .customers import Customer
|
||||||
from .departments import Department
|
from .departments import Department
|
||||||
from .products import (
|
from .products import (
|
||||||
|
|
@ -34,3 +39,5 @@ from .products import (
|
||||||
InventoryAdjustmentType,
|
InventoryAdjustmentType,
|
||||||
InventoryAdjustment,
|
InventoryAdjustment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .batch.pos import POSBatch, POSBatchRow
|
||||||
|
|
|
||||||
0
src/wuttapos/db/model/batch/__init__.py
Normal file
0
src/wuttapos/db/model/batch/__init__.py
Normal file
412
src/wuttapos/db/model/batch/pos.py
Normal file
412
src/wuttapos/db/model/batch/pos.py
Normal file
|
|
@ -0,0 +1,412 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Model for POS Batches
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
from sqlalchemy.ext.orderinglist import ordering_list
|
||||||
|
|
||||||
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
||||||
|
class POSBatch(model.BatchMixin, model.Base):
|
||||||
|
"""
|
||||||
|
Hopefully generic batch used for entering new purchases into the system, etc.?
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "batch_pos"
|
||||||
|
__wutta_hint__ = {
|
||||||
|
"model_title": "POS Batch",
|
||||||
|
"model_title_plural": "POS Batches",
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_type = "pos"
|
||||||
|
|
||||||
|
STATUS_OK = 1
|
||||||
|
STATUS_SUSPENDED = 2
|
||||||
|
|
||||||
|
STATUS = {
|
||||||
|
STATUS_OK: "ok",
|
||||||
|
STATUS_SUSPENDED: "suspended",
|
||||||
|
}
|
||||||
|
|
||||||
|
store_uuid = model.uuid_fk_column("store.uuid", nullable=False)
|
||||||
|
store = orm.relationship(
|
||||||
|
"Store",
|
||||||
|
doc="""
|
||||||
|
Reference to the store where the transaction ocurred.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
terminal_uuid = model.uuid_fk_column("terminal.uuid", nullable=False)
|
||||||
|
terminal = orm.relationship(
|
||||||
|
"Terminal",
|
||||||
|
doc="""
|
||||||
|
Reference to the terminal where the transaction ocurred.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# receipt_number = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
|
# Receipt number for the transaction, if known.
|
||||||
|
# """)
|
||||||
|
|
||||||
|
cashier_uuid = model.uuid_fk_column("employee.uuid", nullable=False)
|
||||||
|
cashier = orm.relationship(
|
||||||
|
"Employee",
|
||||||
|
doc="""
|
||||||
|
Reference to the employee who acted as cashier.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
customer_uuid = model.uuid_fk_column("customer.uuid", nullable=True)
|
||||||
|
customer = orm.relationship(
|
||||||
|
"Customer",
|
||||||
|
doc="""
|
||||||
|
Reference to the customer account for the transaction.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
customer_is_member = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Flag indicating the customer was a "member" at time of sale.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
customer_is_employee = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Flag indicating the customer was an employee at time of sale.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# shopper_number = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
|
# Number of the shopper account for the transaction, if applicable.
|
||||||
|
# """)
|
||||||
|
|
||||||
|
# shopper_name = sa.Column(sa.String(length=255), nullable=True, doc="""
|
||||||
|
# Name of the shopper account for the transaction, if applicable.
|
||||||
|
# """)
|
||||||
|
|
||||||
|
# shopper_uuid = sa.Column(sa.String(length=32), nullable=True)
|
||||||
|
# shopper = orm.relationship(
|
||||||
|
# 'CustomerShopper',
|
||||||
|
# doc="""
|
||||||
|
# Reference to the shopper account for the transaction.
|
||||||
|
# """)
|
||||||
|
|
||||||
|
sales_total = sa.Column(
|
||||||
|
sa.Numeric(precision=9, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Sales total for the transaction.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
fs_total = sa.Column(
|
||||||
|
sa.Numeric(precision=9, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Portion of the sales total which is foodstamp-eligible.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
tax_total = sa.Column(
|
||||||
|
sa.Numeric(precision=9, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Tax total for the transaction.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
fs_tender_total = sa.Column(
|
||||||
|
sa.Numeric(precision=9, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Foodstamp tender total for the transaction.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
tender_total = sa.Column(
|
||||||
|
sa.Numeric(precision=9, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Tender total for the transaction.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
void = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
default=False,
|
||||||
|
doc="""
|
||||||
|
Flag indicating if the transaction was voided.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
training_mode = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
default=False,
|
||||||
|
doc="""
|
||||||
|
Flag indicating if the transaction was rang in training mode,
|
||||||
|
i.e. not real / should not go on the books.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_balance(self):
|
||||||
|
return (
|
||||||
|
(self.sales_total or 0) + (self.tax_total or 0) + (self.tender_total or 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_fs_balance(self):
|
||||||
|
return (self.fs_total or 0) + (self.fs_tender_total or 0)
|
||||||
|
|
||||||
|
|
||||||
|
class POSBatchRow(model.BatchRowMixin, model.Base):
|
||||||
|
"""
|
||||||
|
Row of data within a POS batch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "batch_pos_row"
|
||||||
|
__batch_class__ = POSBatch
|
||||||
|
|
||||||
|
STATUS_OK = 1
|
||||||
|
|
||||||
|
STATUS = {
|
||||||
|
STATUS_OK: "ok",
|
||||||
|
}
|
||||||
|
|
||||||
|
modified_by_uuid = model.uuid_fk_column("user.uuid", nullable=False)
|
||||||
|
modified_by = orm.relationship(
|
||||||
|
"User",
|
||||||
|
doc="""
|
||||||
|
Reference to the user who added this row to the batch.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
row_type = sa.Column(
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Type of item represented by this row, e.g. "item" or "return" or
|
||||||
|
"tender" etc.
|
||||||
|
|
||||||
|
.. todo::
|
||||||
|
need to figure out how to manage/track POSBatchRow.row_type
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
item_entry = sa.Column(
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Raw/original entry value for the item, if applicable.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
description = sa.Column(
|
||||||
|
sa.String(length=100),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Description for the row.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
product_uuid = model.uuid_fk_column("product.uuid", nullable=True)
|
||||||
|
product = orm.relationship(
|
||||||
|
"Product",
|
||||||
|
doc="""
|
||||||
|
Reference to the associated product, if applicable.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# department_uuid = model.uuid_fk_column("department.uuid", nullable=True)
|
||||||
|
# department = orm.relationship(
|
||||||
|
# "Department",
|
||||||
|
# doc="""
|
||||||
|
# Reference to the associated department, if applicable.
|
||||||
|
# """,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# subdepartment_number = sa.Column(
|
||||||
|
# sa.Integer(),
|
||||||
|
# nullable=True,
|
||||||
|
# doc="""
|
||||||
|
# Number of the subdepartment to which the product belongs.
|
||||||
|
# """,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# subdepartment_name = sa.Column(
|
||||||
|
# sa.String(length=30),
|
||||||
|
# nullable=True,
|
||||||
|
# doc="""
|
||||||
|
# Name of the subdepartment to which the product belongs.
|
||||||
|
# """,
|
||||||
|
# )
|
||||||
|
|
||||||
|
foodstamp_eligible = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Indicates the item is eligible for purchase with food stamps
|
||||||
|
or equivalent.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
sold_by_weight = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Flag indicating the item is sold by weight.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = sa.Column(
|
||||||
|
sa.Numeric(precision=8, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Quantity for the item.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
cost = sa.Column(
|
||||||
|
sa.Numeric(precision=8, scale=3),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Internal cost for the item sold.
|
||||||
|
|
||||||
|
NOTE: this may need to change at some point, hence the "generic"
|
||||||
|
naming so far. would we need to record multiple kinds of costs?
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
reg_price = sa.Column(
|
||||||
|
sa.Numeric(precision=8, scale=3),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Regular price for the item.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
cur_price = sa.Column(
|
||||||
|
sa.Numeric(precision=8, scale=3),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Current price for the item.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
cur_price_type = sa.Column(
|
||||||
|
sa.Integer(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Type code for the current price, if applicable.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
cur_price_start = sa.Column(
|
||||||
|
sa.DateTime(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Start date for current price, if applicable.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
cur_price_end = sa.Column(
|
||||||
|
sa.DateTime(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
End date for current price, if applicable.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
txn_price = sa.Column(
|
||||||
|
sa.Numeric(precision=8, scale=3),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Actual price paid for the item.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
txn_price_adjusted = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Flag indicating the actual price was manually adjusted.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
sales_total = sa.Column(
|
||||||
|
sa.Numeric(precision=9, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Sales total for the item.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
tax_code = sa.Column(
|
||||||
|
sa.String(length=30),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Unique "code" for the item tax rate, if applicable.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# tax_uuid = model.uuid_fk_column("tax.uuid", nullable=True)
|
||||||
|
# tax = orm.relationship(
|
||||||
|
# "Tax",
|
||||||
|
# doc="""
|
||||||
|
# Reference to the associated tax, if applicable.
|
||||||
|
# """,
|
||||||
|
# )
|
||||||
|
|
||||||
|
tender_total = sa.Column(
|
||||||
|
sa.Numeric(precision=9, scale=2),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Tender total for the item.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# tender_uuid = model.uuid_fk_column("tender.uuid", nullable=True)
|
||||||
|
# tender = orm.relationship(
|
||||||
|
# "Tender",
|
||||||
|
# doc="""
|
||||||
|
# Reference to the associated tender, if applicable.
|
||||||
|
# """,
|
||||||
|
# )
|
||||||
|
|
||||||
|
void = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
default=False,
|
||||||
|
doc="""
|
||||||
|
Flag indicating the line item was voided.
|
||||||
|
""",
|
||||||
|
)
|
||||||
67
src/wuttapos/db/model/employees.py
Normal file
67
src/wuttapos/db/model/employees.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Model definition for Employees
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
||||||
|
class Employee(model.Base):
|
||||||
|
"""
|
||||||
|
Represents a current or former employee.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "employee"
|
||||||
|
__versioned__ = {}
|
||||||
|
__wutta_hint__ = {
|
||||||
|
"model_title": "Employee",
|
||||||
|
"model_title_plural": "Employees",
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
person_uuid = model.uuid_fk_column("person.uuid", nullable=True)
|
||||||
|
person = orm.relationship(
|
||||||
|
"Person",
|
||||||
|
doc="""
|
||||||
|
Reference to the person who is/was the employee.
|
||||||
|
""",
|
||||||
|
backref=orm.backref(
|
||||||
|
"employee",
|
||||||
|
uselist=False,
|
||||||
|
doc="""
|
||||||
|
Reference to the employee record for the person, if applicable.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = sa.Column(
|
||||||
|
sa.String(length=100),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Internal name for the employee.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
public_name = sa.Column(
|
||||||
|
sa.String(length=100),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Name of the employee, for display to the public (if different).
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
active = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
default=True,
|
||||||
|
doc="""
|
||||||
|
Indicates the employee is currently active.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or ""
|
||||||
52
src/wuttapos/db/model/stores.py
Normal file
52
src/wuttapos/db/model/stores.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Model definition for Stores
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
||||||
|
class Store(model.Base):
|
||||||
|
"""
|
||||||
|
Represents a single location, physical or virtual, where sales happen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "store"
|
||||||
|
__versioned__ = {}
|
||||||
|
__wutta_hint__ = {
|
||||||
|
"model_title": "Store",
|
||||||
|
"model_title_plural": "Stores",
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
store_id = sa.Column(
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Unique identifier for the store.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
name = sa.Column(
|
||||||
|
sa.String(length=100),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Name for the store.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
active = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
default=True,
|
||||||
|
doc="""
|
||||||
|
Indicates the store is currently active.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or ""
|
||||||
51
src/wuttapos/db/model/taxes.py
Normal file
51
src/wuttapos/db/model/taxes.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Model definition for Taxes
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
||||||
|
class Tax(model.Base):
|
||||||
|
"""
|
||||||
|
Represents a type/rate of sales tax to track.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "tax"
|
||||||
|
__versioned__ = {}
|
||||||
|
__wutta_hint__ = {
|
||||||
|
"model_title": "Tax",
|
||||||
|
"model_title_plural": "Taxes",
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
tax_id = sa.Column(
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Unique identifier for this tax rate.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
name = sa.Column(
|
||||||
|
sa.String(length=50),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Name for the tax rate.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
rate = sa.Column(
|
||||||
|
sa.Numeric(precision=7, scale=5),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Percentage rate for the tax, e.g. 8.25.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or ""
|
||||||
92
src/wuttapos/db/model/tenders.py
Normal file
92
src/wuttapos/db/model/tenders.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Model definition for Tenders
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
||||||
|
class Tender(model.Base):
|
||||||
|
"""
|
||||||
|
Represents a tender (payment type) for the POS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "tender"
|
||||||
|
__versioned__ = {}
|
||||||
|
__wutta_hint__ = {
|
||||||
|
"model_title": "Tender",
|
||||||
|
"model_title_plural": "Tenders",
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
tender_id = sa.Column(
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Unique identifier for the tender.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
name = sa.Column(
|
||||||
|
sa.String(length=50),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Name for the tender type.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = sa.Column(
|
||||||
|
sa.Text(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Arbitrary notes for the tender type.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
is_cash = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Indicates this tender type is a form of "cash" conceptually.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
is_foodstamp = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Indicates this tender type is a form of "food stamps" conceptually.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
allow_cashback = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Indicates "cash back" should be allowed when overpaying with this tender.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
kick_drawer = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Indicates the cash drawer should kick open when accepting this tender.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
active = sa.Column(
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
default=True,
|
||||||
|
doc="""
|
||||||
|
Indicates this tender is currently active (acceptable for payment).
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or ""
|
||||||
43
src/wuttapos/db/model/terminals.py
Normal file
43
src/wuttapos/db/model/terminals.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Model definition for Terminals
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
||||||
|
class Terminal(model.Base):
|
||||||
|
"""
|
||||||
|
Represents a POS terminal (lane).
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "terminal"
|
||||||
|
__versioned__ = {}
|
||||||
|
__wutta_hint__ = {
|
||||||
|
"model_title": "Terminal",
|
||||||
|
"model_title_plural": "Terminals",
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
terminal_id = sa.Column(
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Unique identifier for the terminal.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
name = sa.Column(
|
||||||
|
sa.String(length=50),
|
||||||
|
nullable=False,
|
||||||
|
doc="""
|
||||||
|
Name for the terminal.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or ""
|
||||||
57
src/wuttapos/employment.py
Normal file
57
src/wuttapos/employment.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Employment handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttjamaican.app import GenericHandler
|
||||||
|
|
||||||
|
|
||||||
|
class EmploymentHandler(GenericHandler):
|
||||||
|
"""
|
||||||
|
Base class and default implementation for employment handlers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_employee(self, obj):
|
||||||
|
"""
|
||||||
|
Returns the Employee associated with the given object, if any.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
if isinstance(obj, model.Employee):
|
||||||
|
employee = obj
|
||||||
|
return employee
|
||||||
|
|
||||||
|
if person := self.app.get_person(obj):
|
||||||
|
if person.employee:
|
||||||
|
return person.employee
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# def make_employee(self, person):
|
||||||
|
# """
|
||||||
|
# Create and return a new employee record.
|
||||||
|
# """
|
||||||
|
# employee = self.model.Employee()
|
||||||
|
# employee.person = person
|
||||||
|
# return employee
|
||||||
68
src/wuttapos/enum.py
Normal file
68
src/wuttapos/enum.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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 enum values
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from wuttjamaican.enum import *
|
||||||
|
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# POS Batch Row Type
|
||||||
|
########################################
|
||||||
|
|
||||||
|
POS_ROW_TYPE_SET_CUSTOMER = "set_customer"
|
||||||
|
POS_ROW_TYPE_SWAP_CUSTOMER = "swap_customer"
|
||||||
|
POS_ROW_TYPE_DEL_CUSTOMER = "del_customer"
|
||||||
|
POS_ROW_TYPE_SELL = "sell"
|
||||||
|
POS_ROW_TYPE_OPEN_RING = "openring"
|
||||||
|
POS_ROW_TYPE_BADSCAN = "badscan"
|
||||||
|
POS_ROW_TYPE_BADPRICE = "badprice"
|
||||||
|
POS_ROW_TYPE_ADJUST_PRICE = "adjust_price"
|
||||||
|
POS_ROW_TYPE_VOID_LINE = "void_line"
|
||||||
|
POS_ROW_TYPE_VOID_TXN = "void_txn"
|
||||||
|
POS_ROW_TYPE_SUSPEND = "suspend"
|
||||||
|
POS_ROW_TYPE_RESUME = "resume"
|
||||||
|
POS_ROW_TYPE_TENDER = "tender"
|
||||||
|
POS_ROW_TYPE_CHANGE_BACK = "change_back"
|
||||||
|
|
||||||
|
POS_ROW_TYPE = OrderedDict(
|
||||||
|
[
|
||||||
|
(POS_ROW_TYPE_SET_CUSTOMER, "set customer"),
|
||||||
|
(POS_ROW_TYPE_SWAP_CUSTOMER, "swap customer"),
|
||||||
|
(POS_ROW_TYPE_DEL_CUSTOMER, "del customer"),
|
||||||
|
(POS_ROW_TYPE_SELL, "sell"),
|
||||||
|
(POS_ROW_TYPE_OPEN_RING, "open ring"),
|
||||||
|
(POS_ROW_TYPE_BADSCAN, "bad scan"),
|
||||||
|
(POS_ROW_TYPE_BADPRICE, "bad price"),
|
||||||
|
(POS_ROW_TYPE_ADJUST_PRICE, "adjust price"),
|
||||||
|
(POS_ROW_TYPE_VOID_LINE, "void line"),
|
||||||
|
(POS_ROW_TYPE_VOID_TXN, "void txn"),
|
||||||
|
(POS_ROW_TYPE_SUSPEND, "suspend"),
|
||||||
|
(POS_ROW_TYPE_RESUME, "resume"),
|
||||||
|
(POS_ROW_TYPE_TENDER, "tender"),
|
||||||
|
(POS_ROW_TYPE_CHANGE_BACK, "change back"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
|
||||||
# Copyright © 2026 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/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Transaction Handler
|
|
||||||
"""
|
|
||||||
|
|
||||||
import decimal
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from wuttjamaican.app import GenericHandler
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionHandler(GenericHandler):
|
|
||||||
"""
|
|
||||||
Base class and default implementation for the :term:`transaction
|
|
||||||
handler`.
|
|
||||||
|
|
||||||
This is responsible for business logic while a transaction is
|
|
||||||
being rang up.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_terminal_id(self):
|
|
||||||
"""
|
|
||||||
Returns the ID string for current POS terminal.
|
|
||||||
"""
|
|
||||||
return self.config.get("wuttapos.terminal_id")
|
|
||||||
|
|
||||||
def get_screen_txn_display(self, txn, **kwargs):
|
|
||||||
"""
|
|
||||||
Should return the text to be used for displaying transaction
|
|
||||||
identifier within the header of POS screen.
|
|
||||||
"""
|
|
||||||
return "-".join([txn["terminal_id"], txn["cashier_id"], txn["transaction_id"]])
|
|
||||||
|
|
||||||
def get_screen_cust_display(self, txn=None, customer=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Should return the text to be used for displaying customer
|
|
||||||
identifier / name etc. within the header of POS screen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not customer and txn:
|
|
||||||
return txn["customer_id"]
|
|
||||||
|
|
||||||
if not customer:
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: what about person_number
|
|
||||||
return str(customer.card_number)
|
|
||||||
|
|
||||||
# TODO: should also filter this by terminal?
|
|
||||||
def get_current_transaction(
|
|
||||||
self,
|
|
||||||
user,
|
|
||||||
# terminal_id=None,
|
|
||||||
training_mode=False,
|
|
||||||
# create=True,
|
|
||||||
return_created=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Get the "current" POS transaction for the given user, creating
|
|
||||||
it as needed.
|
|
||||||
|
|
||||||
:param training_mode: Flag indicating whether the transaction
|
|
||||||
should be in training mode. The lookup will be restricted
|
|
||||||
according to the value of this flag. If a new batch is
|
|
||||||
created, it will be assigned this flag value.
|
|
||||||
"""
|
|
||||||
if not user:
|
|
||||||
raise ValueError("must specify a user")
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
created = False
|
|
||||||
lines = []
|
|
||||||
if not lines:
|
|
||||||
# if not create:
|
|
||||||
if return_created:
|
|
||||||
return None, False
|
|
||||||
return None
|
|
||||||
49
src/wuttapos/people.py
Normal file
49
src/wuttapos/people.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
People Handler
|
||||||
|
|
||||||
|
This is a :term:`handler` to manage "people" in the DB.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttjamaican import people as base
|
||||||
|
|
||||||
|
|
||||||
|
class PeopleHandler(base.PeopleHandler):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_person(self, obj):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# upstream logic may be good enough
|
||||||
|
if person := super().get_person(obj):
|
||||||
|
return person
|
||||||
|
|
||||||
|
# employee
|
||||||
|
if isinstance(obj, model.Employee):
|
||||||
|
employee = obj
|
||||||
|
return employee.person
|
||||||
|
|
||||||
|
return None
|
||||||
518
src/wuttapos/products.py
Normal file
518
src/wuttapos/products.py
Normal file
|
|
@ -0,0 +1,518 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Products Handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
# import decimal
|
||||||
|
import logging
|
||||||
|
import uuid as _uuid
|
||||||
|
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from wuttjamaican.app import GenericHandler
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductsHandler(GenericHandler):
|
||||||
|
"""
|
||||||
|
Base class and default implementation for product handlers.
|
||||||
|
|
||||||
|
A products handler of course should get the final say in how products are
|
||||||
|
handled. This means everything from pricing, to whether or not a
|
||||||
|
particular product can be deleted, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def locate_product_for_entry(
|
||||||
|
self, session, entry, include_not_for_sale=False, **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This method aims to provide sane default logic for locating a
|
||||||
|
:class:`~rattail.db.model.products.Product` record for the
|
||||||
|
given "entry" value.
|
||||||
|
|
||||||
|
The default logic here will try to honor the "configured"
|
||||||
|
product key field, and prefer that when attempting the lookup.
|
||||||
|
|
||||||
|
:param session: Reference to current DB session.
|
||||||
|
|
||||||
|
:param entry: Value to use for lookup. This is most often a
|
||||||
|
simple string, but the method can handle a few others. For
|
||||||
|
instance it is common to read values from a spreadsheet,
|
||||||
|
and sometimes those come through as integers etc. If this
|
||||||
|
value is a :class:`~rattail.gpc.GPC` instance, special
|
||||||
|
logic may be used for the lookup.
|
||||||
|
|
||||||
|
:param lookup_fields: Optional list of fields to use for
|
||||||
|
lookup. The default value is ``['uuid', '_product_key_']``
|
||||||
|
which means to lookup by UUID as well as "product key"
|
||||||
|
field, which is configurable. You can include any of the
|
||||||
|
following in ``lookup_fields``:
|
||||||
|
|
||||||
|
* ``uuid``
|
||||||
|
* ``_product_key_`` - :meth:`locate_product_for_key`
|
||||||
|
* ``upc`` - :meth:`locate_product_for_upc`
|
||||||
|
* ``item_id`` - :meth:`locate_product_for_item_id`
|
||||||
|
* ``scancode`` - :meth:`locate_product_for_scancode`
|
||||||
|
* ``vendor_code`` - :meth:`locate_product_for_vendor_code`
|
||||||
|
* ``alt_code`` - :meth:`locate_product_for_alt_code`
|
||||||
|
|
||||||
|
:param include_not_for_sale: Optional flag to include items
|
||||||
|
which are "not for sale" in the search results.
|
||||||
|
|
||||||
|
:returns: First :class:`~rattail.db.model.products.Product`
|
||||||
|
instance found if there was a match; otherwise ``None``.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
if not entry:
|
||||||
|
return
|
||||||
|
|
||||||
|
# figure out which fields we should match on
|
||||||
|
# TODO: let config declare default lookup_fields
|
||||||
|
lookup_fields = kwargs.pop(
|
||||||
|
"lookup_fields",
|
||||||
|
[
|
||||||
|
"uuid",
|
||||||
|
"product_id",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs["include_not_for_sale"] = include_not_for_sale
|
||||||
|
|
||||||
|
# try to locate product by uuid before other, more specific key
|
||||||
|
if "uuid" in lookup_fields:
|
||||||
|
if isinstance(entry, (_uuid.UUID, str)):
|
||||||
|
if product := session.get(model.Product, entry):
|
||||||
|
return product
|
||||||
|
# # TODO: should we ever return deleted product?
|
||||||
|
# if product and not product.deleted:
|
||||||
|
# if include_not_for_sale or not product.not_for_sale:
|
||||||
|
# return product
|
||||||
|
|
||||||
|
lookups = {
|
||||||
|
"uuid": None,
|
||||||
|
"product_id": self.locate_product_for_id,
|
||||||
|
# "upc": self.locate_product_for_upc,
|
||||||
|
# "item_id": self.locate_product_for_item_id,
|
||||||
|
# "scancode": self.locate_product_for_scancode,
|
||||||
|
# "vendor_code": self.locate_product_for_vendor_code,
|
||||||
|
# "alt_code": self.locate_product_for_alt_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
for field in lookup_fields:
|
||||||
|
if field in lookups:
|
||||||
|
lookup = lookups[field]
|
||||||
|
if lookup:
|
||||||
|
product = lookup(session, entry, **kwargs)
|
||||||
|
if product:
|
||||||
|
return product
|
||||||
|
else:
|
||||||
|
log.warning("unknown lookup field: %s", field)
|
||||||
|
|
||||||
|
def locate_product_for_id(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
entry,
|
||||||
|
# include_not_for_sale=False,
|
||||||
|
# include_deleted=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Locate the product which matches the given item ID.
|
||||||
|
|
||||||
|
This will do a lookup on the
|
||||||
|
:attr:`rattail.db.model.products.Product.item_id` field only.
|
||||||
|
|
||||||
|
Note that instead of calling this method directly, you might
|
||||||
|
consider calling :meth:`locate_product_for_key` instead.
|
||||||
|
|
||||||
|
:param session: Current session for Rattail DB.
|
||||||
|
|
||||||
|
:param entry: Item ID value as string.
|
||||||
|
|
||||||
|
:param include_not_for_sale: Optional flag to include items
|
||||||
|
which are "not for sale" in the search results.
|
||||||
|
|
||||||
|
:param include_deleted: Whether "deleted" products should ever
|
||||||
|
match (and be returned).
|
||||||
|
|
||||||
|
:returns: First :class:`~rattail.db.model.products.Product`
|
||||||
|
instance found if there was a match; otherwise ``None``.
|
||||||
|
"""
|
||||||
|
if not entry:
|
||||||
|
return
|
||||||
|
|
||||||
|
# assume entry is string
|
||||||
|
entry = str(entry)
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
products = session.query(model.Product)
|
||||||
|
# if not include_deleted:
|
||||||
|
# products = products.filter(model.Product.deleted == False)
|
||||||
|
# if not include_not_for_sale:
|
||||||
|
# products = products.filter(model.Product.not_for_sale == False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return products.filter(model.Product.product_id == entry).one()
|
||||||
|
except orm.exc.NoResultFound:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search_products(self, session, entry, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform a product search across multiple fields, and return
|
||||||
|
results as JSON data rows.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
final_results = []
|
||||||
|
|
||||||
|
# first we'll attempt "lookup" logic..
|
||||||
|
|
||||||
|
lookup_fields = kwargs.get(
|
||||||
|
"lookup_fields",
|
||||||
|
[
|
||||||
|
# "_product_key_",
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup_fields:
|
||||||
|
product = self.locate_product_for_entry(
|
||||||
|
session, entry, lookup_fields=lookup_fields
|
||||||
|
)
|
||||||
|
if product:
|
||||||
|
final_results.append(product)
|
||||||
|
|
||||||
|
# then we'll attempt "search" logic..
|
||||||
|
|
||||||
|
search_fields = kwargs.get(
|
||||||
|
"search_fields",
|
||||||
|
[
|
||||||
|
"product_id",
|
||||||
|
"brand",
|
||||||
|
"description",
|
||||||
|
"size",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
searches = {
|
||||||
|
"product_id": self.search_products_for_product_id,
|
||||||
|
"brand": self.search_products_for_brand,
|
||||||
|
"description": self.search_products_for_description,
|
||||||
|
"size": self.search_products_for_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
for field in search_fields:
|
||||||
|
if field in searches:
|
||||||
|
search = searches[field]
|
||||||
|
if search:
|
||||||
|
products = search(session, entry, **kwargs)
|
||||||
|
final_results.extend(products)
|
||||||
|
else:
|
||||||
|
log.warning("unknown search field: %s", field)
|
||||||
|
|
||||||
|
return [self.normalize_product(c) for c in final_results]
|
||||||
|
|
||||||
|
def search_products_for_product_id(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
entry,
|
||||||
|
# include_not_for_sale=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Search for products where the
|
||||||
|
:attr:`~rattail.db.model.products.Product.item_id` contains
|
||||||
|
the given value.
|
||||||
|
|
||||||
|
:param entry: Search term.
|
||||||
|
|
||||||
|
:param include_not_for_sale: Optional flag to include items
|
||||||
|
which are "not for sale" in the search results.
|
||||||
|
|
||||||
|
:returns: List of products matching the search.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
entry = entry.lower()
|
||||||
|
|
||||||
|
products = session.query(model.Product).filter(
|
||||||
|
model.Product.product_id.ilike(f"%{entry}%")
|
||||||
|
)
|
||||||
|
|
||||||
|
# if not include_not_for_sale:
|
||||||
|
# products = products.filter(model.Product.not_for_sale == False)
|
||||||
|
|
||||||
|
return products.all()
|
||||||
|
|
||||||
|
def search_products_for_brand(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
entry,
|
||||||
|
# include_not_for_sale=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Search for products where the brand
|
||||||
|
:attr:`~rattail.db.model.products.Brand.name` contains the
|
||||||
|
given value.
|
||||||
|
|
||||||
|
:param entry: Search term.
|
||||||
|
|
||||||
|
:param include_not_for_sale: Optional flag to include items
|
||||||
|
which are "not for sale" in the search results.
|
||||||
|
|
||||||
|
:returns: List of products matching the search.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
entry = entry.lower()
|
||||||
|
|
||||||
|
products = session.query(model.Product).filter(
|
||||||
|
model.Product.brand_name.ilike(f"%{entry}%")
|
||||||
|
)
|
||||||
|
|
||||||
|
# if not include_not_for_sale:
|
||||||
|
# products = products.filter(model.Product.not_for_sale == False)
|
||||||
|
|
||||||
|
return products.all()
|
||||||
|
|
||||||
|
def search_products_for_description(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
entry,
|
||||||
|
# include_not_for_sale=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Search for products where the
|
||||||
|
:attr:`~rattail.db.model.products.Product.description`
|
||||||
|
contains the given value.
|
||||||
|
|
||||||
|
:param entry: Search term.
|
||||||
|
|
||||||
|
:param include_not_for_sale: Optional flag to include items
|
||||||
|
which are "not for sale" in the search results.
|
||||||
|
|
||||||
|
:returns: List of products matching the search.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
entry = entry.lower()
|
||||||
|
|
||||||
|
products = session.query(model.Product).filter(
|
||||||
|
model.Product.description.ilike(f"%{entry}%")
|
||||||
|
)
|
||||||
|
|
||||||
|
# if not include_not_for_sale:
|
||||||
|
# products = products.filter(model.Product.not_for_sale == False)
|
||||||
|
|
||||||
|
return products.all()
|
||||||
|
|
||||||
|
def search_products_for_size(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
entry,
|
||||||
|
# include_not_for_sale=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Search for products where the
|
||||||
|
:attr:`~rattail.db.model.products.Product.size` contains the
|
||||||
|
given value.
|
||||||
|
|
||||||
|
:param entry: Search term.
|
||||||
|
|
||||||
|
:param include_not_for_sale: Optional flag to include items
|
||||||
|
which are "not for sale" in the search results.
|
||||||
|
|
||||||
|
:returns: List of products matching the search.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
entry = entry.lower()
|
||||||
|
|
||||||
|
products = session.query(model.Product).filter(
|
||||||
|
model.Product.size.ilike(f"%{entry}%")
|
||||||
|
)
|
||||||
|
|
||||||
|
# if not include_not_for_sale:
|
||||||
|
# products = products.filter(model.Product.not_for_sale == False)
|
||||||
|
|
||||||
|
return products.all()
|
||||||
|
|
||||||
|
def normalize_product(self, product, fields=None):
|
||||||
|
"""
|
||||||
|
Normalize the given product to a JSON-serializable dict.
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"uuid": product.uuid,
|
||||||
|
"product_id": product.product_id,
|
||||||
|
"description": product.description,
|
||||||
|
"size": product.size,
|
||||||
|
"_str": str(product),
|
||||||
|
}
|
||||||
|
|
||||||
|
if not fields:
|
||||||
|
fields = [
|
||||||
|
"brand_name",
|
||||||
|
"full_description",
|
||||||
|
"department_name",
|
||||||
|
"unit_price_display",
|
||||||
|
]
|
||||||
|
|
||||||
|
if "brand_name" in fields:
|
||||||
|
data["brand_name"] = product.brand_name
|
||||||
|
|
||||||
|
if "full_description" in fields:
|
||||||
|
data["full_description"] = product.full_description
|
||||||
|
|
||||||
|
if "department_name" in fields:
|
||||||
|
data["department_name"] = (
|
||||||
|
product.department.name if product.department else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if "unit_price_display" in fields:
|
||||||
|
data["unit_price_display"] = self.app.render_currency(
|
||||||
|
product.unit_price_reg
|
||||||
|
)
|
||||||
|
|
||||||
|
# if "vendor_name" in fields:
|
||||||
|
# vendor = product.cost.vendor if product.cost else None
|
||||||
|
# data["vendor_name"] = vendor.name if vendor else None
|
||||||
|
|
||||||
|
# if "costs" in fields:
|
||||||
|
# costs = []
|
||||||
|
# for cost in product.costs:
|
||||||
|
# costs.append(
|
||||||
|
# {
|
||||||
|
# "uuid": cost.uuid,
|
||||||
|
# "vendor_uuid": cost.vendor_uuid,
|
||||||
|
# "vendor_name": cost.vendor.name,
|
||||||
|
# "preference": cost.preference,
|
||||||
|
# "code": cost.code,
|
||||||
|
# "case_size": cost.case_size,
|
||||||
|
# "case_cost": cost.case_cost,
|
||||||
|
# "unit_cost": cost.unit_cost,
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
# data["costs"] = costs
|
||||||
|
|
||||||
|
# current_price = None
|
||||||
|
# if not product.not_for_sale:
|
||||||
|
|
||||||
|
# margin_fields = [
|
||||||
|
# "true_margin",
|
||||||
|
# "true_margin_display",
|
||||||
|
# ]
|
||||||
|
# if any([f in fields for f in margin_fields]):
|
||||||
|
# if product.volatile:
|
||||||
|
# data["true_margin"] = product.volatile.true_margin
|
||||||
|
# data["true_margin_display"] = self.app.render_percent(
|
||||||
|
# product.volatile.true_margin, places=2
|
||||||
|
# )
|
||||||
|
|
||||||
|
# current_fields = [
|
||||||
|
# "current_price",
|
||||||
|
# "current_price_display",
|
||||||
|
# "current_ends",
|
||||||
|
# "current_ends_display",
|
||||||
|
# ]
|
||||||
|
# if any([f in fields for f in current_fields]):
|
||||||
|
# current_price = product.current_price
|
||||||
|
# if current_price:
|
||||||
|
# if current_price.price:
|
||||||
|
# data["current_price"] = float(current_price.price)
|
||||||
|
# data["current_price_display"] = self.render_price(current_price)
|
||||||
|
# current_ends = current_price.ends
|
||||||
|
# if current_ends:
|
||||||
|
# current_ends = self.app.localtime(
|
||||||
|
# current_ends, from_utc=True
|
||||||
|
# ).date()
|
||||||
|
# data["current_ends"] = str(current_ends)
|
||||||
|
# data["current_ends_display"] = self.app.render_date(
|
||||||
|
# current_ends
|
||||||
|
# )
|
||||||
|
|
||||||
|
# sale_fields = [
|
||||||
|
# "sale_price",
|
||||||
|
# "sale_price_display",
|
||||||
|
# "sale_ends",
|
||||||
|
# "sale_ends_display",
|
||||||
|
# ]
|
||||||
|
# if any([f in fields for f in sale_fields]):
|
||||||
|
# sale_price = product.sale_price
|
||||||
|
# if sale_price:
|
||||||
|
# if sale_price.price:
|
||||||
|
# data["sale_price"] = float(sale_price.price)
|
||||||
|
# data["sale_price_display"] = self.render_price(sale_price)
|
||||||
|
# sale_ends = sale_price.ends
|
||||||
|
# if sale_ends:
|
||||||
|
# sale_ends = self.app.localtime(sale_ends, from_utc=True).date()
|
||||||
|
# data["sale_ends"] = str(sale_ends)
|
||||||
|
# data["sale_ends_display"] = self.app.render_date(sale_ends)
|
||||||
|
|
||||||
|
# tpr_fields = [
|
||||||
|
# "tpr_price",
|
||||||
|
# "tpr_price_display",
|
||||||
|
# "tpr_ends",
|
||||||
|
# "tpr_ends_display",
|
||||||
|
# ]
|
||||||
|
# if any([f in fields for f in tpr_fields]):
|
||||||
|
# tpr_price = product.tpr_price
|
||||||
|
# if tpr_price:
|
||||||
|
# if tpr_price.price:
|
||||||
|
# data["tpr_price"] = float(tpr_price.price)
|
||||||
|
# data["tpr_price_display"] = self.render_price(tpr_price)
|
||||||
|
# tpr_ends = tpr_price.ends
|
||||||
|
# if tpr_ends:
|
||||||
|
# tpr_ends = self.app.localtime(tpr_ends, from_utc=True).date()
|
||||||
|
# data["tpr_ends"] = str(tpr_ends)
|
||||||
|
# data["tpr_ends_display"] = self.app.render_date(tpr_ends)
|
||||||
|
|
||||||
|
if "case_size" in fields:
|
||||||
|
data["case_size"] = self.app.render_quantity(self.get_case_size(product))
|
||||||
|
|
||||||
|
# if "case_price" in fields or "case_price_display" in fields:
|
||||||
|
# case_price = None
|
||||||
|
# if product.regular_price and product.regular_price is not None:
|
||||||
|
# case_size = self.get_case_size(product)
|
||||||
|
# # use "current" price if there is one, else normal unit price
|
||||||
|
# unit_price = product.regular_price.price
|
||||||
|
# if current_price:
|
||||||
|
# unit_price = current_price.price
|
||||||
|
# case_price = (case_size or 1) * unit_price
|
||||||
|
# case_price = case_price.quantize(decimal.Decimal("0.01"))
|
||||||
|
# data["case_price"] = str(case_price) if case_price is not None else None
|
||||||
|
# data["case_price_display"] = self.app.render_currency(case_price)
|
||||||
|
|
||||||
|
# if "uom_choices" in fields:
|
||||||
|
# data["uom_choices"] = self.get_uom_choices(product)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_case_size(self, product):
|
||||||
|
"""
|
||||||
|
Return the effective case size for the given product.
|
||||||
|
"""
|
||||||
|
if product.case_size is not None:
|
||||||
|
return product.case_size
|
||||||
|
|
||||||
|
# cost = product.cost
|
||||||
|
# if cost:
|
||||||
|
# return cost.case_size
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
@ -27,6 +27,50 @@ Form schema types
|
||||||
from wuttaweb.forms.schema import ObjectRef
|
from wuttaweb.forms.schema import ObjectRef
|
||||||
|
|
||||||
|
|
||||||
|
class StoreRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Schema type for a
|
||||||
|
:class:`~wuttapos.db.model.stores.Store` reference field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
return model.Store
|
||||||
|
|
||||||
|
def sort_query(self, query):
|
||||||
|
return query.order_by(self.model_class.name)
|
||||||
|
|
||||||
|
def get_object_url(self, obj):
|
||||||
|
store = obj
|
||||||
|
return self.request.route_url("stores.view", uuid=store.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Schema type for a
|
||||||
|
:class:`~wuttapos.db.model.terminals.Terminal` reference field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
return model.Terminal
|
||||||
|
|
||||||
|
def sort_query(self, query):
|
||||||
|
return query.order_by(self.model_class.name)
|
||||||
|
|
||||||
|
def get_object_url(self, obj):
|
||||||
|
terminal = obj
|
||||||
|
return self.request.route_url("terminals.view", uuid=terminal.uuid)
|
||||||
|
|
||||||
|
|
||||||
class DepartmentRef(ObjectRef):
|
class DepartmentRef(ObjectRef):
|
||||||
"""
|
"""
|
||||||
Schema type for a
|
Schema type for a
|
||||||
|
|
@ -49,6 +93,50 @@ class DepartmentRef(ObjectRef):
|
||||||
return self.request.route_url("departments.view", uuid=department.uuid)
|
return self.request.route_url("departments.view", uuid=department.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Schema type for a
|
||||||
|
:class:`~wuttapos.db.model.employees.Employee` reference field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
return model.Employee
|
||||||
|
|
||||||
|
def sort_query(self, query):
|
||||||
|
return query.order_by(self.model_class.name)
|
||||||
|
|
||||||
|
def get_object_url(self, obj):
|
||||||
|
employee = obj
|
||||||
|
return self.request.route_url("employees.view", uuid=employee.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Schema type for a
|
||||||
|
:class:`~wuttapos.db.model.customers.Customer` reference field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
return model.Customer
|
||||||
|
|
||||||
|
def sort_query(self, query):
|
||||||
|
return query.order_by(self.model_class.name)
|
||||||
|
|
||||||
|
def get_object_url(self, obj):
|
||||||
|
customer = obj
|
||||||
|
return self.request.route_url("customers.view", uuid=customer.uuid)
|
||||||
|
|
||||||
|
|
||||||
class ProductRef(ObjectRef):
|
class ProductRef(ObjectRef):
|
||||||
"""
|
"""
|
||||||
Schema type for a
|
Schema type for a
|
||||||
|
|
|
||||||
|
|
@ -33,19 +33,16 @@ class WuttaPosMenuHandler(base.MenuHandler):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def make_menus(self, request, **kwargs):
|
def make_menus(self, request, **kwargs):
|
||||||
|
|
||||||
# nb. the products menu is just an example; you should
|
|
||||||
# replace it and add more as needed
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
self.make_customers_menu(request),
|
self.make_people_menu(request),
|
||||||
self.make_products_menu(request),
|
self.make_products_menu(request),
|
||||||
self.make_admin_menu(request, include_people=True),
|
self.make_batches_menu(request),
|
||||||
|
self.make_admin_menu(request),
|
||||||
]
|
]
|
||||||
|
|
||||||
def make_customers_menu(self, request):
|
def make_people_menu(self, request):
|
||||||
return {
|
return {
|
||||||
"title": "Customers",
|
"title": "People",
|
||||||
"type": "menu",
|
"type": "menu",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -53,6 +50,16 @@ class WuttaPosMenuHandler(base.MenuHandler):
|
||||||
"route": "customers",
|
"route": "customers",
|
||||||
"perm": "customers.list",
|
"perm": "customers.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Employees",
|
||||||
|
"route": "employees",
|
||||||
|
"perm": "employees.list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "All People",
|
||||||
|
"route": "people",
|
||||||
|
"perm": "people.list",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,3 +102,46 @@ class WuttaPosMenuHandler(base.MenuHandler):
|
||||||
# },
|
# },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def make_batches_menu(self, request):
|
||||||
|
return {
|
||||||
|
"title": "Batches",
|
||||||
|
"type": "menu",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "POS",
|
||||||
|
"route": "batch.pos",
|
||||||
|
"perm": "batch.pos.list",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_admin_menu(self, request, **kwargs):
|
||||||
|
kwargs.setdefault("include_people", False)
|
||||||
|
menu = super().make_admin_menu(request, **kwargs)
|
||||||
|
|
||||||
|
menu["items"] = [
|
||||||
|
{
|
||||||
|
"title": "Stores",
|
||||||
|
"route": "stores",
|
||||||
|
"perm": "stores.list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Terminals",
|
||||||
|
"route": "terminals",
|
||||||
|
"perm": "terminals.list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tenders",
|
||||||
|
"route": "tenders",
|
||||||
|
"perm": "tenders.list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Taxes",
|
||||||
|
"route": "taxes",
|
||||||
|
"perm": "taxes.list",
|
||||||
|
},
|
||||||
|
{"type": "sep"},
|
||||||
|
] + menu["items"]
|
||||||
|
|
||||||
|
return menu
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,16 @@ def includeme(config):
|
||||||
config.include("wuttaweb.views.essential")
|
config.include("wuttaweb.views.essential")
|
||||||
|
|
||||||
# wuttapos
|
# wuttapos
|
||||||
|
config.include("wuttapos.server.views.stores")
|
||||||
|
config.include("wuttapos.server.views.terminals")
|
||||||
|
config.include("wuttapos.server.views.employees")
|
||||||
|
config.include("wuttapos.server.views.tenders")
|
||||||
|
config.include("wuttapos.server.views.taxes")
|
||||||
config.include("wuttapos.server.views.departments")
|
config.include("wuttapos.server.views.departments")
|
||||||
config.include("wuttapos.server.views.products")
|
config.include("wuttapos.server.views.products")
|
||||||
config.include("wuttapos.server.views.inventory_adjustments")
|
config.include("wuttapos.server.views.inventory_adjustments")
|
||||||
config.include("wuttapos.server.views.customers")
|
config.include("wuttapos.server.views.customers")
|
||||||
|
config.include("wuttapos.server.views.batch.pos")
|
||||||
|
|
||||||
# TODO: these should probably live elsewhere?
|
# TODO: these should probably live elsewhere?
|
||||||
config.add_wutta_permission_group("pos", "POS", overwrite=False)
|
config.add_wutta_permission_group("pos", "POS", overwrite=False)
|
||||||
|
|
|
||||||
0
src/wuttapos/server/views/batch/__init__.py
Normal file
0
src/wuttapos/server/views/batch/__init__.py
Normal file
161
src/wuttapos/server/views/batch/pos.py
Normal file
161
src/wuttapos/server/views/batch/pos.py
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# WuttaPOS -- Point of Sale system based on Wutta Framework
|
||||||
|
# Copyright © 2026 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Master view for POS Batches
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttaweb.views.batch import BatchMasterView
|
||||||
|
|
||||||
|
from wuttapos.db.model import POSBatch
|
||||||
|
from wuttapos.server.forms.schema import StoreRef, TerminalRef, EmployeeRef, CustomerRef
|
||||||
|
|
||||||
|
|
||||||
|
class POSBatchView(BatchMasterView):
|
||||||
|
"""
|
||||||
|
Master view for POS Batches
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = POSBatch
|
||||||
|
|
||||||
|
route_prefix = "batch.pos"
|
||||||
|
url_prefix = "/batch/pos"
|
||||||
|
|
||||||
|
creatable = False
|
||||||
|
editable = False
|
||||||
|
# nb. allow delete for now, at least is useful in dev?
|
||||||
|
deletable = True
|
||||||
|
executable = False
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"terminal_id": "Terminal ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"id",
|
||||||
|
"created",
|
||||||
|
"store",
|
||||||
|
"terminal",
|
||||||
|
"cashier",
|
||||||
|
"customer",
|
||||||
|
"row_count",
|
||||||
|
"sales_total",
|
||||||
|
"void",
|
||||||
|
"training_mode",
|
||||||
|
"status_code",
|
||||||
|
"executed",
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"id",
|
||||||
|
"terminal",
|
||||||
|
"cashier",
|
||||||
|
"customer",
|
||||||
|
"customer_is_member",
|
||||||
|
"customer_is_employee",
|
||||||
|
"params",
|
||||||
|
"row_count",
|
||||||
|
"sales_total",
|
||||||
|
# 'taxes',
|
||||||
|
"tender_total",
|
||||||
|
"fs_tender_total",
|
||||||
|
"balance",
|
||||||
|
"void",
|
||||||
|
"training_mode",
|
||||||
|
"status_code",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"executed",
|
||||||
|
"executed_by",
|
||||||
|
]
|
||||||
|
|
||||||
|
filter_defaults = {
|
||||||
|
"executed": {"active": True, "verb": "is_null"},
|
||||||
|
}
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
"sequence",
|
||||||
|
"row_type",
|
||||||
|
"item_entry",
|
||||||
|
"description",
|
||||||
|
"product",
|
||||||
|
"reg_price",
|
||||||
|
"txn_price",
|
||||||
|
"quantity",
|
||||||
|
"sales_total",
|
||||||
|
"tender_total",
|
||||||
|
"tax_code",
|
||||||
|
"modified_by",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_batch_handler(self):
|
||||||
|
"""
|
||||||
|
Must return the :term:`batch handler` for use with this view.
|
||||||
|
|
||||||
|
There is no default logic; subclass must override.
|
||||||
|
"""
|
||||||
|
spec = "wuttapos.batch.pos:POSBatchHandler"
|
||||||
|
factory = self.app.load_object(spec)
|
||||||
|
return factory(self.config)
|
||||||
|
|
||||||
|
def configure_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_grid(g)
|
||||||
|
|
||||||
|
# links
|
||||||
|
g.set_link("terminal")
|
||||||
|
g.set_link("cashier")
|
||||||
|
g.set_link("customer")
|
||||||
|
|
||||||
|
def grid_row_class(self, batch, data, i):
|
||||||
|
if batch.training_mode:
|
||||||
|
return "has-background-warning"
|
||||||
|
if batch.void:
|
||||||
|
return "has-background-danger-light"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def configure_form(self, form):
|
||||||
|
f = form
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# store
|
||||||
|
f.set_node("store", StoreRef(self.request))
|
||||||
|
|
||||||
|
# terminal
|
||||||
|
f.set_node("terminal", TerminalRef(self.request))
|
||||||
|
|
||||||
|
# cashier
|
||||||
|
f.set_node("cashier", EmployeeRef(self.request))
|
||||||
|
|
||||||
|
# customer
|
||||||
|
f.set_node("customer", CustomerRef(self.request))
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
POSBatchView = kwargs.get("POSBatchView", base["POSBatchView"])
|
||||||
|
POSBatchView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
58
src/wuttapos/server/views/employees.py
Normal file
58
src/wuttapos/server/views/employees.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Master view for Employees
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttaweb.views import MasterView
|
||||||
|
from wuttaweb.forms.schema import PersonRef
|
||||||
|
|
||||||
|
from wuttapos.db.model import Employee
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Employees
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = Employee
|
||||||
|
model_title = "Employee"
|
||||||
|
model_title_plural = "Employees"
|
||||||
|
|
||||||
|
route_prefix = "employees"
|
||||||
|
url_prefix = "/employees"
|
||||||
|
|
||||||
|
creatable = True
|
||||||
|
editable = True
|
||||||
|
deletable = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"person",
|
||||||
|
"name",
|
||||||
|
"public_name",
|
||||||
|
"active",
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"person",
|
||||||
|
"name",
|
||||||
|
"public_name",
|
||||||
|
"active",
|
||||||
|
]
|
||||||
|
|
||||||
|
def configure_form(self, form):
|
||||||
|
f = form
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# person
|
||||||
|
f.set_node("person", PersonRef(self.request))
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
EmployeeView = kwargs.get("EmployeeView", base["EmployeeView"])
|
||||||
|
EmployeeView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
52
src/wuttapos/server/views/stores.py
Normal file
52
src/wuttapos/server/views/stores.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Master view for Stores
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttapos.db.model.stores import Store
|
||||||
|
|
||||||
|
from wuttaweb.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
class StoreView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Stores
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = Store
|
||||||
|
model_title = "Store"
|
||||||
|
model_title_plural = "Stores"
|
||||||
|
|
||||||
|
route_prefix = "stores"
|
||||||
|
url_prefix = "/stores"
|
||||||
|
|
||||||
|
creatable = True
|
||||||
|
editable = True
|
||||||
|
deletable = True
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"store_id": "Store ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"store_id",
|
||||||
|
"name",
|
||||||
|
"active",
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"store_id",
|
||||||
|
"name",
|
||||||
|
"active",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
StoreView = kwargs.get("StoreView", base["StoreView"])
|
||||||
|
StoreView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
52
src/wuttapos/server/views/taxes.py
Normal file
52
src/wuttapos/server/views/taxes.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Master view for Taxes
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttapos.db.model.taxes import Tax
|
||||||
|
|
||||||
|
from wuttaweb.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
class TaxView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Taxes
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = Tax
|
||||||
|
model_title = "Tax"
|
||||||
|
model_title_plural = "Taxes"
|
||||||
|
|
||||||
|
route_prefix = "taxes"
|
||||||
|
url_prefix = "/taxes"
|
||||||
|
|
||||||
|
creatable = True
|
||||||
|
editable = True
|
||||||
|
deletable = True
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"tax_id": "Tax ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"tax_id",
|
||||||
|
"name",
|
||||||
|
"rate",
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"tax_id",
|
||||||
|
"name",
|
||||||
|
"rate",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
TaxView = kwargs.get("TaxView", base["TaxView"])
|
||||||
|
TaxView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
76
src/wuttapos/server/views/tenders.py
Normal file
76
src/wuttapos/server/views/tenders.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Master view for Tenders
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttapos.db.model.tenders import Tender
|
||||||
|
|
||||||
|
from wuttaweb.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
class TenderView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Tenders
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = Tender
|
||||||
|
model_title = "Tender"
|
||||||
|
model_title_plural = "Tenders"
|
||||||
|
|
||||||
|
route_prefix = "tenders"
|
||||||
|
url_prefix = "/tenders"
|
||||||
|
|
||||||
|
creatable = True
|
||||||
|
editable = True
|
||||||
|
deletable = True
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"tender_id": "Tender ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"tender_id",
|
||||||
|
"name",
|
||||||
|
"is_cash",
|
||||||
|
"is_foodstamp",
|
||||||
|
"allow_cashback",
|
||||||
|
"kick_drawer",
|
||||||
|
"active",
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"tender_id",
|
||||||
|
"name",
|
||||||
|
"notes",
|
||||||
|
"is_cash",
|
||||||
|
"is_foodstamp",
|
||||||
|
"allow_cashback",
|
||||||
|
"kick_drawer",
|
||||||
|
"active",
|
||||||
|
]
|
||||||
|
|
||||||
|
def configure_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_grid(g)
|
||||||
|
|
||||||
|
# links
|
||||||
|
g.set_link("tender_id")
|
||||||
|
g.set_link("name")
|
||||||
|
|
||||||
|
def configure_form(self, form):
|
||||||
|
f = form
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# notes
|
||||||
|
f.set_widget("notes", "notes")
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
TenderView = kwargs.get("TenderView", base["TenderView"])
|
||||||
|
TenderView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
50
src/wuttapos/server/views/terminals.py
Normal file
50
src/wuttapos/server/views/terminals.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""
|
||||||
|
Master view for Terminals
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttapos.db.model.terminals import Terminal
|
||||||
|
|
||||||
|
from wuttaweb.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Terminals
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = Terminal
|
||||||
|
model_title = "Terminal"
|
||||||
|
model_title_plural = "Terminals"
|
||||||
|
|
||||||
|
route_prefix = "terminals"
|
||||||
|
url_prefix = "/terminals"
|
||||||
|
|
||||||
|
creatable = True
|
||||||
|
editable = True
|
||||||
|
deletable = True
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"terminal_id": "Terminal ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"terminal_id",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"terminal_id",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
TerminalView = kwargs.get("TerminalView", base["TerminalView"])
|
||||||
|
TerminalView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
@ -38,6 +38,7 @@ import flet as ft
|
||||||
|
|
||||||
import wuttapos
|
import wuttapos
|
||||||
from wuttapos.terminal.controls.buttons import make_button
|
from wuttapos.terminal.controls.buttons import make_button
|
||||||
|
from wuttapos.terminal.util import get_pos_batch_handler
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -47,7 +48,7 @@ def main(page: ft.Page):
|
||||||
config = make_config()
|
config = make_config()
|
||||||
app = config.get_app()
|
app = config.get_app()
|
||||||
model = app.model
|
model = app.model
|
||||||
handler = app.get_transaction_handler()
|
handler = get_pos_batch_handler(config)
|
||||||
|
|
||||||
# nb. as of python 3.10 the original hook is accessible, we needn't save the ref
|
# nb. as of python 3.10 the original hook is accessible, we needn't save the ref
|
||||||
# cf. https://docs.python.org/3/library/threading.html#threading.__excepthook__
|
# cf. https://docs.python.org/3/library/threading.html#threading.__excepthook__
|
||||||
|
|
@ -264,11 +265,10 @@ def main(page: ft.Page):
|
||||||
page.session.set("user_uuid", user.uuid.hex)
|
page.session.set("user_uuid", user.uuid.hex)
|
||||||
page.session.set("user_display", str(user))
|
page.session.set("user_display", str(user))
|
||||||
|
|
||||||
txn = handler.get_current_transaction(user, create=False)
|
if batch := handler.get_current_batch(user, create=False):
|
||||||
if txn:
|
page.session.set("txn_display", batch.id_str)
|
||||||
page.session.set("txn_display", handler.get_screen_txn_display(txn))
|
if batch.customer:
|
||||||
if txn["customer_id"]:
|
page.session.set("cust_uuid", batch.customer.uuid)
|
||||||
page.session.set("cust_uuid", txn["customer_id"])
|
|
||||||
page.session.set(
|
page.session.set(
|
||||||
"cust_display", handler.get_screen_cust_display(txn=txn)
|
"cust_display", handler.get_screen_cust_display(txn=txn)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class WuttaCustomerLookup(WuttaLookup):
|
||||||
|
|
||||||
def get_results_columns(self):
|
def get_results_columns(self):
|
||||||
return [
|
return [
|
||||||
self.app.get_customer_key_label(),
|
"Customer ID",
|
||||||
"Name",
|
"Name",
|
||||||
"Phone",
|
"Phone",
|
||||||
"Email",
|
"Email",
|
||||||
|
|
@ -42,7 +42,7 @@ class WuttaCustomerLookup(WuttaLookup):
|
||||||
|
|
||||||
def make_result_row(self, customer):
|
def make_result_row(self, customer):
|
||||||
return [
|
return [
|
||||||
customer["_customer_key_"],
|
customer["customer_id"],
|
||||||
customer["name"],
|
customer["name"],
|
||||||
customer["phone_number"],
|
customer["phone_number"],
|
||||||
customer["email_address"],
|
customer["email_address"],
|
||||||
|
|
|
||||||
|
|
@ -39,38 +39,30 @@ class WuttaDepartmentLookup(WuttaLookup):
|
||||||
|
|
||||||
def get_results_columns(self):
|
def get_results_columns(self):
|
||||||
return [
|
return [
|
||||||
"Number",
|
"Department ID",
|
||||||
"Name",
|
"Name",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_results(self, session, entry):
|
def get_results(self, session, entry):
|
||||||
corepos = self.app.get_corepos_handler()
|
model = self.app.model
|
||||||
op_model = corepos.get_model_lane_op()
|
query = session.query(model.Department).order_by(model.Department.name)
|
||||||
op_session = corepos.make_session_lane_op()
|
|
||||||
|
|
||||||
query = op_session.query(op_model.Department).order_by(
|
|
||||||
op_model.Department.number
|
|
||||||
)
|
|
||||||
|
|
||||||
if entry:
|
if entry:
|
||||||
query = query.filter(op_model.Department.name.ilike(f"%{entry}%"))
|
query = query.filter(model.Department.name.ilike(f"%{entry}%"))
|
||||||
|
|
||||||
departments = []
|
departments = []
|
||||||
for dept in query:
|
for dept in query:
|
||||||
departments.append(
|
departments.append(
|
||||||
{
|
{
|
||||||
# TODO
|
"uuid": dept.uuid,
|
||||||
# 'uuid': dept.uuid,
|
"department_id": dept.department_id,
|
||||||
"uuid": str(dept.number),
|
|
||||||
"number": str(dept.number),
|
|
||||||
"name": dept.name,
|
"name": dept.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
op_session.close()
|
|
||||||
return departments
|
return departments
|
||||||
|
|
||||||
def make_result_row(self, dept):
|
def make_result_row(self, dept):
|
||||||
return [
|
return [
|
||||||
dept["number"],
|
dept["department_id"],
|
||||||
dept["name"],
|
dept["name"],
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class WuttaProductLookup(WuttaLookup):
|
||||||
|
|
||||||
def get_results_columns(self):
|
def get_results_columns(self):
|
||||||
return [
|
return [
|
||||||
self.app.get_product_key_label(),
|
"Product ID",
|
||||||
"Description",
|
"Description",
|
||||||
"Price",
|
"Price",
|
||||||
]
|
]
|
||||||
|
|
@ -41,7 +41,7 @@ class WuttaProductLookup(WuttaLookup):
|
||||||
|
|
||||||
def make_result_row(self, product):
|
def make_result_row(self, product):
|
||||||
return [
|
return [
|
||||||
product["product_key"],
|
product["product_id"],
|
||||||
product["full_description"],
|
product["full_description"],
|
||||||
product["unit_price_display"],
|
product["unit_price_display"],
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -35,35 +35,28 @@ class WuttaTxnItem(ft.Row):
|
||||||
|
|
||||||
font_size = 24
|
font_size = 24
|
||||||
|
|
||||||
def __init__(self, config, line, page=None, *args, **kwargs):
|
def __init__(self, config, row, page=None, *args, **kwargs):
|
||||||
self.on_reset = kwargs.pop("on_reset", None)
|
self.on_reset = kwargs.pop("on_reset", None)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.app = config.get_app()
|
self.app = config.get_app()
|
||||||
self.enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
|
||||||
self.line = line
|
self.row = row
|
||||||
|
|
||||||
self.major_style = ft.TextStyle(size=self.font_size, weight=ft.FontWeight.BOLD)
|
self.major_style = ft.TextStyle(size=self.font_size, weight=ft.FontWeight.BOLD)
|
||||||
|
|
||||||
self.minor_style = ft.TextStyle(size=int(self.font_size * 0.8), italic=True)
|
self.minor_style = ft.TextStyle(size=int(self.font_size * 0.8), italic=True)
|
||||||
|
|
||||||
# if self.row.row_type in (self.enum.POS_ROW_TYPE_SELL,
|
if self.row.row_type in (enum.POS_ROW_TYPE_SELL, enum.POS_ROW_TYPE_OPEN_RING):
|
||||||
# self.enum.POS_ROW_TYPE_OPEN_RING):
|
self.build_item_sell()
|
||||||
# self.build_item_sell()
|
|
||||||
|
|
||||||
# elif self.row.row_type in (self.enum.POS_ROW_TYPE_TENDER,
|
# elif self.row.row_type in (self.enum.POS_ROW_TYPE_TENDER,
|
||||||
# self.enum.POS_ROW_TYPE_CHANGE_BACK):
|
# self.enum.POS_ROW_TYPE_CHANGE_BACK):
|
||||||
# self.build_item_tender()
|
# self.build_item_tender()
|
||||||
|
|
||||||
if self.line.trans_type in ("I",):
|
|
||||||
self.build_item_sell()
|
|
||||||
|
|
||||||
elif self.line.trans_type in ("T",):
|
|
||||||
self.build_item_tender()
|
|
||||||
|
|
||||||
def build_item_sell(self):
|
def build_item_sell(self):
|
||||||
|
|
||||||
self.quantity = ft.TextSpan(style=self.minor_style)
|
self.quantity = ft.TextSpan(style=self.minor_style)
|
||||||
|
|
@ -84,7 +77,7 @@ class WuttaTxnItem(ft.Row):
|
||||||
self.controls = [
|
self.controls = [
|
||||||
ft.Text(
|
ft.Text(
|
||||||
spans=[
|
spans=[
|
||||||
ft.TextSpan(f"{self.line.description}", style=self.major_style),
|
ft.TextSpan(f"{self.row.description}", style=self.major_style),
|
||||||
ft.TextSpan("× ", style=self.minor_style),
|
ft.TextSpan("× ", style=self.minor_style),
|
||||||
self.quantity,
|
self.quantity,
|
||||||
ft.TextSpan(" @ ", style=self.minor_style),
|
ft.TextSpan(" @ ", style=self.minor_style),
|
||||||
|
|
@ -105,13 +98,13 @@ class WuttaTxnItem(ft.Row):
|
||||||
self.controls = [
|
self.controls = [
|
||||||
ft.Text(
|
ft.Text(
|
||||||
spans=[
|
spans=[
|
||||||
ft.TextSpan(f"{self.line.description}", style=self.major_style),
|
ft.TextSpan(f"{self.row.description}", style=self.major_style),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ft.Text(
|
ft.Text(
|
||||||
spans=[
|
spans=[
|
||||||
ft.TextSpan(
|
ft.TextSpan(
|
||||||
self.app.render_currency(self.line.total),
|
self.app.render_currency(self.row.tender_total),
|
||||||
style=self.major_style,
|
style=self.major_style,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -127,9 +120,9 @@ class WuttaTxnItem(ft.Row):
|
||||||
self.on_reset(e=e)
|
self.on_reset(e=e)
|
||||||
|
|
||||||
def refresh(self, update=True):
|
def refresh(self, update=True):
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
# if self.row.void:
|
if self.row.void:
|
||||||
if self.line.voided:
|
|
||||||
self.major_style.color = None
|
self.major_style.color = None
|
||||||
self.major_style.decoration = ft.TextDecoration.LINE_THROUGH
|
self.major_style.decoration = ft.TextDecoration.LINE_THROUGH
|
||||||
self.major_style.weight = None
|
self.major_style.weight = None
|
||||||
|
|
@ -142,30 +135,31 @@ class WuttaTxnItem(ft.Row):
|
||||||
self.minor_style.color = None
|
self.minor_style.color = None
|
||||||
self.minor_style.decoration = None
|
self.minor_style.decoration = None
|
||||||
|
|
||||||
# if self.row.row_type in (self.enum.POS_ROW_TYPE_SELL,
|
if self.row.row_type in (enum.POS_ROW_TYPE_SELL, enum.POS_ROW_TYPE_OPEN_RING):
|
||||||
# self.enum.POS_ROW_TYPE_OPEN_RING):
|
self.quantity.text = self.app.render_quantity(self.row.quantity)
|
||||||
if self.line.trans_type in ("I",):
|
self.txn_price.text = self.app.render_currency(self.row.txn_price)
|
||||||
self.quantity.text = self.app.render_quantity(self.line.ItemQtty)
|
self.sales_total.text = self.app.render_currency(self.row.sales_total)
|
||||||
self.txn_price.text = self.app.render_currency(self.line.unitPrice)
|
self.fs_flag.text = "FS " if self.row.foodstamp_eligible else ""
|
||||||
self.sales_total.text = self.app.render_currency(self.line.total)
|
self.tax_flag.text = f"T{self.row.tax_code} " if self.row.tax_code else ""
|
||||||
self.fs_flag.text = "FS " if self.line.foodstamp else ""
|
|
||||||
self.tax_flag.text = f"T{self.line.tax} " if self.line.tax else ""
|
|
||||||
|
|
||||||
if self.line.voided:
|
# if self.line.voided:
|
||||||
|
if self.row.void:
|
||||||
self.sales_total_style.color = None
|
self.sales_total_style.color = None
|
||||||
self.sales_total_style.decoration = ft.TextDecoration.LINE_THROUGH
|
self.sales_total_style.decoration = ft.TextDecoration.LINE_THROUGH
|
||||||
self.sales_total_style.weight = None
|
self.sales_total_style.weight = None
|
||||||
else:
|
else:
|
||||||
# if (self.row.row_type == self.enum.POS_ROW_TYPE_SELL
|
if (
|
||||||
# and self.row.txn_price_adjusted):
|
self.row.row_type == enum.POS_ROW_TYPE_SELL
|
||||||
# self.sales_total_style.color = 'orange'
|
and self.row.txn_price_adjusted
|
||||||
# elif (self.row.row_type == self.enum.POS_ROW_TYPE_SELL
|
):
|
||||||
# and self.row.cur_price and self.row.cur_price < self.row.reg_price):
|
self.sales_total_style.color = "orange"
|
||||||
# self.sales_total_style.color = 'green'
|
elif (
|
||||||
# else:
|
self.row.row_type == enum.POS_ROW_TYPE_SELL
|
||||||
# self.sales_total_style.color = None
|
and self.row.cur_price
|
||||||
|
and self.row.cur_price < self.row.reg_price
|
||||||
# TODO
|
):
|
||||||
|
self.sales_total_style.color = "green"
|
||||||
|
else:
|
||||||
self.sales_total_style.color = None
|
self.sales_total_style.color = None
|
||||||
|
|
||||||
self.sales_total_style.decoration = None
|
self.sales_total_style.decoration = None
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class WuttaTransactionLookup(WuttaLookup):
|
||||||
|
|
||||||
def get_results_columns(self):
|
def get_results_columns(self):
|
||||||
return [
|
return [
|
||||||
"Date/Time",
|
"Created",
|
||||||
"Terminal",
|
"Terminal",
|
||||||
"Txn ID",
|
"Txn ID",
|
||||||
"Cashier",
|
"Cashier",
|
||||||
|
|
@ -56,39 +56,37 @@ class WuttaTransactionLookup(WuttaLookup):
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_results(self, session, entry):
|
def get_results(self, session, entry):
|
||||||
# model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# # TODO: how to deal with 'modes'
|
# TODO: how to deal with 'modes'
|
||||||
# assert self.mode == 'resume'
|
assert self.mode == "resume"
|
||||||
# training = bool(self.mypage.session.get('training'))
|
training = bool(self.mypage.session.get("training"))
|
||||||
# query = session.query(model.POSBatch)\
|
query = (
|
||||||
# .filter(model.POSBatch.status_code == model.POSBatch.STATUS_SUSPENDED)\
|
session.query(model.POSBatch)
|
||||||
# .filter(model.POSBatch.executed == None)\
|
.filter(model.POSBatch.status_code == model.POSBatch.STATUS_SUSPENDED)
|
||||||
# .filter(model.POSBatch.training_mode == training)\
|
.filter(model.POSBatch.executed == None)
|
||||||
# .order_by(model.POSBatch.created.desc())
|
.filter(model.POSBatch.training_mode == training)
|
||||||
|
.order_by(model.POSBatch.created.desc())
|
||||||
|
)
|
||||||
|
|
||||||
# transactions = []
|
transactions = []
|
||||||
# for batch in query:
|
for batch in query:
|
||||||
# # TODO: should use 'suspended' timestamp instead here?
|
transactions.append(
|
||||||
# # dt = self.app.localtime(batch.created, from_utc=True)
|
{
|
||||||
# dt = batch.created
|
"uuid": batch.uuid,
|
||||||
# transactions.append({
|
"created": self.app.render_datetime(batch.created),
|
||||||
# 'uuid': batch.uuid,
|
"terminal": batch.terminal.terminal_id,
|
||||||
# 'datetime': self.app.render_datetime(dt),
|
"txnid": batch.id_str,
|
||||||
# 'terminal': batch.terminal_id,
|
"cashier": batch.cashier.name,
|
||||||
# 'txnid': batch.id_str,
|
"customer": batch.customer.name,
|
||||||
# 'cashier': str(batch.cashier or ''),
|
"balance": self.app.render_currency(batch.get_balance()),
|
||||||
# 'customer': str(batch.customer or ''),
|
}
|
||||||
# 'balance': self.app.render_currency(batch.get_balance()),
|
)
|
||||||
# })
|
return transactions
|
||||||
# return transactions
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
return []
|
|
||||||
|
|
||||||
def make_result_row(self, txn):
|
def make_result_row(self, txn):
|
||||||
return [
|
return [
|
||||||
txn["datetime"],
|
txn["created"],
|
||||||
txn["terminal"],
|
txn["terminal"],
|
||||||
txn["txnid"],
|
txn["txnid"],
|
||||||
txn["cashier"],
|
txn["cashier"],
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,14 @@ def show_snackbar(page, text, bgcolor="yellow"):
|
||||||
)
|
)
|
||||||
page.overlay.append(snack_bar)
|
page.overlay.append(snack_bar)
|
||||||
snack_bar.open = True
|
snack_bar.open = True
|
||||||
|
|
||||||
|
|
||||||
|
def get_pos_batch_handler(config):
|
||||||
|
"""
|
||||||
|
Official way of obtaining the POS batch handler.
|
||||||
|
|
||||||
|
Code should use this where possible to make later refactoring
|
||||||
|
easier, should it be needed.
|
||||||
|
"""
|
||||||
|
app = config.get_app()
|
||||||
|
return app.get_batch_handler("pos", default="wuttapos.batch.pos:POSBatchHandler")
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import flet as ft
|
||||||
|
|
||||||
from wuttapos.terminal.controls.header import WuttaHeader
|
from wuttapos.terminal.controls.header import WuttaHeader
|
||||||
from wuttapos.terminal.controls.buttons import make_button
|
from wuttapos.terminal.controls.buttons import make_button
|
||||||
from wuttapos.terminal.util import show_snackbar
|
from wuttapos.terminal.util import show_snackbar, get_pos_batch_handler
|
||||||
|
|
||||||
|
|
||||||
class WuttaView(ft.View):
|
class WuttaView(ft.View):
|
||||||
|
|
@ -56,14 +56,14 @@ class WuttaView(ft.View):
|
||||||
return [self.build_header()]
|
return [self.build_header()]
|
||||||
|
|
||||||
def build_header(self):
|
def build_header(self):
|
||||||
handler = self.get_transaction_handler()
|
handler = self.get_batch_handler()
|
||||||
self.header = WuttaHeader(
|
self.header = WuttaHeader(
|
||||||
self.config, on_reset=self.reset, terminal_id=handler.get_terminal_id()
|
self.config, on_reset=self.reset, terminal_id=handler.get_terminal_id()
|
||||||
)
|
)
|
||||||
return self.header
|
return self.header
|
||||||
|
|
||||||
def get_transaction_handler(self):
|
def get_batch_handler(self):
|
||||||
return self.app.get_transaction_handler()
|
return get_pos_batch_handler(self.config)
|
||||||
|
|
||||||
def make_button(self, *args, **kwargs):
|
def make_button(self, *args, **kwargs):
|
||||||
return make_button(*args, **kwargs)
|
return make_button(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -112,37 +112,39 @@ class POSView(WuttaView):
|
||||||
|
|
||||||
self.show_snackbar(f"CUSTOMER SET: {customer}", bgcolor="green")
|
self.show_snackbar(f"CUSTOMER SET: {customer}", bgcolor="green")
|
||||||
|
|
||||||
def refresh_totals(self, txn):
|
def refresh_totals(self, batch):
|
||||||
reg = ft.TextStyle(size=22)
|
reg = ft.TextStyle(size=22)
|
||||||
bold = ft.TextStyle(size=24, weight=ft.FontWeight.BOLD)
|
bold = ft.TextStyle(size=24, weight=ft.FontWeight.BOLD)
|
||||||
|
|
||||||
self.subtotals.spans.clear()
|
self.subtotals.spans.clear()
|
||||||
|
|
||||||
sales_total = txn["sales_total"]
|
sales_total = batch.sales_total or 0
|
||||||
self.subtotals.spans.append(ft.TextSpan("Sales ", style=reg))
|
self.subtotals.spans.append(ft.TextSpan("Sales ", style=reg))
|
||||||
total = self.app.render_currency(sales_total)
|
total = self.app.render_currency(sales_total)
|
||||||
self.subtotals.spans.append(ft.TextSpan(total, style=bold))
|
self.subtotals.spans.append(ft.TextSpan(total, style=bold))
|
||||||
|
|
||||||
tax_total = 0
|
tax_total = 0
|
||||||
for tax_id, tax in sorted(txn["taxes"].items()):
|
# for tax_id, tax in sorted(txn["taxes"].items()):
|
||||||
if tax["tax_total"]:
|
# if tax["tax_total"]:
|
||||||
self.subtotals.spans.append(
|
# self.subtotals.spans.append(
|
||||||
ft.TextSpan(f" Tax {tax_id} ", style=reg)
|
# ft.TextSpan(f" Tax {tax_id} ", style=reg)
|
||||||
)
|
# )
|
||||||
total = self.app.render_currency(tax["tax_total"])
|
# total = self.app.render_currency(tax["tax_total"])
|
||||||
self.subtotals.spans.append(ft.TextSpan(total, style=bold))
|
# self.subtotals.spans.append(ft.TextSpan(total, style=bold))
|
||||||
tax_total += tax["tax_total"]
|
# tax_total += tax["tax_total"]
|
||||||
|
|
||||||
tender_total = sum(
|
tender_total = 0
|
||||||
[tender["tender_total"] for tender in txn["tenders"].values()]
|
# tender_total = sum(
|
||||||
)
|
# [tender["tender_total"] for tender in txn["tenders"].values()]
|
||||||
|
# )
|
||||||
if tender_total:
|
if tender_total:
|
||||||
self.subtotals.spans.append(ft.TextSpan(f" Tend ", style=reg))
|
self.subtotals.spans.append(ft.TextSpan(f" Tend ", style=reg))
|
||||||
total = self.app.render_currency(tender_total)
|
total = self.app.render_currency(tender_total)
|
||||||
self.subtotals.spans.append(ft.TextSpan(total, style=bold))
|
self.subtotals.spans.append(ft.TextSpan(total, style=bold))
|
||||||
|
|
||||||
self.fs_balance.spans.clear()
|
self.fs_balance.spans.clear()
|
||||||
fs_total = txn["foodstamp"]
|
# fs_total = txn["foodstamp"]
|
||||||
|
fs_total = 0
|
||||||
fs_balance = fs_total + tender_total
|
fs_balance = fs_total + tender_total
|
||||||
if fs_balance:
|
if fs_balance:
|
||||||
self.fs_balance.spans.append(ft.TextSpan("FS ", style=reg))
|
self.fs_balance.spans.append(ft.TextSpan("FS ", style=reg))
|
||||||
|
|
@ -162,6 +164,8 @@ class POSView(WuttaView):
|
||||||
self.totals_row.bgcolor = "orange"
|
self.totals_row.bgcolor = "orange"
|
||||||
|
|
||||||
def attempt_add_product(self, uuid=None, record_badscan=False):
|
def attempt_add_product(self, uuid=None, record_badscan=False):
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
handler = self.get_batch_handler()
|
handler = self.get_batch_handler()
|
||||||
user = self.get_current_user(session)
|
user = self.get_current_user(session)
|
||||||
|
|
@ -175,10 +179,9 @@ class POSView(WuttaView):
|
||||||
product = None
|
product = None
|
||||||
item_entry = entry
|
item_entry = entry
|
||||||
if uuid:
|
if uuid:
|
||||||
product = session.get(self.model.Product, uuid)
|
product = session.get(model.Product, uuid)
|
||||||
assert product
|
assert product
|
||||||
key = self.app.get_product_key_field()
|
item_entry = product.product_id or uuid
|
||||||
item_entry = str(getattr(product, key) or "") or uuid
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
row = handler.process_entry(
|
row = handler.process_entry(
|
||||||
|
|
@ -198,12 +201,13 @@ class POSView(WuttaView):
|
||||||
if row:
|
if row:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
if row.row_type == self.enum.POS_ROW_TYPE_BADPRICE:
|
if row.row_type == enum.POS_ROW_TYPE_BADPRICE:
|
||||||
self.show_snackbar(
|
self.show_snackbar(
|
||||||
f"Product has invalid price: {row.item_entry}", bgcolor="yellow"
|
f"Product has invalid price: {row.item_entry}", bgcolor="yellow"
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
session.expunge(row)
|
||||||
self.add_row_item(row, scroll=True)
|
self.add_row_item(row, scroll=True)
|
||||||
self.refresh_totals(batch)
|
self.refresh_totals(batch)
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
@ -248,10 +252,11 @@ class POSView(WuttaView):
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def customer_lookup(self, value=None, user=None):
|
def customer_lookup(self, value=None, user=None):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
def select(uuid):
|
def select(uuid):
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
customer = session.get(self.model.Customer, uuid)
|
customer = session.get(model.Customer, uuid)
|
||||||
self.set_customer(customer, user=user)
|
self.set_customer(customer, user=user)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
session.close()
|
||||||
|
|
@ -263,46 +268,42 @@ class POSView(WuttaView):
|
||||||
dlg.open = False
|
dlg.open = False
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
dlg = ft.AlertDialog(
|
||||||
self.reset()
|
modal=True,
|
||||||
|
title=ft.Text("Customer Lookup"),
|
||||||
|
content=WuttaCustomerLookup(
|
||||||
|
self.config, initial_search=value, on_select=select, on_cancel=cancel
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# dlg = ft.AlertDialog(
|
# self.page.open(dlg)
|
||||||
# modal=True,
|
|
||||||
# title=ft.Text("Customer Lookup"),
|
|
||||||
# content=WuttaCustomerLookup(self.config, initial_search=value,
|
|
||||||
# on_select=select, on_cancel=cancel),
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # self.page.open(dlg)
|
self.page.dialog = dlg
|
||||||
|
dlg.open = True
|
||||||
# self.page.dialog = dlg
|
self.page.update()
|
||||||
# dlg.open = True
|
|
||||||
# self.page.update()
|
|
||||||
|
|
||||||
def customer_info(self):
|
def customer_info(self):
|
||||||
# clientele = self.app.get_clientele_handler()
|
model = self.app.model
|
||||||
# session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
|
clientele = self.app.get_clientele_handler()
|
||||||
|
|
||||||
# entry = self.main_input.value
|
entry = self.main_input.value
|
||||||
# if entry:
|
if entry:
|
||||||
# different = True
|
different = True
|
||||||
# customer = clientele.locate_customer_for_entry(session, entry)
|
customer = clientele.locate_customer_for_entry(session, entry)
|
||||||
# if not customer:
|
if not customer:
|
||||||
# session.close()
|
session.close()
|
||||||
# self.show_snackbar(f"CUSTOMER NOT FOUND: {entry}", bgcolor='yellow')
|
self.show_snackbar(f"CUSTOMER NOT FOUND: {entry}", bgcolor="yellow")
|
||||||
# self.page.update()
|
self.page.update()
|
||||||
# return
|
return
|
||||||
|
|
||||||
# else:
|
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()
|
|
||||||
|
|
||||||
info = "TODO: customer info"
|
|
||||||
different = False
|
different = False
|
||||||
|
customer = session.get(model.Customer, self.page.session.get("cust_uuid"))
|
||||||
|
assert customer
|
||||||
|
|
||||||
|
info = clientele.get_customer_info_markdown(customer)
|
||||||
|
session.close()
|
||||||
|
|
||||||
def close(e):
|
def close(e):
|
||||||
dlg.open = False
|
dlg.open = False
|
||||||
|
|
@ -523,22 +524,18 @@ class POSView(WuttaView):
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def remove_customer(self, user):
|
def remove_customer(self, user):
|
||||||
|
session = self.app.make_session()
|
||||||
|
handler = self.get_batch_handler()
|
||||||
|
batch = self.get_current_batch(session)
|
||||||
|
user = session.get(user.__class__, user.uuid)
|
||||||
|
handler.set_customer(batch, None, user=user)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
# session = self.app.make_session()
|
self.page.session.set("cust_uuid", None)
|
||||||
# handler = self.get_batch_handler()
|
self.page.session.set("cust_display", None)
|
||||||
# batch = self.get_current_batch(session)
|
self.informed_refresh()
|
||||||
# user = session.get(user.__class__, user.uuid)
|
self.show_snackbar("CUSTOMER REMOVED", bgcolor="yellow")
|
||||||
# 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()
|
|
||||||
# self.show_snackbar("CUSTOMER REMOVED", bgcolor='yellow')
|
|
||||||
# self.reset()
|
|
||||||
|
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def replace_customer(self, user):
|
def replace_customer(self, user):
|
||||||
|
|
@ -745,19 +742,16 @@ class POSView(WuttaView):
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def suspend_transaction(self, user):
|
def suspend_transaction(self, user):
|
||||||
# session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
# batch = self.get_current_batch(session)
|
batch = self.get_current_batch(session)
|
||||||
# user = session.get(user.__class__, user.uuid)
|
user = session.get(user.__class__, user.uuid)
|
||||||
# handler = self.get_batch_handler()
|
handler = self.get_batch_handler()
|
||||||
|
|
||||||
# handler.suspend_transaction(batch, user)
|
handler.suspend_transaction(batch, user)
|
||||||
|
|
||||||
# session.commit()
|
session.commit()
|
||||||
# session.close()
|
session.close()
|
||||||
# self.clear_all()
|
self.clear_all()
|
||||||
# self.reset()
|
|
||||||
|
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def get_current_user(self, session):
|
def get_current_user(self, session):
|
||||||
|
|
@ -766,6 +760,12 @@ class POSView(WuttaView):
|
||||||
if uuid:
|
if uuid:
|
||||||
return session.get(model.User, uuid)
|
return session.get(model.User, uuid)
|
||||||
|
|
||||||
|
def refresh_training(self):
|
||||||
|
if self.page.session.get("training"):
|
||||||
|
self.bgcolor = "#E4D97C"
|
||||||
|
else:
|
||||||
|
self.bgcolor = None
|
||||||
|
|
||||||
def get_current_batch(self, session, user=None, create=True):
|
def get_current_batch(self, session, user=None, create=True):
|
||||||
handler = self.get_batch_handler()
|
handler = self.get_batch_handler()
|
||||||
|
|
||||||
|
|
@ -783,35 +783,11 @@ class POSView(WuttaView):
|
||||||
|
|
||||||
return batch
|
return batch
|
||||||
|
|
||||||
def refresh_training(self):
|
|
||||||
if self.page.session.get("training"):
|
|
||||||
self.bgcolor = "#E4D97C"
|
|
||||||
else:
|
|
||||||
self.bgcolor = None
|
|
||||||
|
|
||||||
def get_current_transaction(self, session, user=None, create=True):
|
|
||||||
handler = self.get_transaction_handler()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
user = self.get_current_user(session)
|
|
||||||
|
|
||||||
training = bool(self.page.session.get("training"))
|
|
||||||
txn, created = handler.get_current_transaction(
|
|
||||||
user, training_mode=training, create=create, return_created=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if created:
|
|
||||||
self.page.session.set("txn_display", handler.get_screen_txn_display(txn))
|
|
||||||
self.informed_refresh()
|
|
||||||
|
|
||||||
return txn
|
|
||||||
|
|
||||||
def did_mount(self):
|
def did_mount(self):
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
|
|
||||||
txn = self.get_current_transaction(session, create=False)
|
if batch := self.get_current_batch(session, create=False):
|
||||||
if txn:
|
self.load_batch(batch)
|
||||||
self.load_transaction(txn)
|
|
||||||
else:
|
else:
|
||||||
self.page.session.set("txn_display", None)
|
self.page.session.set("txn_display", None)
|
||||||
self.page.session.set("cust_uuid", None)
|
self.page.session.set("cust_uuid", None)
|
||||||
|
|
@ -826,21 +802,27 @@ class POSView(WuttaView):
|
||||||
session.close()
|
session.close()
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def load_transaction(self, txn):
|
def load_batch(self, batch):
|
||||||
"""
|
"""
|
||||||
Load the given data as the current transaction.
|
Load the given data as the current transaction.
|
||||||
"""
|
"""
|
||||||
handler = self.get_transaction_handler()
|
session = self.app.get_session(batch)
|
||||||
self.page.session.set("txn_display", handler.get_screen_txn_display(txn))
|
handler = self.get_batch_handler()
|
||||||
self.page.session.set("cust_uuid", txn["customer_id"])
|
self.page.session.set("txn_display", handler.get_screen_txn_display(batch))
|
||||||
self.page.session.set("cust_display", handler.get_screen_cust_display(txn=txn))
|
self.page.session.set(
|
||||||
|
"cust_uuid", batch.customer.uuid if batch.customer else None
|
||||||
|
)
|
||||||
|
self.page.session.set(
|
||||||
|
"cust_display", handler.get_screen_cust_display(batch=batch)
|
||||||
|
)
|
||||||
|
|
||||||
self.items.controls.clear()
|
self.items.controls.clear()
|
||||||
for line in txn["lines"]:
|
for row in batch.rows:
|
||||||
self.add_row_item(line)
|
session.expunge(row)
|
||||||
|
self.add_row_item(row)
|
||||||
self.items.scroll_to(offset=-1, duration=100)
|
self.items.scroll_to(offset=-1, duration=100)
|
||||||
|
|
||||||
self.refresh_totals(txn)
|
self.refresh_totals(batch)
|
||||||
self.informed_refresh()
|
self.informed_refresh()
|
||||||
|
|
||||||
def not_supported(self, e=None, feature=None):
|
def not_supported(self, e=None, feature=None):
|
||||||
|
|
@ -871,6 +853,7 @@ class POSView(WuttaView):
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
def adjust_price(self, user):
|
def adjust_price(self, user):
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
def cancel(e):
|
def cancel(e):
|
||||||
dlg.open = False
|
dlg.open = False
|
||||||
|
|
@ -910,12 +893,12 @@ class POSView(WuttaView):
|
||||||
self.refresh_totals(batch)
|
self.refresh_totals(batch)
|
||||||
|
|
||||||
# update item display
|
# update item display
|
||||||
|
session.expunge(row)
|
||||||
self.selected_item.data["row"] = row
|
self.selected_item.data["row"] = row
|
||||||
self.selected_item.content.row = row
|
self.selected_item.content.row = row
|
||||||
self.selected_item.content.refresh()
|
self.selected_item.content.refresh()
|
||||||
self.items.update()
|
self.items.update()
|
||||||
|
|
||||||
session.expunge_all()
|
|
||||||
session.close()
|
session.close()
|
||||||
self.clear_item_selection()
|
self.clear_item_selection()
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
@ -942,7 +925,7 @@ class POSView(WuttaView):
|
||||||
current_price = self.app.render_currency(row.cur_price)
|
current_price = self.app.render_currency(row.cur_price)
|
||||||
if current_price:
|
if current_price:
|
||||||
current_price += " [{}]".format(
|
current_price += " [{}]".format(
|
||||||
self.enum.PRICE_TYPE.get(row.cur_price_type, row.cur_price_type)
|
enum.PRICE_TYPE.get(row.cur_price_type, row.cur_price_type)
|
||||||
)
|
)
|
||||||
|
|
||||||
dlg = ft.AlertDialog(
|
dlg = ft.AlertDialog(
|
||||||
|
|
@ -1054,29 +1037,26 @@ class POSView(WuttaView):
|
||||||
self.show_snackbar("TODO: Drawer Kick", bgcolor="yellow")
|
self.show_snackbar("TODO: Drawer Kick", bgcolor="yellow")
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def add_row_item(self, line, scroll=False):
|
def add_row_item(self, row, scroll=False):
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
# TODO: row types ugh
|
# TODO: row types ugh
|
||||||
if line.trans_type not in ("I", "T"):
|
if row.row_type not in (
|
||||||
|
enum.POS_ROW_TYPE_SELL,
|
||||||
|
enum.POS_ROW_TYPE_OPEN_RING,
|
||||||
|
enum.POS_ROW_TYPE_TENDER,
|
||||||
|
enum.POS_ROW_TYPE_CHANGE_BACK,
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# # TODO: row types ugh
|
|
||||||
# if row.row_type not in (self.enum.POS_ROW_TYPE_SELL,
|
|
||||||
# self.enum.POS_ROW_TYPE_OPEN_RING,
|
|
||||||
# self.enum.POS_ROW_TYPE_TENDER,
|
|
||||||
# self.enum.POS_ROW_TYPE_CHANGE_BACK):
|
|
||||||
# return
|
|
||||||
|
|
||||||
self.items.controls.append(
|
self.items.controls.append(
|
||||||
ft.Container(
|
ft.Container(
|
||||||
content=WuttaTxnItem(self.config, line),
|
content=WuttaTxnItem(self.config, row),
|
||||||
border=ft.border.only(bottom=ft.border.BorderSide(1, "gray")),
|
border=ft.border.only(bottom=ft.border.BorderSide(1, "gray")),
|
||||||
padding=ft.padding.only(5, 5, 5, 5),
|
padding=ft.padding.only(5, 5, 5, 5),
|
||||||
on_click=self.list_item_click,
|
on_click=self.list_item_click,
|
||||||
# data={'row': row},
|
data={"row": row},
|
||||||
data={"line": line},
|
key=row.uuid,
|
||||||
# key=row.uuid,
|
|
||||||
key=line.trans_id,
|
|
||||||
bgcolor="white",
|
bgcolor="white",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1172,19 +1152,16 @@ class POSView(WuttaView):
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def void_transaction(self, user):
|
def void_transaction(self, user):
|
||||||
# session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
# batch = self.get_current_batch(session)
|
batch = self.get_current_batch(session)
|
||||||
# user = session.get(user.__class__, user.uuid)
|
user = session.get(user.__class__, user.uuid)
|
||||||
# handler = self.get_batch_handler()
|
handler = self.get_batch_handler()
|
||||||
|
|
||||||
# handler.void_batch(batch, user)
|
handler.void_batch(batch, user)
|
||||||
|
|
||||||
# session.commit()
|
session.commit()
|
||||||
# session.close()
|
session.close()
|
||||||
# self.clear_all()
|
self.clear_all()
|
||||||
# self.reset()
|
|
||||||
|
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def clear_item_selection(self):
|
def clear_item_selection(self):
|
||||||
|
|
@ -1236,6 +1213,7 @@ class POSView(WuttaView):
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def cmd_adjust_price_dwim(self, entry=None, **kwargs):
|
def cmd_adjust_price_dwim(self, entry=None, **kwargs):
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
if not len(self.items.controls):
|
if not len(self.items.controls):
|
||||||
self.show_snackbar("There are no line items", bgcolor="yellow")
|
self.show_snackbar("There are no line items", bgcolor="yellow")
|
||||||
|
|
@ -1248,19 +1226,19 @@ class POSView(WuttaView):
|
||||||
self.page.update()
|
self.page.update()
|
||||||
return
|
return
|
||||||
|
|
||||||
# row = self.selected_item.data['row']
|
row = self.selected_item.data["row"]
|
||||||
# if row.void or row.row_type not in (self.enum.POS_ROW_TYPE_SELL,
|
if row.void or row.row_type not in (
|
||||||
# self.enum.POS_ROW_TYPE_OPEN_RING):
|
enum.POS_ROW_TYPE_SELL,
|
||||||
# self.show_snackbar("This item cannot be adjusted", bgcolor='yellow')
|
enum.POS_ROW_TYPE_OPEN_RING,
|
||||||
# self.main_input.focus()
|
):
|
||||||
# self.page.update()
|
self.show_snackbar("This item cannot be adjusted", bgcolor="yellow")
|
||||||
# return
|
self.main_input.focus()
|
||||||
|
|
||||||
# self.authorized_action('pos.override_price', self.adjust_price,
|
|
||||||
# message="Adjust Price")
|
|
||||||
|
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.authorized_action(
|
||||||
|
"pos.override_price", self.adjust_price, message="Adjust Price"
|
||||||
|
)
|
||||||
|
|
||||||
def cmd_context_menu(self, entry=None, **kwargs):
|
def cmd_context_menu(self, entry=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1304,26 +1282,20 @@ class POSView(WuttaView):
|
||||||
|
|
||||||
value = self.main_input.value
|
value = self.main_input.value
|
||||||
if value:
|
if value:
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
if not self.attempt_add_product():
|
||||||
self.reset()
|
self.item_lookup(value)
|
||||||
# if not self.attempt_add_product():
|
|
||||||
# self.item_lookup(value)
|
|
||||||
|
|
||||||
elif self.selected_item:
|
elif self.selected_item:
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
row = self.selected_item.data["row"]
|
||||||
self.reset()
|
if row.product_uuid:
|
||||||
# row = self.selected_item.data['row']
|
if self.attempt_add_product(uuid=row.product_uuid):
|
||||||
# if row.product_uuid:
|
self.clear_item_selection()
|
||||||
# if self.attempt_add_product(uuid=row.product_uuid):
|
self.page.update()
|
||||||
# self.clear_item_selection()
|
else:
|
||||||
# self.page.update()
|
self.item_lookup()
|
||||||
# else:
|
|
||||||
# self.item_lookup()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# self.item_lookup()
|
self.item_lookup()
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def cmd_item_menu_dept(self, entry=None, **kwargs):
|
def cmd_item_menu_dept(self, entry=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1373,9 +1345,9 @@ class POSView(WuttaView):
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
txn = self.get_current_transaction(session, create=False)
|
batch = self.get_current_batch(session, create=False)
|
||||||
session.close()
|
session.close()
|
||||||
if txn:
|
if batch:
|
||||||
self.show_snackbar("TRANSACTION IN PROGRESS")
|
self.show_snackbar("TRANSACTION IN PROGRESS")
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
|
@ -1429,10 +1401,10 @@ class POSView(WuttaView):
|
||||||
def cmd_no_sale_dwim(self, entry=None, **kwargs):
|
def cmd_no_sale_dwim(self, entry=None, **kwargs):
|
||||||
|
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
txn = self.get_current_transaction(session, create=False)
|
batch = self.get_current_batch(session, create=False)
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
if txn:
|
if batch:
|
||||||
self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor="yellow")
|
self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor="yellow")
|
||||||
self.page.update()
|
self.page.update()
|
||||||
return
|
return
|
||||||
|
|
@ -1453,27 +1425,27 @@ class POSView(WuttaView):
|
||||||
return
|
return
|
||||||
|
|
||||||
def select(uuid):
|
def select(uuid):
|
||||||
# session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
# user = self.get_current_user(session)
|
user = self.get_current_user(session)
|
||||||
# batch = self.get_current_batch(session, user=user)
|
batch = self.get_current_batch(session, user=user)
|
||||||
# handler = self.get_batch_handler()
|
handler = self.get_batch_handler()
|
||||||
|
|
||||||
# quantity = 1
|
quantity = 1
|
||||||
# if self.set_quantity.data is not None:
|
if self.set_quantity.data is not None:
|
||||||
# quantity = self.set_quantity.data
|
quantity = self.set_quantity.data
|
||||||
|
|
||||||
# row = handler.add_open_ring(batch, uuid, amount, quantity=quantity, user=user)
|
row = handler.add_open_ring(
|
||||||
# session.commit()
|
batch, uuid, amount, quantity=quantity, user=user
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
# self.add_row_item(row, scroll=True)
|
session.refresh(row)
|
||||||
# self.refresh_totals(batch)
|
session.expunge(row)
|
||||||
# session.close()
|
self.add_row_item(row, scroll=True)
|
||||||
|
self.refresh_totals(batch)
|
||||||
# dlg.open = False
|
session.close()
|
||||||
# self.reset()
|
|
||||||
|
|
||||||
dlg.open = False
|
dlg.open = False
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def cancel(e):
|
def cancel(e):
|
||||||
|
|
@ -1503,9 +1475,8 @@ class POSView(WuttaView):
|
||||||
def cmd_refresh_txn(self, entry=None, **kwargs):
|
def cmd_refresh_txn(self, entry=None, **kwargs):
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
|
|
||||||
txn = self.get_current_transaction(session, create=False)
|
if batch := self.get_current_batch(session, create=False):
|
||||||
if txn:
|
self.load_batch(batch)
|
||||||
self.load_transaction(txn)
|
|
||||||
else:
|
else:
|
||||||
self.page.session.set("txn_display", None)
|
self.page.session.set("txn_display", None)
|
||||||
self.page.session.set("cust_uuid", None)
|
self.page.session.set("cust_uuid", None)
|
||||||
|
|
@ -1523,36 +1494,33 @@ class POSView(WuttaView):
|
||||||
|
|
||||||
def cmd_resume_txn(self, entry=None, **kwargs):
|
def cmd_resume_txn(self, entry=None, **kwargs):
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
txn = self.get_current_transaction(session, create=False)
|
batch = self.get_current_batch(session, create=False)
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
# can't resume if txn in progress
|
# can't resume if txn in progress
|
||||||
if txn:
|
if batch:
|
||||||
self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor="yellow")
|
self.show_snackbar("TRANSACTION IN PROGRESS", bgcolor="yellow")
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
|
|
||||||
def select(uuid):
|
def select(uuid):
|
||||||
# session = self.app.make_session()
|
model = self.app.model
|
||||||
# user = self.get_current_user(session)
|
session = self.app.make_session()
|
||||||
# handler = self.get_batch_handler()
|
user = self.get_current_user(session)
|
||||||
|
handler = self.get_batch_handler()
|
||||||
|
|
||||||
# # TODO: this would need to work differently if suspended
|
# TODO: this would need to work differently if suspended
|
||||||
# # txns are kept in a central server DB
|
# txns are kept in a central server DB
|
||||||
# batch = session.get(self.app.model.POSBatch, uuid)
|
batch = session.get(model.POSBatch, uuid)
|
||||||
|
|
||||||
# batch = handler.resume_transaction(batch, user)
|
batch = handler.resume_transaction(batch, user)
|
||||||
# session.commit()
|
session.commit()
|
||||||
|
|
||||||
# session.refresh(batch)
|
session.refresh(batch)
|
||||||
# self.load_batch(batch)
|
self.load_batch(batch)
|
||||||
# session.close()
|
session.close()
|
||||||
|
|
||||||
# dlg.open = False
|
|
||||||
# self.reset()
|
|
||||||
|
|
||||||
dlg.open = False
|
dlg.open = False
|
||||||
self.show_snackbar("TODO: not implemented", bgcolor="yellow")
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def cancel(e):
|
def cancel(e):
|
||||||
|
|
@ -1614,11 +1582,11 @@ class POSView(WuttaView):
|
||||||
def cmd_suspend_txn(self, entry=None, **kwargs):
|
def cmd_suspend_txn(self, entry=None, **kwargs):
|
||||||
|
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
txn = self.get_current_transaction(session, create=False)
|
batch = self.get_current_batch(session, create=False)
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
# nothing to suspend if no txn
|
# nothing to suspend if no txn
|
||||||
if not txn:
|
if not batch:
|
||||||
self.show_snackbar("NO TRANSACTION", bgcolor="yellow")
|
self.show_snackbar("NO TRANSACTION", bgcolor="yellow")
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
|
|
@ -1807,13 +1775,13 @@ class POSView(WuttaView):
|
||||||
# self.reset()
|
# self.reset()
|
||||||
|
|
||||||
def cmd_void_dwim(self, entry=None, **kwargs):
|
def cmd_void_dwim(self, entry=None, **kwargs):
|
||||||
|
enum = self.app.enum
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
txn = self.get_current_transaction(session, create=False)
|
batch = self.get_current_batch(session, create=False)
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
# nothing to void if no txn
|
# nothing to void if no txn
|
||||||
if not txn:
|
if not batch:
|
||||||
self.show_snackbar("NO TRANSACTION", bgcolor="yellow")
|
self.show_snackbar("NO TRANSACTION", bgcolor="yellow")
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
|
|
@ -1836,8 +1804,8 @@ class POSView(WuttaView):
|
||||||
self.show_snackbar("LINE ALREADY VOID", bgcolor="yellow")
|
self.show_snackbar("LINE ALREADY VOID", bgcolor="yellow")
|
||||||
|
|
||||||
elif row.row_type not in (
|
elif row.row_type not in (
|
||||||
self.enum.POS_ROW_TYPE_SELL,
|
enum.POS_ROW_TYPE_SELL,
|
||||||
self.enum.POS_ROW_TYPE_OPEN_RING,
|
enum.POS_ROW_TYPE_OPEN_RING,
|
||||||
):
|
):
|
||||||
# cannot void line unless of type 'sell'
|
# cannot void line unless of type 'sell'
|
||||||
self.show_snackbar("LINE DOES NOT ALLOW VOID", bgcolor="yellow")
|
self.show_snackbar("LINE DOES NOT ALLOW VOID", bgcolor="yellow")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue