Add PurchaseCredit and friends to schema

Also allow missing cost when refreshing purchase batch row
This commit is contained in:
Lance Edgar 2016-12-10 09:05:21 -06:00
parent ad98038239
commit ebb395a11e
5 changed files with 420 additions and 17 deletions

View file

@ -84,8 +84,7 @@ class PurchaseBatchHandler(BatchHandler):
""" """
batch = row.batch batch = row.batch
product = row.product product = row.product
cost = row.product.cost_for_vendor(batch.vendor) cost = row.product.cost_for_vendor(batch.vendor) or row.product.cost
assert cost
row.upc = product.upc row.upc = product.upc
row.brand_name = unicode(product.brand or '') row.brand_name = unicode(product.brand or '')
row.description = product.description row.description = product.description
@ -96,18 +95,25 @@ class PurchaseBatchHandler(BatchHandler):
else: else:
row.department_number = None row.department_number = None
row.department_name = None row.department_name = None
row.vendor_code = cost.code row.vendor_code = cost.code if cost else None
row.case_quantity = cost.case_size row.case_quantity = cost.case_size if cost else None
if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW: if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW:
row.po_unit_cost = cost.unit_cost if cost:
row.po_total = row.po_unit_cost * self.get_units_ordered(row) row.po_unit_cost = cost.unit_cost
batch.po_total = (batch.po_total or 0) + row.po_total row.po_total = row.po_unit_cost * self.get_units_ordered(row)
batch.po_total = (batch.po_total or 0) + row.po_total
else:
row.po_unit_cost = None
row.po_total = None
elif batch.mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING, elif batch.mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING,
self.enum.PURCHASE_BATCH_MODE_COSTING): self.enum.PURCHASE_BATCH_MODE_COSTING):
row.invoice_unit_cost = cost.unit_cost row.invoice_unit_cost = (cost.unit_cost if cost else None) or row.po_unit_cost
row.invoice_total = row.invoice_unit_cost * self.get_units_shipped(row) if row.invoice_unit_cost:
batch.invoice_total = (batch.invoice_total or 0) + row.invoice_total row.invoice_total = row.invoice_unit_cost * self.get_units_accounted_for(row)
batch.invoice_total = (batch.invoice_total or 0) + row.invoice_total
else:
row.invoice_total = None
if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW: if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW:
row.status_code = row.STATUS_OK row.status_code = row.STATUS_OK
@ -116,10 +122,11 @@ class PurchaseBatchHandler(BatchHandler):
self.enum.PURCHASE_BATCH_MODE_COSTING): self.enum.PURCHASE_BATCH_MODE_COSTING):
if (row.cases_received is None and row.units_received is None and if (row.cases_received is None and row.units_received is None and
row.cases_damaged is None and row.units_damaged is None and row.cases_damaged is None and row.units_damaged is None and
row.cases_expired is None and row.units_expired is None): row.cases_expired is None and row.units_expired is None and
row.cases_mispick is None and row.units_mispick is None):
row.status_code = row.STATUS_INCOMPLETE row.status_code = row.STATUS_INCOMPLETE
else: else:
if self.get_units_ordered() != self.get_units_shipped(row): if self.get_units_ordered(row) != self.get_units_accounted_for(row):
row.status_code = row.STATUS_ORDERED_RECEIVED_DIFFER row.status_code = row.STATUS_ORDERED_RECEIVED_DIFFER
else: else:
row.status_code = row.STATUS_OK row.status_code = row.STATUS_OK
@ -133,6 +140,10 @@ class PurchaseBatchHandler(BatchHandler):
units_expired = (row.units_expired or 0) + row.case_quantity * (row.cases_expired or 0) units_expired = (row.units_expired or 0) + row.case_quantity * (row.cases_expired or 0)
return units_received + units_damaged + units_expired return units_received + units_damaged + units_expired
def get_units_accounted_for(self, row):
units_mispick = (row.units_mispick or 0) + row.case_quantity * (row.cases_mispick or 0)
return self.get_units_shipped(row) + units_mispick
def execute(self, batch, user, progress=None): def execute(self, batch, user, progress=None):
""" """
Default behavior for executing a purchase batch will create a new Default behavior for executing a purchase batch will create a new

View file

