Add generic "products" batch type, can convert to labels or pricing batch

This commit is contained in:
Lance Edgar 2019-04-19 13:21:31 -05:00
parent 8fa7c6e732
commit 437230b958
7 changed files with 438 additions and 17 deletions

View file

@ -193,8 +193,7 @@ class HandheldBatchHandler(BatchHandler):
default='rattail.batch.inventory:InventoryBatchHandler')
session = orm.object_session(handheld_batches[0])
batch = handler.make_batch(session, created_by=user, handheld_batches=handheld_batches)
handler.populate(batch, progress=progress)
batch.rowcount = len(batch.active_rows())
handler.do_populate(batch, user, progress=progress)
return batch
def make_label_batch(self, handheld_batches, user, progress=None):
@ -202,6 +201,5 @@ class HandheldBatchHandler(BatchHandler):
default='rattail.batch.labels:LabelBatchHandler')
session = orm.object_session(handheld_batches[0])
batch = handler.make_batch(session, created_by=user, handheld_batches=handheld_batches)
handler.populate(batch, progress=progress)
batch.rowcount = len(batch.active_rows())
handler.do_populate(batch, user, progress=progress)
return batch

View file

@ -87,35 +87,42 @@ class LabelBatchHandler(BatchHandler):
"""
Pre-fill batch with row data from handheld batch, etc.
"""
assert batch.handheld_batch or batch.filename or batch.products
session = orm.object_session(batch)
if batch.label_profile:
self.label_profile = batch.label_profile
else:
self.label_profile = self.get_label_profile(session)
self.setup_populate(batch, progress=progress)
if hasattr(batch, 'product_batch') and batch.product_batch:
self.populate_from_product_batch(batch, progress=progress)
return
assert batch.handheld_batch or batch.filename or batch.products
label_code = self.label_profile.code if self.label_profile else None
def append(item, i):
row = model.LabelBatchRow()
row.label_code = self.label_profile.code if self.label_profile else None
row.label_code = label_code
row.label_profile = self.label_profile
with session.no_autoflush:
if isinstance(item, model.Product):
row.product = item
row.upc = row.product.upc
row.label_quantity = 1
if batch.static_prices and hasattr(item, '_batch_price'):
row.regular_price = item._batch_price
else: # item is handheld batch row
row.upc = item.upc
row.product = item.product
row.label_quantity = item.units or 1
# copy these in case product is null
row.item_entry = item.item_entry
row.item_id = item.item_id
row.upc = item.upc
row.brand_name = item.brand_name
row.description = item.description
row.size = item.size
batch.add_row(row)
self.refresh_row(row)
self.add_row(batch, row)
if i % 200 == 0:
session.flush()
if batch.handheld_batch:
data = batch.handheld_batch.active_rows()
@ -131,6 +138,28 @@ class LabelBatchHandler(BatchHandler):
self.progress_loop(append, data, progress,
message="Adding initial rows to batch")
def populate_from_product_batch(self, batch, progress=None):
"""
Populate label batch from product batch.
"""
session = orm.object_session(batch)
product_batch = batch.product_batch
label_code = self.label_profile.code if self.label_profile else None
def add(prow, i):
row = model.LabelBatchRow()
row.label_code = label_code
row.label_profile = self.label_profile
row.label_quantity = 1
with session.no_autoflush:
row.product = prow.product
self.add_row(batch, row)
if i % 200 == 0:
session.flush()
self.progress_loop(add, product_batch.active_rows(), progress,
message="Adding initial rows to batch")
def set_options_from_file(self, batch):
"""
Set various batch options, if any are present within the data file.
@ -211,14 +240,19 @@ class LabelBatchHandler(BatchHandler):
Inspect a row from the source data and populate additional attributes
for it, according to what we find in the database.
"""
if not row.product and row.upc:
session = orm.object_session(row)
row.product = api.get_product_by_upc(session, row.upc)
if not row.product:
row.status_code = row.STATUS_PRODUCT_NOT_FOUND
return
session = orm.object_session(row)
if row.item_entry:
row.product = self.locate_product_for_entry(session, row.item_entry)
if not row.product and row.upc:
row.product = api.get_product_by_upc(session, row.upc)
if not row.product:
row.status_code = row.STATUS_PRODUCT_NOT_FOUND
return
product = row.product
row.item_id = product.item_id
row.upc = product.upc
row.brand_name = six.text_type(product.brand or '')
row.description = product.description
row.size = product.size

View file

@ -60,6 +60,10 @@ class PricingBatchHandler(BatchHandler):
if batch.input_filename:
return self.populate_from_file(batch, progress=progress)
if hasattr(batch, 'product_batch') and batch.product_batch:
self.populate_from_product_batch(batch, progress=progress)
return
assert batch.products
session = orm.object_session(batch)
@ -97,6 +101,25 @@ class PricingBatchHandler(BatchHandler):
self.progress_loop(append, excel_rows, progress,
message="Adding initial rows to batch")
def populate_from_product_batch(self, batch, progress=None):
"""
Populate pricing batch from product batch.
"""
session = orm.object_session(batch)
product_batch = batch.product_batch
def add(prow, i):
row = model.PricingBatchRow()
row.item_entry = prow.item_entry
with session.no_autoflush:
row.product = prow.product
self.add_row(batch, row)
if i % 200 == 0:
session.flush()
self.progress_loop(add, product_batch.active_rows(), progress,
message="Adding initial rows to batch")
def refresh_row(self, row):
"""
Inspect a row from the source data and populate additional attributes

134
rattail/batch/product.py Normal file
View file

@ -0,0 +1,134 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2019 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail 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.
#
# Rattail 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
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Handler for generic product batches
"""
from __future__ import unicode_literals, absolute_import
from sqlalchemy import orm
from rattail.db import model
from rattail.batch import BatchHandler, get_batch_handler
class ProductBatchHandler(BatchHandler):
"""
Handler for generic product batches.
"""
batch_model_class = model.ProductBatch
def should_populate(self, batch):
if batch.input_filename:
return True
return False
def populate(self, batch, progress=None):
if batch.input_filename:
return self.populate_from_file(batch, progress=progress)
def populate_from_file(self, batch, progress=None):
raise NotImplementedError
def refresh_row(self, row):
if not row.product:
if not row.item_entry:
row.status_code = row.STATUS_MISSING_KEY
return
session = orm.object_session(row)
row.product = self.locate_product_for_entry(session, row.item_entry)
if not row.product:
row.status_code = row.STATUS_PRODUCT_NOT_FOUND
return
product = row.product
row.upc = product.upc
row.item_id = product.item_id
row.brand_name = product.brand.name if product.brand else None
row.description = product.description
row.size = product.size
dept = product.department
row.department = dept
row.department_number = dept.number if dept else None
row.department_name = dept.name if dept else None
subdept = product.subdepartment
row.subdepartment = subdept
row.subdepartment_number = subdept.number if subdept else None
row.subdepartment_name = subdept.name if subdept else None
cost = product.cost
row.vendor = cost.vendor if cost else None
row.vendor_item_code = cost.code if cost else None
row.regular_cost = cost.unit_cost if cost else None
row.current_cost = cost.discount_cost if cost else None
row.current_cost_starts = cost.discount_starts if row.current_cost else None
row.current_cost_ends = cost.discount_ends if row.current_cost else None
regprice = product.regular_price
curprice = product.current_price
sugprice = product.suggested_price
row.regular_price = regprice.price if regprice else None
row.current_price = curprice.price if curprice else None
row.current_price_starts = curprice.starts if curprice else None
row.current_price_ends = curprice.ends if curprice else None
row.suggested_price = sugprice.price if sugprice else None
row.status_code = row.STATUS_OK
def execute(self, batch, user=None, action='make_label_batch', progress=None, **kwargs):
if action == 'make_label_batch':
result = self.make_label_batch(batch, user, progress=progress)
elif action == 'make_pricing_batch':
result = self.make_pricing_batch(batch, user, progress=progress)
else:
raise RuntimeError("Batch execution action is not supported: {}".format(action))
return result
def make_label_batch(self, product_batch, user, progress=None):
handler = get_batch_handler(self.config, 'labels',
default='rattail.batch.labels:LabelBatchHandler')
session = orm.object_session(product_batch)
label_batch = handler.make_batch(session, created_by=user,
description=product_batch.description,
notes=product_batch.notes)
label_batch.product_batch = product_batch
handler.do_populate(label_batch, user, progress=progress)
return label_batch
def make_pricing_batch(self, product_batch, user, progress=None):
handler = get_batch_handler(self.config, 'pricing',
default='rattail.batch.pricing:PricingBatchHandler')
session = orm.object_session(product_batch)
pricing_batch = handler.make_batch(session, created_by=user,
description=product_batch.description,
notes=product_batch.notes)
pricing_batch.product_batch = product_batch
handler.do_populate(pricing_batch, user, progress=progress)
return pricing_batch

