Add initial Shopfoo pattern feature

data models, import/export, web views etc.
This commit is contained in:
Lance Edgar 2020-08-19 22:21:00 -05:00
parent a7656928f5
commit 852f9d4902
19 changed files with 515 additions and 1 deletions

46
rattail_demo/commands.py Normal file
View file

@ -0,0 +1,46 @@
# -*- coding: utf-8; -*-
"""
Rattail Demo Commands
"""
import sys
from rattail import commands
from rattail_demo import __version__
def main(*args):
"""
Main entry point for Rattail Demo command system
"""
args = list(args or sys.argv[1:])
cmd = Command()
cmd.run(*args)
class Command(commands.Command):
"""
Main command for Rattail Demo
"""
name = 'rattail-demo'
version = __version__
description = "Rattail Demo (custom Rattail system)"
long_description = ""
class ExportShopfoo(commands.ExportFileSubcommand):
"""
Export data to the Shopfoo system
"""
name = 'export-shopfoo'
description = __doc__.strip()
handler_spec = 'rattail_demo.shopfoo.importing.rattail:FromRattailToShopfoo'
class ImportSelf(commands.ImportSubcommand):
"""
Update "cascading" Rattail data based on "core" Rattail data
"""
name = 'import-self'
description = __doc__.strip()
handler_spec = 'rattail_demo.importing.local:FromRattailDemoToSelf'

View file

@ -20,4 +20,4 @@ class DemoConfigExtension(ConfigExtension):
config.setdefault('tailbone', 'menus', 'rattail_demo.web.menus')
# default import handlers
config.setdefault('rattail.importing', 'versions.handler', 'rattail_corepos.importing.versions:FromRattailToRattailVersions')
config.setdefault('rattail.importing', 'versions.handler', 'rattail_demo.importing.versions:FromRattailDemoToRattailDemoVersions')

