Changeset - 4c19bc4118ff
[Not reviewed]
0 7 1
Lance Edgar (lance) - 3 years ago 2021-12-22 16:30:38
lance@edbob.org
Add basic "pending product" support for new custorder batch
8 files changed with 424 insertions and 28 deletions:
0 comments (0 inline, 0 general)
rattail/app.py
Show inline comments
 
@@ -526,12 +526,32 @@ class AppHandler(object):
 
        Returns a "properly formatted" string based on the given phone number.
 
        """
 
        from rattail.db.util import format_phone_number
 

	
 
        return format_phone_number(number)
 

	
 
    def make_gpc(self, value, **kwargs):
 
        """
 
        Convenience method; shortcut to
 
        :meth:`rattail.products.ProductsHandler.make_upc()`.
 
        """
 
        products_handler = self.get_products_handler()
 
        return products_handler.make_gpc(value, **kwargs)
 

	
 
    def render_gpc(self, value, **kwargs):
 
        """
 
        Returns a human-friendly display string for the given "upc" value.
 

	
 
        :param value: Should be a :class:`~rattail.gpc.GPC` instance.
 
        """
 
        if value:
 
            return value.pretty()
 

	
 
    # TODO: decide which one of these to stick with
 
    render_upc = render_gpc
 

	
 
    def render_currency(self, value, scale=2, **kwargs):
 
        """
 
        Must return a human-friendly display string for the given currency
 
        value, e.g. ``Decimal('4.20')`` becomes ``"$4.20"``.
 
        """
 
        if value is not None:
rattail/batch/custorder.py
Show inline comments
 
@@ -28,13 +28,12 @@ from __future__ import unicode_literals, absolute_import, division
 

	
 
import re
 
import decimal
 

	
 
import six
 
import sqlalchemy as sa
 
from sqlalchemy import orm
 

	
 
from rattail.db import model
 
from rattail.batch import BatchHandler
 
from rattail.util import OrderedDict
 
from rattail.time import localtime
 

	
 
@@ -81,22 +80,32 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        all, to choose from existing contact info options for the
 
        customer, vs. they just have to go with whatever the handler
 
        auto-provides.
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'new_orders.allow_contact_info_choice',
 
                                   default=True)
 
                                   default=False)
 

	
 
    def should_restrict_contact_info(self):
 
    def allow_contact_info_creation(self):
 
        """
 
        Returns a boolean indicating whether contact info should be
 
        "restricted" - i.e. user can only choose from existing contact
 
        info and cannot override by e.g. entering a new phone number.
 
        Returns a boolean indicating whether the user is allowed to
 
        enter *new* contact info for the customer.  This setting
 
        should only be honored if :meth:`allow_contact_info_choice()`
 
        also returns true.
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'new_orders.restrict_contact_info',
 
                                   'new_orders.allow_contact_info_create',
 
                                   default=False)
 

	
 
    def allow_unknown_product(self):
 
        """
 
        Returns a boolean indicating whether "unknown" products are
 
        allowed on new orders.
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'allow_unknown_product',
 
                                   default=False)
 

	
 
    def product_price_may_be_questionable(self):
 
        """
 
        Returns a boolean indicating whether "any" product's price may
 
        be questionable.  So this isn't saying that a price *is*
 
