Improve tax support for POS batches
also add ref to tender, in batch rows
This commit is contained in:
parent
f8ea49d7f3
commit
b22f89f973
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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="""
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue