Changeset - 37b8de91180d
[Not reviewed]
0 4 1
Lance Edgar (lance) - 3 years ago 2021-10-18 18:28:07
lance@edbob.org
Add basic "price needs confirmation" support for custorder
5 files changed with 117 insertions and 9 deletions:
0 comments (0 inline, 0 general)
rattail/batch/custorder.py
Show inline comments
 
@@ -399,24 +399,26 @@ class CustomerOrderBatchHandler(BatchHandler):
 

	
 
    def add_product(self, batch, product, order_quantity, order_uom,
 
                    **kwargs):
 
        """
 
        Add a new row to the batch, for the given product and order
 
        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 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)
 
                if product:
 
                    row.product = product
 
            if not row.product:
 
@@ -554,37 +556,69 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            'product_description',
 
            'product_size',
 
            'product_weighed',
 
            'department_number',
 
            'department_name',
 
            'case_quantity',
 
            'order_quantity',
 
            'order_uom',
 
            'product_unit_cost',
 
            'unit_price',
 
            'discount_percent',
 
            'total_price',
 
            'price_needs_confirmation',
 
            'paid_amount',
 
            'payment_transaction_number',
 
        ]
 

	
 
        def convert(row, i):
 

	
 
            # add new order item
 
            item = model.CustomerOrderItem()
 
            item.sequence = i
 
            item.status_code = self.enum.CUSTORDER_ITEM_STATUS_INITIATED
 
            for field in row_fields:
 
                setattr(item, field, getattr(row, field))
 
            order.items.append(item)
 

	
 
            # attach event
 
            item.events.append(model.CustomerOrderItemEvent(
 
                type_code=self.enum.CUSTORDER_ITEM_EVENT_INITIATED,
 
                user=user))
 
            # 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.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)
 
        which must occur before the item is ready to proceed.  For
 
        instance the default logic will leave it there if the price
 
        needs to be confirmed, but you can override as needed, for
 
        instance if you require payment up-front.
 

	
 
        The second status is ``CUSTORDER_ITEM_STATUS_READY`` which
 
        indicates the item is ready to proceed.  The default logic
 
        will auto-advance the item to this status if the price does
 
        *not* need to be confirmed.  Again you may need to override
 
        e.g. to prevent this until up-front payment is received.
 
        """
 
        # set "initiated" status
 
        item.status_code = self.enum.CUSTORDER_ITEM_STATUS_INITIATED
 
        item.add_event(self.enum.CUSTORDER_ITEM_EVENT_INITIATED, user)
 

	
 
        # but if the price is good...
 
        if not item.price_needs_confirmation:
 

	
 
            # then we set "ready" status
 
            item.status_code = self.enum.CUSTORDER_ITEM_STATUS_READY
 
            item.status_text = "everything looks normal"
 

	
 
            item.add_event(self.enum.CUSTORDER_ITEM_EVENT_READY, user,
 
                           note=item.status_text)
rattail/db/alembic/versions/8856f697902d_add_custorder_item_status_text.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8 -*-
 
