Changeset - 2f408c18736e
[Not reviewed]
0 4 1
Lance Edgar - 6 years ago 2019-03-08 14:33:01
ledgar@techsupport.coop
Add `ProductVolatile` model, for "volatile" product attributes

at least that's the idea...hopefully this table "wins the war" for this concept
5 files changed with 100 insertions and 1 deletions:
0 comments (0 inline, 0 general)
rattail/db/alembic/versions/5b393c108673_add_product_volatile.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
"""add product_volatile
 

	
 
Revision ID: 5b393c108673
 
Revises: 6118198dc4db
 
Create Date: 2019-03-08 13:14:11.645892
 

	
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
# revision identifiers, used by Alembic.
 
revision = '5b393c108673'
 
down_revision = '6118198dc4db'
 
branch_labels = None
 
depends_on = None
 

	
 
from alembic import op
 
import sqlalchemy as sa
 
import rattail.db.types
 

	
 

	
 

	
 
def upgrade():
 

	
 
    # product_volatile
 
    op.create_table('product_volatile',
 
                    sa.Column('uuid', sa.String(length=32), nullable=False),
 
                    sa.Column('product_uuid', sa.String(length=32), nullable=False),
 
                    sa.Column('true_cost', sa.Numeric(precision=9, scale=5), nullable=True),
 
                    sa.Column('true_margin', sa.Numeric(precision=8, scale=5), nullable=True),
 
                    sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'], name='product_volatile_fk_product'),
 
                    sa.PrimaryKeyConstraint('uuid')
 
    )
 

	
 

	
 
def downgrade():
 

	
 
    # product_volatile
 
    op.drop_table('product_volatile')
rattail/db/model/__init__.py
Show inline comments
 
@@ -23,49 +23,50 @@
 
"""
 
Rattail data models
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
from .core import Base, ModelBase, uuid_column, getset_factory, GPCType, Setting, Change, Note
 
from .contact import PhoneNumber, EmailAddress, MailingAddress
 

	
 
from .people import Person, PersonPhoneNumber, PersonEmailAddress, PersonMailingAddress, PersonNote
 
from .users import Role, Permission, User, UserRole, UserEvent
 
from .stores import Store, StorePhoneNumber, StoreEmailAddress
 
from .customers import (Customer, CustomerPhoneNumber, CustomerEmailAddress, CustomerMailingAddress,
 
                        CustomerGroup, CustomerGroupAssignment, CustomerPerson, CustomerNote)
 
from .members import Member, MemberPhoneNumber, MemberEmailAddress, MemberMailingAddress
 

	
 
from .org import Department, Subdepartment, Category, Family, ReportCode, DepositLink
 
from .employees import (Employee, EmployeePhoneNumber, EmployeeEmailAddress,
 
                        EmployeeStore, EmployeeDepartment, EmployeeHistory)
 
from .shifts import ScheduledShift, WorkedShift
 

	
 
from .vendors import Vendor, VendorPhoneNumber, VendorEmailAddress, VendorContact
 
from .products import (Brand, Tax, Product, ProductImage, ProductCode,
 
                       ProductCost, ProductFutureCost, ProductPrice,
 
                       ProductInventory, ProductStoreInfo, InventoryAdjustmentReason)
 
                       ProductInventory, ProductStoreInfo, ProductVolatile,
 
                       InventoryAdjustmentReason)
 
from .purchase import (PurchaseBase, PurchaseItemBase, PurchaseCreditBase,
 
                       Purchase, PurchaseItem, PurchaseCredit)
 

	
 
from .custorders import CustomerOrder, CustomerOrderItem, CustomerOrderItemEvent
 

	
 
from .messages import Message, MessageRecipient
 

	
 
from .datasync import DataSyncChange
 
from .labels import LabelProfile
 
from .bouncer import EmailAttempt, EmailBounce
 
from .tempmon import TempmonClient, TempmonProbe, TempmonReading
 
from .upgrades import Upgrade, UpgradeRequirement
 

	
 
from .exports import ExportMixin
 
from .reports import ReportOutput
 
from .batch import BatchMixin, BaseFileBatchMixin, FileBatchMixin, BatchRowMixin, ProductBatchRowMixin
 
from .batch.dynamic import DynamicBatchMixin, ImporterBatch
 
from .batch.handheld import HandheldBatch, HandheldBatchRow
 
from .batch.inventory import InventoryBatch, InventoryBatchRow
 
from .batch.labels import LabelBatch, LabelBatchRow
 
from .batch.pricing import PricingBatch, PricingBatchRow
 
from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchRowClaim, PurchaseBatchCredit
 
from .batch.vendorcatalog import VendorCatalog, VendorCatalogRow
 
from .batch.vendorinvoice import VendorInvoice, VendorInvoiceRow
rattail/db/model/products.py
Show inline comments
 