@@ -457,18 +466,21 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        """
 
        products = self.app.get_products_handler()
 
        vendor = product.cost.vendor if product.cost else None
 
        info = {
 
            'uuid': product.uuid,
 
            'upc': six.text_type(product.upc),
 
            'item_id': product.item_id,
 
            'scancode': product.scancode,
 
            'upc_pretty': product.upc.pretty(),
 
            'brand_name': product.brand.name if product.brand else None,
 
            'description': product.description,
 
            'size': product.size,
 
            'full_description': product.full_description,
 
            'case_quantity': self.app.render_quantity(self.get_case_size_for_product(product)),
 
            'unit_price': float(product.regular_price.price) if product.regular_price and product.regular_price.price is not None else None,
 
            'unit_price_display': products.render_price(product.regular_price),
 
            'department_name': product.department.name if product.department else None,
 
            'vendor_name': vendor.name if vendor else None,
 
            'url': products.get_url(product),
 
            'image_url': products.get_image_url(product),
 
            'uom_choices': self.uom_choices_for_product(product),
 
@@ -507,12 +519,34 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            info['key'] = info['upc_pretty']
 
        else:
 
            info['key'] = getattr(product, key, info['upc_pretty'])
 

	
 
        return info
 

	
 
    def uom_choices_for_row(self, row):
 
        """
 
        Return a list of UOM choices for the given batch row.
 
        """
 
        if row.product:
 
            return self.uom_choices_for_product(row.product)
 

	
 
        choices = []
 

	
 
        # Each
 
        unit_name = self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_EACH]
 
        choices.append({'key': self.enum.UNIT_OF_MEASURE_EACH,
 
                        'value': unit_name})
 

	
 
        # Case
 
        case_text = self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_CASE]
 
        if case_text:
 
            choices.append({'key': self.enum.UNIT_OF_MEASURE_CASE,
 
                            'value': case_text})
 

	
 
        return choices
 

	
 
    def uom_choices_for_product(self, product):
 
        """
 
        Return a list of UOM choices for the given product.
 
        """
 
        choices = []
 

	
 
@@ -572,41 +606,141 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        row.order_uom = order_uom
 
        if 'price_needs_confirmation' in kwargs:
 
            row.price_needs_confirmation = kwargs['price_needs_confirmation']
 
        self.add_row(batch, row)
 
        return row
 

	
 
    def add_pending_product(self, batch, pending_info,
 
                            order_quantity, order_uom,
 
                            **kwargs):
 
        """
 
        Add a new row to the batch, for the given "pending" product
 
        and order quantity.
 

	
 
        :param batch: Reference to the current order batch.
 

	
 
        :param pending_info: Dict of information about a new pending product.
 

	
 
        :param order_quantity: Quantity of the item to be added to the order.
 

	
 
        :param order_uom: Unit of measure for the order quantity.
 
        """
 
        session = self.app.get_session(batch)
 

	
 
        # make a new pending product
 
        pending_product = self.make_pending_product(**pending_info)
 
        session.add(pending_product)
 
        session.flush()
 

	
 
        # TODO: seems like make_pending_product() should do this?
 
        if pending_product.department and not pending_product.department_name:
 
            pending_product.department_name = pending_product.department.name
 

	
 
        # make a new row, w/ pending product
 
        row = self.make_row()
 
        row.pending_product = pending_product
 
        row.order_quantity = order_quantity
 
        row.order_uom = order_uom
 
        self.add_row(batch, row)
 
        return row
 

	
 
    def make_pending_product(self, **kwargs):
 
        products_handler = self.app.get_products_handler()
 
        if 'status_code' not in kwargs:
 
            kwargs['status_code'] = self.enum.PENDING_PRODUCT_STATUS_PENDING
 
        return products_handler.make_pending_product(**kwargs)
 

	
 
    def update_pending_product(self, row, data):
 
        """
 
        Update the pending product data for the given batch row.
 
        """
 
        # create pending product if needed
 
        pending = row.pending_product
 
        if not pending:
 
            pending = self.make_pending_product(**data)
 

	
 
        simple_fields = [
 
            'scancode',
 
            'item_id',
 
            'item_type',
 
            'department_name',
 
            'department_uuid',
 
            'brand_name',
 
            'brand_uuid',
 
            'description',
 
            'size',
 
            'vendor_name',
 
            'vendor_uuid',
 
            'vendor_item_code',
 
            'special_order',
 
            'notes',
 
        ]
 

	
 
        decimal_fields = [
 
            'unit_cost',
 
            'case_size',
 
            'case_cost',
 
            'regular_price_amount',
 
        ]
 

	
 
        # update pending product info
 
        if 'upc' in data:
 
            pending.upc = self.app.make_gpc(data['upc']) if data['upc'] else None
 
        for field in simple_fields:
 
            if field in data:
 
                setattr(pending, field, data[field])
 
        for field in decimal_fields:
 
            if field in data:
 
                value = data[field]
 
                value = decimal.Decimal(value) if value is not None else None
 
                setattr(pending, field, value)
 

	
 
        # refresh the row per new pending product info
 
        self.refresh_row(row)
 

	
 
    def refresh_row(self, row):
 
        if not row.product:
 
            if row.item_entry:
 
                session = orm.object_session(row)
 
                # TODO: should do more than just query for uuid here
 
                product = session.query(model.Product).get(row.item_entry)
 
                session = self.app.get_session(row)
 
                product = self.locate_product_for_entry(session, row.item_entry)
 
                if product:
 
                    row.product = product
 
            if not row.product:
 
            if not row.product and not row.pending_product:
 
                row.status_code = row.STATUS_PRODUCT_NOT_FOUND
 
                return
 

	
 
        product = row.product
 
        row.product_upc = product.upc
 
        row.product_brand = six.text_type(product.brand or "")
 
        row.product_description = product.description
 
        row.product_size = product.size
 
        row.product_weighed = product.weighed
 
        row.case_quantity = self.get_case_size_for_product(product)
 
        if product:
 

	
 
        department = product.department
 
        row.department_number = department.number if department else None
 
        row.department_name = department.name if department else None
 
            row.product_upc = product.upc
 
            row.product_item_id = product.item_id
 
            row.product_scancode = product.scancode
 
            row.product_brand = six.text_type(product.brand or "")
 
            row.product_description = product.description
 
            row.product_size = product.size
 
            row.product_weighed = product.weighed
 
            row.case_quantity = self.get_case_size_for_product(product)
 

	
 
        cost = product.cost
 
        row.product_unit_cost = cost.unit_cost if cost else None
 
            department = product.department
 
            row.department_number = department.number if department else None
 
            row.department_name = department.name if department else None
 

	
 
        regprice = product.regular_price
 
        row.unit_price = regprice.price if regprice else None
 
            cost = product.cost
 
            row.product_unit_cost = cost.unit_cost if cost else None
 

	
 
            regprice = product.regular_price
 
            row.unit_regular_price = regprice.price if regprice else None
 

	
 
            curprice = product.current_price
 
            row.unit_sale_price = curprice.price if curprice else None
 
            row.sale_ends = curprice.ends if curprice else None
 

	
 
            row.unit_price = row.unit_sale_price or row.unit_regular_price
 

	
 
        else:
 
            self.refresh_row_from_pending_product(row)
 

	
 
        # we need to know if total price is updated
 
        old_total = row.total_price
 

	
 
        # maybe update total price
 
        if row.unit_price is None:
 
@@ -622,25 +756,86 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        if row.total_price != old_total:
 
            batch = row.batch
 
            batch.total_price = ((batch.total_price or 0)
 
                                 + (row.total_price or 0)
 
                                 - (old_total or 0))
 

	
 
        row.status_code = row.STATUS_OK
 
        if not row.product and row.pending_product:
 
            row.status_code = row.STATUS_PENDING_PRODUCT
 
        else:
 
            row.status_code = row.STATUS_OK
 

	
 
    def refresh_row_from_pending_product(self, row):
 
        """
 
        Refresh basic row attributes from its pending product.
 
        """
 
        pending = row.pending_product
 
        if not pending:
 
            return
 

	
 
        row.product_upc = pending.upc
 
        row.product_item_id = pending.item_id
 
        row.product_scancode = pending.scancode
 
        row.product_brand = six.text_type(pending.brand or pending.brand_name or '')
 
        row.product_description = pending.description
 
        row.product_size = pending.size
 
        # TODO: is this even important?  pending does not have it
 
        # row.product_weighed = pending.weighed
 
        row.case_quantity = pending.case_size
 

	
 
        if pending.department:
 
            row.department = pending.department
 
            row.department_number = pending.department.number
 
            row.department_name = pending.department.name
 
        elif pending.department_name:
 
            row.department = None
 
            row.department_number = None
 
            row.department_name = pending.department_name
 
        else:
 
            row.department = None
 
            row.department_number = None
 
            row.department_name = None
 

	
 
        row.product_unit_cost = pending.unit_cost
 
        row.unit_regular_price = pending.regular_price_amount
 
        row.unit_price = row.unit_regular_price
 
        row.unit_sale_price = None
 
        row.sale_ends = None
 

	
 
    def remove_row(self, row):
 
        batch = row.batch
 

	
 
        if not row.removed:
 
            row.removed = True
 

	
 
            if row.total_price:
 
                batch.total_price = (batch.total_price or 0) - row.total_price
 

	
 
        self.refresh_batch_status(batch)
 

	
 
    def delete_extra_data(self, batch, progress=None, **kwargs):
 

	
 
        # do the normal stuff (e.g. delete input files)
 
        super(CustomerOrderBatchHandler, self).delete_extra_data(
 
            batch, progress=progress, **kwargs)
 

	
 
        session = self.app.get_session(batch)
 

	
 
        # delete pending customer if present
 
        pending = batch.pending_customer
 
        if pending:
 
            batch.pending_customer = None
 
            session.delete(pending)
 

	
 
        # delete any pending products if present
 
        for row in batch.data_rows:
 
            pending = row.pending_product
 
            if pending:
 
                row.pending_product = None
 
                session.delete(pending)
 

	
 
    def execute(self, batch, user=None, progress=None, **kwargs):
 
        """
 
        Default behavior here will create and return a new rattail
 
        Customer Order.  It also may "add contact info" e.g. to the
 
        customer record.  Override as needed.
 
        """
 
@@ -710,12 +905,13 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        order.status_code = self.enum.CUSTORDER_STATUS_ORDERED
 
        for field in batch_fields:
 
            setattr(order, field, getattr(batch, field))
 

	
 
        row_fields = [
 
            'product',
 
            'pending_product',
 
            'product_upc',
 
            'product_brand',
 
            'product_description',
 
            'product_size',
 
            'product_weighed',
 
            'department_number',
 
@@ -744,13 +940,13 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            # set initial status and attach events
 
            self.set_initial_item_status(item, user)
 

	
 
        self.progress_loop(convert, batch.active_rows(), progress,
 
                           message="Converting batch rows to order items")
 

	
 
        session = orm.object_session(batch)
 
        session = self.app.get_session(batch)
 
        session.add(order)
 
        session.flush()
 

	
 
        return order
 

	
 
    def set_initial_item_status(self, item, user, **kwargs):
rattail/db/alembic/versions/08cc2ef12c18_add_more_custorder_stuff.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
"""add more custorder stuff
 

	
 