@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
"""add purchase credits
Revision ID: 193c2dcdd14d
Revises: 123af5f6a0bc
Create Date: 2016-12-09 17:03:39.001046
"""
from __future__ import unicode_literals
# revision identifiers, used by Alembic.
revision = '193c2dcdd14d'
down_revision = u'123af5f6a0bc'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
from sqlalchemy.dialects import postgresql
batch_id_seq = sa.Sequence('batch_id_seq')
def upgrade():
# purchase_batch_row
op.add_column('purchase_batch_row', sa.Column('cases_mispick', sa.Numeric(precision=10, scale=4), nullable=True))
op.add_column('purchase_batch_row', sa.Column('units_mispick', sa.Numeric(precision=10, scale=4), nullable=True))
# purchase_item
op.add_column('purchase_item', sa.Column('cases_mispick', sa.Numeric(precision=10, scale=4), nullable=True))
op.add_column('purchase_item', sa.Column('units_mispick', sa.Numeric(precision=10, scale=4), nullable=True))
# purchase_credit
op.create_table('purchase_credit',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('store_uuid', sa.String(length=32), nullable=False),
sa.Column('vendor_uuid', sa.String(length=32), nullable=False),
sa.Column('date_ordered', sa.Date(), nullable=True),
sa.Column('date_shipped', sa.Date(), nullable=True),
sa.Column('date_received', sa.Date(), nullable=True),
sa.Column('invoice_number', sa.String(length=20), nullable=True),
sa.Column('invoice_date', sa.Date(), nullable=True),
sa.Column('credit_type', sa.String(length=20), nullable=False),
sa.Column('product_uuid', sa.String(length=32), nullable=True),
sa.Column('upc', rattail.db.types.GPCType(), nullable=True),
sa.Column('brand_name', sa.String(length=100), nullable=True),
sa.Column('description', sa.String(length=60), nullable=False),
sa.Column('size', sa.String(length=255), nullable=True),
sa.Column('department_number', sa.Integer(), nullable=True),
sa.Column('department_name', sa.String(length=30), nullable=True),
sa.Column('case_quantity', sa.Numeric(precision=8, scale=2), nullable=True),
sa.Column('cases_shorted', sa.Numeric(precision=10, scale=4), nullable=True),
sa.Column('units_shorted', sa.Numeric(precision=10, scale=4), nullable=True),
sa.Column('expiration_date', sa.Date(), nullable=True),
sa.Column('invoice_line_number', sa.Integer(), nullable=True),
sa.Column('invoice_case_cost', sa.Numeric(precision=7, scale=3), nullable=True),
sa.Column('invoice_unit_cost', sa.Numeric(precision=7, scale=3), nullable=True),
sa.Column('invoice_total', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('purchase_uuid', sa.String(length=32), nullable=True),
sa.Column('item_uuid', sa.String(length=32), nullable=True),
sa.Column('status', sa.Integer(), nullable=True),
sa.Column('mispick_brand_name', sa.String(length=100), nullable=True),
sa.Column('mispick_description', sa.String(length=60), nullable=True),
sa.Column('mispick_product_uuid', sa.String(length=32), nullable=True),
sa.Column('mispick_size', sa.String(length=255), nullable=True),
sa.Column('mispick_upc', rattail.db.types.GPCType(), nullable=True),
sa.ForeignKeyConstraint(['item_uuid'], [u'purchase_item.uuid'], name=u'purchase_credit_fk_item'),
sa.ForeignKeyConstraint(['product_uuid'], [u'product.uuid'], name=u'purchase_credit_fk_product'),
sa.ForeignKeyConstraint(['purchase_uuid'], [u'purchase.uuid'], name=u'purchase_credit_fk_purchase'),
sa.ForeignKeyConstraint(['store_uuid'], [u'store.uuid'], name=u'purchase_credit_fk_store'),
sa.ForeignKeyConstraint(['vendor_uuid'], [u'vendor.uuid'], name=u'purchase_credit_fk_vendor'),
sa.ForeignKeyConstraint(['mispick_product_uuid'], [u'product.uuid'], name=u'purchase_credit_fk_mispick_product'),
sa.PrimaryKeyConstraint('uuid')
)
# purchase_batch_credit
op.create_table('purchase_batch_credit',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('row_uuid', sa.String(length=32), nullable=True),
sa.Column('store_uuid', sa.String(length=32), nullable=False),
sa.Column('vendor_uuid', sa.String(length=32), nullable=False),
sa.Column('date_ordered', sa.Date(), nullable=True),
sa.Column('date_shipped', sa.Date(), nullable=True),
sa.Column('date_received', sa.Date(), nullable=True),
sa.Column('invoice_number', sa.String(length=20), nullable=True),
sa.Column('invoice_date', sa.Date(), nullable=True),
sa.Column('credit_type', sa.String(length=20), nullable=False),
sa.Column('product_uuid', sa.String(length=32), nullable=True),
sa.Column('upc', rattail.db.types.GPCType(), nullable=True),
sa.Column('brand_name', sa.String(length=100), nullable=True),
sa.Column('description', sa.String(length=60), nullable=False),
sa.Column('size', sa.String(length=255), nullable=True),
sa.Column('department_number', sa.Integer(), nullable=True),
sa.Column('department_name', sa.String(length=30), nullable=True),
sa.Column('case_quantity', sa.Numeric(precision=8, scale=2), nullable=True),
sa.Column('cases_shorted', sa.Numeric(precision=10, scale=4), nullable=True),
sa.Column('units_shorted', sa.Numeric(precision=10, scale=4), nullable=True),
sa.Column('expiration_date', sa.Date(), nullable=True),
sa.Column('invoice_line_number', sa.Integer(), nullable=True),
sa.Column('invoice_case_cost', sa.Numeric(precision=7, scale=3), nullable=True),
sa.Column('invoice_unit_cost', sa.Numeric(precision=7, scale=3), nullable=True),
sa.Column('invoice_total', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('mispick_brand_name', sa.String(length=100), nullable=True),
sa.Column('mispick_description', sa.String(length=60), nullable=True),
sa.Column('mispick_product_uuid', sa.String(length=32), nullable=True),
sa.Column('mispick_size', sa.String(length=255), nullable=True),
sa.Column('mispick_upc', rattail.db.types.GPCType(), nullable=True),
sa.ForeignKeyConstraint(['product_uuid'], [u'product.uuid'], name=u'purchase_batch_credit_fk_product'),
sa.ForeignKeyConstraint(['row_uuid'], [u'purchase_batch_row.uuid'], name=u'purchase_batch_credit_fk_row'),
sa.ForeignKeyConstraint(['store_uuid'], [u'store.uuid'], name=u'purchase_batch_credit_fk_store'),
sa.ForeignKeyConstraint(['vendor_uuid'], [u'vendor.uuid'], name=u'purchase_batch_credit_fk_vendor'),
sa.ForeignKeyConstraint(['mispick_product_uuid'], [u'product.uuid'], name=u'purchase_batch_credit_fk_mispick_product'),
sa.PrimaryKeyConstraint('uuid')
)
def downgrade():
# purchase_batch_credit
op.drop_table('purchase_batch_credit')
# purchase_credit
op.drop_table('purchase_credit')
# purchase_item
op.drop_column('purchase_item', 'units_mispick')
op.drop_column('purchase_item', 'cases_mispick')
# purchase_batch_row
op.drop_column('purchase_batch_row', 'units_mispick')
op.drop_column('purchase_batch_row', 'cases_mispick')

View file

@ -40,7 +40,8 @@ from .shifts import ScheduledShift, WorkedShift
from .vendors import Vendor, VendorPhoneNumber, VendorEmailAddress, VendorContact from .vendors import Vendor, VendorPhoneNumber, VendorEmailAddress, VendorContact
from .products import Brand, Tax, Product, ProductCode, ProductCost, ProductPrice from .products import Brand, Tax, Product, ProductCode, ProductCost, ProductPrice
from .purchase import PurchaseBase, PurchaseItemBase, Purchase, PurchaseItem from .purchase import (PurchaseBase, PurchaseItemBase, PurchaseCreditBase,
Purchase, PurchaseItem, PurchaseCredit)
from .messages import Message, MessageRecipient from .messages import Message, MessageRecipient
@ -55,6 +56,6 @@ from .batch.handheld import HandheldBatch, HandheldBatchRow
from .batch.inventory import InventoryBatch, InventoryBatchRow from .batch.inventory import InventoryBatch, InventoryBatchRow
from .batch.labels import LabelBatch, LabelBatchRow from .batch.labels import LabelBatch, LabelBatchRow
from .batch.pricing import PricingBatch, PricingBatchRow from .batch.pricing import PricingBatch, PricingBatchRow
from .batch.purchase import PurchaseBatch, PurchaseBatchRow from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchCredit
from .batch.vendorcatalog import VendorCatalog, VendorCatalogRow from .batch.vendorcatalog import VendorCatalog, VendorCatalogRow
from .batch.vendorinvoice import VendorInvoice, VendorInvoiceRow from .batch.vendorinvoice import VendorInvoice, VendorInvoiceRow

View file

@ -30,9 +30,10 @@ 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 rattail.db.model import (Base, PurchaseBase, PurchaseItemBase, from rattail.db.model import (Base, uuid_column, BatchMixin, BatchRowMixin,
Purchase, PurchaseItem, PurchaseBase, PurchaseItemBase, PurchaseCreditBase,
BatchMixin, BatchRowMixin) Purchase, PurchaseItem)
from rattail.util import pretty_quantity
class PurchaseBatch(BatchMixin, PurchaseBase, Base): class PurchaseBatch(BatchMixin, PurchaseBase, Base):
@ -113,3 +114,51 @@ class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
Reference to the purchase item with which the batch row is associated. Reference to the purchase item with which the batch row is associated.
May be null, e.g. in the case of a "new purchase" batch. May be null, e.g. in the case of a "new purchase" batch.
""") """)
class PurchaseBatchCredit(PurchaseCreditBase, Base):
"""
Represents a working copy of purchase credit tied to a batch row.
"""
__tablename__ = 'purchase_batch_credit'
@declared_attr
def __table_args__(cls):
return cls.__purchasecredit_table_args__() + (
sa.ForeignKeyConstraint(['row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_credit_fk_row'),
)
uuid = uuid_column()
row_uuid = sa.Column(sa.String(length=32), nullable=True)
row = orm.relationship(
PurchaseBatchRow,
doc="""
Reference to the batch row with which the credit is associated.
""",
backref=orm.backref(
'credits',
doc="""
List of :class:`PurchaseBatchCredit` instances for the row.
"""))
def __unicode__(self):
if self.cases_shorted is not None and self.units_shorted is not None:
qty = "{} cases, {} units".format(
pretty_quantity(self.cases_shorted),
pretty_quantity(self.units_shorted))
elif self.cases_shorted is not None:
qty = "{} cases".format(pretty_quantity(self.cases_shorted))
elif self.units_shorted is not None:
qty = "{} units".format(pretty_quantity(self.units_shorted))
else:
qty = "??"
qty += " {}".format(self.credit_type)
if self.credit_type == 'expired' and self.expiration_date:
qty += " ({})".format(self.expiration_date)
if self.credit_type == 'mispick' and self.mispick_product:
qty += " ({})".format(self.mispick_product)
if self.invoice_total:
return "{} = ${:0.2f}".format(qty, self.invoice_total)
return qty

View file

@ -251,6 +251,172 @@ class PurchaseItemBase(object):
Number of units of product which were shipped expired. Number of units of product which were shipped expired.
""") """)
cases_mispick = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
Number of cases of product for which mispick was shipped.
""")
units_mispick = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
Number of units of product for which mispick was shipped.
""")
class PurchaseCreditBase(object):
"""
Base class for purchase credits.
"""
@declared_attr
def __table_args__(cls):
return cls.__purchasecredit_table_args__()
@classmethod
def __purchasecredit_table_args__(cls):
return (
sa.ForeignKeyConstraint(['store_uuid'], ['store.uuid'], name='{}_fk_store'.format(cls.__tablename__)),
sa.ForeignKeyConstraint(['vendor_uuid'], ['vendor.uuid'], name='{}_fk_vendor'.format(cls.__tablename__)),
sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'], name='{}_fk_product'.format(cls.__tablename__)),
sa.ForeignKeyConstraint(['mispick_product_uuid'], ['product.uuid'], name='{}_fk_mispick_product'.format(cls.__tablename__)),
)
store_uuid = sa.Column(sa.String(length=32), nullable=False)
@declared_attr
def store(cls):
return orm.relationship(
Store,
doc="""
Reference to the :class:`Store` for which the purchase was made.
""")
vendor_uuid = sa.Column(sa.String(length=32), nullable=False)
@declared_attr
def vendor(cls):
return orm.relationship(
Vendor,
doc="""
Reference to the :class:`Vendor` to which the purchase was made.
""")
date_ordered = sa.Column(sa.Date(), nullable=True, doc="""
Date on which the purchase order was first submitted to the vendor.
""")
date_shipped = sa.Column(sa.Date(), nullable=True, doc="""
Date on which the order was shipped from the vendor.
""")
date_received = sa.Column(sa.Date(), nullable=True, doc="""
Date on which the order was received at the store.
""")
invoice_number = sa.Column(sa.String(length=20), nullable=True, doc="""
Invoice number, e.g. for cross-reference with another system.
""")
invoice_date = sa.Column(sa.Date(), nullable=True, doc="""
Invoice date, if applicable.
""")
credit_type = sa.Column(sa.String(length=20), nullable=False, doc="""
Type of the credit, i.e. damaged/expired/mispick
""")
product_uuid = sa.Column(sa.String(length=32), nullable=True)
@declared_attr
def product(cls):
return orm.relationship(
Product,
foreign_keys=[cls.product_uuid],
doc="""
Reference to the :class:`Product` with which the credit is associated.
""")
upc = sa.Column(GPCType(), nullable=True, doc="""
Product UPC for the credit item.
""")
brand_name = sa.Column(sa.String(length=100), nullable=True, doc="""
Brand name for the credit item.
""")
description = sa.Column(sa.String(length=60), nullable=False, default='', doc="""
Product description for the credit item.
""")
size = sa.Column(sa.String(length=255), nullable=True, doc="""
Product size for the credit item.
""")
department_number = sa.Column(sa.Integer(), nullable=True, doc="""
Number of the department to which the product belongs.
""")
department_name = sa.Column(sa.String(length=30), nullable=True, doc="""
Name of the department to which the product belongs.
""")
case_quantity = sa.Column(sa.Numeric(precision=8, scale=2), nullable=True, doc="""
Number of units in a single case of product.
""")
cases_shorted = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
Number of cases of product which were ordered but not received.
""")
units_shorted = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
Number of cases of product which were ordered but not received.
""")
expiration_date = sa.Column(sa.Date(), nullable=True, doc="""
Expiration date marked on expired product, if applicable.
""")
invoice_line_number = sa.Column(sa.Integer(), nullable=True, doc="""
Line number from the invoice if known, for cross-reference.
""")
invoice_case_cost = sa.Column(sa.Numeric(precision=7, scale=3), nullable=True, doc="""
Actual cost per case of product, per invoice.
""")
invoice_unit_cost = sa.Column(sa.Numeric(precision=7, scale=3), nullable=True, doc="""
Actual cost per single unit of product, per invoice.
""")
invoice_total = sa.Column(sa.Numeric(precision=7, scale=2), nullable=True, doc="""
Actual total cost for line item, per invoice.
""")
mispick_product_uuid = sa.Column(sa.String(length=32), nullable=True)
@declared_attr
def mispick_product(cls):
return orm.relationship(
Product,
foreign_keys=[cls.mispick_product_uuid],
doc="""
Reference to the :class:`Product` which was shipped in place of the
one which was ordered.
""")
mispick_upc = sa.Column(GPCType(), nullable=True, doc="""
Product UPC for the mispick item.
""")
mispick_brand_name = sa.Column(sa.String(length=100), nullable=True, doc="""
Brand name for the mispick item.
""")
mispick_description = sa.Column(sa.String(length=60), nullable=True, default='', doc="""
Product description for the mispick item.
""")
mispick_size = sa.Column(sa.String(length=255), nullable=True, doc="""
Product size for the mispick item.
""")
class Purchase(PurchaseBase, Base): class Purchase(PurchaseBase, Base):
""" """
@ -319,3 +485,44 @@ class PurchaseItem(PurchaseItemBase, Base):
Numeric code used to signify current status for the line item, e.g. for Numeric code used to signify current status for the line item, e.g. for
highlighting rows when invoice cost differed from expected/PO cost (?) highlighting rows when invoice cost differed from expected/PO cost (?)
""") """)
class PurchaseCredit(PurchaseCreditBase, Base):
"""
Represents a purchase credit item.
"""
__tablename__ = 'purchase_credit'
@declared_attr
def __table_args__(cls):
return cls.__purchasecredit_table_args__() + (
sa.ForeignKeyConstraint(['purchase_uuid'], ['purchase.uuid'], name='purchase_credit_fk_purchase'),
sa.ForeignKeyConstraint(['item_uuid'], ['purchase_item.uuid'], name='purchase_credit_fk_item'),
)
uuid = uuid_column()
purchase_uuid = sa.Column(sa.String(length=32), nullable=True)
purchase = orm.relationship(
Purchase,
doc="""
Reference to the :class:`Purchase` to which the credit applies.
""",
backref=orm.backref(
'credits',
doc="""
List of :class:`PurchaseCredit` instances for the purchase.
"""))
item_uuid = sa.Column(sa.String(length=32), nullable=True)
item = orm.relationship(
PurchaseItem,
doc="""
Reference to the purchase item with which the credit is associated.
""")
status = sa.Column(sa.Integer(), nullable=True, doc="""
Numeric code used to signify current status for the credit.
""")