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
"""
import decimal
from sqlalchemy import orm
from rattail.batch import BatchHandler
@ -149,8 +151,8 @@ class POSBatchHandler(BatchHandler):
# 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 product:
if not product:
return
# product located, so add item row
row = self.make_row()
@ -187,11 +189,15 @@ class POSBatchHandler(BatchHandler):
row.sales_total = row.txn_price * row.quantity
batch.sales_total = (batch.sales_total or 0) + row.sales_total
row.tax1 = product.tax1
row.tax2 = product.tax2
tax = product.tax
if tax:
row.tax_code = tax.code
row.tax_rate = tax.rate
if row.txn_price:
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
@ -199,6 +205,41 @@ class POSBatchHandler(BatchHandler):
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.
@ -212,6 +253,19 @@ class POSBatchHandler(BatchHandler):
self.add_row(batch, 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):
"""
Return the tender record corresponding to the given key.
@ -276,6 +330,8 @@ class POSBatchHandler(BatchHandler):
# adjust totals
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
row = self.make_row()
@ -301,6 +357,8 @@ class POSBatchHandler(BatchHandler):
# adjust batch totals
if 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
row = self.make_row()
@ -406,7 +464,7 @@ class POSBatchHandler(BatchHandler):
row.user = user
row.row_type = self.enum.POS_ROW_TYPE_CHANGE_BACK
row.item_entry = item_entry
row.description = "CHANGE BACK"
row.description = "CHANGE DUE"
row.tender_total = -balance
row.tender = cash
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('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
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.create_foreign_key('batch_pos_row_fk_tender', 'batch_pos_row', 'tender', ['tender_uuid'], ['uuid'])
@ -37,6 +57,16 @@ def downgrade():
# batch_pos_row
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', '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
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.newproduct import NewProductBatch, NewProductBatchRow
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.product import ProductBatch, ProductBatchRow
from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchRowClaim, PurchaseBatchCredit

View file

@ -27,8 +27,9 @@ Models for POS transaction batch
import sqlalchemy as sa
from sqlalchemy import orm
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
@ -143,12 +144,8 @@ class POSBatch(BatchMixin, Base):
Sales total for the transaction.
""")
tax1_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""
Tax 1 total for the transaction.
""")
tax2_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""
Tax 2 total for the transaction.
tax_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=True, doc="""
Tax total for the transaction.
""")
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):
return ((self.sales_total or 0)
+ (self.tax1_total or 0)
+ (self.tax2_total or 0)
+ (self.tax_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):
"""
Row of data within a POS batch.
@ -297,12 +336,8 @@ class POSBatchRow(BatchRowMixin, Base):
Sales total for the item.
""")
tax1 = sa.Column(sa.Boolean(), nullable=True, doc="""
Flag indicating Tax 1 should be added for the item.
""")
tax2 = sa.Column(sa.Boolean(), nullable=True, doc="""
Flag indicating Tax 2 should be added for the item.
tax_code = sa.Column(sa.String(length=30), nullable=True, doc="""
Unique "code" for the item tax rate, if applicable.
""")
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):
if self.description:
return self.description
return "{} ({}%)".format(self.code, pretty_quantity(self.rate))
if self.rate is not None:
return f"{self.description} {self.rate:0.3f} %"
return self.description or ''
class Product(Base):