feat: add initial support for order item events
so far just attaching events on creation, but then can view them
This commit is contained in:
		
							parent
							
								
									c79b0262f3
								
							
						
					
					
						commit
						b4deea76e0
					
				
					 10 changed files with 456 additions and 13 deletions
				
			
		| 
						 | 
					@ -994,9 +994,35 @@ class NewOrderBatchHandler(BatchHandler):
 | 
				
			||||||
            order.items.append(item)
 | 
					            order.items.append(item)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # set item status
 | 
					            # set item status
 | 
				
			||||||
            item.status_code = enum.ORDER_ITEM_STATUS_INITIATED
 | 
					            self.set_initial_item_status(item, user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.app.progress_loop(convert, rows, progress,
 | 
					        self.app.progress_loop(convert, rows, progress,
 | 
				
			||||||
                               message="Converting batch rows to order items")
 | 
					                               message="Converting batch rows to order items")
 | 
				
			||||||
        session.flush()
 | 
					        session.flush()
 | 
				
			||||||
        return order
 | 
					        return order
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_initial_item_status(self, item, user, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Set the initial status and attach event(s) for the given item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is called from :meth:`make_new_order()` for each item
 | 
				
			||||||
 | 
					        after it is added to the order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Default logic will set status to
 | 
				
			||||||
 | 
					        :data:`~sideshow.enum.ORDER_ITEM_STATUS_READY` and attach 2
 | 
				
			||||||
 | 
					        events:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * :data:`~sideshow.enum.ORDER_ITEM_EVENT_INITIATED`
 | 
				
			||||||
 | 
					        * :data:`~sideshow.enum.ORDER_ITEM_EVENT_READY`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param item: :class:`~sideshow.db.model.orders.OrderItem`
 | 
				
			||||||
 | 
					           being added to the new order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param user:
 | 
				
			||||||
 | 
					           :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
 | 
				
			||||||
 | 
					           is performing the action.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        enum = self.app.enum
 | 
				
			||||||
 | 
					        item.add_event(enum.ORDER_ITEM_EVENT_INITIATED, user)
 | 
				
			||||||
 | 
					        item.add_event(enum.ORDER_ITEM_EVENT_READY, user)
 | 
				
			||||||
 | 
					        item.status_code = enum.ORDER_ITEM_STATUS_READY
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -156,6 +156,19 @@ def upgrade() -> None:
 | 
				
			||||||
                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order_item'))
 | 
					                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order_item'))
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # sideshow_order_item_event
 | 
				
			||||||
 | 
					    op.create_table('sideshow_order_item_event',
 | 
				
			||||||
 | 
					                    sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
 | 
				
			||||||
 | 
					                    sa.Column('item_uuid', wuttjamaican.db.util.UUID(), nullable=False),
 | 
				
			||||||
 | 
					                    sa.Column('type_code', sa.Integer(), nullable=False),
 | 
				
			||||||
 | 
					                    sa.Column('occurred', sa.DateTime(timezone=True), nullable=False),
 | 
				
			||||||
 | 
					                    sa.Column('actor_uuid', wuttjamaican.db.util.UUID(), nullable=False),
 | 
				
			||||||
 | 
					                    sa.Column('note', sa.Text(), nullable=True),
 | 
				
			||||||
 | 
					                    sa.ForeignKeyConstraint(['actor_uuid'], ['user.uuid'], name=op.f('fk_sideshow_order_item_event_actor_uuid_user')),
 | 
				
			||||||
 | 
					                    sa.ForeignKeyConstraint(['item_uuid'], ['sideshow_order_item.uuid'], name=op.f('fk_sideshow_order_item_event_item_uuid_sideshow_order_item')),
 | 
				
			||||||
 | 
					                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_order_item_event'))
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # sideshow_batch_neworder
 | 
					    # sideshow_batch_neworder
 | 
				
			||||||
    op.create_table('sideshow_batch_neworder',
 | 
					    op.create_table('sideshow_batch_neworder',
 | 
				
			||||||
                    sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
 | 
					                    sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
 | 
				
			||||||
| 
						 | 
					@ -227,6 +240,9 @@ def downgrade() -> None:
 | 
				
			||||||
    op.drop_table('sideshow_batch_neworder_row')
 | 
					    op.drop_table('sideshow_batch_neworder_row')
 | 
				
			||||||
    op.drop_table('sideshow_batch_neworder')
 | 
					    op.drop_table('sideshow_batch_neworder')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # sideshow_order_item_event
 | 
				
			||||||
 | 
					    op.drop_table('sideshow_order_item_event')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # sideshow_order_item
 | 
					    # sideshow_order_item
 | 
				
			||||||
    op.drop_table('sideshow_order_item')
 | 
					    op.drop_table('sideshow_order_item')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,6 +32,7 @@ Primary :term:`data models <data model>`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* :class:`~sideshow.db.model.orders.Order`
 | 
					* :class:`~sideshow.db.model.orders.Order`
 | 
				
			||||||
* :class:`~sideshow.db.model.orders.OrderItem`
 | 
					* :class:`~sideshow.db.model.orders.OrderItem`
 | 
				
			||||||
 | 
					* :class:`~sideshow.db.model.orders.OrderItemEvent`
 | 
				
			||||||
* :class:`~sideshow.db.model.customers.LocalCustomer`
 | 
					* :class:`~sideshow.db.model.customers.LocalCustomer`
 | 
				
			||||||
* :class:`~sideshow.db.model.products.LocalProduct`
 | 
					* :class:`~sideshow.db.model.products.LocalProduct`
 | 
				
			||||||
* :class:`~sideshow.db.model.customers.PendingCustomer`
 | 
					* :class:`~sideshow.db.model.customers.PendingCustomer`
 | 
				
			||||||
| 
						 | 
					@ -49,7 +50,7 @@ from wuttjamaican.db.model import *
 | 
				
			||||||
# sideshow models
 | 
					# sideshow models
 | 
				
			||||||
from .customers import LocalCustomer, PendingCustomer
 | 
					from .customers import LocalCustomer, PendingCustomer
 | 
				
			||||||
from .products import LocalProduct, PendingProduct
 | 
					from .products import LocalProduct, PendingProduct
 | 
				
			||||||
from .orders import Order, OrderItem
 | 
					from .orders import Order, OrderItem, OrderItemEvent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# batch models
 | 
					# batch models
 | 
				
			||||||
from .batch.neworder import NewOrderBatch, NewOrderBatchRow
 | 
					from .batch.neworder import NewOrderBatch, NewOrderBatchRow
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  Sideshow -- Case/Special Order Tracker
 | 
					#  Sideshow -- Case/Special Order Tracker
 | 
				
			||||||
#  Copyright © 2024 Lance Edgar
 | 
					#  Copyright © 2024-2025 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Sideshow.
 | 
					#  This file is part of Sideshow.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -332,6 +332,16 @@ class OrderItem(model.Base):
 | 
				
			||||||
    applicable/known.
 | 
					    applicable/known.
 | 
				
			||||||
    """)
 | 
					    """)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    events = orm.relationship(
 | 
				
			||||||
 | 
					        'OrderItemEvent',
 | 
				
			||||||
 | 
					        order_by='OrderItemEvent.occurred, OrderItemEvent.uuid',
 | 
				
			||||||
 | 
					        cascade='all, delete-orphan',
 | 
				
			||||||
 | 
					        cascade_backrefs=False,
 | 
				
			||||||
 | 
					        back_populates='item',
 | 
				
			||||||
 | 
					        doc="""
 | 
				
			||||||
 | 
					        List of :class:`OrderItemEvent` records for the item.
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def full_description(self):
 | 
					    def full_description(self):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
| 
						 | 
					@ -344,3 +354,52 @@ class OrderItem(model.Base):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.full_description
 | 
					        return self.full_description
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_event(self, type_code, user, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Convenience method to add a new :class:`OrderItemEvent` for
 | 
				
			||||||
 | 
					        the item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        kwargs['type_code'] = type_code
 | 
				
			||||||
 | 
					        kwargs['actor'] = user
 | 
				
			||||||
 | 
					        self.events.append(OrderItemEvent(**kwargs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OrderItemEvent(model.Base):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    An event in the life of an :term:`order item`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __tablename__ = 'sideshow_order_item_event'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uuid = model.uuid_column()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    item_uuid = model.uuid_fk_column('sideshow_order_item.uuid', nullable=False)
 | 
				
			||||||
 | 
					    item = orm.relationship(
 | 
				
			||||||
 | 
					        OrderItem,
 | 
				
			||||||
 | 
					        cascade_backrefs=False,
 | 
				
			||||||
 | 
					        back_populates='events',
 | 
				
			||||||
 | 
					        doc="""
 | 
				
			||||||
 | 
					        Reference to the :class:`OrderItem` to which the event
 | 
				
			||||||
 | 
					        pertains.
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    type_code = sa.Column(sa.Integer, nullable=False, doc="""
 | 
				
			||||||
 | 
					    Code indicating the type of event; values must be defined in
 | 
				
			||||||
 | 
					    :data:`~sideshow.enum.ORDER_ITEM_EVENT`.
 | 
				
			||||||
 | 
					    """)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    occurred = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc="""
 | 
				
			||||||
 | 
					    Date and time when the event occurred.
 | 
				
			||||||
 | 
					    """)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    actor_uuid = model.uuid_fk_column('user.uuid', nullable=False)
 | 
				
			||||||
 | 
					    actor = orm.relationship(
 | 
				
			||||||
 | 
					        model.User,
 | 
				
			||||||
 | 
					        doc="""
 | 
				
			||||||
 | 
					        :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
 | 
				
			||||||
 | 
					        performed the action.
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    note = sa.Column(sa.Text(), nullable=True, doc="""
 | 
				
			||||||
 | 
					    Optional note recorded for the event.
 | 
				
			||||||
 | 
					    """)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  Sideshow -- Case/Special Order Tracker
 | 
					#  Sideshow -- Case/Special Order Tracker
 | 
				
			||||||
#  Copyright © 2024 Lance Edgar
 | 
					#  Copyright © 2024-2025 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Sideshow.
 | 
					#  This file is part of Sideshow.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -108,23 +108,101 @@ class PendingProductStatus(Enum):
 | 
				
			||||||
########################################
 | 
					########################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_UNINITIATED       = 1
 | 
					ORDER_ITEM_STATUS_UNINITIATED       = 1
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the item is "not yet initiated" - this probably is not
 | 
				
			||||||
 | 
					useful but exists as a possibility just in case.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_INITIATED         = 10
 | 
					ORDER_ITEM_STATUS_INITIATED         = 10
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the item is "initiated" (aka. created) but not yet "ready"
 | 
				
			||||||
 | 
					for buyer/PO.  This may imply the price needs confirmation etc.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_PAID_BEFORE       = 50
 | 
					ORDER_ITEM_STATUS_PAID_BEFORE       = 50
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer has fully paid for the item, up-front before
 | 
				
			||||||
 | 
					the buyer places PO etc.  It implies the item is not yet "ready" for
 | 
				
			||||||
 | 
					some reason.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO: deprecate / remove this one
 | 
					# TODO: deprecate / remove this one
 | 
				
			||||||
ORDER_ITEM_STATUS_PAID              = ORDER_ITEM_STATUS_PAID_BEFORE
 | 
					ORDER_ITEM_STATUS_PAID              = ORDER_ITEM_STATUS_PAID_BEFORE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_READY             = 100
 | 
					ORDER_ITEM_STATUS_READY             = 100
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the item is "ready" for buyer to include it on a vendor
 | 
				
			||||||
 | 
					purchase order.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_PLACED            = 200
 | 
					ORDER_ITEM_STATUS_PLACED            = 200
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the buyer has placed a vendor purchase order which includes
 | 
				
			||||||
 | 
					this item.  The item is thereby "on order" until the truck arrives.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_RECEIVED          = 300
 | 
					ORDER_ITEM_STATUS_RECEIVED          = 300
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the item has been received as part of a vendor delivery.
 | 
				
			||||||
 | 
					The item is thereby "on hand" until customer comes in for pickup.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_CONTACTED         = 350
 | 
					ORDER_ITEM_STATUS_CONTACTED         = 350
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer has been notified that the item is "on hand"
 | 
				
			||||||
 | 
					and awaiting their pickup.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_CONTACT_FAILED    = 375
 | 
					ORDER_ITEM_STATUS_CONTACT_FAILED    = 375
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the attempt(s) to notify customer have failed.  The item is
 | 
				
			||||||
 | 
					on hand but the customer does not know to pickup.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_DELIVERED         = 500
 | 
					ORDER_ITEM_STATUS_DELIVERED         = 500
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer has picked up the item.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_PAID_AFTER        = 550
 | 
					ORDER_ITEM_STATUS_PAID_AFTER        = 550
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer has fully paid for the item, as part of their
 | 
				
			||||||
 | 
					pickup.  This completes the cycle for orders which require payment on
 | 
				
			||||||
 | 
					the tail end.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_CANCELED          = 900
 | 
					ORDER_ITEM_STATUS_CANCELED          = 900
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the order item has been canceled.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_REFUND_PENDING    = 910
 | 
					ORDER_ITEM_STATUS_REFUND_PENDING    = 910
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the order item has been canceled, and the customer is due a
 | 
				
			||||||
 | 
					(pending) refund.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_REFUNDED          = 920
 | 
					ORDER_ITEM_STATUS_REFUNDED          = 920
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the order item has been canceled, and the customer has been
 | 
				
			||||||
 | 
					given a refund.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_RESTOCKED         = 930
 | 
					ORDER_ITEM_STATUS_RESTOCKED         = 930
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the product has been restocked, e.g. after the order item
 | 
				
			||||||
 | 
					was canceled.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_EXPIRED           = 940
 | 
					ORDER_ITEM_STATUS_EXPIRED           = 940
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the order item and/or product has expired.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS_INACTIVE          = 950
 | 
					ORDER_ITEM_STATUS_INACTIVE          = 950
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the order item has become inactive.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ORDER_ITEM_STATUS = OrderedDict([
 | 
					ORDER_ITEM_STATUS = OrderedDict([
 | 
				
			||||||
    (ORDER_ITEM_STATUS_UNINITIATED,         "uninitiated"),
 | 
					    (ORDER_ITEM_STATUS_UNINITIATED,         "uninitiated"),
 | 
				
			||||||
| 
						 | 
					@ -144,3 +222,169 @@ ORDER_ITEM_STATUS = OrderedDict([
 | 
				
			||||||
    (ORDER_ITEM_STATUS_EXPIRED,             "expired"),
 | 
					    (ORDER_ITEM_STATUS_EXPIRED,             "expired"),
 | 
				
			||||||
    (ORDER_ITEM_STATUS_INACTIVE,            "inactive"),
 | 
					    (ORDER_ITEM_STATUS_INACTIVE,            "inactive"),
 | 
				
			||||||
])
 | 
					])
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Dict of possible code -> label options for :term:`order item` status.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					These codes are referenced by:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* :attr:`sideshow.db.model.orders.OrderItem.status_code`
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					########################################
 | 
				
			||||||
 | 
					# Order Item Event Type
 | 
				
			||||||
 | 
					########################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_INITIATED          = 10
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the item was "initiated" - this occurs when the
 | 
				
			||||||
 | 
					:term:`order` is first created.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_PRICE_CONFIRMED    = 20
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the item's price was confirmed by a user who is authorized
 | 
				
			||||||
 | 
					to do that.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_PAYMENT_RECEIVED   = 50
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates payment was received for the item.  This may occur toward
 | 
				
			||||||
 | 
					the beginning, or toward the end, of the item's life cycle depending
 | 
				
			||||||
 | 
					on app configuration etc.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: deprecate / remove this
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_PAID               = ORDER_ITEM_EVENT_PAYMENT_RECEIVED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_READY              = 100
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the item has become "ready" for buyer placement on a new
 | 
				
			||||||
 | 
					vendor purchase order.  Often this will occur when the :term:`order`
 | 
				
			||||||
 | 
					is first created, if the data is suitable.  However this may be
 | 
				
			||||||
 | 
					delayed if e.g. the price needs confirmation.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_CUSTOMER_RESOLVED  = 120
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer for the :term:`order` has been assigned to a
 | 
				
			||||||
 | 
					"proper" (existing) account.  This may happen (after the fact) if the
 | 
				
			||||||
 | 
					order was first created with a new/unknown customer.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_PRODUCT_RESOLVED   = 140
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the product for the :term:`order item` has been assigned to
 | 
				
			||||||
 | 
					a "proper" (existing) product record.  This may happen (after the
 | 
				
			||||||
 | 
					fact) if the order was first created with a new/unknown product.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_PLACED             = 200
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the buyer has placed a vendor purchase order which includes
 | 
				
			||||||
 | 
					this item.  So the item is "on order" until the truck arrives.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_RECEIVED           = 300
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the receiver has found the item while receiving a vendor
 | 
				
			||||||
 | 
					delivery.  The item is set aside and is "on hand" until customer comes
 | 
				
			||||||
 | 
					in to pick it up.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_CONTACTED          = 350
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer has been contacted, to notify them of the item
 | 
				
			||||||
 | 
					being on hand and ready for pickup.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_CONTACT_FAILED     = 375
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates an attempt was made to contact the customer, to notify them
 | 
				
			||||||
 | 
					of item being on hand, but the attempt failed, e.g. due to bad phone
 | 
				
			||||||
 | 
					or email on file.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_DELIVERED          = 500
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer has picked up the item.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_STATUS_CHANGE      = 700
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates a manual status change was made.  Such an event should
 | 
				
			||||||
 | 
					ideally contain a note with further explanation.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_NOTE_ADDED         = 750
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates an arbitrary note was added.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_CANCELED           = 900
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the :term:`order item` was canceled.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_REFUND_PENDING     = 910
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer is due a (pending) refund for the item.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_REFUNDED           = 920
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the customer has been refunded for the item.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_RESTOCKED          = 930
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the product has been restocked, e.g. due to the order item
 | 
				
			||||||
 | 
					being canceled.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_EXPIRED            = 940
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the order item (or its product) has expired.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_INACTIVE           = 950
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Indicates the order item has become inactive.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT_OTHER              = 999
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Arbitrary event type which does not signify anything in particular.
 | 
				
			||||||
 | 
					If used, the event should be given an explanatory note.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ORDER_ITEM_EVENT = OrderedDict([
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_INITIATED,            "initiated"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_PRICE_CONFIRMED,      "price confirmed"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_PAYMENT_RECEIVED,     "payment received"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_READY,                "ready to proceed"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_CUSTOMER_RESOLVED,    "customer resolved"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_PRODUCT_RESOLVED,     "product resolved"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_PLACED,               "placed with vendor"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_RECEIVED,             "received from vendor"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_CONTACTED,            "customer contacted"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_CONTACT_FAILED,       "contact failed"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_DELIVERED,            "delivered"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_STATUS_CHANGE,        "status change"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_NOTE_ADDED,           "note added"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_CANCELED,             "canceled"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_REFUND_PENDING,       "refund pending"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_REFUNDED,             "refunded"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_RESTOCKED,            "restocked"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_EXPIRED,              "expired"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_INACTIVE,             "inactive"),
 | 
				
			||||||
 | 
					    (ORDER_ITEM_EVENT_OTHER,                "other"),
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Dict of possible code -> label options for :term:`order item` event
 | 
				
			||||||
 | 
					types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					These codes are referenced by:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* :attr:`sideshow.db.model.orders.OrderItemEvent.type_code`
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@
 | 
				
			||||||
        <div class="panel-block">
 | 
					        <div class="panel-block">
 | 
				
			||||||
          <div style="width: 100%;">
 | 
					          <div style="width: 100%;">
 | 
				
			||||||
            <b-field horizontal label="ID">
 | 
					            <b-field horizontal label="ID">
 | 
				
			||||||
              <span>Order ID ${order.order_id} — Item #${item.sequence}</span>
 | 
					              <span>${h.link_to(f"Order ID {order.order_id}", url('orders.view', uuid=order.uuid))} — Item #${item.sequence}</span>
 | 
				
			||||||
            </b-field>
 | 
					            </b-field>
 | 
				
			||||||
            <b-field horizontal label="Order Qty">
 | 
					            <b-field horizontal label="Order Qty">
 | 
				
			||||||
              <span>${order_qty_uom_text|n}</span>
 | 
					              <span>${order_qty_uom_text|n}</span>
 | 
				
			||||||
| 
						 | 
					@ -151,4 +151,40 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div style="padding: 0 2rem;">
 | 
				
			||||||
 | 
					    <nav class="panel" style="width: 100%;">
 | 
				
			||||||
 | 
					      <p class="panel-heading">Events</p>
 | 
				
			||||||
 | 
					      <div class="panel-block">
 | 
				
			||||||
 | 
					        <div style="width: 100%;">
 | 
				
			||||||
 | 
					          ${events_grid.render_table_element()}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="render_vue_templates()">
 | 
				
			||||||
 | 
					  ${parent.render_vue_templates()}
 | 
				
			||||||
 | 
					  ${events_grid.render_vue_template()}
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="modify_vue_vars()">
 | 
				
			||||||
 | 
					  ${parent.modify_vue_vars()}
 | 
				
			||||||
 | 
					  <script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ## TODO: ugh the hackiness
 | 
				
			||||||
 | 
					    ThisPageData.gridContext = {
 | 
				
			||||||
 | 
					        % for key, data in form.grid_vue_context.items():
 | 
				
			||||||
 | 
					            '${key}': ${json.dumps(data)|n},
 | 
				
			||||||
 | 
					        % endfor
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="make_vue_components()">
 | 
				
			||||||
 | 
					  ${parent.make_vue_components()}
 | 
				
			||||||
 | 
					  ${events_grid.render_vue_finalize()}
 | 
				
			||||||
</%def>
 | 
					</%def>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,8 @@ import logging
 | 
				
			||||||
import colander
 | 
					import colander
 | 
				
			||||||
from sqlalchemy import orm
 | 
					from sqlalchemy import orm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from webhelpers2.html import tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.views import MasterView
 | 
					from wuttaweb.views import MasterView
 | 
				
			||||||
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
 | 
					from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1173,12 +1175,38 @@ class OrderItemView(MasterView):
 | 
				
			||||||
    def get_template_context(self, context):
 | 
					    def get_template_context(self, context):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        if self.viewing:
 | 
					        if self.viewing:
 | 
				
			||||||
 | 
					            model = self.app.model
 | 
				
			||||||
 | 
					            enum = self.app.enum
 | 
				
			||||||
 | 
					            route_prefix = self.get_route_prefix()
 | 
				
			||||||
            item = context['instance']
 | 
					            item = context['instance']
 | 
				
			||||||
 | 
					            form = context['form']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            context['item'] = item
 | 
					            context['item'] = item
 | 
				
			||||||
            context['order'] = item.order
 | 
					            context['order'] = item.order
 | 
				
			||||||
            context['order_qty_uom_text'] = self.order_handler.get_order_qty_uom_text(
 | 
					            context['order_qty_uom_text'] = self.order_handler.get_order_qty_uom_text(
 | 
				
			||||||
                item.order_qty, item.order_uom, case_size=item.case_size, html=True)
 | 
					                item.order_qty, item.order_uom, case_size=item.case_size, html=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            grid = self.make_grid(key=f'{route_prefix}.view.events',
 | 
				
			||||||
 | 
					                                  model_class=model.OrderItemEvent,
 | 
				
			||||||
 | 
					                                  data=item.events,
 | 
				
			||||||
 | 
					                                  columns=[
 | 
				
			||||||
 | 
					                                      'occurred',
 | 
				
			||||||
 | 
					                                      'actor',
 | 
				
			||||||
 | 
					                                      'type_code',
 | 
				
			||||||
 | 
					                                      'note',
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                  labels={
 | 
				
			||||||
 | 
					                                      'occurred': "Date/Time",
 | 
				
			||||||
 | 
					                                      'actor': "User",
 | 
				
			||||||
 | 
					                                      'type_code': "Event Type",
 | 
				
			||||||
 | 
					                                  })
 | 
				
			||||||
 | 
					            grid.set_renderer('type_code', lambda e, k, v: enum.ORDER_ITEM_EVENT[v])
 | 
				
			||||||
 | 
					            if self.request.has_perm('users.view'):
 | 
				
			||||||
 | 
					                grid.set_renderer('actor', lambda e, k, v: tags.link_to(
 | 
				
			||||||
 | 
					                    e.actor, self.request.route_url('users.view', uuid=e.actor.uuid)))
 | 
				
			||||||
 | 
					            form.add_grid_vue_context(grid)
 | 
				
			||||||
 | 
					            context['events_grid'] = grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_xref_buttons(self, item):
 | 
					    def get_xref_buttons(self, item):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1109,6 +1109,20 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
				
			||||||
        self.assertEqual(item.unit_cost, decimal.Decimal('3.99'))
 | 
					        self.assertEqual(item.unit_cost, decimal.Decimal('3.99'))
 | 
				
			||||||
        self.assertEqual(item.unit_price_reg, decimal.Decimal('5.99'))
 | 
					        self.assertEqual(item.unit_price_reg, decimal.Decimal('5.99'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_set_initial_item_status(self):
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					        enum = self.app.enum
 | 
				
			||||||
 | 
					        handler = self.make_handler()
 | 
				
			||||||
 | 
					        user = model.User(username='barney')
 | 
				
			||||||
 | 
					        item = model.OrderItem()
 | 
				
			||||||
 | 
					        self.assertIsNone(item.status_code)
 | 
				
			||||||
 | 
					        self.assertEqual(len(item.events), 0)
 | 
				
			||||||
 | 
					        handler.set_initial_item_status(item, user)
 | 
				
			||||||
 | 
					        self.assertEqual(item.status_code, enum.ORDER_ITEM_STATUS_READY)
 | 
				
			||||||
 | 
					        self.assertEqual(len(item.events), 2)
 | 
				
			||||||
 | 
					        self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_INITIATED)
 | 
				
			||||||
 | 
					        self.assertEqual(item.events[1].type_code, enum.ORDER_ITEM_EVENT_READY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_execute(self):
 | 
					    def test_execute(self):
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
        enum = self.app.enum
 | 
					        enum = self.app.enum
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,11 @@ class TestOrder(DataTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestOrderItem(DataTestCase):
 | 
					class TestOrderItem(DataTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_config(self, **kw):
 | 
				
			||||||
 | 
					        config = super().make_config(**kw)
 | 
				
			||||||
 | 
					        config.setdefault('wutta.enum_spec', 'sideshow.enum')
 | 
				
			||||||
 | 
					        return config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_full_description(self):
 | 
					    def test_full_description(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        item = mod.OrderItem()
 | 
					        item = mod.OrderItem()
 | 
				
			||||||
| 
						 | 
					@ -44,3 +49,15 @@ class TestOrderItem(DataTestCase):
 | 
				
			||||||
                             product_description='Vinegar',
 | 
					                             product_description='Vinegar',
 | 
				
			||||||
                             product_size='32oz')
 | 
					                             product_size='32oz')
 | 
				
			||||||
        self.assertEqual(str(item), "Bragg Vinegar 32oz")
 | 
					        self.assertEqual(str(item), "Bragg Vinegar 32oz")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_add_event(self):
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					        enum = self.app.enum
 | 
				
			||||||
 | 
					        user = model.User(username='barney')
 | 
				
			||||||
 | 
					        item = mod.OrderItem()
 | 
				
			||||||
 | 
					        self.assertEqual(item.events, [])
 | 
				
			||||||
 | 
					        item.add_event(enum.ORDER_ITEM_EVENT_INITIATED, user)
 | 
				
			||||||
 | 
					        item.add_event(enum.ORDER_ITEM_EVENT_READY, user)
 | 
				
			||||||
 | 
					        self.assertEqual(len(item.events), 2)
 | 
				
			||||||
 | 
					        self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_INITIATED)
 | 
				
			||||||
 | 
					        self.assertEqual(item.events[1].type_code, enum.ORDER_ITEM_EVENT_READY)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1338,14 +1338,16 @@ class TestOrderItemView(WebTestCase):
 | 
				
			||||||
        item = model.OrderItem(order_qty=2, order_uom=enum.ORDER_UOM_CASE, case_size=8)
 | 
					        item = model.OrderItem(order_qty=2, order_uom=enum.ORDER_UOM_CASE, case_size=8)
 | 
				
			||||||
        order.items.append(item)
 | 
					        order.items.append(item)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with patch.object(view, 'viewing', new=True):
 | 
					        with patch.object(self.request, 'is_root', new=True):
 | 
				
			||||||
            context = view.get_template_context({'instance': item})
 | 
					            with patch.object(view, 'viewing', new=True):
 | 
				
			||||||
            self.assertIn('item', context)
 | 
					                form = view.make_model_form(model_instance=item)
 | 
				
			||||||
            self.assertIs(context['item'], item)
 | 
					                context = view.get_template_context({'instance': item, 'form': form})
 | 
				
			||||||
            self.assertIn('order', context)
 | 
					                self.assertIn('item', context)
 | 
				
			||||||
            self.assertIs(context['order'], order)
 | 
					                self.assertIs(context['item'], item)
 | 
				
			||||||
            self.assertIn('order_qty_uom_text', context)
 | 
					                self.assertIn('order', context)
 | 
				
			||||||
            self.assertEqual(context['order_qty_uom_text'], "2 Cases (× 8 = 16 Units)")
 | 
					                self.assertIs(context['order'], order)
 | 
				
			||||||
 | 
					                self.assertIn('order_qty_uom_text', context)
 | 
				
			||||||
 | 
					                self.assertEqual(context['order_qty_uom_text'], "2 Cases (× 8 = 16 Units)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_xref_buttons(self):
 | 
					    def test_get_xref_buttons(self):
 | 
				
			||||||
        self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
 | 
					        self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue