Compare commits
6 commits
e7a5f6c09e
...
b35c9bcdfa
Author | SHA1 | Date | |
---|---|---|---|
|
b35c9bcdfa | ||
|
7167c6a7cc | ||
|
9d378a0c5f | ||
|
b4deea76e0 | ||
|
c79b0262f3 | ||
|
13d576295e |
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
|||
## v0.4.0 (2025-01-23)
|
||||
|
||||
### Feat
|
||||
|
||||
- add initial workflow master views, UI features
|
||||
- add tools to change order item status; add notes
|
||||
- add initial support for order item events
|
||||
|
||||
### Fix
|
||||
|
||||
- customize "view order item" page w/ panels
|
||||
- add loading overlay for expensive calls in orders/create
|
||||
- hide local customer when not applicable, for order view
|
||||
|
||||
## v0.3.0 (2025-01-13)
|
||||
|
||||
### Feat
|
||||
|
|
6
docs/api/sideshow.orders.rst
Normal file
6
docs/api/sideshow.orders.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``sideshow.orders``
|
||||
===================
|
||||
|
||||
.. automodule:: sideshow.orders
|
||||
:members:
|
|
@ -45,12 +45,27 @@ Glossary
|
|||
"submits" the order, the batch is executed which creates a true
|
||||
:term:`order`.
|
||||
|
||||
The batch handler is responsible for business logic for the order
|
||||
creation step; the :term:`order handler` is responsible for
|
||||
everything thereafter.
|
||||
|
||||
:class:`~sideshow.batch.neworder.NewOrderBatchHandler` is the
|
||||
default handler for this.
|
||||
|
||||
order
|
||||
This is the central focus of the app; it refers to a customer
|
||||
case/special order which is tracked over time, from placement to
|
||||
fulfillment. Each order may have one or more :term:`order items
|
||||
<order item>`.
|
||||
|
||||
order handler
|
||||
The :term:`handler` responsible for business logic surrounding
|
||||
:term:`order` workflows *after* initial creation. (Whereas the
|
||||
:term:`new order batch` handler is responsible for creation.)
|
||||
|
||||
:class:`~sideshow.orders.OrderHandler` is the default handler for
|
||||
this.
|
||||
|
||||
order item
|
||||
This is effectively a "line item" within an :term:`order`. It
|
||||
represents a particular product, with quantity and pricing
|
||||
|
|
|
@ -39,6 +39,7 @@ the narrative docs are pretty scant. That will eventually change.
|
|||
api/sideshow.db.model.orders
|
||||
api/sideshow.db.model.products
|
||||
api/sideshow.enum
|
||||
api/sideshow.orders
|
||||
api/sideshow.web
|
||||
api/sideshow.web.app
|
||||
api/sideshow.web.forms
|
||||
|
|
|
@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "Sideshow"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
description = "Case/Special Order Tracker"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
@ -33,7 +33,7 @@ license = {text = "GNU General Public License v3+"}
|
|||
requires-python = ">= 3.8"
|
||||
dependencies = [
|
||||
"psycopg2",
|
||||
"WuttaWeb>=0.20.1",
|
||||
"WuttaWeb>=0.20.5",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
@ -44,6 +44,9 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` tracks
|
||||
all user input until they "submit" (execute) at which point an
|
||||
:class:`~sideshow.db.model.orders.Order` is created.
|
||||
|
||||
After the batch has executed the :term:`order handler` takes over
|
||||
responsibility for the rest of the order lifecycle.
|
||||
"""
|
||||
model_class = NewOrderBatch
|
||||
|
||||
|
@ -991,9 +994,35 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
order.items.append(item)
|
||||
|
||||
# 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,
|
||||
message="Converting batch rows to order items")
|
||||
session.flush()
|
||||
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'))
|
||||
)
|
||||
|
||||
# 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
|
||||
op.create_table('sideshow_batch_neworder',
|
||||
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')
|
||||
|
||||
# sideshow_order_item_event
|
||||
op.drop_table('sideshow_order_item_event')
|
||||
|
||||
# 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.OrderItem`
|
||||
* :class:`~sideshow.db.model.orders.OrderItemEvent`
|
||||
* :class:`~sideshow.db.model.customers.LocalCustomer`
|
||||
* :class:`~sideshow.db.model.products.LocalProduct`
|
||||
* :class:`~sideshow.db.model.customers.PendingCustomer`
|
||||
|
@ -49,7 +50,7 @@ from wuttjamaican.db.model import *
|
|||
# sideshow models
|
||||
from .customers import LocalCustomer, PendingCustomer
|
||||
from .products import LocalProduct, PendingProduct
|
||||
from .orders import Order, OrderItem
|
||||
from .orders import Order, OrderItem, OrderItemEvent
|
||||
|
||||
# batch models
|
||||
from .batch.neworder import NewOrderBatch, NewOrderBatchRow
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Sideshow -- Case/Special Order Tracker
|
||||
# Copyright © 2024 Lance Edgar
|
||||
# Copyright © 2024-2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
|
@ -332,6 +332,16 @@ class OrderItem(model.Base):
|
|||
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
|
||||
def full_description(self):
|
||||
""" """
|
||||
|
@ -344,3 +354,52 @@ class OrderItem(model.Base):
|
|||
|
||||
def __str__(self):
|
||||
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
|
||||
# Copyright © 2024 Lance Edgar
|
||||
# Copyright © 2024-2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
|
@ -108,23 +108,101 @@ class PendingProductStatus(Enum):
|
|||
########################################
|
||||
|
||||
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
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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
|
||||
ORDER_ITEM_STATUS_PAID = ORDER_ITEM_STATUS_PAID_BEFORE
|
||||
|
||||
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
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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
|
||||
"""
|
||||
Indicates the customer has been notified that the item is "on hand"
|
||||
and awaiting their pickup.
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
Indicates the customer has picked up the item.
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
Indicates the order item has been canceled.
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
Indicates the order item has been canceled, and the customer has been
|
||||
given a refund.
|
||||
"""
|
||||
|
||||
ORDER_ITEM_STATUS_RESTOCKED = 930
|
||||
"""
|
||||
Indicates the product has been restocked, e.g. after the order item
|
||||
was canceled.
|
||||
"""
|
||||
|
||||
ORDER_ITEM_STATUS_EXPIRED = 940
|
||||
"""
|
||||
Indicates the order item and/or product has expired.
|
||||
"""
|
||||
|
||||
ORDER_ITEM_STATUS_INACTIVE = 950
|
||||
"""
|
||||
Indicates the order item has become inactive.
|
||||
"""
|
||||
|
||||
ORDER_ITEM_STATUS = OrderedDict([
|
||||
(ORDER_ITEM_STATUS_UNINITIATED, "uninitiated"),
|
||||
|
@ -144,3 +222,176 @@ ORDER_ITEM_STATUS = OrderedDict([
|
|||
(ORDER_ITEM_STATUS_EXPIRED, "expired"),
|
||||
(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_REORDER = 210
|
||||
"""
|
||||
Indicates the item was not received with the delivery on which it was
|
||||
expected, and must be re-ordered from vendor.
|
||||
"""
|
||||
|
||||
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_REORDER, "marked for re-order"),
|
||||
(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, "changed status"),
|
||||
(ORDER_ITEM_EVENT_NOTE_ADDED, "added note"),
|
||||
(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`
|
||||
"""
|
||||
|
|
310
src/sideshow/orders.py
Normal file
310
src/sideshow/orders.py
Normal file
|
@ -0,0 +1,310 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Sideshow -- Case/Special Order Tracker
|
||||
# Copyright © 2024-2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
# Sideshow is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Sideshow is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Sideshow Order Handler
|
||||
"""
|
||||
|
||||
from wuttjamaican.app import GenericHandler
|
||||
|
||||
|
||||
class OrderHandler(GenericHandler):
|
||||
"""
|
||||
Base class and default implementation for the :term:`order
|
||||
handler`.
|
||||
|
||||
This is responsible for business logic involving customer orders
|
||||
after they have been first created. (The :term:`new order batch`
|
||||
handler is responsible for creation logic.)
|
||||
"""
|
||||
|
||||
def get_order_qty_uom_text(self, order_qty, order_uom, case_size=None, html=False):
|
||||
"""
|
||||
Return the display text for a given order quantity.
|
||||
|
||||
Default logic will return something like ``"3 Cases (x 6 = 18
|
||||
Units)"``.
|
||||
|
||||
:param order_qty: Numeric quantity.
|
||||
|
||||
:param order_uom: An order UOM constant; should be something
|
||||
from :data:`~sideshow.enum.ORDER_UOM`.
|
||||
|
||||
:param case_size: Case size for the product, if known.
|
||||
|
||||
:param html: Whether the return value should include any HTML.
|
||||
If false (the default), it will be plain text only. If
|
||||
true, will replace the ``x`` character with ``×``.
|
||||
|
||||
:returns: Display text.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
if order_uom == enum.ORDER_UOM_CASE:
|
||||
if case_size is None:
|
||||
case_qty = unit_qty = '??'
|
||||
else:
|
||||
case_qty = self.app.render_quantity(case_size)
|
||||
unit_qty = self.app.render_quantity(order_qty * case_size)
|
||||
CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE]
|
||||
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
|
||||
order_qty = self.app.render_quantity(order_qty)
|
||||
times = '×' if html else 'x'
|
||||
return (f"{order_qty} {CS} ({times} {case_qty} = {unit_qty} {EA})")
|
||||
|
||||
# units
|
||||
unit_qty = self.app.render_quantity(order_qty)
|
||||
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
|
||||
return f"{unit_qty} {EA}"
|
||||
|
||||
def item_status_to_variant(self, status_code):
|
||||
"""
|
||||
Return a Buefy style variant for the given status code.
|
||||
|
||||
Default logic will return ``None`` for "normal" item status,
|
||||
but may return ``'warning'`` for some (e.g. canceled).
|
||||
|
||||
:param status_code: The status code for an order item.
|
||||
|
||||
:returns: Style variant string (e.g. ``'warning'``) or
|
||||
``None``.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
if status_code in (enum.ORDER_ITEM_STATUS_CANCELED,
|
||||
enum.ORDER_ITEM_STATUS_REFUND_PENDING,
|
||||
enum.ORDER_ITEM_STATUS_REFUNDED,
|
||||
enum.ORDER_ITEM_STATUS_RESTOCKED,
|
||||
enum.ORDER_ITEM_STATUS_EXPIRED,
|
||||
enum.ORDER_ITEM_STATUS_INACTIVE):
|
||||
return 'warning'
|
||||
|
||||
def process_placement(self, items, user, vendor_name=None, po_number=None, note=None):
|
||||
"""
|
||||
Process the "placement" step for the given order items.
|
||||
|
||||
This may eventually do something involving an *actual*
|
||||
purchase order, or at least a minimal representation of one,
|
||||
but for now it does not.
|
||||
|
||||
Instead, this will simply update each item to indicate its new
|
||||
status. A note will be attached to indicate the vendor and/or
|
||||
PO number, if provided.
|
||||
|
||||
:param items: Sequence of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
|
||||
performing the action.
|
||||
|
||||
:param vendor_name: Name of the vendor to which purchase order
|
||||
is placed, if known.
|
||||
|
||||
:param po_number: Purchase order number, if known.
|
||||
|
||||
:param note: Optional *additional* note to be attached to each
|
||||
order item.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
placed = None
|
||||
if vendor_name:
|
||||
placed = f"PO {po_number or ''} for vendor {vendor_name}"
|
||||
elif po_number:
|
||||
placed = f"PO {po_number}"
|
||||
|
||||
for item in items:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_PLACED, user, note=placed)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
item.status_code = enum.ORDER_ITEM_STATUS_PLACED
|
||||
|
||||
def process_receiving(self, items, user, vendor_name=None,
|
||||
invoice_number=None, po_number=None, note=None):
|
||||
"""
|
||||
Process the "receiving" step for the given order items.
|
||||
|
||||
This will update the status for each item, to indicate they
|
||||
are "received".
|
||||
|
||||
TODO: This also should email the customer notifying their
|
||||
items are ready for pickup etc.
|
||||
|
||||
:param items: Sequence of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
|
||||
performing the action.
|
||||
|
||||
:param vendor_name: Name of the vendor, if known.
|
||||
|
||||
:param po_number: Purchase order number, if known.
|
||||
|
||||
:param invoice_number: Invoice number, if known.
|
||||
|
||||
:param note: Optional *additional* note to be attached to each
|
||||
order item.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
received = None
|
||||
if invoice_number and po_number and vendor_name:
|
||||
received = f"invoice {invoice_number} (PO {po_number}) from vendor {vendor_name}"
|
||||
elif invoice_number and vendor_name:
|
||||
received = f"invoice {invoice_number} from vendor {vendor_name}"
|
||||
elif po_number and vendor_name:
|
||||
received = f"PO {po_number} from vendor {vendor_name}"
|
||||
elif vendor_name:
|
||||
received = f"from vendor {vendor_name}"
|
||||
|
||||
for item in items:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_RECEIVED, user, note=received)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
item.status_code = enum.ORDER_ITEM_STATUS_RECEIVED
|
||||
|
||||
def process_reorder(self, items, user, note=None):
|
||||
"""
|
||||
Process the "reorder" step for the given order items.
|
||||
|
||||
This will update the status for each item, to indicate they
|
||||
are "ready" (again) for placement.
|
||||
|
||||
:param items: Sequence of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
|
||||
performing the action.
|
||||
|
||||
:param note: Optional *additional* note to be attached to each
|
||||
order item.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
for item in items:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_REORDER, user)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
item.status_code = enum.ORDER_ITEM_STATUS_READY
|
||||
|
||||
def process_contact_success(self, items, user, note=None):
|
||||
"""
|
||||
Process the "successful contact" step for the given order
|
||||
items.
|
||||
|
||||
This will update the status for each item, to indicate they
|
||||
are "contacted" and awaiting delivery.
|
||||
|
||||
:param items: Sequence of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
|
||||
performing the action.
|
||||
|
||||
:param note: Optional *additional* note to be attached to each
|
||||
order item.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
for item in items:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_CONTACTED, user)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
item.status_code = enum.ORDER_ITEM_STATUS_CONTACTED
|
||||
|
||||
def process_contact_failure(self, items, user, note=None):
|
||||
"""
|
||||
Process the "failed contact" step for the given order items.
|
||||
|
||||
This will update the status for each item, to indicate
|
||||
"contact failed".
|
||||
|
||||
:param items: Sequence of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
|
||||
performing the action.
|
||||
|
||||
:param note: Optional *additional* note to be attached to each
|
||||
order item.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
for item in items:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_CONTACT_FAILED, user)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
item.status_code = enum.ORDER_ITEM_STATUS_CONTACT_FAILED
|
||||
|
||||
def process_delivery(self, items, user, note=None):
|
||||
"""
|
||||
Process the "delivery" step for the given order items.
|
||||
|
||||
This will update the status for each item, to indicate they
|
||||
are "delivered".
|
||||
|
||||
:param items: Sequence of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
|
||||
performing the action.
|
||||
|
||||
:param note: Optional *additional* note to be attached to each
|
||||
order item.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
for item in items:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_DELIVERED, user)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
item.status_code = enum.ORDER_ITEM_STATUS_DELIVERED
|
||||
|
||||
def process_restock(self, items, user, note=None):
|
||||
"""
|
||||
Process the "restock" step for the given order items.
|
||||
|
||||
This will update the status for each item, to indicate they
|
||||
are "restocked".
|
||||
|
||||
:param items: Sequence of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
|
||||
performing the action.
|
||||
|
||||
:param note: Optional *additional* note to be attached to each
|
||||
order item.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
for item in items:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_RESTOCKED, user)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
item.status_code = enum.ORDER_ITEM_STATUS_RESTOCKED
|
|
@ -57,6 +57,27 @@ class SideshowMenuHandler(base.MenuHandler):
|
|||
'perm': 'orders.create',
|
||||
},
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "Placement",
|
||||
'route': 'order_items_placement',
|
||||
'perm': 'order_items_placement.list',
|
||||
},
|
||||
{
|
||||
'title': "Receiving",
|
||||
'route': 'order_items_receiving',
|
||||
'perm': 'order_items_receiving.list',
|
||||
},
|
||||
{
|
||||
'title': "Contact",
|
||||
'route': 'order_items_contact',
|
||||
'perm': 'order_items_contact.list',
|
||||
},
|
||||
{
|
||||
'title': "Delivery",
|
||||
'route': 'order_items_delivery',
|
||||
'perm': 'order_items_delivery.list',
|
||||
},
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "All Order Items",
|
||||
'route': 'order_items',
|
||||
|
|
159
src/sideshow/web/templates/contact/index.mako
Normal file
159
src/sideshow/web/templates/contact/index.mako
Normal file
|
@ -0,0 +1,159 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/index.mako" />
|
||||
|
||||
<%def name="render_grid_tag()">
|
||||
% if master.has_perm('process_contact'):
|
||||
${grid.render_vue_tag(**{'@process-contact-success': "processContactSuccessInit", '@process-contact-failure': "processContactFailureInit"})}
|
||||
% else:
|
||||
${grid.render_vue_tag()}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
${parent.page_content()}
|
||||
|
||||
% if master.has_perm('process_contact'):
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="processContactSuccessShowDialog"
|
||||
% else:
|
||||
:active.sync="processContactSuccessShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
${h.form(url(f'{route_prefix}.process_contact_success'), ref='processContactSuccessForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('item_uuids', **{':value': 'processContactSuccessUuids.join()'})}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Process Contact Success</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
This will mark {{ processContactSuccessUuids.length }}
|
||||
item{{ processContactSuccessUuids.length > 1 ? 's' : '' }}
|
||||
as being "contacted".
|
||||
</p>
|
||||
<b-field horizontal label="Note">
|
||||
<b-input name="note"
|
||||
v-model="processContactSuccessNote"
|
||||
ref="processContactSuccessNote"
|
||||
type="textarea" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="processContactSuccessSubmit()"
|
||||
:disabled="processContactSuccessSubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ processContactSuccessSubmitting ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="processContactSuccessShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="processContactFailureShowDialog"
|
||||
% else:
|
||||
:active.sync="processContactFailureShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
${h.form(url(f'{route_prefix}.process_contact_failure'), ref='processContactFailureForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('item_uuids', **{':value': 'processContactFailureUuids.join()'})}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Process Contact Failure</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
This will mark {{ processContactFailureUuids.length }}
|
||||
item{{ processContactFailureUuids.length > 1 ? 's' : '' }}
|
||||
as being "contact failed".
|
||||
</p>
|
||||
<b-field horizontal label="Note">
|
||||
<b-input name="note"
|
||||
v-model="processContactFailureNote"
|
||||
ref="processContactFailureNote"
|
||||
type="textarea" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="processContactFailureSubmit()"
|
||||
:disabled="processContactFailureSubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ processContactFailureSubmitting ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="processContactFailureShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
% if master.has_perm('process_contact'):
|
||||
<script>
|
||||
|
||||
ThisPageData.processContactSuccessShowDialog = false
|
||||
ThisPageData.processContactSuccessUuids = []
|
||||
ThisPageData.processContactSuccessNote = null
|
||||
ThisPageData.processContactSuccessSubmitting = false
|
||||
|
||||
ThisPage.methods.processContactSuccessInit = function(items) {
|
||||
this.processContactSuccessUuids = items.map((item) => item.uuid)
|
||||
this.processContactSuccessNote = null
|
||||
this.processContactSuccessShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.processContactSuccessNote.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.processContactSuccessSubmit = function() {
|
||||
this.processContactSuccessSubmitting = true
|
||||
this.$refs.processContactSuccessForm.submit()
|
||||
}
|
||||
|
||||
ThisPageData.processContactFailureShowDialog = false
|
||||
ThisPageData.processContactFailureUuids = []
|
||||
ThisPageData.processContactFailureNote = null
|
||||
ThisPageData.processContactFailureSubmitting = false
|
||||
|
||||
ThisPage.methods.processContactFailureInit = function(items) {
|
||||
this.processContactFailureUuids = items.map((item) => item.uuid)
|
||||
this.processContactFailureNote = null
|
||||
this.processContactFailureShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.processContactFailureNote.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.processContactFailureSubmit = function() {
|
||||
this.processContactFailureSubmitting = true
|
||||
this.$refs.processContactFailureForm.submit()
|
||||
}
|
||||
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
173
src/sideshow/web/templates/delivery/index.mako
Normal file
173
src/sideshow/web/templates/delivery/index.mako
Normal file
|
@ -0,0 +1,173 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/index.mako" />
|
||||
|
||||
<%def name="render_grid_tag()">
|
||||
% if master.has_perm('process_delivery') and master.has_perm('process_restock'):
|
||||
${grid.render_vue_tag(**{'@process-delivery': "processDeliveryInit", '@process-restock': "processRestockInit"})}
|
||||
% elif master.has_perm('process_delivery'):
|
||||
${grid.render_vue_tag(**{'@process-delivery': "processDeliveryInit"})}
|
||||
% elif master.has_perm('process_restock'):
|
||||
${grid.render_vue_tag(**{'@process-restock': "processRestockInit"})}
|
||||
% else:
|
||||
${grid.render_vue_tag()}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
${parent.page_content()}
|
||||
|
||||
% if master.has_perm('process_delivery'):
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="processDeliveryShowDialog"
|
||||
% else:
|
||||
:active.sync="processDeliveryShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
${h.form(url(f'{route_prefix}.process_delivery'), ref='processDeliveryForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('item_uuids', **{':value': 'processDeliveryUuids.join()'})}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Process Delivery</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
This will mark {{ processDeliveryUuids.length }}
|
||||
item{{ processDeliveryUuids.length > 1 ? 's' : '' }}
|
||||
as being "delivered".
|
||||
</p>
|
||||
<b-field horizontal label="Note">
|
||||
<b-input name="note"
|
||||
v-model="processDeliveryNote"
|
||||
ref="processDeliveryNote"
|
||||
type="textarea" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="processDeliverySubmit()"
|
||||
:disabled="processDeliverySubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ processDeliverySubmitting ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="processDeliveryShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
% endif
|
||||
|
||||
% if master.has_perm('process_restock'):
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="processRestockShowDialog"
|
||||
% else:
|
||||
:active.sync="processRestockShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
${h.form(url(f'{route_prefix}.process_restock'), ref='processRestockForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('item_uuids', **{':value': 'processRestockUuids.join()'})}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Process Restock</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
This will mark {{ processRestockUuids.length }}
|
||||
item{{ processRestockUuids.length > 1 ? 's' : '' }}
|
||||
as being "restocked".
|
||||
</p>
|
||||
<b-field horizontal label="Note">
|
||||
<b-input name="note"
|
||||
v-model="processRestockNote"
|
||||
ref="processRestockNote"
|
||||
type="textarea" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="processRestockSubmit()"
|
||||
:disabled="processRestockSubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ processRestockSubmitting ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="processRestockShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
|
||||
% if master.has_perm('process_delivery'):
|
||||
|
||||
ThisPageData.processDeliveryShowDialog = false
|
||||
ThisPageData.processDeliveryUuids = []
|
||||
ThisPageData.processDeliveryNote = null
|
||||
ThisPageData.processDeliverySubmitting = false
|
||||
|
||||
ThisPage.methods.processDeliveryInit = function(items) {
|
||||
this.processDeliveryUuids = items.map((item) => item.uuid)
|
||||
this.processDeliveryNote = null
|
||||
this.processDeliveryShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.processDeliveryNote.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.processDeliverySubmit = function() {
|
||||
this.processDeliverySubmitting = true
|
||||
this.$refs.processDeliveryForm.submit()
|
||||
}
|
||||
|
||||
% endif
|
||||
|
||||
% if master.has_perm('process_restock'):
|
||||
|
||||
ThisPageData.processRestockShowDialog = false
|
||||
ThisPageData.processRestockUuids = []
|
||||
ThisPageData.processRestockNote = null
|
||||
ThisPageData.processRestockSubmitting = false
|
||||
|
||||
ThisPage.methods.processRestockInit = function(items) {
|
||||
this.processRestockUuids = items.map((item) => item.uuid)
|
||||
this.processRestockNote = null
|
||||
this.processRestockShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.processRestockNote.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.processRestockSubmit = function() {
|
||||
this.processRestockSubmitting = true
|
||||
this.$refs.processRestockForm.submit()
|
||||
}
|
||||
|
||||
% endif
|
||||
|
||||
</script>
|
||||
</%def>
|
411
src/sideshow/web/templates/order-items/view.mako
Normal file
411
src/sideshow/web/templates/order-items/view.mako
Normal file
|
@ -0,0 +1,411 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="content_title()">
|
||||
(${app.enum.ORDER_ITEM_STATUS[item.status_code]})
|
||||
${instance_title}
|
||||
</%def>
|
||||
|
||||
<%def name="extra_styles()">
|
||||
${parent.extra_styles()}
|
||||
<style>
|
||||
|
||||
nav .field .field-label .label {
|
||||
white-space: nowrap;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
<div style="padding: 2rem; display: flex; justify-content: space-evenly; gap: 2rem;">
|
||||
<div style="flex-grow: 1;">
|
||||
|
||||
<nav class="panel" style="width: 100%;">
|
||||
<p class="panel-heading">Order Item</p>
|
||||
<div class="panel-block">
|
||||
<div style="width: 100%;">
|
||||
<b-field horizontal label="ID">
|
||||
<span>${h.link_to(f"Order ID {order.order_id}", url('orders.view', uuid=order.uuid))} — Item #${item.sequence}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Order Qty">
|
||||
<span>${order_qty_uom_text|n}</span>
|
||||
</b-field>
|
||||
% if item.discount_percent:
|
||||
<b-field horizontal label="Discount">
|
||||
<span>${app.render_percent(item.discount_percent)}</span>
|
||||
</b-field>
|
||||
% endif
|
||||
<b-field horizontal label="Total Due">
|
||||
<span>${app.render_currency(item.total_price)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Total Paid">
|
||||
<span>${app.render_currency(item.paid_amount)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Status">
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<span
|
||||
% if item_status_variant:
|
||||
class="has-background-${item_status_variant}"
|
||||
% endif
|
||||
% if master.has_perm('change_status'):
|
||||
style="padding: 0.25rem;"
|
||||
% endif
|
||||
>
|
||||
${app.enum.ORDER_ITEM_STATUS[item.status_code]}
|
||||
</span>
|
||||
% if master.has_perm('change_status'):
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="edit"
|
||||
@click="changeStatusInit()">
|
||||
Change Status
|
||||
</b-button>
|
||||
<${b}-modal
|
||||
% if request.use_oruga:
|
||||
v-model:active="changeStatusShowDialog"
|
||||
% else:
|
||||
:active.sync="changeStatusShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
||||
<h4 class="block is-size-4">Change Item Status</h4>
|
||||
|
||||
<b-field horizontal label="Current Status">
|
||||
<span>{{ changeStatusCodes[changeStatusOldCode] }}</span>
|
||||
</b-field>
|
||||
|
||||
<br />
|
||||
|
||||
<b-field horizontal label="New Status"
|
||||
:type="changeStatusNewCode ? null : 'is-danger'">
|
||||
<b-select v-model="changeStatusNewCode">
|
||||
<option v-for="status in changeStatusCodeOptions"
|
||||
:key="status.key"
|
||||
:value="status.key">
|
||||
{{ status.label }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Note">
|
||||
<b-input v-model="changeStatusNote"
|
||||
type="textarea" rows="4" />
|
||||
</b-field>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary"
|
||||
:disabled="changeStatusSaveDisabled"
|
||||
icon-pack="fas"
|
||||
icon-left="save"
|
||||
@click="changeStatusSave()">
|
||||
{{ changeStatusSubmitting ? "Working, please wait..." : "Update Status" }}
|
||||
</b-button>
|
||||
<b-button @click="changeStatusShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
${h.form(master.get_action_url('change_status', item), ref='changeStatusForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('new_status', **{'v-model': 'changeStatusNewCode'})}
|
||||
## ${h.hidden('uuids', **{':value': 'changeStatusCheckedRows.map((row) => {return row.uuid}).join()'})}
|
||||
${h.hidden('note', **{':value': 'changeStatusNote'})}
|
||||
${h.end_form()}
|
||||
|
||||
% endif
|
||||
</div>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="panel" style="width: 100%;">
|
||||
<p class="panel-heading">Pricing</p>
|
||||
<div class="panel-block">
|
||||
<div style="width: 100%;">
|
||||
<b-field horizontal label="Unit Cost">
|
||||
<span>${app.render_currency(item.unit_cost, scale=4)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Unit Price Reg.">
|
||||
<span>${app.render_currency(item.unit_price_reg)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Unit Price Sale">
|
||||
<span>${app.render_currency(item.unit_price_sale)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Sale Ends">
|
||||
<span>${app.render_datetime(item.sale_ends)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Unit Price Quoted">
|
||||
<span>${app.render_currency(item.unit_price_quoted)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Case Size">
|
||||
<span>${app.render_quantity(item.case_size)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Case Price Quoted">
|
||||
<span>${app.render_currency(item.case_price_quoted)}</span>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
<div style="flex-grow: 1;">
|
||||
|
||||
<nav class="panel" style="width: 100%;">
|
||||
<p class="panel-heading">Customer</p>
|
||||
<div class="panel-block">
|
||||
<div style="width: 100%;">
|
||||
<b-field horizontal label="Customer ID">
|
||||
<span>${order.customer_id}</span>
|
||||
</b-field>
|
||||
% if not order.customer_id and order.local_customer:
|
||||
<b-field horizontal label="Local Customer">
|
||||
<span>${h.link_to(order.local_customer, url('local_customers.view', uuid=order.local_customer.uuid))}</span>
|
||||
</b-field>
|
||||
% endif
|
||||
% if not order.customer_id and order.pending_customer:
|
||||
<b-field horizontal label="Pending Customer">
|
||||
<span>${h.link_to(order.pending_customer, url('pending_customers.view', uuid=order.pending_customer.uuid))}</span>
|
||||
</b-field>
|
||||
% endif
|
||||
<b-field horizontal label="Customer Name">
|
||||
<span>${order.customer_name}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Phone Number">
|
||||
<span>${order.phone_number}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Email Address">
|
||||
<span>${order.email_address}</span>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="panel" style="width: 100%;">
|
||||
<p class="panel-heading">Product</p>
|
||||
<div class="panel-block">
|
||||
<div style="width: 100%;">
|
||||
<b-field horizontal label="Product ID">
|
||||
<span>${item.product_id}</span>
|
||||
</b-field>
|
||||
% if not item.product_id and item.local_product:
|
||||
<b-field horizontal label="Local Product">
|
||||
<span>${h.link_to(item.local_product, url('local_products.view', uuid=order.local_product.uuid))}</span>
|
||||
</b-field>
|
||||
% endif
|
||||
% if not item.product_id and item.pending_product:
|
||||
<b-field horizontal label="Pending Product">
|
||||
<span>${h.link_to(item.pending_product, url('pending_products.view', uuid=order.pending_product.uuid))}</span>
|
||||
</b-field>
|
||||
% endif
|
||||
<b-field horizontal label="Scancode">
|
||||
<span>${item.product_scancode}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Brand">
|
||||
<span>${item.product_brand}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Description">
|
||||
<span>${item.product_description}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Size">
|
||||
<span>${item.product_size}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Sold by Weight">
|
||||
<span>${app.render_boolean(item.product_weighed)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Department">
|
||||
<span>${item.department_name}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Special Order">
|
||||
<span>${app.render_boolean(item.special_order)}</span>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 2rem;">
|
||||
<nav class="panel" style="width: 100%;">
|
||||
<p class="panel-heading"
|
||||
% if master.has_perm('add_note'):
|
||||
style="display: flex; gap: 2rem; align-items: center;"
|
||||
% endif
|
||||
>
|
||||
<span>Events</span>
|
||||
% if master.has_perm('add_note'):
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="plus"
|
||||
@click="addNoteInit()">
|
||||
Add Note
|
||||
</b-button>
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="addNoteShowDialog"
|
||||
% else:
|
||||
:active.sync="addNoteShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Add Note</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<b-field>
|
||||
<b-input type="textarea" rows="8"
|
||||
v-model="addNoteText"
|
||||
ref="addNoteText"
|
||||
expanded />
|
||||
</b-field>
|
||||
## <b-field>
|
||||
## <b-checkbox v-model="addNoteApplyAll">
|
||||
## Apply to all products on this order
|
||||
## </b-checkbox>
|
||||
## </b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="addNoteSave()"
|
||||
:disabled="addNoteSaveDisabled"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ addNoteSubmitting ? "Working, please wait..." : "Add Note" }}
|
||||
</b-button>
|
||||
<b-button @click="addNoteShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
${h.form(master.get_action_url('add_note', item), ref='addNoteForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('note', **{':value': 'addNoteText'})}
|
||||
## ${h.hidden('uuids', **{':value': 'changeStatusCheckedRows.map((row) => {return row.uuid}).join()'})}
|
||||
${h.end_form()}
|
||||
|
||||
% endif
|
||||
|
||||
</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>
|
||||
|
||||
% if master.has_perm('add_note'):
|
||||
|
||||
ThisPageData.addNoteShowDialog = false
|
||||
ThisPageData.addNoteText = null
|
||||
## ThisPageData.addNoteApplyAll = false
|
||||
ThisPageData.addNoteSubmitting = false
|
||||
|
||||
ThisPage.computed.addNoteSaveDisabled = function() {
|
||||
if (!this.addNoteText) {
|
||||
return true
|
||||
}
|
||||
if (this.addNoteSubmitting) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
ThisPage.methods.addNoteInit = function() {
|
||||
this.addNoteText = null
|
||||
## this.addNoteApplyAll = false
|
||||
this.addNoteShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.addNoteText.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.addNoteSave = function() {
|
||||
this.addNoteSubmitting = true
|
||||
this.$refs.addNoteForm.submit()
|
||||
}
|
||||
|
||||
% endif
|
||||
|
||||
% if master.has_perm('change_status'):
|
||||
|
||||
ThisPageData.changeStatusCodes = ${json.dumps(app.enum.ORDER_ITEM_STATUS)|n}
|
||||
ThisPageData.changeStatusCodeOptions = ${json.dumps([dict(key=k, label=v) for k, v in app.enum.ORDER_ITEM_STATUS.items()])|n}
|
||||
|
||||
ThisPageData.changeStatusShowDialog = false
|
||||
ThisPageData.changeStatusOldCode = ${instance.status_code}
|
||||
ThisPageData.changeStatusNewCode = null
|
||||
ThisPageData.changeStatusNote = null
|
||||
ThisPageData.changeStatusSubmitting = false
|
||||
|
||||
ThisPage.computed.changeStatusSaveDisabled = function() {
|
||||
if (!this.changeStatusNewCode) {
|
||||
return true
|
||||
}
|
||||
if (this.changeStatusSubmitting) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
ThisPage.methods.changeStatusInit = function() {
|
||||
this.changeStatusNewCode = null
|
||||
// clear out any checked rows
|
||||
// this.changeStatusCheckedRows.length = 0
|
||||
this.changeStatusNote = null
|
||||
this.changeStatusShowDialog = true
|
||||
}
|
||||
|
||||
ThisPage.methods.changeStatusSave = function() {
|
||||
if (this.changeStatusNewCode == this.changeStatusOldCode) {
|
||||
alert("You chose the same status it already had...")
|
||||
return
|
||||
}
|
||||
|
||||
this.changeStatusSubmitting = true
|
||||
this.$refs.changeStatusForm.submit()
|
||||
}
|
||||
|
||||
% endif
|
||||
|
||||
## 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>
|
|
@ -81,6 +81,7 @@
|
|||
</template>
|
||||
|
||||
<div class="panel-block">
|
||||
<b-loading v-model="customerLoading" :is-full-page="false" />
|
||||
<div style="width: 100%;">
|
||||
|
||||
<div style="display: flex; flex-direction: row;">
|
||||
|
@ -143,7 +144,7 @@
|
|||
icon-pack="fas"
|
||||
icon-left="redo"
|
||||
:disabled="refreshingCustomer">
|
||||
{{ refreshingCustomer ? "Refreshing" : "Refresh" }}
|
||||
{{ refreshingCustomer ? "Working, please wait..." : "Refresh" }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-field>
|
||||
|
@ -348,6 +349,7 @@
|
|||
>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<b-loading v-model="editItemLoading" :is-full-page="false" />
|
||||
|
||||
<${b}-tabs :animated="false"
|
||||
% if request.use_oruga:
|
||||
|
@ -829,6 +831,7 @@
|
|||
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
|
||||
|
||||
customerPanelOpen: false,
|
||||
customerLoading: false,
|
||||
customerIsKnown: ${json.dumps(customer_is_known)|n},
|
||||
customerID: ${json.dumps(customer_id)|n},
|
||||
customerName: ${json.dumps(customer_name)|n},
|
||||
|
@ -853,6 +856,7 @@
|
|||
|
||||
editItemRow: null,
|
||||
editItemShowDialog: false,
|
||||
editItemLoading: false,
|
||||
itemDialogSaving: false,
|
||||
% if request.use_oruga:
|
||||
itemDialogTab: 'product',
|
||||
|
@ -1132,6 +1136,7 @@
|
|||
},
|
||||
|
||||
customerChanged(customerID, callback) {
|
||||
this.customerLoading = true
|
||||
|
||||
const params = {}
|
||||
if (customerID) {
|
||||
|
@ -1149,7 +1154,9 @@
|
|||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
this.customerLoading = false
|
||||
}, response => {
|
||||
this.customerLoading = false
|
||||
this.$buefy.toast.open({
|
||||
message: "Update failed: " + (response.data.error || "(unknown error)"),
|
||||
type: 'is-danger',
|
||||
|
@ -1159,9 +1166,9 @@
|
|||
},
|
||||
|
||||
refreshCustomer() {
|
||||
this.refreshingContact = true
|
||||
this.refreshingCustomer = true
|
||||
this.customerChanged(this.customerID, () => {
|
||||
this.refreshingContact = false
|
||||
this.refreshingCustomer = false
|
||||
this.$buefy.toast.open({
|
||||
message: "Customer info has been refreshed.",
|
||||
type: 'is-success',
|
||||
|
@ -1321,6 +1328,7 @@
|
|||
|
||||
productChanged(productID) {
|
||||
if (productID) {
|
||||
this.editItemLoading = true
|
||||
const params = {
|
||||
action: 'get_product_info',
|
||||
product_id: productID,
|
||||
|
@ -1364,8 +1372,11 @@
|
|||
this.refreshProductDescription += 1
|
||||
this.refreshTotalPrice += 1
|
||||
|
||||
this.editItemLoading = false
|
||||
|
||||
}, response => {
|
||||
this.clearProduct()
|
||||
this.editItemLoading = false
|
||||
})
|
||||
} else {
|
||||
this.clearProduct()
|
||||
|
@ -1514,6 +1525,7 @@
|
|||
|
||||
itemDialogAttemptSave() {
|
||||
this.itemDialogSaving = true
|
||||
this.editItemLoading = true
|
||||
|
||||
const params = {
|
||||
order_qty: this.productQuantity,
|
||||
|
@ -1551,9 +1563,11 @@
|
|||
this.batchTotalPriceDisplay = response.data.batch.total_price_display
|
||||
|
||||
this.itemDialogSaving = false
|
||||
this.editItemLoading = false
|
||||
this.editItemShowDialog = false
|
||||
}, response => {
|
||||
this.itemDialogSaving = false
|
||||
this.editItemLoading = false
|
||||
})
|
||||
},
|
||||
|
||||
|
|
104
src/sideshow/web/templates/placement/index.mako
Normal file
104
src/sideshow/web/templates/placement/index.mako
Normal file
|
@ -0,0 +1,104 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/index.mako" />
|
||||
|
||||
<%def name="render_grid_tag()">
|
||||
% if master.has_perm('process_placement'):
|
||||
${grid.render_vue_tag(**{'@process-placement': "processPlacementInit"})}
|
||||
% else:
|
||||
${grid.render_vue_tag()}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
${parent.page_content()}
|
||||
% if master.has_perm('process_placement'):
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="processPlacementShowDialog"
|
||||
% else:
|
||||
:active.sync="processPlacementShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
${h.form(url(f'{route_prefix}.process_placement'), ref='processPlacementForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('item_uuids', **{':value': 'processPlacementUuids.join()'})}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Process Placement</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
This will mark {{ processPlacementUuids.length }}
|
||||
item{{ processPlacementUuids.length > 1 ? 's' : '' }} as
|
||||
being "placed" on order from vendor.
|
||||
</p>
|
||||
<b-field horizontal label="Vendor"
|
||||
:type="processPlacementVendor ? null : 'is-danger'">
|
||||
<b-input name="vendor_name"
|
||||
v-model="processPlacementVendor"
|
||||
ref="processPlacementVendor" />
|
||||
</b-field>
|
||||
<b-field horizontal label="PO Number">
|
||||
<b-input name="po_number"
|
||||
v-model="processPlacementNumber" />
|
||||
</b-field>
|
||||
<b-field horizontal label="Note">
|
||||
<b-input name="note"
|
||||
v-model="processPlacementNote"
|
||||
type="textarea" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="processPlacementSubmit()"
|
||||
:disabled="!processPlacementVendor || processPlacementSubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ processPlacementSubmitting ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="processPlacementShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</${b}-modal>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
% if master.has_perm('process_placement'):
|
||||
<script>
|
||||
|
||||
ThisPageData.processPlacementShowDialog = false
|
||||
ThisPageData.processPlacementUuids = []
|
||||
ThisPageData.processPlacementVendor = null
|
||||
ThisPageData.processPlacementNumber = null
|
||||
ThisPageData.processPlacementNote = null
|
||||
ThisPageData.processPlacementSubmitting = false
|
||||
|
||||
ThisPage.methods.processPlacementInit = function(items) {
|
||||
this.processPlacementUuids = items.map((item) => item.uuid)
|
||||
this.processPlacementVendor = null
|
||||
this.processPlacementNumber = null
|
||||
this.processPlacementNote = null
|
||||
this.processPlacementShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.processPlacementVendor.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.processPlacementSubmit = function() {
|
||||
this.processPlacementSubmitting = true
|
||||
this.$refs.processPlacementForm.submit()
|
||||
}
|
||||
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
192
src/sideshow/web/templates/receiving/index.mako
Normal file
192
src/sideshow/web/templates/receiving/index.mako
Normal file
|
@ -0,0 +1,192 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/index.mako" />
|
||||
|
||||
<%def name="render_grid_tag()">
|
||||
% if master.has_perm('process_receiving') and master.has_perm('process_reorder'):
|
||||
${grid.render_vue_tag(**{'@process-receiving': "processReceivingInit", '@process-reorder': "processReorderInit"})}
|
||||
% elif master.has_perm('process_receiving'):
|
||||
${grid.render_vue_tag(**{'@process-receiving': "processReceivingInit"})}
|
||||
% elif master.has_perm('process_reorder'):
|
||||
${grid.render_vue_tag(**{'@process-reorder': "processReorderInit"})}
|
||||
% else:
|
||||
${grid.render_vue_tag()}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
${parent.page_content()}
|
||||
|
||||
% if master.has_perm('process_receiving'):
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="processReceivingShowDialog"
|
||||
% else:
|
||||
:active.sync="processReceivingShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
${h.form(url(f'{route_prefix}.process_receiving'), ref='processReceivingForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('item_uuids', **{':value': 'processReceivingUuids.join()'})}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Process Receiving</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
This will mark {{ processReceivingUuids.length }}
|
||||
item{{ processReceivingUuids.length > 1 ? 's' : '' }}
|
||||
as being "received" from vendor.
|
||||
</p>
|
||||
<b-field horizontal label="Vendor"
|
||||
:type="processReceivingVendor ? null : 'is-danger'">
|
||||
<b-input name="vendor_name"
|
||||
v-model="processReceivingVendor"
|
||||
ref="processReceivingVendor" />
|
||||
</b-field>
|
||||
<b-field horizontal label="Invoice Number">
|
||||
<b-input name="invoice_number"
|
||||
v-model="processReceivingInvoiceNumber" />
|
||||
</b-field>
|
||||
<b-field horizontal label="PO Number">
|
||||
<b-input name="po_number"
|
||||
v-model="processReceivingPoNumber" />
|
||||
</b-field>
|
||||
<b-field horizontal label="Note">
|
||||
<b-input name="note"
|
||||
v-model="processReceivingNote"
|
||||
type="textarea" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="processReceivingSubmit()"
|
||||
:disabled="!processReceivingVendor || processReceivingSubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ processReceivingSubmitting ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="processReceivingShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
% endif
|
||||
|
||||
% if master.has_perm('process_reorder'):
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="processReorderShowDialog"
|
||||
% else:
|
||||
:active.sync="processReorderShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
${h.form(url(f'{route_prefix}.process_reorder'), ref='processReorderForm')}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('item_uuids', **{':value': 'processReorderUuids.join()'})}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Process Re-Order</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
This will mark {{ processReorderUuids.length }}
|
||||
item{{ processReorderUuids.length > 1 ? 's' : '' }}
|
||||
as being "ready" for placement (again).
|
||||
</p>
|
||||
<b-field horizontal label="Note">
|
||||
<b-input name="note"
|
||||
v-model="processReorderNote"
|
||||
ref="processReorderNote"
|
||||
type="textarea" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="processReorderSubmit()"
|
||||
:disabled="processReorderSubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ processReorderSubmitting ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="processReorderShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
|
||||
% if master.has_perm('process_receiving'):
|
||||
|
||||
ThisPageData.processReceivingShowDialog = false
|
||||
ThisPageData.processReceivingUuids = []
|
||||
ThisPageData.processReceivingVendor = null
|
||||
ThisPageData.processReceivingInvoiceNumber = null
|
||||
ThisPageData.processReceivingPoNumber = null
|
||||
ThisPageData.processReceivingNote = null
|
||||
ThisPageData.processReceivingSubmitting = false
|
||||
|
||||
ThisPage.methods.processReceivingInit = function(items) {
|
||||
this.processReceivingUuids = items.map((item) => item.uuid)
|
||||
this.processReceivingVendor = null
|
||||
this.processReceivingInvoiceNumber = null
|
||||
this.processReceivingPoNumber = null
|
||||
this.processReceivingNote = null
|
||||
this.processReceivingShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.processReceivingVendor.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.processReceivingSubmit = function() {
|
||||
this.processReceivingSubmitting = true
|
||||
this.$refs.processReceivingForm.submit()
|
||||
}
|
||||
|
||||
% endif
|
||||
|
||||
% if master.has_perm('process_reorder'):
|
||||
|
||||
ThisPageData.processReorderShowDialog = false
|
||||
ThisPageData.processReorderUuids = []
|
||||
ThisPageData.processReorderNote = null
|
||||
ThisPageData.processReorderSubmitting = false
|
||||
|
||||
ThisPage.methods.processReorderInit = function(items) {
|
||||
this.processReorderUuids = items.map((item) => item.uuid)
|
||||
this.processReorderNote = null
|
||||
this.processReorderShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.processReorderNote.focus()
|
||||
})
|
||||
}
|
||||
|
||||
ThisPage.methods.processReorderSubmit = function() {
|
||||
this.processReorderSubmitting = true
|
||||
this.$refs.processReorderForm.submit()
|
||||
}
|
||||
|
||||
% endif
|
||||
|
||||
</script>
|
||||
</%def>
|
|
@ -28,12 +28,16 @@ import decimal
|
|||
import logging
|
||||
|
||||
import colander
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
from webhelpers2.html import tags, HTML
|
||||
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
|
||||
|
||||
from sideshow.db.model import Order, OrderItem
|
||||
from sideshow.orders import OrderHandler
|
||||
from sideshow.batch.neworder import NewOrderBatchHandler
|
||||
from sideshow.web.forms.schema import (OrderRef,
|
||||
LocalCustomerRef, LocalProductRef,
|
||||
|
@ -58,10 +62,16 @@ class OrderView(MasterView):
|
|||
Note that the "edit" view is not exposed here; user must perform
|
||||
various other workflow actions to modify the order.
|
||||
|
||||
.. attribute:: order_handler
|
||||
|
||||
Reference to the :term:`order handler` as returned by
|
||||
:meth:`get_order_handler()`. This gets set in the constructor.
|
||||
|
||||
.. attribute:: batch_handler
|
||||
|
||||
Reference to the new order batch handler, as returned by
|
||||
:meth:`get_batch_handler()`. This gets set in the constructor.
|
||||
Reference to the :term:`new order batch` handler, as returned
|
||||
by :meth:`get_batch_handler()`. This gets set in the
|
||||
constructor.
|
||||
"""
|
||||
model_class = Order
|
||||
editable = False
|
||||
|
@ -142,21 +152,22 @@ class OrderView(MasterView):
|
|||
'unit_price_reg',
|
||||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.order_handler = self.get_order_handler()
|
||||
|
||||
# order_id
|
||||
g.set_link('order_id')
|
||||
def get_order_handler(self):
|
||||
"""
|
||||
Returns the configured :term:`order handler`.
|
||||
|
||||
# customer_id
|
||||
g.set_link('customer_id')
|
||||
You normally would not need to call this, and can use
|
||||
:attr:`order_handler` instead.
|
||||
|
||||
# customer_name
|
||||
g.set_link('customer_name')
|
||||
|
||||
# total_price
|
||||
g.set_renderer('total_price', g.render_currency)
|
||||
:rtype: :class:`~sideshow.orders.OrderHandler`
|
||||
"""
|
||||
if hasattr(self, 'order_handler'):
|
||||
return self.order_handler
|
||||
return OrderHandler(self.config)
|
||||
|
||||
def get_batch_handler(self):
|
||||
"""
|
||||
|
@ -174,6 +185,22 @@ class OrderView(MasterView):
|
|||
return self.batch_handler
|
||||
return self.app.get_batch_handler('neworder')
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
||||
# order_id
|
||||
g.set_link('order_id')
|
||||
|
||||
# customer_id
|
||||
g.set_link('customer_id')
|
||||
|
||||
# customer_name
|
||||
g.set_link('customer_name')
|
||||
|
||||
# total_price
|
||||
g.set_renderer('total_price', g.render_currency)
|
||||
|
||||
def create(self):
|
||||
"""
|
||||
Instead of the typical "create" view, this displays a "wizard"
|
||||
|
@ -670,8 +697,6 @@ class OrderView(MasterView):
|
|||
|
||||
def normalize_row(self, row):
|
||||
""" """
|
||||
enum = self.app.enum
|
||||
|
||||
data = {
|
||||
'uuid': row.uuid.hex,
|
||||
'sequence': row.sequence,
|
||||
|
@ -750,21 +775,8 @@ class OrderView(MasterView):
|
|||
}
|
||||
|
||||
# display text for order qty/uom
|
||||
if row.order_uom == enum.ORDER_UOM_CASE:
|
||||
order_qty = self.app.render_quantity(row.order_qty)
|
||||
if row.case_size is None:
|
||||
case_qty = unit_qty = '??'
|
||||
else:
|
||||
case_qty = self.app.render_quantity(row.case_size)
|
||||
unit_qty = self.app.render_quantity(row.order_qty * row.case_size)
|
||||
CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE]
|
||||
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
|
||||
data['order_qty_display'] = (f"{order_qty} {CS} "
|
||||
f"(× {case_qty} = {unit_qty} {EA})")
|
||||
else:
|
||||
unit_qty = self.app.render_quantity(row.order_qty)
|
||||
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
|
||||
data['order_qty_display'] = f"{unit_qty} {EA}"
|
||||
data['order_qty_display'] = self.order_handler.get_order_qty_uom_text(
|
||||
row.order_qty, row.order_uom, case_size=row.case_size, html=True)
|
||||
|
||||
return data
|
||||
|
||||
|
@ -823,7 +835,7 @@ class OrderView(MasterView):
|
|||
def configure_row_grid(self, g):
|
||||
""" """
|
||||
super().configure_row_grid(g)
|
||||
enum = self.app.enum
|
||||
# enum = self.app.enum
|
||||
|
||||
# sequence
|
||||
g.set_label('sequence', "Seq.", column_only=True)
|
||||
|
@ -851,6 +863,15 @@ class OrderView(MasterView):
|
|||
# status_code
|
||||
g.set_renderer('status_code', self.render_status_code)
|
||||
|
||||
# TODO: upstream should set this automatically
|
||||
g.row_class = self.row_grid_row_class
|
||||
|
||||
def row_grid_row_class(self, item, data, i):
|
||||
""" """
|
||||
variant = self.order_handler.item_status_to_variant(item.status_code)
|
||||
if variant:
|
||||
return f'has-background-{variant}'
|
||||
|
||||
def render_status_code(self, item, key, value):
|
||||
""" """
|
||||
enum = self.app.enum
|
||||
|
@ -957,11 +978,26 @@ class OrderItemView(MasterView):
|
|||
* ``/order-items/``
|
||||
* ``/order-items/XXX``
|
||||
|
||||
This class serves both as a proper master view (for "all" order
|
||||
items) as well as a base class for other "workflow" master views,
|
||||
each of which auto-filters by order item status:
|
||||
|
||||
* :class:`PlacementView`
|
||||
* :class:`ReceivingView`
|
||||
* :class:`ContactView`
|
||||
* :class:`DeliveryView`
|
||||
|
||||
Note that this does not expose create, edit or delete. The user
|
||||
must perform various other workflow actions to modify the item.
|
||||
|
||||
.. attribute:: order_handler
|
||||
|
||||
Reference to the :term:`order handler` as returned by
|
||||
:meth:`get_order_handler()`.
|
||||
"""
|
||||
model_class = OrderItem
|
||||
model_title = "Order Item"
|
||||
model_title = "Order Item (All)"
|
||||
model_title_plural = "Order Items (All)"
|
||||
route_prefix = 'order_items'
|
||||
url_prefix = '/order-items'
|
||||
creatable = False
|
||||
|
@ -1030,6 +1066,29 @@ class OrderItemView(MasterView):
|
|||
'payment_transaction_number',
|
||||
]
|
||||
|
||||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.order_handler = self.get_order_handler()
|
||||
|
||||
def get_order_handler(self):
|
||||
"""
|
||||
Returns the configured :term:`order handler`.
|
||||
|
||||
You normally would not need to call this, and can use
|
||||
:attr:`order_handler` instead.
|
||||
|
||||
:rtype: :class:`~sideshow.orders.OrderHandler`
|
||||
"""
|
||||
if hasattr(self, 'order_handler'):
|
||||
return self.order_handler
|
||||
return OrderHandler(self.config)
|
||||
|
||||
def get_fallback_templates(self, template):
|
||||
""" """
|
||||
templates = super().get_fallback_templates(template)
|
||||
templates.insert(0, f'/order-items/{template}.mako')
|
||||
return templates
|
||||
|
||||
def get_query(self, session=None):
|
||||
""" """
|
||||
query = super().get_query(session=session)
|
||||
|
@ -1084,12 +1143,11 @@ class OrderItemView(MasterView):
|
|||
enum = self.app.enum
|
||||
return enum.ORDER_ITEM_STATUS[value]
|
||||
|
||||
def get_instance_title(self, item):
|
||||
def grid_row_class(self, item, data, i):
|
||||
""" """
|
||||
enum = self.app.enum
|
||||
title = str(item)
|
||||
status = enum.ORDER_ITEM_STATUS[item.status_code]
|
||||
return f"({status}) {title}"
|
||||
variant = self.order_handler.item_status_to_variant(item.status_code)
|
||||
if variant:
|
||||
return f'has-background-{variant}'
|
||||
|
||||
def configure_form(self, f):
|
||||
""" """
|
||||
|
@ -1139,6 +1197,54 @@ class OrderItemView(MasterView):
|
|||
# paid_amount
|
||||
f.set_node('paid_amount', WuttaMoney(self.request))
|
||||
|
||||
def get_template_context(self, context):
|
||||
""" """
|
||||
if self.viewing:
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
route_prefix = self.get_route_prefix()
|
||||
item = context['instance']
|
||||
form = context['form']
|
||||
|
||||
context['item'] = item
|
||||
context['order'] = item.order
|
||||
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)
|
||||
context['item_status_variant'] = self.order_handler.item_status_to_variant(item.status_code)
|
||||
|
||||
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])
|
||||
grid.set_renderer('note', self.render_event_note)
|
||||
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
|
||||
|
||||
def render_event_note(self, event, key, value):
|
||||
""" """
|
||||
enum = self.app.enum
|
||||
if event.type_code == enum.ORDER_ITEM_EVENT_NOTE_ADDED:
|
||||
return HTML.tag('span', class_='has-background-info-light',
|
||||
style='padding: 0.25rem 0.5rem;',
|
||||
c=[value])
|
||||
return value
|
||||
|
||||
def get_xref_buttons(self, item):
|
||||
""" """
|
||||
buttons = super().get_xref_buttons(item)
|
||||
|
@ -1151,6 +1257,685 @@ class OrderItemView(MasterView):
|
|||
|
||||
return buttons
|
||||
|
||||
def add_note(self):
|
||||
"""
|
||||
View which adds a note to an order item. This is POST-only;
|
||||
will redirect back to the item view.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
item = self.get_instance()
|
||||
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, self.request.user,
|
||||
note=self.request.POST['note'])
|
||||
|
||||
return self.redirect(self.get_action_url('view', item))
|
||||
|
||||
def change_status(self):
|
||||
"""
|
||||
View which changes status for an order item. This is
|
||||
POST-only; will redirect back to the item view.
|
||||
"""
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
main_item = self.get_instance()
|
||||
session = self.Session()
|
||||
redirect = self.redirect(self.get_action_url('view', main_item))
|
||||
|
||||
extra_note = self.request.POST.get('note')
|
||||
|
||||
# validate new status
|
||||
new_status_code = int(self.request.POST['new_status'])
|
||||
if new_status_code not in enum.ORDER_ITEM_STATUS:
|
||||
self.request.session.flash("Invalid status code", 'error')
|
||||
return redirect
|
||||
new_status_text = enum.ORDER_ITEM_STATUS[new_status_code]
|
||||
|
||||
# locate all items to which new status will be applied
|
||||
items = [main_item]
|
||||
# uuids = self.request.POST.get('uuids')
|
||||
# if uuids:
|
||||
# for uuid in uuids.split(','):
|
||||
# item = Session.get(model.OrderItem, uuid)
|
||||
# if item:
|
||||
# items.append(item)
|
||||
|
||||
# update item(s)
|
||||
for item in items:
|
||||
if item.status_code != new_status_code:
|
||||
|
||||
# event: change status
|
||||
note = 'status changed from "{}" to "{}"'.format(
|
||||
enum.ORDER_ITEM_STATUS[item.status_code],
|
||||
new_status_text)
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_STATUS_CHANGE,
|
||||
self.request.user, note=note)
|
||||
|
||||
# event: add note
|
||||
if extra_note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED,
|
||||
self.request.user, note=extra_note)
|
||||
|
||||
# new status
|
||||
item.status_code = new_status_code
|
||||
|
||||
self.request.session.flash(f"Status has been updated to: {new_status_text}")
|
||||
return redirect
|
||||
|
||||
def get_order_items(self, uuids):
|
||||
"""
|
||||
This method provides common logic to fetch a list of order
|
||||
items based on a list of UUID keys. It is used by various
|
||||
workflow action methods.
|
||||
|
||||
Note that if no order items are found, this will set a flash
|
||||
warning message and raise a redirect back to the index page.
|
||||
|
||||
:param uuids: List (or comma-delimited string) of UUID keys.
|
||||
|
||||
:returns: List of :class:`~sideshow.db.model.orders.OrderItem`
|
||||
records.
|
||||
"""
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
if uuids is None:
|
||||
uuids = []
|
||||
elif isinstance(uuids, str):
|
||||
uuids = uuids.split(',')
|
||||
|
||||
items = []
|
||||
for uuid in uuids:
|
||||
if isinstance(uuid, str):
|
||||
uuid = uuid.strip()
|
||||
if uuid:
|
||||
try:
|
||||
item = session.get(model.OrderItem, uuid)
|
||||
except sa.exc.StatementError:
|
||||
pass # nb. invalid UUID
|
||||
else:
|
||||
if item:
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
self.request.session.flash("Must specify valid order item(s).", 'warning')
|
||||
raise self.redirect(self.get_index_url())
|
||||
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
""" """
|
||||
cls._order_item_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _order_item_defaults(cls, config):
|
||||
""" """
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
model_title = cls.get_model_title()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
|
||||
# fix perm group
|
||||
config.add_wutta_permission_group(permission_prefix,
|
||||
model_title_plural,
|
||||
overwrite=False)
|
||||
|
||||
# add note
|
||||
config.add_route(f'{route_prefix}.add_note',
|
||||
f'{instance_url_prefix}/add_note',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='add_note',
|
||||
route_name=f'{route_prefix}.add_note',
|
||||
renderer='json',
|
||||
permission=f'{permission_prefix}.add_note')
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.add_note',
|
||||
f"Add note for {model_title}")
|
||||
|
||||
# change status
|
||||
config.add_route(f'{route_prefix}.change_status',
|
||||
f'{instance_url_prefix}/change-status',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='change_status',
|
||||
route_name=f'{route_prefix}.change_status',
|
||||
renderer='json',
|
||||
permission=f'{permission_prefix}.change_status')
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.change_status',
|
||||
f"Change status for {model_title}")
|
||||
|
||||
|
||||
class PlacementView(OrderItemView):
|
||||
"""
|
||||
Master view for the "placement" phase of
|
||||
:class:`~sideshow.db.model.orders.OrderItem`; route prefix is
|
||||
``placement``. This is a subclass of :class:`OrderItemView`.
|
||||
|
||||
This class auto-filters so only order items with the following
|
||||
status codes are shown:
|
||||
|
||||
* :data:`~sideshow.enum.ORDER_ITEM_STATUS_READY`
|
||||
|
||||
Notable URLs provided by this class:
|
||||
|
||||
* ``/placement/``
|
||||
* ``/placement/XXX``
|
||||
"""
|
||||
model_title = "Order Item (Placement)"
|
||||
model_title_plural = "Order Items (Placement)"
|
||||
route_prefix = 'order_items_placement'
|
||||
url_prefix = '/placement'
|
||||
|
||||
def get_query(self, session=None):
|
||||
""" """
|
||||
query = super().get_query(session=session)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
return query.filter(model.OrderItem.status_code == enum.ORDER_ITEM_STATUS_READY)
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
||||
# checkable
|
||||
if self.has_perm('process_placement'):
|
||||
g.checkable = True
|
||||
|
||||
# tool button: Order Placed
|
||||
if self.has_perm('process_placement'):
|
||||
button = self.make_button("Order Placed", primary=True,
|
||||
icon_left='arrow-circle-right',
|
||||
**{'@click': "$emit('process-placement', checkedRows)",
|
||||
':disabled': '!checkedRows.length'})
|
||||
g.add_tool(button, key='process_placement')
|
||||
|
||||
def process_placement(self):
|
||||
"""
|
||||
View to process the "placement" step for some order item(s).
|
||||
|
||||
This requires a POST request with data:
|
||||
|
||||
:param item_uuids: Comma-delimited list of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` UUID keys.
|
||||
|
||||
:param vendor_name: Optional name of vendor.
|
||||
|
||||
:param po_number: Optional PO number.
|
||||
|
||||
:param note: Optional note text from the user.
|
||||
|
||||
This invokes
|
||||
:meth:`~sideshow.orders.OrderHandler.process_placement()` on
|
||||
the :attr:`~OrderItemView.order_handler`, then redirects user
|
||||
back to the index page.
|
||||
"""
|
||||
items = self.get_order_items(self.request.POST.get('item_uuids', ''))
|
||||
vendor_name = self.request.POST.get('vendor_name', '').strip() or None
|
||||
po_number = self.request.POST.get('po_number', '').strip() or None
|
||||
note = self.request.POST.get('note', '').strip() or None
|
||||
|
||||
self.order_handler.process_placement(items, self.request.user,
|
||||
vendor_name=vendor_name,
|
||||
po_number=po_number,
|
||||
note=note)
|
||||
|
||||
self.request.session.flash(f"{len(items)} Order Items were marked as placed")
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._order_item_defaults(config)
|
||||
cls._placement_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _placement_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
|
||||
# process placement
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.process_placement',
|
||||
f"Process placement for {model_title_plural}")
|
||||
config.add_route(f'{route_prefix}.process_placement',
|
||||
f'{url_prefix}/process-placement',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='process_placement',
|
||||
route_name=f'{route_prefix}.process_placement',
|
||||
permission=f'{permission_prefix}.process_placement')
|
||||
|
||||
|
||||
class ReceivingView(OrderItemView):
|
||||
"""
|
||||
Master view for the "receiving" phase of
|
||||
:class:`~sideshow.db.model.orders.OrderItem`; route prefix is
|
||||
``receiving``. This is a subclass of :class:`OrderItemView`.
|
||||
|
||||
This class auto-filters so only order items with the following
|
||||
status codes are shown:
|
||||
|
||||
* :data:`~sideshow.enum.ORDER_ITEM_STATUS_PLACED`
|
||||
|
||||
Notable URLs provided by this class:
|
||||
|
||||
* ``/receiving/``
|
||||
* ``/receiving/XXX``
|
||||
"""
|
||||
model_title = "Order Item (Receiving)"
|
||||
model_title_plural = "Order Items (Receiving)"
|
||||
route_prefix = 'order_items_receiving'
|
||||
url_prefix = '/receiving'
|
||||
|
||||
def get_query(self, session=None):
|
||||
""" """
|
||||
query = super().get_query(session=session)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
return query.filter(model.OrderItem.status_code == enum.ORDER_ITEM_STATUS_PLACED)
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
||||
# checkable
|
||||
if self.has_any_perm('process_receiving', 'process_reorder'):
|
||||
g.checkable = True
|
||||
|
||||
# tool button: Received
|
||||
if self.has_perm('process_receiving'):
|
||||
button = self.make_button("Received", primary=True,
|
||||
icon_left='arrow-circle-right',
|
||||
**{'@click': "$emit('process-receiving', checkedRows)",
|
||||
':disabled': '!checkedRows.length'})
|
||||
g.add_tool(button, key='process_receiving')
|
||||
|
||||
# tool button: Re-Order
|
||||
if self.has_perm('process_reorder'):
|
||||
button = self.make_button("Re-Order",
|
||||
icon_left='redo',
|
||||
**{'@click': "$emit('process-reorder', checkedRows)",
|
||||
':disabled': '!checkedRows.length'})
|
||||
g.add_tool(button, key='process_reorder')
|
||||
|
||||
def process_receiving(self):
|
||||
"""
|
||||
View to process the "receiving" step for some order item(s).
|
||||
|
||||
This requires a POST request with data:
|
||||
|
||||
:param item_uuids: Comma-delimited list of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` UUID keys.
|
||||
|
||||
:param vendor_name: Optional name of vendor.
|
||||
|
||||
:param invoice_number: Optional invoice number.
|
||||
|
||||
:param po_number: Optional PO number.
|
||||
|
||||
:param note: Optional note text from the user.
|
||||
|
||||
This invokes
|
||||
:meth:`~sideshow.orders.OrderHandler.process_receiving()` on
|
||||
the :attr:`~OrderItemView.order_handler`, then redirects user
|
||||
back to the index page.
|
||||
"""
|
||||
items = self.get_order_items(self.request.POST.get('item_uuids', ''))
|
||||
vendor_name = self.request.POST.get('vendor_name', '').strip() or None
|
||||
invoice_number = self.request.POST.get('invoice_number', '').strip() or None
|
||||
po_number = self.request.POST.get('po_number', '').strip() or None
|
||||
note = self.request.POST.get('note', '').strip() or None
|
||||
|
||||
self.order_handler.process_receiving(items, self.request.user,
|
||||
vendor_name=vendor_name,
|
||||
invoice_number=invoice_number,
|
||||
po_number=po_number,
|
||||
note=note)
|
||||
|
||||
self.request.session.flash(f"{len(items)} Order Items were marked as received")
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
def process_reorder(self):
|
||||
"""
|
||||
View to process the "reorder" step for some order item(s).
|
||||
|
||||
This requires a POST request with data:
|
||||
|
||||
:param item_uuids: Comma-delimited list of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` UUID keys.
|
||||
|
||||
:param note: Optional note text from the user.
|
||||
|
||||
This invokes
|
||||
:meth:`~sideshow.orders.OrderHandler.process_reorder()` on the
|
||||
:attr:`~OrderItemView.order_handler`, then redirects user back
|
||||
to the index page.
|
||||
"""
|
||||
items = self.get_order_items(self.request.POST.get('item_uuids', ''))
|
||||
note = self.request.POST.get('note', '').strip() or None
|
||||
|
||||
self.order_handler.process_reorder(items, self.request.user, note=note)
|
||||
|
||||
self.request.session.flash(f"{len(items)} Order Items were marked as ready for placement")
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._order_item_defaults(config)
|
||||
cls._receiving_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _receiving_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
|
||||
# process receiving
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.process_receiving',
|
||||
f"Process receiving for {model_title_plural}")
|
||||
config.add_route(f'{route_prefix}.process_receiving',
|
||||
f'{url_prefix}/process-receiving',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='process_receiving',
|
||||
route_name=f'{route_prefix}.process_receiving',
|
||||
permission=f'{permission_prefix}.process_receiving')
|
||||
|
||||
# process reorder
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.process_reorder',
|
||||
f"Process re-order for {model_title_plural}")
|
||||
config.add_route(f'{route_prefix}.process_reorder',
|
||||
f'{url_prefix}/process-reorder',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='process_reorder',
|
||||
route_name=f'{route_prefix}.process_reorder',
|
||||
permission=f'{permission_prefix}.process_reorder')
|
||||
|
||||
|
||||
class ContactView(OrderItemView):
|
||||
"""
|
||||
Master view for the "contact" phase of
|
||||
:class:`~sideshow.db.model.orders.OrderItem`; route prefix is
|
||||
``contact``. This is a subclass of :class:`OrderItemView`.
|
||||
|
||||
This class auto-filters so only order items with the following
|
||||
status codes are shown:
|
||||
|
||||
* :data:`~sideshow.enum.ORDER_ITEM_STATUS_RECEIVED`
|
||||
* :data:`~sideshow.enum.ORDER_ITEM_STATUS_CONTACT_FAILED`
|
||||
|
||||
Notable URLs provided by this class:
|
||||
|
||||
* ``/contact/``
|
||||
* ``/contact/XXX``
|
||||
"""
|
||||
model_title = "Order Item (Contact)"
|
||||
model_title_plural = "Order Items (Contact)"
|
||||
route_prefix = 'order_items_contact'
|
||||
url_prefix = '/contact'
|
||||
|
||||
def get_query(self, session=None):
|
||||
""" """
|
||||
query = super().get_query(session=session)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
return query.filter(model.OrderItem.status_code.in_((
|
||||
enum.ORDER_ITEM_STATUS_RECEIVED,
|
||||
enum.ORDER_ITEM_STATUS_CONTACT_FAILED)))
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
||||
# checkable
|
||||
if self.has_perm('process_contact'):
|
||||
g.checkable = True
|
||||
|
||||
# tool button: Contact Success
|
||||
if self.has_perm('process_contact'):
|
||||
button = self.make_button("Contact Success", primary=True,
|
||||
icon_left='phone',
|
||||
**{'@click': "$emit('process-contact-success', checkedRows)",
|
||||
':disabled': '!checkedRows.length'})
|
||||
g.add_tool(button, key='process_contact_success')
|
||||
|
||||
# tool button: Contact Failure
|
||||
if self.has_perm('process_contact'):
|
||||
button = self.make_button("Contact Failure", variant='is-warning',
|
||||
icon_left='phone',
|
||||
**{'@click': "$emit('process-contact-failure', checkedRows)",
|
||||
':disabled': '!checkedRows.length'})
|
||||
g.add_tool(button, key='process_contact_failure')
|
||||
|
||||
def process_contact_success(self):
|
||||
"""
|
||||
View to process the "contact success" step for some order
|
||||
item(s).
|
||||
|
||||
This requires a POST request with data:
|
||||
|
||||
:param item_uuids: Comma-delimited list of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` UUID keys.
|
||||
|
||||
:param note: Optional note text from the user.
|
||||
|
||||
This invokes
|
||||
:meth:`~sideshow.orders.OrderHandler.process_contact_success()`
|
||||
on the :attr:`~OrderItemView.order_handler`, then redirects
|
||||
user back to the index page.
|
||||
"""
|
||||
items = self.get_order_items(self.request.POST.get('item_uuids', ''))
|
||||
note = self.request.POST.get('note', '').strip() or None
|
||||
|
||||
self.order_handler.process_contact_success(items, self.request.user, note=note)
|
||||
|
||||
self.request.session.flash(f"{len(items)} Order Items were marked as contacted")
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
def process_contact_failure(self):
|
||||
"""
|
||||
View to process the "contact failure" step for some order
|
||||
item(s).
|
||||
|
||||
This requires a POST request with data:
|
||||
|
||||
:param item_uuids: Comma-delimited list of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` UUID keys.
|
||||
|
||||
:param note: Optional note text from the user.
|
||||
|
||||
This invokes
|
||||
:meth:`~sideshow.orders.OrderHandler.process_contact_failure()`
|
||||
on the :attr:`~OrderItemView.order_handler`, then redirects
|
||||
user back to the index page.
|
||||
"""
|
||||
items = self.get_order_items(self.request.POST.get('item_uuids', ''))
|
||||
note = self.request.POST.get('note', '').strip() or None
|
||||
|
||||
self.order_handler.process_contact_failure(items, self.request.user, note=note)
|
||||
|
||||
self.request.session.flash(f"{len(items)} Order Items were marked as contact failed")
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._order_item_defaults(config)
|
||||
cls._contact_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _contact_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
|
||||
# common perm for processing contact success + failure
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.process_contact',
|
||||
f"Process contact success/failure for {model_title_plural}")
|
||||
|
||||
# process contact success
|
||||
config.add_route(f'{route_prefix}.process_contact_success',
|
||||
f'{url_prefix}/process-contact-success',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='process_contact_success',
|
||||
route_name=f'{route_prefix}.process_contact_success',
|
||||
permission=f'{permission_prefix}.process_contact')
|
||||
|
||||
# process contact failure
|
||||
config.add_route(f'{route_prefix}.process_contact_failure',
|
||||
f'{url_prefix}/process-contact-failure',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='process_contact_failure',
|
||||
route_name=f'{route_prefix}.process_contact_failure',
|
||||
permission=f'{permission_prefix}.process_contact')
|
||||
|
||||
|
||||
class DeliveryView(OrderItemView):
|
||||
"""
|
||||
Master view for the "delivery" phase of
|
||||
:class:`~sideshow.db.model.orders.OrderItem`; route prefix is
|
||||
``delivery``. This is a subclass of :class:`OrderItemView`.
|
||||
|
||||
This class auto-filters so only order items with the following
|
||||
status codes are shown:
|
||||
|
||||
* :data:`~sideshow.enum.ORDER_ITEM_STATUS_RECEIVED`
|
||||
* :data:`~sideshow.enum.ORDER_ITEM_STATUS_CONTACTED`
|
||||
|
||||
Notable URLs provided by this class:
|
||||
|
||||
* ``/delivery/``
|
||||
* ``/delivery/XXX``
|
||||
"""
|
||||
model_title = "Order Item (Delivery)"
|
||||
model_title_plural = "Order Items (Delivery)"
|
||||
route_prefix = 'order_items_delivery'
|
||||
url_prefix = '/delivery'
|
||||
|
||||
def get_query(self, session=None):
|
||||
""" """
|
||||
query = super().get_query(session=session)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
return query.filter(model.OrderItem.status_code.in_((
|
||||
enum.ORDER_ITEM_STATUS_RECEIVED,
|
||||
enum.ORDER_ITEM_STATUS_CONTACTED)))
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
||||
# checkable
|
||||
if self.has_any_perm('process_delivery', 'process_restock'):
|
||||
g.checkable = True
|
||||
|
||||
# tool button: Delivered
|
||||
if self.has_perm('process_delivery'):
|
||||
button = self.make_button("Delivered", primary=True,
|
||||
icon_left='check',
|
||||
**{'@click': "$emit('process-delivery', checkedRows)",
|
||||
':disabled': '!checkedRows.length'})
|
||||
g.add_tool(button, key='process_delivery')
|
||||
|
||||
# tool button: Restocked
|
||||
if self.has_perm('process_restock'):
|
||||
button = self.make_button("Restocked",
|
||||
icon_left='redo',
|
||||
**{'@click': "$emit('process-restock', checkedRows)",
|
||||
':disabled': '!checkedRows.length'})
|
||||
g.add_tool(button, key='process_restock')
|
||||
|
||||
def process_delivery(self):
|
||||
"""
|
||||
View to process the "delivery" step for some order item(s).
|
||||
|
||||
This requires a POST request with data:
|
||||
|
||||
:param item_uuids: Comma-delimited list of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` UUID keys.
|
||||
|
||||
:param note: Optional note text from the user.
|
||||
|
||||
This invokes
|
||||
:meth:`~sideshow.orders.OrderHandler.process_delivery()` on
|
||||
the :attr:`~OrderItemView.order_handler`, then redirects user
|
||||
back to the index page.
|
||||
"""
|
||||
items = self.get_order_items(self.request.POST.get('item_uuids', ''))
|
||||
note = self.request.POST.get('note', '').strip() or None
|
||||
|
||||
self.order_handler.process_delivery(items, self.request.user, note=note)
|
||||
|
||||
self.request.session.flash(f"{len(items)} Order Items were marked as delivered")
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
def process_restock(self):
|
||||
"""
|
||||
View to process the "restock" step for some order item(s).
|
||||
|
||||
This requires a POST request with data:
|
||||
|
||||
:param item_uuids: Comma-delimited list of
|
||||
:class:`~sideshow.db.model.orders.OrderItem` UUID keys.
|
||||
|
||||
:param note: Optional note text from the user.
|
||||
|
||||
This invokes
|
||||
:meth:`~sideshow.orders.OrderHandler.process_restock()` on the
|
||||
:attr:`~OrderItemView.order_handler`, then redirects user back
|
||||
to the index page.
|
||||
"""
|
||||
items = self.get_order_items(self.request.POST.get('item_uuids', ''))
|
||||
note = self.request.POST.get('note', '').strip() or None
|
||||
|
||||
self.order_handler.process_restock(items, self.request.user, note=note)
|
||||
|
||||
self.request.session.flash(f"{len(items)} Order Items were marked as restocked")
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._order_item_defaults(config)
|
||||
cls._delivery_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _delivery_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
|
||||
# process delivery
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.process_delivery',
|
||||
f"Process delivery for {model_title_plural}")
|
||||
config.add_route(f'{route_prefix}.process_delivery',
|
||||
f'{url_prefix}/process-delivery',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='process_delivery',
|
||||
route_name=f'{route_prefix}.process_delivery',
|
||||
permission=f'{permission_prefix}.process_delivery')
|
||||
|
||||
# process restock
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.process_restock',
|
||||
f"Process restock for {model_title_plural}")
|
||||
config.add_route(f'{route_prefix}.process_restock',
|
||||
f'{url_prefix}/process-restock',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='process_restock',
|
||||
route_name=f'{route_prefix}.process_restock',
|
||||
permission=f'{permission_prefix}.process_restock')
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
@ -1161,6 +1946,18 @@ def defaults(config, **kwargs):
|
|||
OrderItemView = kwargs.get('OrderItemView', base['OrderItemView'])
|
||||
OrderItemView.defaults(config)
|
||||
|
||||
PlacementView = kwargs.get('PlacementView', base['PlacementView'])
|
||||
PlacementView.defaults(config)
|
||||
|
||||
ReceivingView = kwargs.get('ReceivingView', base['ReceivingView'])
|
||||
ReceivingView.defaults(config)
|
||||
|
||||
ContactView = kwargs.get('ContactView', base['ContactView'])
|
||||
ContactView.defaults(config)
|
||||
|
||||
DeliveryView = kwargs.get('DeliveryView', base['DeliveryView'])
|
||||
DeliveryView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
|
@ -1109,6 +1109,20 @@ class TestNewOrderBatchHandler(DataTestCase):
|
|||
self.assertEqual(item.unit_cost, decimal.Decimal('3.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):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
|
|
@ -19,6 +19,11 @@ class TestOrder(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):
|
||||
|
||||
item = mod.OrderItem()
|
||||
|
@ -44,3 +49,15 @@ class TestOrderItem(DataTestCase):
|
|||
product_description='Vinegar',
|
||||
product_size='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)
|
||||
|
|
419
tests/test_orders.py
Normal file
419
tests/test_orders.py
Normal file
|
@ -0,0 +1,419 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from wuttjamaican.testing import DataTestCase
|
||||
|
||||
from sideshow import orders as mod
|
||||
|
||||
|
||||
class TestOrderHandler(DataTestCase):
|
||||
|
||||
def make_config(self, **kwargs):
|
||||
config = super().make_config(**kwargs)
|
||||
config.setdefault('wutta.model_spec', 'sideshow.db.model')
|
||||
config.setdefault('wutta.enum_spec', 'sideshow.enum')
|
||||
return config
|
||||
|
||||
def make_handler(self):
|
||||
return mod.OrderHandler(self.config)
|
||||
|
||||
def test_get_order_qty_uom_text(self):
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# typical, plain text
|
||||
text = handler.get_order_qty_uom_text(2, enum.ORDER_UOM_CASE, case_size=12)
|
||||
self.assertEqual(text, "2 Cases (x 12 = 24 Units)")
|
||||
|
||||
# typical w/ html
|
||||
text = handler.get_order_qty_uom_text(2, enum.ORDER_UOM_CASE, case_size=12, html=True)
|
||||
self.assertEqual(text, "2 Cases (× 12 = 24 Units)")
|
||||
|
||||
# unknown case size
|
||||
text = handler.get_order_qty_uom_text(2, enum.ORDER_UOM_CASE)
|
||||
self.assertEqual(text, "2 Cases (x ?? = ?? Units)")
|
||||
text = handler.get_order_qty_uom_text(2, enum.ORDER_UOM_CASE, html=True)
|
||||
self.assertEqual(text, "2 Cases (× ?? = ?? Units)")
|
||||
|
||||
# units only
|
||||
text = handler.get_order_qty_uom_text(2, enum.ORDER_UOM_UNIT)
|
||||
self.assertEqual(text, "2 Units")
|
||||
text = handler.get_order_qty_uom_text(2, enum.ORDER_UOM_UNIT, html=True)
|
||||
self.assertEqual(text, "2 Units")
|
||||
|
||||
def test_item_status_to_variant(self):
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# typical
|
||||
self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_INITIATED))
|
||||
self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_READY))
|
||||
self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_PLACED))
|
||||
self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_RECEIVED))
|
||||
self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_CONTACTED))
|
||||
self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_PAID))
|
||||
|
||||
# warning
|
||||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_CANCELED), 'warning')
|
||||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_REFUND_PENDING), 'warning')
|
||||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_REFUNDED), 'warning')
|
||||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_RESTOCKED), 'warning')
|
||||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_EXPIRED), 'warning')
|
||||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_INACTIVE), 'warning')
|
||||
|
||||
def test_process_placement(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item3)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# two items are updated
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
handler.process_placement([item1, item2], user,
|
||||
vendor_name="Acme Dist", po_number='ACME123')
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertEqual(item1.events[0].note, "PO ACME123 for vendor Acme Dist")
|
||||
self.assertEqual(item2.events[0].note, "PO ACME123 for vendor Acme Dist")
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_PLACED)
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_PLACED)
|
||||
|
||||
# update last item, without vendor name but extra note
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
handler.process_placement([item3], user, po_number="939234", note="extra note")
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item3.events), 2)
|
||||
self.assertEqual(item3.events[0].note, "PO 939234")
|
||||
self.assertEqual(item3.events[1].note, "extra note")
|
||||
self.assertEqual(item3.events[0].type_code, enum.ORDER_ITEM_EVENT_PLACED)
|
||||
self.assertEqual(item3.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_receiving(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item3)
|
||||
item4 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item4)
|
||||
item5 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item5)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# all info provided
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
handler.process_receiving([item1], user, vendor_name="Acme Dist",
|
||||
invoice_number='INV123', po_number='123')
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(item1.events[0].note, "invoice INV123 (PO 123) from vendor Acme Dist")
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_RECEIVED)
|
||||
|
||||
# missing PO number
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
handler.process_receiving([item2], user, vendor_name="Acme Dist", invoice_number='INV123')
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertEqual(item2.events[0].note, "invoice INV123 from vendor Acme Dist")
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_RECEIVED)
|
||||
|
||||
# missing invoice number
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
handler.process_receiving([item3], user, vendor_name="Acme Dist", po_number='123')
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item3.events), 1)
|
||||
self.assertEqual(item3.events[0].note, "PO 123 from vendor Acme Dist")
|
||||
self.assertEqual(item3.events[0].type_code, enum.ORDER_ITEM_EVENT_RECEIVED)
|
||||
|
||||
# vendor name only
|
||||
self.assertEqual(item4.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item4.events), 0)
|
||||
handler.process_receiving([item4], user, vendor_name="Acme Dist")
|
||||
self.assertEqual(item4.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item4.events), 1)
|
||||
self.assertEqual(item4.events[0].note, "from vendor Acme Dist")
|
||||
self.assertEqual(item4.events[0].type_code, enum.ORDER_ITEM_EVENT_RECEIVED)
|
||||
|
||||
# no info; extra note
|
||||
self.assertEqual(item5.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item5.events), 0)
|
||||
handler.process_receiving([item5], user, note="extra note")
|
||||
self.assertEqual(item5.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item5.events), 2)
|
||||
self.assertIsNone(item5.events[0].note)
|
||||
self.assertEqual(item5.events[0].type_code, enum.ORDER_ITEM_EVENT_RECEIVED)
|
||||
self.assertEqual(item5.events[1].note, "extra note")
|
||||
self.assertEqual(item5.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_reorder(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item3)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# two items are updated
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
handler.process_reorder([item1, item2], user)
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertIsNone(item2.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_REORDER)
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_REORDER)
|
||||
|
||||
# update last item, with extra note
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
handler.process_reorder([item3], user, note="extra note")
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(len(item3.events), 2)
|
||||
self.assertIsNone(item3.events[0].note)
|
||||
self.assertEqual(item3.events[1].note, "extra note")
|
||||
self.assertEqual(item3.events[0].type_code, enum.ORDER_ITEM_EVENT_REORDER)
|
||||
self.assertEqual(item3.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_contact_success(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item3)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# two items are updated
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
handler.process_contact_success([item1, item2], user)
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertIsNone(item2.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACTED)
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACTED)
|
||||
|
||||
# update last item, with extra note
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
handler.process_contact_success([item3], user, note="extra note")
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
self.assertEqual(len(item3.events), 2)
|
||||
self.assertIsNone(item3.events[0].note)
|
||||
self.assertEqual(item3.events[1].note, "extra note")
|
||||
self.assertEqual(item3.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACTED)
|
||||
self.assertEqual(item3.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_contact_failure(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item3)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# two items are updated
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
handler.process_contact_failure([item1, item2], user)
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_CONTACT_FAILED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_CONTACT_FAILED)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertIsNone(item2.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACT_FAILED)
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACT_FAILED)
|
||||
|
||||
# update last item, with extra note
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
handler.process_contact_failure([item3], user, note="extra note")
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_CONTACT_FAILED)
|
||||
self.assertEqual(len(item3.events), 2)
|
||||
self.assertIsNone(item3.events[0].note)
|
||||
self.assertEqual(item3.events[1].note, "extra note")
|
||||
self.assertEqual(item3.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACT_FAILED)
|
||||
self.assertEqual(item3.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_delivery(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item3)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# two items are updated
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
handler.process_delivery([item1, item2], user)
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_DELIVERED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_DELIVERED)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertIsNone(item2.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_DELIVERED)
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_DELIVERED)
|
||||
|
||||
# update last item, with extra note
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
handler.process_delivery([item3], user, note="extra note")
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_DELIVERED)
|
||||
self.assertEqual(len(item3.events), 2)
|
||||
self.assertIsNone(item3.events[0].note)
|
||||
self.assertEqual(item3.events[1].note, "extra note")
|
||||
self.assertEqual(item3.events[0].type_code, enum.ORDER_ITEM_EVENT_DELIVERED)
|
||||
self.assertEqual(item3.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_restock(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item3)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# two items are updated
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
handler.process_restock([item1, item2], user)
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RESTOCKED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_RESTOCKED)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertIsNone(item2.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_RESTOCKED)
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_RESTOCKED)
|
||||
|
||||
# update last item, with extra note
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
handler.process_restock([item3], user, note="extra note")
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_RESTOCKED)
|
||||
self.assertEqual(len(item3.events), 2)
|
||||
self.assertIsNone(item3.events[0].note)
|
||||
self.assertEqual(item3.events[1].note, "extra note")
|
||||
self.assertEqual(item3.events[0].type_code, enum.ORDER_ITEM_EVENT_RESTOCKED)
|
||||
self.assertEqual(item3.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
|
@ -11,6 +11,7 @@ from pyramid.response import Response
|
|||
from wuttaweb.forms.schema import WuttaMoney
|
||||
|
||||
from sideshow.batch.neworder import NewOrderBatchHandler
|
||||
from sideshow.orders import OrderHandler
|
||||
from sideshow.testing import WebTestCase
|
||||
from sideshow.web.views import orders as mod
|
||||
from sideshow.web.forms.schema import OrderRef, PendingProductRef
|
||||
|
@ -30,6 +31,13 @@ class TestOrderView(WebTestCase):
|
|||
def make_handler(self):
|
||||
return NewOrderBatchHandler(self.config)
|
||||
|
||||
def test_order_handler(self):
|
||||
view = self.make_view()
|
||||
handler = view.order_handler
|
||||
self.assertIsInstance(handler, OrderHandler)
|
||||
handler2 = view.get_order_handler()
|
||||
self.assertIs(handler2, handler)
|
||||
|
||||
def test_configure_grid(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
|
@ -1175,6 +1183,19 @@ class TestOrderView(WebTestCase):
|
|||
view.configure_row_grid(grid)
|
||||
self.assertIn('product_scancode', grid.linked_columns)
|
||||
|
||||
def test_row_grid_row_class(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
||||
# typical
|
||||
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertIsNone(view.row_grid_row_class(item, {}, 1))
|
||||
|
||||
# warning
|
||||
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_CANCELED)
|
||||
self.assertEqual(view.row_grid_row_class(item, {}, 1), 'has-background-warning')
|
||||
|
||||
def test_render_status_code(self):
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
@ -1244,17 +1265,28 @@ class TestOrderView(WebTestCase):
|
|||
self.assertTrue(self.session.query(model.Setting).count() > 1)
|
||||
|
||||
|
||||
class TestOrderItemView(WebTestCase):
|
||||
class OrderItemViewTestMixin:
|
||||
|
||||
def make_view(self):
|
||||
return mod.OrderItemView(self.request)
|
||||
def test_common_order_handler(self):
|
||||
view = self.make_view()
|
||||
handler = view.order_handler
|
||||
self.assertIsInstance(handler, OrderHandler)
|
||||
handler2 = view.get_order_handler()
|
||||
self.assertIs(handler2, handler)
|
||||
|
||||
def test_get_query(self):
|
||||
def test_common_get_fallback_templates(self):
|
||||
view = self.make_view()
|
||||
|
||||
templates = view.get_fallback_templates('view')
|
||||
self.assertEqual(templates, ['/order-items/view.mako',
|
||||
'/master/view.mako'])
|
||||
|
||||
def test_common_get_query(self):
|
||||
view = self.make_view()
|
||||
query = view.get_query(session=self.session)
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
|
||||
def test_configure_grid(self):
|
||||
def test_common_configure_grid(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
@ -1262,7 +1294,7 @@ class TestOrderItemView(WebTestCase):
|
|||
view.configure_grid(grid)
|
||||
self.assertIn('order_id', grid.linked_columns)
|
||||
|
||||
def test_render_order_id(self):
|
||||
def test_common_render_order_id(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
order = model.Order(order_id=42)
|
||||
|
@ -1270,25 +1302,26 @@ class TestOrderItemView(WebTestCase):
|
|||
order.items.append(item)
|
||||
self.assertEqual(view.render_order_id(item, None, None), 42)
|
||||
|
||||
def test_render_status_code(self):
|
||||
def test_common_render_status_code(self):
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED),
|
||||
'initiated')
|
||||
|
||||
def test_get_instance_title(self):
|
||||
def test_common_grid_row_class(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
||||
item = model.OrderItem(product_brand='Bragg',
|
||||
product_description='Vinegar',
|
||||
product_size='32oz',
|
||||
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
||||
title = view.get_instance_title(item)
|
||||
self.assertEqual(title, "(initiated) Bragg Vinegar 32oz")
|
||||
# typical
|
||||
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertIsNone(view.grid_row_class(item, {}, 1))
|
||||
|
||||
def test_configure_form(self):
|
||||
# warning
|
||||
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_CANCELED)
|
||||
self.assertEqual(view.grid_row_class(item, {}, 1), 'has-background-warning')
|
||||
|
||||
def test_common_configure_form(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
@ -1314,7 +1347,45 @@ class TestOrderItemView(WebTestCase):
|
|||
self.assertIsInstance(schema['order'].typ, OrderRef)
|
||||
self.assertNotIn('pending_product', form)
|
||||
|
||||
def test_get_xref_buttons(self):
|
||||
def test_common_get_template_context(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
||||
order = model.Order()
|
||||
item = model.OrderItem(order_qty=2, order_uom=enum.ORDER_UOM_CASE, case_size=8)
|
||||
order.items.append(item)
|
||||
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
with patch.object(view, 'viewing', new=True):
|
||||
form = view.make_model_form(model_instance=item)
|
||||
context = view.get_template_context({'instance': item, 'form': form})
|
||||
self.assertIn('item', context)
|
||||
self.assertIs(context['item'], item)
|
||||
self.assertIn('order', context)
|
||||
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_common_render_event_note(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
||||
# typical
|
||||
event = model.OrderItemEvent(type_code=enum.ORDER_ITEM_EVENT_READY, note='testing')
|
||||
result = view.render_event_note(event, 'note', 'testing')
|
||||
self.assertEqual(result, 'testing')
|
||||
|
||||
# user note
|
||||
event = model.OrderItemEvent(type_code=enum.ORDER_ITEM_EVENT_NOTE_ADDED, note='testing2')
|
||||
result = view.render_event_note(event, 'note', 'testing2')
|
||||
self.assertNotEqual(result, 'testing2')
|
||||
self.assertIn('<span', result)
|
||||
self.assertIn('class="has-background-info-light"', result)
|
||||
self.assertIn('testing2', result)
|
||||
|
||||
def test_common_get_xref_buttons(self):
|
||||
self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
@ -1336,3 +1407,595 @@ class TestOrderItemView(WebTestCase):
|
|||
buttons = view.get_xref_buttons(item)
|
||||
self.assertEqual(len(buttons), 1)
|
||||
self.assertIn("View the Order", buttons[0])
|
||||
|
||||
def test_common_add_note(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
self.pyramid_config.add_route(f'{view.get_route_prefix()}.view',
|
||||
f'{view.get_url_prefix()}/{{uuid}}')
|
||||
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
|
||||
order = model.Order(order_id=42, created_by=user)
|
||||
self.session.add(order)
|
||||
item = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
||||
order.items.append(item)
|
||||
self.session.flush()
|
||||
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
with patch.object(self.request, 'matchdict', new={'uuid': item.uuid}):
|
||||
with patch.object(self.request, 'POST', new={'note': 'testing'}):
|
||||
self.assertEqual(len(item.events), 0)
|
||||
result = view.add_note()
|
||||
self.assertEqual(len(item.events), 1)
|
||||
self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
self.assertEqual(item.events[0].note, 'testing')
|
||||
|
||||
def test_common_change_status(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
self.pyramid_config.add_route(f'{view.get_route_prefix()}.view',
|
||||
f'{view.get_url_prefix()}/{{uuid}}')
|
||||
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
|
||||
order = model.Order(order_id=42, created_by=user)
|
||||
self.session.add(order)
|
||||
item = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
||||
order.items.append(item)
|
||||
self.session.flush()
|
||||
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
with patch.object(self.request, 'user', new=user):
|
||||
with patch.object(self.request, 'matchdict', new={'uuid': item.uuid}):
|
||||
|
||||
# just status change, no note
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'new_status': enum.ORDER_ITEM_STATUS_PLACED}):
|
||||
self.assertEqual(len(item.events), 0)
|
||||
result = view.change_status()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertFalse(self.request.session.peek_flash('error'))
|
||||
self.assertEqual(len(item.events), 1)
|
||||
self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
|
||||
self.assertEqual(item.events[0].note,
|
||||
'status changed from "initiated" to "placed"')
|
||||
|
||||
# status change plus note
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'new_status': enum.ORDER_ITEM_STATUS_RECEIVED,
|
||||
'note': 'check it out'}):
|
||||
self.assertEqual(len(item.events), 1)
|
||||
result = view.change_status()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertFalse(self.request.session.peek_flash('error'))
|
||||
self.assertEqual(len(item.events), 3)
|
||||
self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
|
||||
self.assertEqual(item.events[0].note,
|
||||
'status changed from "initiated" to "placed"')
|
||||
self.assertEqual(item.events[1].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
|
||||
self.assertEqual(item.events[1].note,
|
||||
'status changed from "placed" to "received"')
|
||||
self.assertEqual(item.events[2].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
self.assertEqual(item.events[2].note, "check it out")
|
||||
|
||||
# invalid status
|
||||
with patch.object(self.request, 'POST', new={'new_status': 23432143}):
|
||||
self.assertEqual(len(item.events), 3)
|
||||
result = view.change_status()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertTrue(self.request.session.peek_flash('error'))
|
||||
self.assertEqual(len(item.events), 3)
|
||||
|
||||
|
||||
class TestOrderItemView(OrderItemViewTestMixin, WebTestCase):
|
||||
|
||||
def make_view(self):
|
||||
return mod.OrderItemView(self.request)
|
||||
|
||||
def test_get_order_items(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
self.pyramid_config.add_route('order_items', '/order-items/')
|
||||
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item2)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# no items found
|
||||
self.assertRaises(HTTPFound, view.get_order_items, None)
|
||||
self.assertRaises(HTTPFound, view.get_order_items, '')
|
||||
self.assertRaises(HTTPFound, view.get_order_items, [])
|
||||
self.assertRaises(HTTPFound, view.get_order_items, 'invalid')
|
||||
|
||||
# list of UUID
|
||||
items = view.get_order_items([item1.uuid, item2.uuid])
|
||||
self.assertEqual(len(items), 2)
|
||||
self.assertIs(items[0], item1)
|
||||
self.assertIs(items[1], item2)
|
||||
|
||||
# list of str
|
||||
items = view.get_order_items([item1.uuid.hex, item2.uuid.hex])
|
||||
self.assertEqual(len(items), 2)
|
||||
self.assertIs(items[0], item1)
|
||||
self.assertIs(items[1], item2)
|
||||
|
||||
# comma-delimited str
|
||||
items = view.get_order_items(','.join([item1.uuid.hex, item2.uuid.hex]))
|
||||
self.assertEqual(len(items), 2)
|
||||
self.assertIs(items[0], item1)
|
||||
self.assertIs(items[1], item2)
|
||||
|
||||
|
||||
class TestPlacementView(OrderItemViewTestMixin, WebTestCase):
|
||||
|
||||
def make_view(self):
|
||||
return mod.PlacementView(self.request)
|
||||
|
||||
def test_configure_grid(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# nothing added without perms
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
view.configure_grid(grid)
|
||||
self.assertFalse(grid.checkable)
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
|
||||
# button added with perm
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
view.configure_grid(grid)
|
||||
self.assertTrue(grid.checkable)
|
||||
self.assertEqual(len(grid.tools), 1)
|
||||
self.assertIn('process_placement', grid.tools)
|
||||
tool = grid.tools['process_placement']
|
||||
self.assertIn('<b-button ', tool)
|
||||
self.assertIn('Order Placed', tool)
|
||||
|
||||
def test_process_placement(self):
|
||||
self.pyramid_config.add_route('order_items_placement', '/placement/')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item1)
|
||||
item2 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item2)
|
||||
item3 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item3)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# view only configured for POST
|
||||
with patch.multiple(self.request, method='POST', user=user):
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# redirect if items not specified
|
||||
with patch.object(view.order_handler, 'process_placement') as process_placement:
|
||||
self.assertRaises(HTTPFound, view.process_placement)
|
||||
process_placement.assert_not_called()
|
||||
self.assertTrue(self.request.session.pop_flash('warning'))
|
||||
self.assertFalse(self.request.session.peek_flash())
|
||||
|
||||
# two items are updated
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
self.assertEqual(len(item2.events), 0)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'item_uuids': ','.join([item1.uuid.hex, item2.uuid.hex]),
|
||||
'vendor_name': 'Acme Dist',
|
||||
'po_number': 'ACME123',
|
||||
}):
|
||||
view.process_placement()
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(item2.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(item3.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(len(item1.events), 1)
|
||||
self.assertEqual(len(item2.events), 1)
|
||||
self.assertEqual(len(item3.events), 0)
|
||||
self.assertEqual(item1.events[0].note, "PO ACME123 for vendor Acme Dist")
|
||||
self.assertEqual(item2.events[0].note, "PO ACME123 for vendor Acme Dist")
|
||||
self.assertFalse(self.request.session.peek_flash('warning'))
|
||||
self.assertTrue(self.request.session.pop_flash())
|
||||
|
||||
|
||||
class TestReceivingView(OrderItemViewTestMixin, WebTestCase):
|
||||
|
||||
def make_view(self):
|
||||
return mod.ReceivingView(self.request)
|
||||
|
||||
def test_configure_grid(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# nothing added without perms
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
view.configure_grid(grid)
|
||||
self.assertFalse(grid.checkable)
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
|
||||
# buttons added with perm
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
view.configure_grid(grid)
|
||||
self.assertEqual(len(grid.tools), 2)
|
||||
self.assertTrue(grid.checkable)
|
||||
|
||||
self.assertIn('process_receiving', grid.tools)
|
||||
tool = grid.tools['process_receiving']
|
||||
self.assertIn('<b-button ', tool)
|
||||
self.assertIn('Received', tool)
|
||||
|
||||
self.assertIn('process_reorder', grid.tools)
|
||||
tool = grid.tools['process_reorder']
|
||||
self.assertIn('<b-button ', tool)
|
||||
self.assertIn('Re-Order', tool)
|
||||
|
||||
def test_process_receiving(self):
|
||||
self.pyramid_config.add_route('order_items_receiving', '/receiving/')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item1)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# view only configured for POST
|
||||
with patch.multiple(self.request, method='POST', user=user):
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# redirect if items not specified
|
||||
with patch.object(view.order_handler, 'process_receiving') as process_receiving:
|
||||
self.assertRaises(HTTPFound, view.process_receiving)
|
||||
process_receiving.assert_not_called()
|
||||
self.assertTrue(self.request.session.pop_flash('warning'))
|
||||
self.assertFalse(self.request.session.peek_flash())
|
||||
|
||||
# all info provided
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'item_uuids': item1.uuid.hex,
|
||||
'vendor_name': "Acme Dist",
|
||||
'invoice_number': 'INV123',
|
||||
'po_number': '123',
|
||||
'note': 'extra note',
|
||||
}):
|
||||
view.process_receiving()
|
||||
self.assertFalse(self.request.session.peek_flash('warning'))
|
||||
self.assertTrue(self.request.session.pop_flash())
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 2)
|
||||
self.assertEqual(item1.events[0].note, "invoice INV123 (PO 123) from vendor Acme Dist")
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_RECEIVED)
|
||||
self.assertEqual(item1.events[1].note, "extra note")
|
||||
self.assertEqual(item1.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_reorder(self):
|
||||
self.pyramid_config.add_route('order_items_receiving', '/receiving/')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_PLACED)
|
||||
order.items.append(item1)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# view only configured for POST
|
||||
with patch.multiple(self.request, method='POST', user=user):
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# redirect if items not specified
|
||||
with patch.object(view.order_handler, 'process_reorder') as process_reorder:
|
||||
self.assertRaises(HTTPFound, view.process_reorder)
|
||||
process_reorder.assert_not_called()
|
||||
self.assertTrue(self.request.session.pop_flash('warning'))
|
||||
self.assertFalse(self.request.session.peek_flash())
|
||||
|
||||
# all info provided
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_PLACED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'item_uuids': item1.uuid.hex,
|
||||
'note': 'extra note',
|
||||
}):
|
||||
view.process_reorder()
|
||||
self.assertFalse(self.request.session.peek_flash('warning'))
|
||||
self.assertTrue(self.request.session.pop_flash())
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_READY)
|
||||
self.assertEqual(len(item1.events), 2)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_REORDER)
|
||||
self.assertEqual(item1.events[1].note, "extra note")
|
||||
self.assertEqual(item1.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
|
||||
class TestContactView(OrderItemViewTestMixin, WebTestCase):
|
||||
|
||||
def make_view(self):
|
||||
return mod.ContactView(self.request)
|
||||
|
||||
def test_configure_grid(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# nothing added without perms
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
view.configure_grid(grid)
|
||||
self.assertFalse(grid.checkable)
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
|
||||
# buttons added with perm
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
view.configure_grid(grid)
|
||||
self.assertEqual(len(grid.tools), 2)
|
||||
self.assertTrue(grid.checkable)
|
||||
|
||||
self.assertIn('process_contact_success', grid.tools)
|
||||
tool = grid.tools['process_contact_success']
|
||||
self.assertIn('<b-button ', tool)
|
||||
self.assertIn('Contact Success', tool)
|
||||
|
||||
self.assertIn('process_contact_failure', grid.tools)
|
||||
tool = grid.tools['process_contact_failure']
|
||||
self.assertIn('<b-button ', tool)
|
||||
self.assertIn('Contact Failure', tool)
|
||||
|
||||
def test_process_contact_success(self):
|
||||
self.pyramid_config.add_route('order_items_contact', '/contact/')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item1)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# view only configured for POST
|
||||
with patch.multiple(self.request, method='POST', user=user):
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# redirect if items not specified
|
||||
with patch.object(view.order_handler, 'process_contact_success') as process_contact_success:
|
||||
self.assertRaises(HTTPFound, view.process_contact_success)
|
||||
process_contact_success.assert_not_called()
|
||||
self.assertTrue(self.request.session.pop_flash('warning'))
|
||||
self.assertFalse(self.request.session.peek_flash())
|
||||
|
||||
# all info provided
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'item_uuids': item1.uuid.hex,
|
||||
'note': 'extra note',
|
||||
}):
|
||||
view.process_contact_success()
|
||||
self.assertFalse(self.request.session.peek_flash('warning'))
|
||||
self.assertTrue(self.request.session.pop_flash())
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
self.assertEqual(len(item1.events), 2)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACTED)
|
||||
self.assertEqual(item1.events[1].note, "extra note")
|
||||
self.assertEqual(item1.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_contact_failure(self):
|
||||
self.pyramid_config.add_route('order_items_contact', '/contact/')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
order.items.append(item1)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# view only configured for POST
|
||||
with patch.multiple(self.request, method='POST', user=user):
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# redirect if items not specified
|
||||
with patch.object(view.order_handler, 'process_contact_failure') as process_contact_failure:
|
||||
self.assertRaises(HTTPFound, view.process_contact_failure)
|
||||
process_contact_failure.assert_not_called()
|
||||
self.assertTrue(self.request.session.pop_flash('warning'))
|
||||
self.assertFalse(self.request.session.peek_flash())
|
||||
|
||||
# all info provided
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RECEIVED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'item_uuids': item1.uuid.hex,
|
||||
'note': 'extra note',
|
||||
}):
|
||||
view.process_contact_failure()
|
||||
self.assertFalse(self.request.session.peek_flash('warning'))
|
||||
self.assertTrue(self.request.session.pop_flash())
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_CONTACT_FAILED)
|
||||
self.assertEqual(len(item1.events), 2)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_CONTACT_FAILED)
|
||||
self.assertEqual(item1.events[1].note, "extra note")
|
||||
self.assertEqual(item1.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
|
||||
class TestDeliveryView(OrderItemViewTestMixin, WebTestCase):
|
||||
|
||||
def make_view(self):
|
||||
return mod.DeliveryView(self.request)
|
||||
|
||||
def test_configure_grid(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# nothing added without perms
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
view.configure_grid(grid)
|
||||
self.assertFalse(grid.checkable)
|
||||
self.assertEqual(len(grid.tools), 0)
|
||||
|
||||
# buttons added with perm
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
view.configure_grid(grid)
|
||||
self.assertEqual(len(grid.tools), 2)
|
||||
self.assertTrue(grid.checkable)
|
||||
|
||||
self.assertIn('process_delivery', grid.tools)
|
||||
tool = grid.tools['process_delivery']
|
||||
self.assertIn('<b-button ', tool)
|
||||
self.assertIn('Delivered', tool)
|
||||
|
||||
self.assertIn('process_restock', grid.tools)
|
||||
tool = grid.tools['process_restock']
|
||||
self.assertIn('<b-button ', tool)
|
||||
self.assertIn('Restocked', tool)
|
||||
|
||||
def test_process_delivery(self):
|
||||
self.pyramid_config.add_route('order_items_delivery', '/delivery/')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
order.items.append(item1)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# view only configured for POST
|
||||
with patch.multiple(self.request, method='POST', user=user):
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# redirect if items not specified
|
||||
with patch.object(view.order_handler, 'process_delivery') as process_delivery:
|
||||
self.assertRaises(HTTPFound, view.process_delivery)
|
||||
process_delivery.assert_not_called()
|
||||
self.assertTrue(self.request.session.pop_flash('warning'))
|
||||
self.assertFalse(self.request.session.peek_flash())
|
||||
|
||||
# all info provided
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'item_uuids': item1.uuid.hex,
|
||||
'note': 'extra note',
|
||||
}):
|
||||
view.process_delivery()
|
||||
self.assertFalse(self.request.session.peek_flash('warning'))
|
||||
self.assertTrue(self.request.session.pop_flash())
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_DELIVERED)
|
||||
self.assertEqual(len(item1.events), 2)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_DELIVERED)
|
||||
self.assertEqual(item1.events[1].note, "extra note")
|
||||
self.assertEqual(item1.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
||||
def test_process_restock(self):
|
||||
self.pyramid_config.add_route('order_items_delivery', '/delivery/')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
grid = view.make_grid(model_class=model.OrderItem)
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
order = model.Order(order_id=42, customer_name="Fred Flintstone", created_by=user)
|
||||
item1 = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
order.items.append(item1)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# view only configured for POST
|
||||
with patch.multiple(self.request, method='POST', user=user):
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
|
||||
# redirect if items not specified
|
||||
with patch.object(view.order_handler, 'process_restock') as process_restock:
|
||||
self.assertRaises(HTTPFound, view.process_restock)
|
||||
process_restock.assert_not_called()
|
||||
self.assertTrue(self.request.session.pop_flash('warning'))
|
||||
self.assertFalse(self.request.session.peek_flash())
|
||||
|
||||
# all info provided
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_CONTACTED)
|
||||
self.assertEqual(len(item1.events), 0)
|
||||
with patch.object(self.request, 'POST', new={
|
||||
'item_uuids': item1.uuid.hex,
|
||||
'note': 'extra note',
|
||||
}):
|
||||
view.process_restock()
|
||||
self.assertFalse(self.request.session.peek_flash('warning'))
|
||||
self.assertTrue(self.request.session.pop_flash())
|
||||
self.assertEqual(item1.status_code, enum.ORDER_ITEM_STATUS_RESTOCKED)
|
||||
self.assertEqual(len(item1.events), 2)
|
||||
self.assertIsNone(item1.events[0].note)
|
||||
self.assertEqual(item1.events[0].type_code, enum.ORDER_ITEM_EVENT_RESTOCKED)
|
||||
self.assertEqual(item1.events[1].note, "extra note")
|
||||
self.assertEqual(item1.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
|
|
Loading…
Reference in a new issue