Improve tax support for POS batches

also add ref to tender, in batch rows
This commit is contained in:
Lance Edgar 2023-10-07 16:22:12 -05:00
parent f8ea49d7f3
commit b22f89f973
5 changed files with 183 additions and 60 deletions

View file

@ -24,6 +24,8 @@
POS Batch Handler POS Batch Handler
""" """
import decimal
from sqlalchemy import orm from sqlalchemy import orm
from rattail.batch import BatchHandler from rattail.batch import BatchHandler
@ -149,55 +151,94 @@ class POSBatchHandler(BatchHandler):
# entry? maybe only if config says so, e.g. might be nice to # entry? maybe only if config says so, e.g. might be nice to
# only record badscan if entry truly came from scanner device, # only record badscan if entry truly came from scanner device,
# in which case only the caller would know that # in which case only the caller would know that
if not product:
return
if product: # product located, so add item row
row = self.make_row()
row.user = user
row.item_entry = kwargs.get('item_entry', entry)
row.upc = product.upc
row.item_id = product.item_id
row.product = product
row.brand_name = product.brand.name if product.brand else None
row.description = product.description
row.size = product.size
row.full_description = product.full_description
dept = product.department
if dept:
row.department_number = dept.number
row.department_name = dept.name
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.weighed # TODO?
row.quantity = quantity
# product located, so add item row regprice = product.regular_price
row = self.make_row() if regprice:
row.user = user row.reg_price = regprice.price
row.item_entry = kwargs.get('item_entry', entry)
row.upc = product.upc
row.item_id = product.item_id
row.product = product
row.brand_name = product.brand.name if product.brand else None
row.description = product.description
row.size = product.size
row.full_description = product.full_description
dept = product.department
if dept:
row.department_number = dept.number
row.department_name = dept.name
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.weighed # TODO?
row.quantity = quantity
regprice = product.regular_price txnprice = product.current_price or product.regular_price
if regprice: if txnprice:
row.reg_price = regprice.price row.txn_price = txnprice.price
txnprice = product.current_price or product.regular_price if row.txn_price:
if txnprice: row.sales_total = row.txn_price * row.quantity
row.txn_price = txnprice.price batch.sales_total = (batch.sales_total or 0) + row.sales_total
if row.txn_price: tax = product.tax
row.sales_total = row.txn_price * row.quantity if tax:
batch.sales_total = (batch.sales_total or 0) + row.sales_total row.tax_code = tax.code
row.tax_rate = tax.rate
row.tax1 = product.tax1 if row.txn_price:
row.tax2 = product.tax2 row.row_type = self.enum.POS_ROW_TYPE_SELL
if tax:
self.update_tax(batch, row, tax)
else:
row.row_type = self.enum.POS_ROW_TYPE_BADPRICE
if row.txn_price: self.add_row(batch, row)
row.row_type = self.enum.POS_ROW_TYPE_SELL session.flush()
else: return row
row.row_type = self.enum.POS_ROW_TYPE_BADPRICE
self.add_row(batch, 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() session.flush()
return row
# 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): def record_badscan(self, batch, entry, quantity=1, user=None, **kwargs):
""" """
@ -212,6 +253,19 @@ class POSBatchHandler(BatchHandler):
self.add_row(batch, row) self.add_row(batch, row)
return row 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): def get_tender(self, session, key, **kwargs):
""" """
Return the tender record corresponding to the given key. Return the tender record corresponding to the given key.
@ -276,6 +330,8 @@ class POSBatchHandler(BatchHandler):
# adjust totals # adjust totals
batch.sales_total = (batch.sales_total or 0) - orig_sales_total + orig_row.sales_total batch.sales_total = (batch.sales_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 # add another row indicating who/when
row = self.make_row() row = self.make_row()
@ -301,6 +357,8 @@ class POSBatchHandler(BatchHandler):
# adjust batch totals # adjust batch totals
if orig_row.sales_total: if orig_row.sales_total:
batch.sales_total = (batch.sales_total or 0) - orig_row.sales_total batch.sales_total = (batch.sales_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 # add another row indicating who/when
row = self.make_row() row = self.make_row()
@ -406,7 +464,7 @@ class POSBatchHandler(BatchHandler):
row.user = user row.user = user
row.row_type = self.enum.POS_ROW_TYPE_CHANGE_BACK row.row_type = self.enum.POS_ROW_TYPE_CHANGE_BACK
row.item_entry = item_entry row.item_entry = item_entry
row.description = "CHANGE BACK" row.description = "CHANGE DUE"
row.tender_total = -balance row.tender_total = -balance
row.tender = cash row.tender = cash
batch.tender_total = (batch.tender_total or 0) + row.tender_total batch.tender_total = (batch.tender_total or 0) + row.tender_total

View file

@ -27,7 +27,27 @@ def upgrade():
op.add_column('tender_version', sa.Column('kick_drawer', sa.Boolean(), autoincrement=False, nullable=True)) op.add_column('tender_version', sa.Column('kick_drawer', sa.Boolean(), autoincrement=False, nullable=True))
op.add_column('tender_version', sa.Column('disabled', sa.Boolean(), autoincrement=False, nullable=True)) op.add_column('tender_version', sa.Column('disabled', sa.Boolean(), autoincrement=False, nullable=True))
# batch_pos
op.alter_column('batch_pos', 'tax1_total', new_column_name='tax_total')
op.drop_column('batch_pos', 'tax2_total')
# batch_pos_tax
op.create_table('batch_pos_tax',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('batch_uuid', sa.String(length=32), nullable=False),
sa.Column('tax_uuid', sa.String(length=32), nullable=True),
sa.Column('tax_code', sa.String(length=30), nullable=False),
sa.Column('tax_rate', sa.Numeric(precision=7, scale=5), nullable=False),
sa.Column('tax_total', sa.Numeric(precision=9, scale=2), nullable=True),
sa.ForeignKeyConstraint(['batch_uuid'], ['batch_pos.uuid'], name='batch_pos_tax_fk_batch'),
sa.ForeignKeyConstraint(['tax_uuid'], ['tax.uuid'], name='batch_pos_tax_fk_tax'),
sa.PrimaryKeyConstraint('uuid')
)
# batch_pow_row # batch_pow_row
op.drop_column('batch_pos_row', 'tax2')
op.drop_column('batch_pos_row', 'tax1')
op.add_column('batch_pos_row', sa.Column('tax_code', sa.String(length=30), nullable=True))
op.add_column('batch_pos_row', sa.Column('tender_uuid', sa.String(length=32), nullable=True)) op.add_column('batch_pos_row', sa.Column('tender_uuid', sa.String(length=32), nullable=True))
op.create_foreign_key('batch_pos_row_fk_tender', 'batch_pos_row', 'tender', ['tender_uuid'], ['uuid']) op.create_foreign_key('batch_pos_row_fk_tender', 'batch_pos_row', 'tender', ['tender_uuid'], ['uuid'])
@ -37,6 +57,16 @@ def downgrade():
# batch_pos_row # batch_pos_row
op.drop_constraint('batch_pos_row_fk_tender', 'batch_pos_row', type_='foreignkey') op.drop_constraint('batch_pos_row_fk_tender', 'batch_pos_row', type_='foreignkey')
op.drop_column('batch_pos_row', 'tender_uuid') op.drop_column('batch_pos_row', 'tender_uuid')
op.drop_column('batch_pos_row', 'tax_code')
op.add_column('batch_pos_row', sa.Column('tax1', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('batch_pos_row', sa.Column('tax2', sa.BOOLEAN(), autoincrement=False, nullable=True))
# batch_pos_tax
op.drop_table('batch_pos_tax')
# batch_pos
op.add_column('batch_pos', sa.Column('tax2_total', sa.NUMERIC(precision=9, scale=2), autoincrement=False, nullable=True))
op.alter_column('batch_pos', 'tax_total', new_column_name='tax1_total')
# tender # tender
op.drop_column('tender_version', 'disabled') op.drop_column('tender_version', 'disabled')

View file

@ -89,7 +89,7 @@ from .batch.inventory import InventoryBatch, InventoryBatchRow
from .batch.labels import LabelBatch, LabelBatchRow from .batch.labels import LabelBatch, LabelBatchRow
from .batch.newproduct import NewProductBatch, NewProductBatchRow from .batch.newproduct import NewProductBatch, NewProductBatchRow
from .batch.delproduct import DeleteProductBatch, DeleteProductBatchRow from .batch.delproduct import DeleteProductBatch, DeleteProductBatchRow
from .batch.pos import POSBatch, POSBatchRow from .batch.pos import POSBatch, POSBatchTax, POSBatchRow
from .batch.pricing import PricingBatch, PricingBatchRow from .batch.pricing import PricingBatch, PricingBatchRow
from .batch.product import ProductBatch, ProductBatchRow from .batch.product import ProductBatch, ProductBatchRow
from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchRowClaim, PurchaseBatchCredit from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchRowClaim, PurchaseBatchCredit

View file

@ -27,8 +27,9 @@ Models for POS transaction batch
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm.collections import attribute_mapped_collection
from rattail.db.model import Base, BatchMixin, BatchRowMixin from rattail.db.model import Base, BatchMixin, BatchRowMixin, uuid_column
from rattail.db.types import GPCType from rattail.db.types import GPCType
@ -143,12 +144,8 @@ class POSBatch(BatchMixin, Base):
Sales total for the transaction. Sales total for the transaction.
""") """)
tax1_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc=""" tax_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""
Tax 1 total for the transaction. Tax total for the transaction.
""")
tax2_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""
Tax 2 total for the transaction.
""") """)
tender_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc=""" tender_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""
@ -166,11 +163,53 @@ class POSBatch(BatchMixin, Base):
def get_balance(self): def get_balance(self):
return ((self.sales_total or 0) return ((self.sales_total or 0)
+ (self.tax1_total or 0) + (self.tax_total or 0)
+ (self.tax2_total or 0)
+ (self.tender_total or 0)) + (self.tender_total or 0))
class POSBatchTax(Base):
"""
A tax total for a POS batch.
Each row in the batch may be associated with a tax (or not).
Those which are must be aggregated, to determine overall tax total
for the batch. Arbitrary number of taxes may be involved, hence
we store them in this table.
"""
__tablename__ = 'batch_pos_tax'
__table_args__ = (
sa.ForeignKeyConstraint(['batch_uuid'], ['batch_pos.uuid'],
name='batch_pos_tax_fk_batch'),
sa.ForeignKeyConstraint(['tax_uuid'], ['tax.uuid'],
name='batch_pos_tax_fk_tax'),
)
uuid = uuid_column()
batch_uuid = sa.Column(sa.String(length=32), nullable=False)
batch = orm.relationship(
POSBatch,
backref=orm.backref(
'taxes',
collection_class=attribute_mapped_collection('tax_code'),
))
tax_uuid = sa.Column(sa.String(length=32), nullable=True)
tax = orm.relationship('Tax')
tax_code = sa.Column(sa.String(length=30), nullable=False, doc="""
Unique "code" for the tax rate.
""")
tax_rate = sa.Column(sa.Numeric(precision=7, scale=5), nullable=False, doc="""
Percentage rate for the tax, e.g. 8.25.
""")
tax_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""
Total for the tax.
""")
class POSBatchRow(BatchRowMixin, Base): class POSBatchRow(BatchRowMixin, Base):
""" """
Row of data within a POS batch. Row of data within a POS batch.
@ -297,12 +336,8 @@ class POSBatchRow(BatchRowMixin, Base):
Sales total for the item. Sales total for the item.
""") """)
tax1 = sa.Column(sa.Boolean(), nullable=True, doc=""" tax_code = sa.Column(sa.String(length=30), nullable=True, doc="""
Flag indicating Tax 1 should be added for the item. Unique "code" for the item tax rate, if applicable.
""")
tax2 = sa.Column(sa.Boolean(), nullable=True, doc="""
Flag indicating Tax 2 should be added for the item.
""") """)
tender_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc=""" tender_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""

View file

@ -129,9 +129,9 @@ class Tax(Base):
""") """)
def __str__(self): def __str__(self):
if self.description: if self.rate is not None:
return self.description return f"{self.description} {self.rate:0.3f} %"
return "{} ({}%)".format(self.code, pretty_quantity(self.rate)) return self.description or ''
class Product(Base): class Product(Base):