View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""initial tables
Revision ID: 2108f9efa758
Revises: efb7cd318947
Create Date: 2020-08-19 20:02:15.501843
"""
# revision identifiers, used by Alembic.
revision = '2108f9efa758'
down_revision = None
branch_labels = ('rattail_demo',)
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# demo_shopfoo_product
op.create_table('demo_shopfoo_product',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('product_uuid', sa.String(length=32), nullable=True),
sa.Column('upc', sa.String(length=14), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('price', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('enabled', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'], name='demo_shopfoo_product_fk_product'),
sa.PrimaryKeyConstraint('uuid')
)
op.create_table('demo_shopfoo_product_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('product_uuid', sa.String(length=32), autoincrement=False, nullable=True),
sa.Column('upc', sa.String(length=14), autoincrement=False, nullable=True),
sa.Column('description', sa.String(length=255), autoincrement=False, nullable=True),
sa.Column('price', sa.Numeric(precision=13, scale=2), autoincrement=False, nullable=True),
sa.Column('enabled', sa.Boolean(), autoincrement=False, nullable=True),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('uuid', 'transaction_id')
)
op.create_index(op.f('ix_demo_shopfoo_product_version_end_transaction_id'), 'demo_shopfoo_product_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_demo_shopfoo_product_version_operation_type'), 'demo_shopfoo_product_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_demo_shopfoo_product_version_transaction_id'), 'demo_shopfoo_product_version', ['transaction_id'], unique=False)
# demo_shopfoo_product_export
op.create_table('demo_shopfoo_product_export',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('created_by_uuid', sa.String(length=32), nullable=False),
sa.Column('record_count', sa.Integer(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('uploaded', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name='demo_shopfoo_product_export_fk_created_by'),
sa.PrimaryKeyConstraint('uuid')
)
def downgrade():
# demo_shopfoo_product_export
op.drop_table('demo_shopfoo_product_export')
# demo_shopfoo_product
op.drop_index(op.f('ix_demo_shopfoo_product_version_transaction_id'), table_name='demo_shopfoo_product_version')
op.drop_index(op.f('ix_demo_shopfoo_product_version_operation_type'), table_name='demo_shopfoo_product_version')
op.drop_index(op.f('ix_demo_shopfoo_product_version_end_transaction_id'), table_name='demo_shopfoo_product_version')
op.drop_table('demo_shopfoo_product_version')
op.drop_table('demo_shopfoo_product')

View file

@ -8,3 +8,6 @@ from rattail.db.model import *
# also bring in CORE integration models
from rattail_corepos.db.model import *
# now bring in Demo-specific models
from .shopfoo import ShopfooProduct, ShopfooProductExport

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8; -*-
"""
Database schema extensions for Shopfoo integration
"""
import sqlalchemy as sa
from rattail.db import model
from rattail.db.model.shopfoo import ShopfooProductBase, ShopfooProductExportBase
class ShopfooProduct(ShopfooProductBase, model.Base):
"""
Shopfoo-specific product cache table. Each record in this table *should*
match exactly, what is in the actual "Shopfoo" system (even though that's
made-up in this case).
"""
__tablename__ = 'demo_shopfoo_product'
__versioned__ = {}
upc = sa.Column(sa.String(length=14), nullable=True)
description = sa.Column(sa.String(length=255), nullable=True)
price = sa.Column(sa.Numeric(precision=13, scale=2), nullable=True)
enabled = sa.Column(sa.Boolean(), nullable=True)
def __str__(self):
return self.description or self.upc or ""
class ShopfooProductExport(ShopfooProductExportBase, model.Base):
"""
Shopfoo product exports
"""
__tablename__ = 'demo_shopfoo_product_export'

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
"""
Importing into Rattail Demo
"""
from . import model

View file

@ -0,0 +1,60 @@
# -*- coding: utf-8; -*-
"""
Rattail Demo -> Rattail Demo "self" data import
"""
from rattail.importing.local import FromRattailLocalToRattail, FromRattailLocal
from rattail.importing.shopfoo import ShopfooProductImporterMixin
from rattail.util import OrderedDict
from rattail_demo import importing as rattail_demo_importing
class FromRattailDemoToSelf(FromRattailLocalToRattail):
"""
Handler for Rattail Demo -> Rattail Demo ("self") imports
"""
def get_importers(self):
importers = OrderedDict()
importers['ShopfooProduct'] = ShopfooProductImporter
return importers
class ShopfooProductImporter(ShopfooProductImporterMixin, FromRattailLocal, rattail_demo_importing.model.ShopfooProductImporter):
"""
Product -> ShopfooProduct
"""
supported_fields = [
'uuid',
'product_uuid',
'upc',
'description',
'price',
'enabled',
]
def normalize_base_product_data(self, product):
price = None
if product.regular_price:
price = product.regular_price.price
return {
'product_uuid': product.uuid,
'upc': str(product.upc or '') or None,
'description': product.full_description,
'price': price,
'enabled': True, # will maybe unset this in mark_unwanted()
}
def product_is_unwanted(self, product, data):
if super(ShopfooProductImporter, self).product_is_unwanted(product, data):
return True
if not data['price']: # let's say this is a required field for Shopfoo
return True
return False
def mark_unwanted(self, product, data):
data = super(ShopfooProductImporter, self).mark_unwanted(product, data)
data['enabled'] = False
return data

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8; -*-
"""
Rattail Demo model importers
"""
from rattail.importing.model import ToRattail
from rattail_demo.db import model
##############################
# custom models
##############################
class ShopfooProductImporter(ToRattail):
"""
Importer for ShopfooProduct data
"""
model_class = model.ShopfooProduct

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8; -*-
"""
Rattail Demo -> Rattail Demo "versions" data import
"""
from rattail_demo.db import model
from rattail.importing import versions as base
from rattail_corepos.importing.versions import CoreposVersionMixin
class FromRattailDemoToRattailDemoVersions(base.FromRattailToRattailVersions,
CoreposVersionMixin):
"""
Handler for Rattail Demo -> Rattail Demo "versions" data import
"""
def get_importers(self):
importers = super(FromRattailDemoToRattailDemoVersions, self).get_importers()
importers = self.add_corepos_importers(importers)
importers['ShopfooProduct'] = ShopfooProductImporter
return importers
class ShopfooProductImporter(base.VersionImporter):
host_model_class = model.ShopfooProduct

View file

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
"""
Importing into Shopfoo
"""
from . import model

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8; -*-
"""
Shopfoo model importers
"""
from rattail_demo.db import model
from rattail.importing.exporters import ToCSV
from rattail.shopfoo.importing.model import ProductImporterMixin
class ToShopfoo(ToCSV):
pass
class ProductImporter(ProductImporterMixin, ToShopfoo):
"""
Shopfoo product data importer
"""
key = 'uuid'
simple_fields = [
'uuid',
'product_uuid',
'upc',
'description',
'price',
'enabled',
]
export_model_class = model.ShopfooProductExport

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8; -*-
"""
Rattail -> Shopfoo importing
"""
from rattail import importing
from rattail.util import OrderedDict
from rattail_demo.db import model
from rattail_demo.shopfoo import importing as shopfoo_importing
from rattail.shopfoo.importing.rattail import ProductImporterMixin
class FromRattailToShopfoo(importing.FromRattailHandler):
"""
Rattail -> Shopfoo import handler
"""
host_title = "Rattail"
local_title = "Shopfoo"
direction = 'export'
def get_importers(self):
importers = OrderedDict()
importers['Product'] = ProductImporter
return importers
class FromRattail(importing.FromSQLAlchemy):
"""
Base class for Shopfoo -> Rattail importers
"""
class ProductImporter(ProductImporterMixin, FromRattail, shopfoo_importing.model.ProductImporter):
"""
Product data importer
"""
host_model_class = model.ShopfooProduct
supported_fields = [
'uuid',
'product_uuid',
'upc',
'description',
'price',
'enabled',
]
def query(self):
return self.host_session.query(model.ShopfooProduct)\
.order_by(model.ShopfooProduct.upc)
def normalize_host_object(self, product):
# copy all values "as-is" from our cache record
data = dict([(field, getattr(product, field))
for field in self.fields])
# TODO: is it ever a good idea to set this flag? doing so will mean
# the record is *not* included in CSV output file
# data['_deleted_'] = product.deleted_from_shopfoo
return data

View file

@ -126,6 +126,22 @@ def simple_menus(request):
},
],
},
{
'title': "Shopfoo",
'type': 'menu',
'items': [
{
'title': "Products",
'url': url('shopfoo.products'),
'perm': 'shopfoo.products.list',
},
{
'title': "Product Exports",
'url': url('shopfoo.product_exports'),
'perm': 'shopfoo.product_exports.list',
},
],
},
{
'title': "Batches",
'type': 'menu',

View file

@ -37,6 +37,9 @@ def includeme(config):
# core-pos views
config.include('tailbone_corepos.views.corepos')
# shopfoo views
config.include('rattail_demo.web.views.shopfoo')
# batch views
config.include('tailbone.views.handheld')
config.include('tailbone.views.batch.inventory')

View file

@ -0,0 +1,9 @@
# -*- coding: utf-8; -*-
"""
Shopfoo views
"""
def includeme(config):
config.include('rattail_demo.web.views.shopfoo.products')
config.include('rattail_demo.web.views.shopfoo.exports')

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""
Views for Shopfoo product exports
"""
from rattail_demo.db import model
from tailbone.views.exports import ExportMasterView
class ShopfooProductExportView(ExportMasterView):
"""
Master view for Shopfoo product exports.
"""
model_class = model.ShopfooProductExport
route_prefix = 'shopfoo.product_exports'
url_prefix = '/shopfoo/exports/product'
downloadable = True
editable = True
delete_export_files = True
grid_columns = [
'id',
'created',
'created_by',
'filename',
'record_count',
'uploaded',
]
form_fields = [
'id',
'created',
'created_by',
'record_count',
'filename',
'uploaded',
]
def includeme(config):
ShopfooProductExportView.defaults(config)