View file

@ -0,0 +1,97 @@
# -*- coding: utf-8; -*-
"""add product batches
Revision ID: e1685bf9f1ad
Revises: 5dee2f796f25
Create Date: 2019-04-18 19:17:58.520577
"""
from __future__ import unicode_literals, absolute_import
# revision identifiers, used by Alembic.
revision = 'e1685bf9f1ad'
down_revision = '5dee2f796f25'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# batch_product
op.create_table('batch_product',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('created_by_uuid', sa.String(length=32), nullable=False),
sa.Column('cognized', sa.DateTime(), nullable=True),
sa.Column('cognized_by_uuid', sa.String(length=32), nullable=True),
sa.Column('rowcount', sa.Integer(), nullable=True),
sa.Column('complete', sa.Boolean(), nullable=False),
sa.Column('executed', sa.DateTime(), nullable=True),
sa.Column('executed_by_uuid', sa.String(length=32), nullable=True),
sa.Column('purge', sa.Date(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('extra_data', sa.Text(), nullable=True),
sa.Column('status_code', sa.Integer(), nullable=True),
sa.Column('status_text', sa.String(length=255), nullable=True),
sa.Column('input_filename', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['cognized_by_uuid'], ['user.uuid'], name='batch_product_fk_cognized_by'),
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name='batch_product_fk_created_by'),
sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name='batch_product_fk_executed_by'),
sa.PrimaryKeyConstraint('uuid')
)
# batch_product_row
op.create_table('batch_product_row',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('batch_uuid', sa.String(length=32), 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.Column('removed', sa.Boolean(), nullable=False),
sa.Column('item_entry', sa.String(length=20), nullable=True),
sa.Column('upc', rattail.db.types.GPCType(), nullable=True),
sa.Column('item_id', sa.String(length=20), nullable=True),
sa.Column('product_uuid', sa.String(length=32), nullable=True),
sa.Column('brand_name', sa.String(length=100), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
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('subdepartment_number', sa.Integer(), nullable=True),
sa.Column('subdepartment_name', sa.String(length=30), nullable=True),
sa.Column('department_uuid', sa.String(length=32), nullable=True),
sa.Column('subdepartment_uuid', sa.String(length=32), nullable=True),
sa.Column('vendor_uuid', sa.String(length=32), nullable=True),
sa.Column('vendor_item_code', sa.String(length=20), nullable=True),
sa.Column('regular_cost', sa.Numeric(precision=9, scale=5), nullable=True),
sa.Column('current_cost', sa.Numeric(precision=9, scale=5), nullable=True),
sa.Column('current_cost_starts', sa.DateTime(), nullable=True),
sa.Column('current_cost_ends', sa.DateTime(), nullable=True),
sa.Column('regular_price', sa.Numeric(precision=8, scale=3), nullable=True),
sa.Column('current_price', sa.Numeric(precision=8, scale=3), nullable=True),
sa.Column('current_price_starts', sa.DateTime(), nullable=True),
sa.Column('current_price_ends', sa.DateTime(), nullable=True),
sa.Column('suggested_price', sa.Numeric(precision=8, scale=3), nullable=True),
sa.ForeignKeyConstraint(['batch_uuid'], ['batch_product.uuid'], name='batch_product_row_fk_batch'),
sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'], name='batch_product_row_fk_product'),
sa.ForeignKeyConstraint(['department_uuid'], ['department.uuid'], name='batch_product_row_fk_department'),
sa.ForeignKeyConstraint(['subdepartment_uuid'], ['subdepartment.uuid'], name='batch_product_row_fk_subdepartment'),
sa.ForeignKeyConstraint(['vendor_uuid'], ['vendor.uuid'], name='batch_product_row_fk_vendor'),
sa.PrimaryKeyConstraint('uuid')
)
def downgrade():
# batch_product*
op.drop_table('batch_product_row')
op.drop_table('batch_product')

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
# Copyright © 2010-2019 Lance Edgar
#
# This file is part of Rattail.
#
@ -68,6 +68,7 @@ from .batch.inventory import InventoryBatch, InventoryBatchRow
from .batch.labels import LabelBatch, LabelBatchRow
from .batch.newproduct import NewProductBatch, NewProductBatchRow
from .batch.pricing import PricingBatch, PricingBatchRow
from .batch.product import ProductBatch, ProductBatchRow
from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchRowClaim, PurchaseBatchCredit
from .batch.vendorcatalog import VendorCatalog, VendorCatalogRow
from .batch.vendorinvoice import VendorInvoice, VendorInvoiceRow

View file

@ -0,0 +1,134 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2019 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail 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.
#
# Rattail 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
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Data model for generic "product" batches
"""
from __future__ import unicode_literals, absolute_import
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr
from rattail.db.model import (Base, BatchMixin, ProductBatchRowMixin,
Vendor, Department, Subdepartment)
from rattail.db.core import filename_column
class ProductBatch(BatchMixin, Base):
"""
Primary data model for product batches.
"""
__tablename__ = 'batch_product'
__batchrow_class__ = 'ProductBatchRow'
batch_key = 'product'
model_title = "Product Batch"
model_title_plural = "Product Batches"
input_filename = filename_column(nullable=True, doc="""
Base name of the input data file, if applicable.
""")
class ProductBatchRow(ProductBatchRowMixin, Base):
"""
Row of data within a product batch.
"""
__tablename__ = 'batch_product_row'
__batch_class__ = ProductBatch
@declared_attr
def __table_args__(cls):
return cls.__default_table_args__() + (
sa.ForeignKeyConstraint(['department_uuid'], ['department.uuid'],
name='batch_product_row_fk_department'),
sa.ForeignKeyConstraint(['subdepartment_uuid'], ['subdepartment.uuid'],
name='batch_product_row_fk_subdepartment'),
sa.ForeignKeyConstraint(['vendor_uuid'], ['vendor.uuid'],
name='batch_product_row_fk_vendor'),
)
STATUS_OK = 1
STATUS_MISSING_KEY = 2
STATUS_PRODUCT_NOT_FOUND = 3
STATUS = {
STATUS_OK : "ok",
STATUS_MISSING_KEY : "missing product key",
STATUS_PRODUCT_NOT_FOUND : "product not found",
}
department_uuid = sa.Column(sa.String(length=32), nullable=True)
department = orm.relationship(Department, doc="""
Reference to the department to which the product belongs.
""")
subdepartment_uuid = sa.Column(sa.String(length=32), nullable=True)
subdepartment = orm.relationship(Subdepartment, doc="""
Reference to the subdepartment to which the product belongs.
""")
vendor_uuid = sa.Column(sa.String(length=32), nullable=True)
vendor = orm.relationship(Vendor, doc="""
Reference to the "preferred" vendor from which product may be purchased.
""")
vendor_item_code = sa.Column(sa.String(length=20), nullable=True, doc="""
Vendor-specific item code (SKU) for the product.
""")
regular_cost = sa.Column(sa.Numeric(precision=9, scale=5), nullable=True, doc="""
The "base" unit cost for the item, i.e. with no discounts applied.
""")
current_cost = sa.Column(sa.Numeric(precision=9, scale=5), nullable=True, doc="""
The "true" unit cost for the item, i.e. with discounts applied.
""")
current_cost_starts = sa.Column(sa.DateTime(), nullable=True, doc="""
Date/time when the current cost starts, if applicable.
""")
current_cost_ends = sa.Column(sa.DateTime(), nullable=True, doc="""
Date/time when the current cost ends, if applicable.
""")
regular_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
The "regular" unit price for the item.
""")
current_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
The "current" unit price for the item.
""")
current_price_starts = sa.Column(sa.DateTime(), nullable=True, doc="""
Date/time when the current price starts, if applicable.
""")
current_price_ends = sa.Column(sa.DateTime(), nullable=True, doc="""
Date/time when the current price ends, if applicable.
""")
suggested_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
The "suggested" retail price for the item.
""")