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
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2017 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/>.
 
#
 
################################################################################
 
"""
 
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
 
@@ -800,75 +800,122 @@ class ProductInventory(Base):
 

	
 
    on_hand = sa.Column(sa.Numeric(precision=9, scale=4), nullable=True, doc="""
 
    Unit quantity of product which is currently on hand.
 
    """)
 

	
 
    on_order = sa.Column(sa.Numeric(precision=9, scale=4), nullable=True, doc="""
 
    Unit quantity of product which is currently on order.
 
    """)
 

	
 

	
 
class ProductStoreInfo(Base):
 
    """
 
    General store-specific info for a product.
 
    """
 
    __tablename__ = 'product_store_info'
 
    __table_args__ = (
 
        sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'], name='product_store_info_fk_product'),
 
        sa.ForeignKeyConstraint(['store_uuid'], ['store.uuid'], name='product_store_info_fk_store'),
 
    )
 

	
 
    uuid = uuid_column()
 

	
 
    product_uuid = sa.Column(sa.String(length=32), nullable=False)
 
    product = orm.relationship(
 
        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.
 
    """)
 

	
 
    def __str__(self):
 
        return self.description or ""
rattail/importing/model.py
Show inline comments
 
@@ -2197,96 +2197,103 @@ class ProductPriceImporter(ToRattail):
 
        return data
 

	
 
    def update_object(self, price, data, local_data=None):
 
        price = super(ProductPriceImporter, self).update_object(price, data, local_data)
 

	
 
        if self.fields_active(self.product_reference_fields):
 
            if not price.product:
 
                self.session.add(price)
 
                self.session.flush()
 
                self.session.refresh(price)
 
            product = price.product
 
            assert product
 
            assert price in product.prices
 

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

	
 
            if 'product_regular_price' in self.fields:
 
                if data['product_regular_price']:
 
                    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):
 
    """
 
    Importer for CustomerOrderItem data
 
    """
 
    model_class = model.CustomerOrderItem
 

	
 

	
 
class CustomerOrderItemEventImporter(ToRattail):
 
    """
 
    Importer for CustomerOrderItemEvent data
 
    """
 
    model_class = model.CustomerOrderItemEvent
 

	
 
    def setup(self):
 
        super(CustomerOrderItemEventImporter, self).setup()
 

	
 
        self.start_date = self.args.start_date
 
        if self.start_date:
 
            midnight = datetime.datetime.combine(self.start_date, datetime.time(0))
 
            self.start_time = localtime(self.config, midnight)
 

	
 
        self.end_date = self.args.end_date
 
        if self.end_date:
 
            midnight = datetime.datetime.combine(self.end_date + datetime.timedelta(days=1), datetime.time(0))
 
            self.end_time = localtime(self.config, midnight)
rattail/importing/rattail.py
Show inline comments
 
@@ -67,96 +67,97 @@ class FromRattailToRattailBase(object):
 
    """
 

	
 
    def get_importers(self):
 
        importers = OrderedDict()
 
        importers['Person'] = PersonImporter
 
        importers['PersonEmailAddress'] = PersonEmailAddressImporter
 
        importers['PersonPhoneNumber'] = PersonPhoneNumberImporter
 
        importers['PersonMailingAddress'] = PersonMailingAddressImporter
 
        importers['User'] = UserImporter
 
        importers['AdminUser'] = AdminUserImporter
 
        importers['Message'] = MessageImporter
 
        importers['MessageRecipient'] = MessageRecipientImporter
 
        importers['Store'] = StoreImporter
 
        importers['StorePhoneNumber'] = StorePhoneNumberImporter
 
        importers['Employee'] = EmployeeImporter
 
        importers['EmployeeStore'] = EmployeeStoreImporter
 
        importers['EmployeeEmailAddress'] = EmployeeEmailAddressImporter
 
        importers['EmployeePhoneNumber'] = EmployeePhoneNumberImporter
 
        importers['ScheduledShift'] = ScheduledShiftImporter
 
        importers['WorkedShift'] = WorkedShiftImporter
 
        importers['Customer'] = CustomerImporter
 
        importers['CustomerGroup'] = CustomerGroupImporter
 
        importers['CustomerGroupAssignment'] = CustomerGroupAssignmentImporter
 
        importers['CustomerPerson'] = CustomerPersonImporter
 
        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
 
    def local_title(self):
 
        return self.config.node_title()
 

	
 
    def make_host_session(self):
 
        return Session(bind=self.config.rattail_engines[self.dbkey])
 

	
 

	
 
class FromRattailToRattailExport(FromRattailToRattailBase, FromRattailHandler, ToRattailHandler):
 
    """
 
    Handler for Rattail (local) -> Rattail (other) data export.
 
    """
 

	
 
    @property
 
    def host_title(self):
 
        return self.config.node_title()
 

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

	
 
    def make_session(self):
 
        return Session(bind=self.config.rattail_engines[self.dbkey])
 

	
 

	
 
@@ -319,72 +320,75 @@ class DepositLinkImporter(FromRattail, model.DepositLinkImporter):
 

	
 
class TaxImporter(FromRattail, model.TaxImporter):
 
    pass
 

	
 
class InventoryAdjustmentReasonImporter(FromRattail, model.InventoryAdjustmentReasonImporter):
 
    pass
 

	
 
class BrandImporter(FromRattail, model.BrandImporter):
 
    pass
 

	
 
class ProductImporter(FromRattail, model.ProductImporter):
 

	
 
    # TODO...
 
    @property
 
    def simple_fields(self):
 
        fields = super(ProductImporter, self).simple_fields
 
        fields.remove('regular_price_uuid')
 
        fields.remove('current_price_uuid')
 
        return fields
 

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

	
 
        # make sure potential unit items (i.e. rows with NULL unit_uuid) come
 
        # 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)