Revision ID: 08cc2ef12c18
 
Revises: 3f3523703313
 
Create Date: 2021-12-21 17:00:11.841243
 

	
 
"""
 

	
 
from __future__ import unicode_literals
 

	
 
# revision identifiers, used by Alembic.
 
revision = '08cc2ef12c18'
 
down_revision = '3f3523703313'
 
branch_labels = None
 
depends_on = None
 

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

	
 

	
 

	
 
def upgrade():
 

	
 
    # pending_product
 
    op.add_column('pending_product', sa.Column('vendor_name', sa.String(length=50), nullable=True))
 
    op.add_column('pending_product', sa.Column('vendor_uuid', sa.String(length=32), nullable=True))
 
    op.add_column('pending_product', sa.Column('vendor_item_code', sa.String(length=20), nullable=True))
 
    op.add_column('pending_product', sa.Column('unit_cost', sa.Numeric(precision=10, scale=5), nullable=True))
 
    op.create_foreign_key('pending_product_fk_vendor', 'pending_product', 'vendor', ['vendor_uuid'], ['uuid'])
 

	
 
    # batch_custorder_row
 
    op.add_column('batch_custorder_row', sa.Column('pending_product_uuid', sa.String(length=32), nullable=True))
 
    op.create_foreign_key('batch_custorder_row_fk_pending_product', 'batch_custorder_row', 'pending_product', ['pending_product_uuid'], ['uuid'])
 
    op.add_column('batch_custorder_row', sa.Column('unit_regular_price', sa.Numeric(precision=8, scale=3), nullable=True))
 
    op.add_column('batch_custorder_row', sa.Column('unit_sale_price', sa.Numeric(precision=8, scale=3), nullable=True))
 
    op.add_column('batch_custorder_row', sa.Column('sale_ends', sa.DateTime(), nullable=True))
 
    op.add_column('batch_custorder_row', sa.Column('product_scancode', sa.String(length=14), nullable=True))
 
    op.add_column('batch_custorder_row', sa.Column('product_item_id', sa.String(length=50), nullable=True))
 

	
 
    # custorder_item
 
    op.add_column('custorder_item', sa.Column('pending_product_uuid', sa.String(length=32), nullable=True))
 
    op.create_foreign_key('custorder_item_fk_pending_product', 'custorder_item', 'pending_product', ['pending_product_uuid'], ['uuid'])
 
    op.add_column('custorder_item', sa.Column('unit_regular_price', sa.Numeric(precision=8, scale=3), nullable=True))
 
    op.add_column('custorder_item', sa.Column('unit_sale_price', sa.Numeric(precision=8, scale=3), nullable=True))
 
    op.add_column('custorder_item', sa.Column('sale_ends', sa.DateTime(), nullable=True))
 
    op.add_column('custorder_item', sa.Column('product_scancode', sa.String(length=14), nullable=True))
 
    op.add_column('custorder_item', sa.Column('product_item_id', sa.String(length=50), nullable=True))
 

	
 

	
 
def downgrade():
 

	
 
    # custorder_item
 
    # op.drop_column('custorder_item', 'product_item_id')
 
    # op.drop_column('custorder_item', 'product_scancode')
 
    op.drop_column('custorder_item', 'sale_ends')
 
    op.drop_column('custorder_item', 'unit_sale_price')
 
    op.drop_column('custorder_item', 'unit_regular_price')
 
    op.drop_constraint('custorder_item_fk_pending_product', 'custorder_item', type_='foreignkey')
 
    op.drop_column('custorder_item', 'pending_product_uuid')
 

	
 
    # batch_custorder_row
 
    # op.drop_column('batch_custorder_row', 'product_item_id')
 
    # op.drop_column('batch_custorder_row', 'product_scancode')
 
    op.drop_column('batch_custorder_row', 'sale_ends')
 
    op.drop_column('batch_custorder_row', 'unit_sale_price')
 
    op.drop_column('batch_custorder_row', 'unit_regular_price')
 
    op.drop_constraint('batch_custorder_row_fk_pending_product', 'batch_custorder_row', type_='foreignkey')
 
    op.drop_column('batch_custorder_row', 'pending_product_uuid')
 

	
 
    # pending_product
 
    op.drop_constraint('pending_product_fk_vendor', 'pending_product', type_='foreignkey')
 
    op.drop_column('pending_product', 'unit_cost')
 
    op.drop_column('pending_product', 'vendor_item_code')
 
    op.drop_column('pending_product', 'vendor_uuid')
 
    op.drop_column('pending_product', 'vendor_name')
rattail/db/model/batch/custorder.py
Show inline comments
 
@@ -91,17 +91,19 @@ class CustomerOrderBatchRow(BatchRowMixin, CustomerOrderItemBase, Base):
 
                                    name='batch_custorder_row_fk_item'),
 
        )
 

	
 
    STATUS_OK                           = 1
 
    STATUS_PRODUCT_NOT_FOUND            = 2
 
    # STATUS_PRICE_NEEDS_CONFIRMATION     = 3
 
    STATUS_PENDING_PRODUCT              = 4
 

	
 
    STATUS = {
 
        STATUS_OK                       : "ok",
 
        STATUS_PRODUCT_NOT_FOUND        : "product not found",
 
        # STATUS_PRICE_NEEDS_CONFIRMATION : "price needs to be confirmed",
 
        STATUS_PENDING_PRODUCT          : "has pending product",
 
    }
 

	
 
    item_entry = sa.Column(sa.String(length=32), nullable=True, doc="""
 
    Raw entry value, as obtained from the initial data source, and which is
 
    used to locate the product within the system.  This raw value is preserved
 
    in case the initial lookup fails and a refresh must attempt further