View file

@ -0,0 +1,70 @@
# -*- coding: utf-8; -*-
"""
Shopfoo product views
"""
from rattail_demo.db import model
from tailbone.views import MasterView
class ShopfooProductView(MasterView):
"""
Shopfoo Product views
"""
model_class = model.ShopfooProduct
url_prefix = '/shopfoo/products'
route_prefix = 'shopfoo.products'
creatable = False
editable = False
bulk_deletable = True
has_versions = True
labels = {
'upc': "UPC",
}
grid_columns = [
'upc',
'description',
'price',
'enabled',
]
form_fields = [
'product',
'upc',
'description',
'price',
'enabled',
]
def configure_grid(self, g):
super(ShopfooProductView, self).configure_grid(g)
g.filters['upc'].default_active = True
g.filters['upc'].default_verb = 'equal'
g.filters['description'].default_active = True
g.filters['description'].default_verb = 'contains'
g.set_sort_defaults('upc')
g.set_type('price', 'currency')
g.set_link('upc')
g.set_link('description')
def grid_extra_class(self, product, i):
if not product.enabled:
return 'warning'
def configure_form(self, f):
super(ShopfooProductView, self).configure_form(f)
f.set_renderer('product', self.render_product)
f.set_type('price', 'currency')
def includeme(config):
ShopfooProductView.defaults(config)

View file

@ -89,5 +89,13 @@ setup(
'rattail.config.extensions': [
'rattail-demo = rattail_demo.config:DemoConfigExtension',
],
'console_scripts': [
'rattail-demo = rattail_demo.commands:main',
],
'rattail_demo.commands': [
'export-shopfoo = rattail_demo.commands:ExportShopfoo',
'import-self = rattail_demo.commands:ImportSelf',
],
},
)