@@ -824,48 +824,95 @@ class ProductStoreInfo(Base):
 
        Product,
 
        doc="""
 
        Product to which this info record pertains.
 
        """,
 
        backref=orm.backref(
 
            'store_infos',
 
            collection_class=attribute_mapped_collection('store_uuid'),
 
            doc="""
 
            List of store-specific info records for the product.
 
            """))
 

	
 
    store_uuid = sa.Column(sa.String(length=32), nullable=False)
 
    store = orm.relationship(
 
        Store,
 
        doc="""
 
        Store to which this info record pertains.
 
        """)
 

	
 
    recently_active = sa.Column(sa.Boolean(), nullable=True, doc="""
 
    Flag indicating the product has seen "recent activity" at the store.  How
 
    this is populated and/or interpreted is up to custom app logic.
 
    """)
 

	
 

	
 
class ProductVolatile(Base):
 
    """
 
    This is the place to find "volatile" data for a given product, or at least
 
    it should be...  As of this writing there are a couple other places to look
 
    but hopefully this table can eventually be "the" place.
 

	
 
    Whether any given value in a given record, applies to the "current" app
 
    node only, or if it applies to all nodes, is up to app logic.
 

	
 
    Note that a custom app should (most likely) *not* bother "extending" this
 
    table, but rather should create a separate table with similar pattern.
 
    """
 
    __tablename__ = 'product_volatile'
 
    __table_args__ = (
 
        sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'], name='product_volatile_fk_product'),
 
    )
 

	
 
    uuid = uuid_column()
 

	
 
    product_uuid = sa.Column(sa.String(length=32), nullable=False)
 
    product = orm.relationship(
 
        Product,
 
        doc="""
 
        Product to which this "volatile" data record pertains.
 
        """,
 
        backref=orm.backref(
 
            'volatile',
 
            uselist=False,
 
            cascade='all, delete-orphan',
 
            doc="""
 
            "Volatile" data record for the product, if any.
 
            """))
 

	
 
    true_cost = sa.Column(sa.Numeric(precision=9, scale=5), nullable=True, doc="""
 
    "True" unit cost for the item, if known.  This might include certain
 
    "allowances" (discounts) currently in effect etc.; really anything which
 
    might not be reflected in "official" unit cost for the product.  Usually,
 
    this value is quite easily calculated and so this field serves as more of
 
    a cache, for sake of SQL access to the values.
 
    """)
 

	
 
    true_margin = sa.Column(sa.Numeric(precision=8, scale=5), nullable=True, doc="""
 
    "True" profit margin for the "regular" unit price vs. the "true" unit cost
 
    (:attr:`true_cost`).
 
    """)
 

	
 

	
 
@six.python_2_unicode_compatible
 
class InventoryAdjustmentReason(Base):
 
    """
 
    Reasons for adjusting product inventory.
 
    """
 
    __tablename__ = 'invadjust_reason'
 
    __table_args__ = (
 
        sa.UniqueConstraint('code', name='invadjust_reason_uq_code'),
 
    )
 

	
 
    uuid = uuid_column()
 

	
 
    code = sa.Column(sa.String(length=20), nullable=False, doc="""
 
    Unique code for the reason.
 
    """)
 

	
 
    description = sa.Column(sa.String(length=255), nullable=False, doc="""
 
    Description for the reason.
 
    """)
 

	
 
    hidden = sa.Column(sa.Boolean(), nullable=True, doc="""
 
    Flag indicating that the reason code should *not* be generally visible for
 
    selection by the user etc.
 
    """)
rattail/importing/model.py
Show inline comments
 
@@ -2221,48 +2221,55 @@ class ProductPriceImporter(ToRattail):
 
                    if product.regular_price is not price:
 
                        product.regular_price = price
 
                else:
 
                    if product.regular_price is price:
 
                        product.regular_price = None
 

	
 
            if 'product_current_price' in self.fields:
 
                if data['product_current_price']:
 
                    if product.current_price is not price:
 
                        product.current_price = price
 
                else:
 
                    if product.current_price is price:
 
                        product.current_price = None
 

	
 
        return price
 

	
 

	
 
class ProductStoreInfoImporter(ToRattail):
 
    """
 
    Data importer for :class:`rattail.db.model.ProductStoreInfo`.
 
    """
 
    model_class = model.ProductStoreInfo
 

	
 

	
 
class ProductVolatileImporter(ToRattail):
 
    """
 
    Data importer for :class:`~rattail.db.model.products.ProductVolatile`.
 
    """
 
    model_class = model.ProductVolatile
 

	
 

	
 
class LabelProfileImporter(ToRattail):
 
    """
 
    Importer for LabelProfile data
 
    """
 
    model_class = model.LabelProfile
 

	
 
    def cache_query(self):
 
        query = super(LabelProfileImporter, self).cache_query()
 

	
 
        if not self.config.getbool('rattail', 'labels.sync_all_profiles', default=False):
 
            # only fetch labels from host which are marked as "sync me"
 
            query = query .filter(self.model_class.sync_me == True)
 

	
 
        return query
 

	
 

	
 
