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
 
@@ -520,24 +520,44 @@ class AppHandler(object):
 
        normal = self.normalize_phone_number(number)
 
        if len(normal) != 10:
 
            return "Phone number must have 10 digits"
 

	
 
    def format_phone_number(self, number):
 
        """
 
        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:
 
            if value < 0:
 
                fmt = "(${{:0,.{}f}})".format(scale)
 
                return fmt.format(0 - value)
 
            fmt = "${{:0,.{}f}}".format(scale)
 
            return fmt.format(value)
 

	
rattail/batch/custorder.py
Show inline comments
 
@@ -22,25 +22,24 @@
 
################################################################################
 
"""
 
Handler for "customer order" batches
 
"""
 

	
 
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
 

	
 

	
 
class CustomerOrderBatchHandler(BatchHandler):
 
    """
 
    Handler for all "customer order" batches, regardless of "mode".  The
 
    handler must inspect the
 
    :attr:`~rattail.db.model.batch.custorder.CustomerOrderBatch.mode` attribute
 
@@ -75,34 +74,44 @@ class CustomerOrderBatchHandler(BatchHandler):
 
                                   'new_order_requires_customer',
 
                                   default=False)
 

	
 
    def allow_contact_info_choice(self):
 
        """
 
        Returns a boolean indicating whether the user is allowed at
 
        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*
 
        questionable but rather that it *may* be, if the user
 
        indicates it.  (That checkbox is only shown for the user if
 
        this flag is true.)
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'product_price_may_be_questionable',
 
@@ -451,30 +460,33 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        return list(products.values())
 

	
 
    def get_product_info(self, batch, product, **kwargs):
 
        """
 
        Return a data dict containing misc. info pertaining to the
 
        given product, for the order batch.
 
        """
 
        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),
 
        }
 

	
 
        # TODO: this was somewhat copied from
 
        # tailbone.views.products.render_price() - should make it part
 
        # of the products handler instead?
 
        sale_price = None
 
@@ -501,24 +513,46 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            case_price = case_price.quantize(decimal.Decimal('0.01'))
 
        info['case_price'] = six.text_type(case_price) if case_price is not None else None
 
        info['case_price_display'] = self.app.render_currency(case_price)
 

	
 
        key = self.config.product_key()
 
        if key == 'upc':
 
            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 = []
 

	
 
        # Each
 
        if not product or not product.weighed:
 
            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})
 

	
 
@@ -566,87 +600,248 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        quantity.
 
        """
 
        row = self.make_row()
 
        row.item_entry = product.uuid
 
        row.product = product
 
        row.order_quantity = order_quantity
 
        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:
 
            row.total_price = None
 
        elif not row.unit_price:
 
            row.total_price = 0
 
        else:
 
            row.total_price = row.unit_price * row.order_quantity
 
            if row.order_uom == self.enum.UNIT_OF_MEASURE_CASE:
 
                row.total_price *= (row.case_quantity or 1)
 

	
 
        # update total price for batch too, if it changed
 
        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.
 
        """
 
        order = self.make_new_order(batch, user=user, progress=progress, **kwargs)
 
        self.update_contact_info(batch, user)
 
        return order
 

	
 
    def update_contact_info(self, batch, user, **kwargs):
 
        """
 
@@ -704,24 +899,25 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            'email_address',
 
            'total_price',
 
        ]
 

	
 
        order = model.CustomerOrder()
 
        order.created_by = user
 
        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',
 
            'department_name',
 
            'case_quantity',
 
            'order_quantity',
 
            'order_uom',
 
            'product_unit_cost',
 
            'unit_price',
 
@@ -738,25 +934,25 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            item = model.CustomerOrderItem()
 
            item.sequence = i
 
            for field in row_fields:
 
                setattr(item, field, getattr(row, field))
 
            order.items.append(item)
 

	
 
            # 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):
 
        """
 
        Set the initial status for the given order item, and attach
 
        any events.
 

	
 
        The first logical status is ``CUSTORDER_ITEM_STATUS_INITIATED``
 
        and an item may stay there if there is some other step(s)
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
 
@@ -85,29 +85,31 @@ class CustomerOrderBatchRow(BatchRowMixin, CustomerOrderItemBase, Base):
 
    __batch_class__ = CustomerOrderBatch
 

	
 
    @declared_attr
 
    def __table_args__(cls):
 
        return cls.__batchrow_table_args__() + cls.__customer_order_item_table_args__() + (
 
            sa.ForeignKeyConstraint(['item_uuid'], ['custorder_item.uuid'],
 
                                    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
 
    lookup(s) later.  Only used by certain batch handlers in practice.
 
    """)
 

	
 
    item_uuid = sa.Column(sa.String(length=32), nullable=True)
 
    item = orm.relationship(
 
        CustomerOrderItem,
rattail/db/model/custorders.py
Show inline comments
 
@@ -26,25 +26,26 @@ Data Models for Customer Orders
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import datetime
 

	
 
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.
 
    """
 

	
 
    @declared_attr
 
    def __table_args__(cls):
 
        return cls.__customer_order_table_args__()
 

	
 
@@ -175,40 +176,61 @@ class CustomerOrderItemBase(object):
 
    """
 

	
 
    @declared_attr
 
    def __table_args__(cls):
 
        return cls.__customer_order_item_table_args__()
 

	
 
    @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="""
 
    Primary description for the product being ordered.  This should be a cache
 
    of :attr:`Product.description`.
 
    """)
 

	
 
    product_size = sa.Column(sa.String(length=30), nullable=True, doc="""
 
    Size of the product being ordered.  This should be a cache of
 
@@ -264,24 +286,37 @@ class CustomerOrderItemBase(object):
 
    product_unit_cost = sa.Column(sa.Numeric(precision=9, scale=5), nullable=True, doc="""
 
    Unit cost of the product being ordered.  This should be a cache of the
 
    relevant :attr:`rattail.db.model.ProductCost.unit_cost`.
 
    """)
 

	
 
    unit_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
 
    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="""
 
    Full price (not including tax etc.) which the customer is asked to pay for the item.
 
    """)
 

	
 
    price_needs_confirmation = sa.Column(sa.Boolean(), nullable=True, doc="""
 
    Flag indicating that the price for this item should be confirmed
 
    by someone, before the order advances to the procurement phase.
rattail/db/model/products.py
Show inline comments
 
@@ -981,24 +981,25 @@ class ProductVolatile(Base):
 
    """)
 

	
 

	
 
class PendingProduct(Base):
 
    """
 
    A "pending" product record, used for new product entry workflow.
 
    """
 
    __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(
 
        'User',
 
        doc="""
 
        Reference to the :class:`~rattail:rattail.db.model.User` who
 
        first entered the record.
 
        """)
 

	
 
@@ -1014,25 +1015,33 @@ class PendingProduct(Base):
 
    item_type = sa.Column(sa.Integer(), nullable=True)
 

	
 
    department_name = sa.Column(sa.String(length=30), nullable=True)
 
    department_uuid = sa.Column(sa.String(length=32), nullable=True)
 
    department = orm.relationship(Department)
 

	
 
    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="""
 
    Status indicator for the new product record.
 
    """)
 

	
 
    @property
 
    def full_description(self):
 
        return make_full_description(self.brand_name,
rattail/products.py
Show inline comments
 
@@ -37,24 +37,70 @@ from rattail.gpc import GPC
 
from rattail.barcodes import upce_to_upca
 

	
 

	
 
class ProductsHandler(GenericHandler):
 
    """
 
    Base class and default implementation for product handlers.
 

	
 
    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
 
        expect them.  Please pass the :param:`only` param if you need
 
        a more focused search etc.
 

	
 
        :param session: Session for the Rattail database.
 

	
 
        :param value: Value to search for.  Can be a GPC object or
 
@@ -243,24 +289,35 @@ class ProductsHandler(GenericHandler):
 
                price.pack_multiple)
 
        if price.price is not None:
 
            if price.multiple is not None and price.multiple > 1:
 
                return "{} / {}".format(
 
                    self.app.render_currency(price.price),
 
                    price.multiple)
 
            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.
 
        :param uppercase: Set to ``True`` to cause all UOM abbreviations to be
 
           upper-cased when adding to the map.
 
        :returns: Dictionary containing all known UOM / SIL code mappings.
 
        """
 
        model = self.model
 

	
rattail/templates/mail/new_email_requested.html.mako
Show inline comments
 
@@ -4,18 +4,18 @@
 
    <h2>Contact Email Update Requested</h2>
 

	
 
    <p>
 
      A request to update a contact's email address was submitted by
 
      ${user_display}.
 
    </p>
 

	
 
    <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)