rattail/db/model/custorders.py
Show inline comments
 
@@ -32,13 +32,14 @@ import six
 
import sqlalchemy as sa
 
from sqlalchemy import orm
 
from sqlalchemy.ext.orderinglist import ordering_list
 
from sqlalchemy.ext.declarative import declared_attr
 

	
 
from rattail.db.model import Base, uuid_column
 
from rattail.db.model import Store, Customer, PendingCustomer, Person, Product, User, Note
 
from rattail.db.model import (Store, Customer, PendingCustomer, Person,
 
                              Product, PendingProduct, User, Note)
 
from rattail.db.types import GPCType
 

	
 

	
 
class CustomerOrderBase(object):
 
    """
 
    Base class for customer orders; defines common fields.
 
@@ -181,28 +182,49 @@ class CustomerOrderItemBase(object):
 
    @classmethod
 
    def __customer_order_item_table_args__(cls):
 
        table_name = cls.__tablename__
 
        return (
 
            sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'],
 
                                    name='{}_fk_product'.format(table_name)),
 
            sa.ForeignKeyConstraint(['pending_product_uuid'], ['pending_product.uuid'],
 
                                    name='{}_fk_pending_product'.format(table_name)),
 
        )
 

	
 
    product_uuid = sa.Column(sa.String(length=32), nullable=True)
 

	
 
    @declared_attr
 
    def product(cls):
 
        return orm.relationship(
 
            Product,
 
            doc="""
 
            Reference to the master product record for the line item.
 
            """)
 

	
 
    pending_product_uuid = sa.Column(sa.String(length=32), nullable=True)
 

	
 
    @declared_attr
 
    def pending_product(cls):
 
        return orm.relationship(
 
            PendingProduct,
 
            doc="""
 
            Reference to the *pending* product record for the order
 
            item, if applicable.
 
            """)
 

	
 
    product_upc = sa.Column(GPCType(), nullable=True, doc="""
 
    UPC for the product associated with the row.
 
    """)
 

	
 
    product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
 
    Scancode for the product, if applicable.
 
    """)
 

	
 
    product_item_id = sa.Column(sa.String(length=50), nullable=True, doc="""
 
    Item ID for the product, if applicable.
 
    """)
 

	
 
    product_brand = sa.Column(sa.String(length=100), nullable=True, doc="""
 
    Brand name for the product being ordered.  This should be a cache of the
 
    relevant :attr:`Brand.name`.
 
    """)
 

	
 
    product_description = sa.Column(sa.String(length=60), nullable=True, doc="""
 
@@ -270,12 +292,25 @@ class CustomerOrderItemBase(object):
 
    Unit price for the product being ordered.  This is the price which is
 
    quoted to the customer and/or charged to the customer, but for a unit only
 
    and *before* any discounts are applied.  It generally will be a cache of
 
    the relevant :attr:`ProductPrice.price`.
 
    """)
 

	
 
    unit_regular_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
 
    Regular price for the item unit.  Note that if a sale price is in
 
    effect, then this may differ from :attr:`unit_price`.
 
    """)
 

	
 
    unit_sale_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
 
    Sale price for the item unit, if applicable.
 
    """)
 

	
 
    sale_ends = sa.Column(sa.DateTime(), nullable=True, doc="""
 
    End date/time for the sale in effect, if any.
 
    """)
 

	
 
    discount_percent = sa.Column(sa.Numeric(precision=5, scale=3), nullable=False, default=0, doc="""
 
    Discount percentage which will be applied to the product's price as part of
 
    calculating the :attr:`total_price` for the item.
 
    """)
 

	
 
    total_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
