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
|
@ -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…
Reference in a new issue