"""add custorder_item.status_text
 

	
 
Revision ID: 8856f697902d
 
Revises: 8b78ef45a36c
 
Create Date: 2021-10-18 11:51:34.326750
 

	
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
# revision identifiers, used by Alembic.
 
revision = '8856f697902d'
 
down_revision = '8b78ef45a36c'
 
branch_labels = None
 
depends_on = None
 

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

	
 

	
 

	
 
def upgrade():
 

	
 
    # custorder_item
 
    op.add_column('custorder_item', sa.Column('price_needs_confirmation', sa.Boolean(), nullable=True))
 
    op.add_column('custorder_item', sa.Column('status_text', sa.String(length=255), nullable=True))
 

	
 
    # batch_custorder_row
 
    op.add_column('batch_custorder_row', sa.Column('price_needs_confirmation', sa.Boolean(), nullable=True))
 

	
 

	
 
def downgrade():
 

	
 
    # batch_custorder_row
 
    op.drop_column('batch_custorder_row', 'price_needs_confirmation')
 

	
 
    # custorder_item
 
    op.drop_column('custorder_item', 'status_text')
 
    op.drop_column('custorder_item', 'price_needs_confirmation')
rattail/db/model/batch/custorder.py
Show inline comments
 
@@ -84,28 +84,30 @@ class CustomerOrderBatchRow(BatchRowMixin, CustomerOrderItemBase, Base):
 
    __tablename__ = 'batch_custorder_row'
 
    __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 = {
 
        STATUS_OK                       : "ok",
 
        STATUS_PRODUCT_NOT_FOUND        : "product not found",
 
        # STATUS_PRICE_NEEDS_CONFIRMATION : "price needs to be confirmed",
 
    }
 

	
 
    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
 
@@ -273,24 +273,35 @@ class CustomerOrderItemBase(object):
 
    the relevant :attr:`ProductPrice.price`.
 
    """)
 

	
 
    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.
 

	
 
    Items/rows with this flag set will probably indicate that also via
 
    their status.
 

	
 
    When the price is eventually confirmed by someone, this flag
 
    should be cleared and probably the status will update as well.
 
    """)
 

	
 
    paid_amount = sa.Column(sa.Numeric(precision=8, scale=3), nullable=False, default=0, doc="""
 
    Amount which the customer has paid toward the :attr:`total_price` of theitem.
 
    """)
 

	
 
    payment_transaction_number = sa.Column(sa.String(length=8), nullable=True, doc="""
 
    Transaction number in which payment for the order was taken, if applicable.
 
    """)
 

	
 
    def __str__(self):
 
        return str(self.product or "(no product)")
 

	
 

	
 
@@ -313,24 +324,36 @@ class CustomerOrderItem(CustomerOrderItemBase, Base):
 
    order = orm.relationship(CustomerOrder, back_populates='items', doc="""
 
    Reference to the :class:`CustomerOrder` instance to which the item belongs.
 
    """)
 

	
 
    sequence = sa.Column(sa.Integer(), nullable=False, doc="""
 
    Numeric sequence for the item, i.e. its "line number".  These values should
 
    obviously increment in sequence and be unique within the context of a
 
    single order.
 
    """)
 

	
 
    status_code = sa.Column(sa.Integer(), nullable=False)
 

	
 
    status_text = sa.Column(sa.String(length=255), nullable=True, doc="""
 
    Text which may briefly explain the batch status code, if needed.
 
    """)
 

	
 
    def add_event(self, type_code, user, **kwargs):
 
        """
 
        Convenience method to add an event for the order item.
 
        """
 
        self.events.append(CustomerOrderItemEvent(type_code=type_code,
 
                                                  user=user,
 
                                                  **kwargs))
 

	
 

	
 
class CustomerOrderItemEvent(Base):
 
    """
 
    An event in the life of a customer order item
 
    """
 
    __tablename__ = 'custorder_item_event'
 
    __table_args__ = (
 
        sa.ForeignKeyConstraint(['item_uuid'], ['custorder_item.uuid'], name='custorder_item_event_fk_item'),
 
        sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], name='custorder_item_event_fk_user'),
 
    )
 

	
 
    uuid = uuid_column()
rattail/enum.py
Show inline comments
 
@@ -86,73 +86,81 @@ CUSTORDER_BATCH_MODE = {
 
CUSTORDER_STATUS_ORDERED                = 10
 
# CUSTORDER_STATUS_PAID                   = 20
 

	
 
CUSTORDER_STATUS = {
 
    CUSTORDER_STATUS_ORDERED            : "ordered",
 
    # CUSTORDER_STATUS_PAID               : "paid",
 
}
 

	
 

	
 
CUSTORDER_ITEM_STATUS_INITIATED         = 10
 
# TODO: deprecate / remove this one
 
CUSTORDER_ITEM_STATUS_ORDERED           = CUSTORDER_ITEM_STATUS_INITIATED
 
CUSTORDER_ITEM_STATUS_PAID              = 20
 
CUSTORDER_ITEM_STATUS_READY             = 20
 
# TODO: deprecate / remove this one
 
CUSTORDER_ITEM_STATUS_PAID              = CUSTORDER_ITEM_STATUS_READY
 
CUSTORDER_ITEM_STATUS_PLACED            = 30
 
CUSTORDER_ITEM_STATUS_RECEIVED          = 40
 
CUSTORDER_ITEM_STATUS_CONTACTED         = 50
 
CUSTORDER_ITEM_STATUS_CONTACT_FAILED    = 60
 
CUSTORDER_ITEM_STATUS_DELIVERED         = 70
 
CUSTORDER_ITEM_STATUS_CANCELED          = 900
 
CUSTORDER_ITEM_STATUS_REFUND_PENDING    = 910
 
CUSTORDER_ITEM_STATUS_REFUNDED          = 920
 
CUSTORDER_ITEM_STATUS_RESTOCKED         = 930
 
CUSTORDER_ITEM_STATUS_EXPIRED           = 940
 
CUSTORDER_ITEM_STATUS_INACTIVE          = 950
 

	
 
CUSTORDER_ITEM_STATUS = OrderedDict([
 
    (CUSTORDER_ITEM_STATUS_INITIATED,           "customer order initiated"),
 
    (CUSTORDER_ITEM_STATUS_PAID,                "payment received"),
 
    # (CUSTORDER_ITEM_STATUS_PAID,                "payment received"),
 
    (CUSTORDER_ITEM_STATUS_READY,               "ready to proceed"),
 
    (CUSTORDER_ITEM_STATUS_PLACED,              "order placed with vendor"),
 
    (CUSTORDER_ITEM_STATUS_RECEIVED,            "received from vendor"),
 
    (CUSTORDER_ITEM_STATUS_CONTACTED,           "customer contacted"),
 
    (CUSTORDER_ITEM_STATUS_CONTACT_FAILED,      "unable to contact customer"),
 
    (CUSTORDER_ITEM_STATUS_DELIVERED,           "delivered to customer"),
 
    (CUSTORDER_ITEM_STATUS_CANCELED,            "canceled"),
 
    (CUSTORDER_ITEM_STATUS_REFUND_PENDING,      "refund pending"),
 
    (CUSTORDER_ITEM_STATUS_REFUNDED,            "refunded"),
 
    (CUSTORDER_ITEM_STATUS_RESTOCKED,           "restocked"),
 
    (CUSTORDER_ITEM_STATUS_EXPIRED,             "expired"),
 
    (CUSTORDER_ITEM_STATUS_INACTIVE,            "inactive"),
 
])
 

	
 

	
 
CUSTORDER_ITEM_EVENT_INITIATED          = 10
 
CUSTORDER_ITEM_EVENT_PAID               = 20
 
CUSTORDER_ITEM_EVENT_PRICE_CONFIRMED    = 15
 
CUSTORDER_ITEM_EVENT_READY              = 20
 
# TODO: deprecate / remove this one
 
CUSTORDER_ITEM_EVENT_PAID               = CUSTORDER_ITEM_EVENT_READY
 
CUSTORDER_ITEM_EVENT_PLACED             = 30
 
CUSTORDER_ITEM_EVENT_RECEIVED           = 40
 
CUSTORDER_ITEM_EVENT_CONTACTED          = 50
 
CUSTORDER_ITEM_EVENT_CONTACT_FAILED     = 60
 
CUSTORDER_ITEM_EVENT_DELIVERED          = 70
 
CUSTORDER_ITEM_EVENT_STATUS_CHANGE      = 500 # nb. this is not in STATUS enum
 
# CUSTORDER_ITEM_EVENT_ADDED_NOTE         = 510 # nb. this is not in STATUS enum
 
CUSTORDER_ITEM_EVENT_CANCELED           = 900
 
CUSTORDER_ITEM_EVENT_REFUND_PENDING     = 910
 
CUSTORDER_ITEM_EVENT_REFUNDED           = 920
 
CUSTORDER_ITEM_EVENT_RESTOCKED          = 930
 
CUSTORDER_ITEM_EVENT_EXPIRED            = 940
 
CUSTORDER_ITEM_EVENT_INACTIVE           = 950
 

	
 
CUSTORDER_ITEM_EVENT = OrderedDict([
 
    (CUSTORDER_ITEM_EVENT_INITIATED,            "customer order initiated"),
 
    (CUSTORDER_ITEM_EVENT_PAID,                 "payment received"),
 
    (CUSTORDER_ITEM_EVENT_PRICE_CONFIRMED,      "price confirmed"),
 
    (CUSTORDER_ITEM_EVENT_READY,                "order becomes ready to proceed"),
 
    # (CUSTORDER_ITEM_EVENT_PAID,                 "payment received"),
 
    (CUSTORDER_ITEM_EVENT_PLACED,               "order placed with vendor"),
 
    (CUSTORDER_ITEM_EVENT_RECEIVED,             "received from vendor"),
 
    (CUSTORDER_ITEM_EVENT_CONTACTED,            "customer contacted"),
 
    (CUSTORDER_ITEM_EVENT_CONTACT_FAILED,       "unable to contact customer"),
 
    (CUSTORDER_ITEM_EVENT_DELIVERED,            "delivered to customer"),
 
    (CUSTORDER_ITEM_EVENT_STATUS_CHANGE,        "manual status change"),
 
    # (CUSTORDER_ITEM_EVENT_ADDED_NOTE,           "added note"),
 
    (CUSTORDER_ITEM_EVENT_CANCELED,             "canceled"),
 
    (CUSTORDER_ITEM_EVENT_REFUND_PENDING,       "refund pending"),
 
    (CUSTORDER_ITEM_EVENT_REFUNDED,             "refunded"),
 
    (CUSTORDER_ITEM_EVENT_RESTOCKED,            "restocked"),
 
    (CUSTORDER_ITEM_EVENT_EXPIRED,              "expired"),
0 comments (0 inline, 0 general)