rattail/db/model/products.py
Show inline comments
 
@@ -987,12 +987,13 @@ class PendingProduct(Base):
 
    """
 
    __tablename__ = 'pending_product'
 
    __table_args__ = (
 
        sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], name='pending_product_fk_user'),
 
        sa.ForeignKeyConstraint(['department_uuid'], ['department.uuid'], name='pending_product_fk_department'),
 
        sa.ForeignKeyConstraint(['brand_uuid'], ['brand.uuid'], name='pending_product_fk_brand'),
 
        sa.ForeignKeyConstraint(['vendor_uuid'], ['vendor.uuid'], name='pending_product_fk_vendor'),
 
    )
 

	
 
    uuid = uuid_column()
 

	
 
    user_uuid = sa.Column(sa.String(length=32), nullable=False)
 
    user = orm.relationship(
 
@@ -1020,13 +1021,21 @@ class PendingProduct(Base):
 
    brand_name = sa.Column(sa.String(length=100), nullable=True)
 
    brand_uuid = sa.Column(sa.String(length=32), nullable=True)
 
    brand = orm.relationship(Brand)
 

	
 
    description = sa.Column(sa.String(length=255), nullable=True)
 
    size = sa.Column(sa.String(length=30), nullable=True)
 

	
 
    vendor_name = sa.Column(sa.String(length=50), nullable=True)
 
    vendor_uuid = sa.Column(sa.String(length=32), nullable=True)
 
    vendor = orm.relationship(Vendor)
 

	
 
    vendor_item_code = sa.Column(sa.String(length=20), nullable=True)
 
    unit_cost = sa.Column(sa.Numeric(precision=10, scale=5), nullable=True)
 
    case_size = sa.Column(sa.Numeric(precision=9, scale=4), nullable=True)
 

	
 
    regular_price_amount = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True)
 
    special_order = sa.Column(sa.Boolean(), nullable=True)
 
    notes = sa.Column(sa.Text(), nullable=True)
 

	
 
    # workflow fields
 
    status_code = sa.Column(sa.Integer(), nullable=True, doc="""