class CustomerOrderImporter(ToRattail):
 
    """
 
    Importer for CustomerOrder data
 
    """
 
    model_class = model.CustomerOrder
 

	
 

	
 
class CustomerOrderItemImporter(ToRattail):
rattail/importing/rattail.py
Show inline comments
 
@@ -91,48 +91,49 @@ class FromRattailToRattailBase(object):
 
        importers['CustomerEmailAddress'] = CustomerEmailAddressImporter
 
        importers['CustomerPhoneNumber'] = CustomerPhoneNumberImporter
 
        importers['Member'] = MemberImporter
 
        importers['MemberEmailAddress'] = MemberEmailAddressImporter
 
        importers['MemberPhoneNumber'] = MemberPhoneNumberImporter
 
        importers['Vendor'] = VendorImporter
 
        importers['VendorEmailAddress'] = VendorEmailAddressImporter
 
        importers['VendorPhoneNumber'] = VendorPhoneNumberImporter
 
        importers['VendorContact'] = VendorContactImporter
 
        importers['Department'] = DepartmentImporter
 
        importers['EmployeeDepartment'] = EmployeeDepartmentImporter
 
        importers['Subdepartment'] = SubdepartmentImporter
 
        importers['Category'] = CategoryImporter
 
        importers['Family'] = FamilyImporter
 
        importers['ReportCode'] = ReportCodeImporter
 
        importers['DepositLink'] = DepositLinkImporter
 
        importers['Tax'] = TaxImporter
 
        importers['InventoryAdjustmentReason'] = InventoryAdjustmentReasonImporter
 
        importers['Brand'] = BrandImporter
 
        importers['Product'] = ProductImporter
 
        importers['ProductCode'] = ProductCodeImporter
 
        importers['ProductCost'] = ProductCostImporter
 
        importers['ProductPrice'] = ProductPriceImporter
 
        importers['ProductStoreInfo'] = ProductStoreInfoImporter
 
        importers['ProductVolatile'] = ProductVolatileImporter
 
        importers['ProductImage'] = ProductImageImporter
 
        importers['LabelProfile'] = LabelProfileImporter
 
        return importers
 

	
 
    def get_default_keys(self):
 
        keys = self.get_importer_keys()
 
        if 'AdminUser' in keys:
 
            keys.remove('AdminUser')
 
        if 'ProductImage' in keys:
 
            keys.remove('ProductImage')
 
        return keys
 

	
 

	
 
class FromRattailToRattailImport(FromRattailToRattailBase, FromRattailHandler, ToRattailHandler):
 
    """
 
    Handler for Rattail (other) -> Rattail (local) data import.
 
    """
 
    dbkey = 'host'
 

	
 
    @property
 
    def host_title(self):
 
        return "{} ({})".format(self.config.app_title(default="Rattail"), self.dbkey)
 

	
 
    @property
 
@@ -343,48 +344,51 @@ class ProductImporter(FromRattail, model.ProductImporter):
 
        # first, so they will be created before pack items reference them
 
        # cf. https://www.postgresql.org/docs/current/static/queries-order.html
 
        # cf. https://stackoverflow.com/a/7622046
 
        query = query.order_by(self.host_model_class.unit_uuid.desc())
 

	
 
        return query
 

	
 

	
 
class ProductCodeImporter(FromRattail, model.ProductCodeImporter):
 
    pass
 

	
 
class ProductCostImporter(FromRattail, model.ProductCostImporter):
 
    pass
 

	
 
class ProductPriceImporter(FromRattail, model.ProductPriceImporter):
 

	
 
    @property
 
    def supported_fields(self):
 
        return super(ProductPriceImporter, self).supported_fields + self.product_reference_fields
 

	
 

	
 
class ProductStoreInfoImporter(FromRattail, model.ProductStoreInfoImporter):
 
    pass
 

	
 
class ProductVolatileImporter(FromRattail, model.ProductVolatileImporter):
 
    pass
 

	
 

	
 
class ProductImageImporter(FromRattail, model.ProductImageImporter):
 
    """
 
    Importer for product images.  Note that this uses the "batch" approach
 
    because fetching all data up front is not performant when the host/local
 
    systems are on different machines etc.
 
    """
 

	
 
    def query(self):
 
        query = self.host_session.query(self.model_class)\
 
                                 .order_by(self.model_class.uuid)
 
        return query[self.host_index:self.host_index + self.batch_size]
 

	
 

	
 
class LabelProfileImporter(FromRattail, model.LabelProfileImporter):
 

	
 
    def query(self):
 
        query = super(LabelProfileImporter, self).query()
 

	
 
        if not self.config.getbool('rattail', 'labels.sync_all_profiles', default=False):
 
            # only fetch labels from host which are marked as "sync me"
 
            query = query .filter(self.model_class.sync_me == True)
 

	
 
        return query.order_by(self.model_class.ordinal)
0 comments (0 inline, 0 general)