rattail/products.py
Show inline comments
 
@@ -43,12 +43,58 @@ class ProductsHandler(GenericHandler):
 

	
 
    A products handler of course should get the final say in how products are
 
    handled.  This means everything from pricing, to whether or not a
 
    particular product can be deleted, etc.
 
    """
 

	
 
    def make_gpc(self, value, **kwargs):
 
        """
 
        Try to convert the given value to a :class:`~rattail.gpc.GPC`
 
        instance, and return the result.
 
        """
 
        return GPC(value)
 

	
 
    def make_full_description(self, product=None,
 
                              brand_name=None, description=None, size=None,
 
                              **kwargs):
 
        """
 
        Return a "full" description for the given product, or
 
        attributes thereof.
 

	
 
        :param product: Optional, but can be a reference to either a
 
           :class:`~rattail.db.model.products.Product` or
 
           :class:`~rattail.db.model.products.PendingProduct`
 
           instance.
 

	
 
        :param brand_name: Optional; brand name as string.  If not
 
           provided then will be obtained from ``product`` param.
 

	
 
        :param description: Optional; description as string.  If not
 
           provided then will be obtained from ``product`` param.
 

	
 
        :param size: Optional; size as string.  If not provided then
 
           will be obtained from ``product`` param.
 
        """
 
        from rattail.db.util import make_full_description
 

	
 
        model = self.model
 

	
 
        if brand_name is None and product:
 
            if product.brand:
 
                brand_name = product.brand.name
 
            elif isinstance(product, model.PendingProduct):
 
                brand_name = product.brand_name
 

	
 
        if description is None and product:
 
            description = product.description
 

	
 
        if size is None and product:
 
            size = product.size
 

	
 
        return make_full_description(brand_name, description, size)
 

	
 
    def find_products_by_key(self, session, value, **kwargs):
 
        """
 
        Locate any products where the "key" matches the given value.
 

	
 
        By default this search is as "thorough" as possible and may
 
        return multiple results in some cases where you might not
 
@@ -249,12 +295,23 @@ class ProductsHandler(GenericHandler):
 
            return self.app.render_currency(price.price)
 
        if price.pack_price is not None:
 
            return "{} / {}".format(
 
                self.app.render_currency(price.pack_price),
 
                price.pack_multiple)
 

	
 
    def make_pending_product(self, **kwargs):
 
        """
 
        Create and return a new
 
        :class:`~rattail.db.model.products.PendingProduct` instance,
 
        per the given kwargs.
 
        """
 
        model = self.model
 
        kwargs.setdefault('status_code', self.enum.PENDING_PRODUCT_STATUS_PENDING)
 
        pending = model.PendingProduct(**kwargs)
 
        return pending
 

	
 
    def get_uom_sil_codes(self, session, uppercase=False, **kwargs):
 
        """
 
        This should return a dict, keys of which are UOM abbreviation strings,
 
        and values of which are corresponding SIL code strings.
 

	
 
        :param session: Reference to current Rattail DB session.
rattail/templates/mail/new_email_requested.html.mako
Show inline comments
 
@@ -10,12 +10,12 @@
 

	
 
    <ul>
 
      <li>
 
        Contact:&nbsp; ${contact_id} ${contact}
 
      </li>
 
      <li>
 
        New Email Address:&nbsp; ${email address}
 
        New Email Address:&nbsp; ${email_address}
 
      </li>
 
    </ul>
 

	
 
  </body>
 
</html>
0 comments (0 inline, 0 general)