feat: add initial workflow master views, UI features

This commit is contained in:
Lance Edgar 2025-01-22 21:32:15 -06:00
parent 9d378a0c5f
commit 7167c6a7cc
10 changed files with 2364 additions and 22 deletions

View file

@ -285,6 +285,12 @@ Indicates the buyer has placed a vendor purchase order which includes
this item. So the item is "on order" until the truck arrives. 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 ORDER_ITEM_EVENT_RECEIVED = 300
""" """
Indicates the receiver has found the item while receiving a vendor Indicates the receiver has found the item while receiving a vendor
@ -366,6 +372,7 @@ ORDER_ITEM_EVENT = OrderedDict([
(ORDER_ITEM_EVENT_CUSTOMER_RESOLVED, "customer resolved"), (ORDER_ITEM_EVENT_CUSTOMER_RESOLVED, "customer resolved"),
(ORDER_ITEM_EVENT_PRODUCT_RESOLVED, "product resolved"), (ORDER_ITEM_EVENT_PRODUCT_RESOLVED, "product resolved"),
(ORDER_ITEM_EVENT_PLACED, "placed with vendor"), (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_RECEIVED, "received from vendor"),
(ORDER_ITEM_EVENT_CONTACTED, "customer contacted"), (ORDER_ITEM_EVENT_CONTACTED, "customer contacted"),
(ORDER_ITEM_EVENT_CONTACT_FAILED, "contact failed"), (ORDER_ITEM_EVENT_CONTACT_FAILED, "contact failed"),

View file

@ -96,3 +96,215 @@ class OrderHandler(GenericHandler):
enum.ORDER_ITEM_STATUS_EXPIRED, enum.ORDER_ITEM_STATUS_EXPIRED,
enum.ORDER_ITEM_STATUS_INACTIVE): enum.ORDER_ITEM_STATUS_INACTIVE):
return 'warning' 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

View file

@ -57,6 +57,27 @@ class SideshowMenuHandler(base.MenuHandler):
'perm': 'orders.create', 'perm': 'orders.create',
}, },
{'type': 'sep'}, {'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", 'title': "All Order Items",
'route': 'order_items', 'route': 'order_items',

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

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

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

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

View file

@ -28,6 +28,7 @@ import decimal
import logging import logging
import colander import colander
import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from webhelpers2.html import tags, HTML from webhelpers2.html import tags, HTML
@ -977,6 +978,15 @@ class OrderItemView(MasterView):
* ``/order-items/`` * ``/order-items/``
* ``/order-items/XXX`` * ``/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 Note that this does not expose create, edit or delete. The user
must perform various other workflow actions to modify the item. must perform various other workflow actions to modify the item.
@ -986,7 +996,8 @@ class OrderItemView(MasterView):
:meth:`get_order_handler()`. :meth:`get_order_handler()`.
""" """
model_class = OrderItem model_class = OrderItem
model_title = "Order Item" model_title = "Order Item (All)"
model_title_plural = "Order Items (All)"
route_prefix = 'order_items' route_prefix = 'order_items'
url_prefix = '/order-items' url_prefix = '/order-items'
creatable = False creatable = False
@ -1072,6 +1083,12 @@ class OrderItemView(MasterView):
return self.order_handler return self.order_handler
return OrderHandler(self.config) 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): def get_query(self, session=None):
""" """ """ """
query = super().get_query(session=session) query = super().get_query(session=session)
@ -1304,13 +1321,56 @@ class OrderItemView(MasterView):
self.request.session.flash(f"Status has been updated to: {new_status_text}") self.request.session.flash(f"Status has been updated to: {new_status_text}")
return redirect 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 @classmethod
def defaults(cls, config): def defaults(cls, config):
""" """
cls._order_item_defaults(config) cls._order_item_defaults(config)
cls._defaults(config) cls._defaults(config)
@classmethod @classmethod
def _order_item_defaults(cls, config): def _order_item_defaults(cls, config):
""" """
route_prefix = cls.get_route_prefix() route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix() permission_prefix = cls.get_permission_prefix()
instance_url_prefix = cls.get_instance_url_prefix() instance_url_prefix = cls.get_instance_url_prefix()
@ -1347,6 +1407,536 @@ class OrderItemView(MasterView):
f"Change status for {model_title}") 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): def defaults(config, **kwargs):
base = globals() base = globals()
@ -1356,6 +1946,18 @@ def defaults(config, **kwargs):
OrderItemView = kwargs.get('OrderItemView', base['OrderItemView']) OrderItemView = kwargs.get('OrderItemView', base['OrderItemView'])
OrderItemView.defaults(config) 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): def includeme(config):
defaults(config) defaults(config)

View file

@ -9,6 +9,7 @@ class TestOrderHandler(DataTestCase):
def make_config(self, **kwargs): def make_config(self, **kwargs):
config = super().make_config(**kwargs) config = super().make_config(**kwargs)
config.setdefault('wutta.model_spec', 'sideshow.db.model')
config.setdefault('wutta.enum_spec', 'sideshow.enum') config.setdefault('wutta.enum_spec', 'sideshow.enum')
return config return config
@ -58,3 +59,361 @@ class TestOrderHandler(DataTestCase):
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_RESTOCKED), '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_EXPIRED), 'warning')
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_INACTIVE), '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)

View file

@ -1265,24 +1265,28 @@ class TestOrderView(WebTestCase):
self.assertTrue(self.session.query(model.Setting).count() > 1) self.assertTrue(self.session.query(model.Setting).count() > 1)
class TestOrderItemView(WebTestCase): class OrderItemViewTestMixin:
def make_view(self): def test_common_order_handler(self):
return mod.OrderItemView(self.request)
def test_order_handler(self):
view = self.make_view() view = self.make_view()
handler = view.order_handler handler = view.order_handler
self.assertIsInstance(handler, OrderHandler) self.assertIsInstance(handler, OrderHandler)
handler2 = view.get_order_handler() handler2 = view.get_order_handler()
self.assertIs(handler2, 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() view = self.make_view()
query = view.get_query(session=self.session) query = view.get_query(session=self.session)
self.assertIsInstance(query, orm.Query) self.assertIsInstance(query, orm.Query)
def test_configure_grid(self): def test_common_configure_grid(self):
model = self.app.model model = self.app.model
view = self.make_view() view = self.make_view()
grid = view.make_grid(model_class=model.OrderItem) grid = view.make_grid(model_class=model.OrderItem)
@ -1290,7 +1294,7 @@ class TestOrderItemView(WebTestCase):
view.configure_grid(grid) view.configure_grid(grid)
self.assertIn('order_id', grid.linked_columns) self.assertIn('order_id', grid.linked_columns)
def test_render_order_id(self): def test_common_render_order_id(self):
model = self.app.model model = self.app.model
view = self.make_view() view = self.make_view()
order = model.Order(order_id=42) order = model.Order(order_id=42)
@ -1298,13 +1302,13 @@ class TestOrderItemView(WebTestCase):
order.items.append(item) order.items.append(item)
self.assertEqual(view.render_order_id(item, None, None), 42) 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 enum = self.app.enum
view = self.make_view() view = self.make_view()
self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED), self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED),
'initiated') 'initiated')
def test_grid_row_class(self): def test_common_grid_row_class(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
view = self.make_view() view = self.make_view()
@ -1317,7 +1321,7 @@ class TestOrderItemView(WebTestCase):
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_CANCELED) item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_CANCELED)
self.assertEqual(view.grid_row_class(item, {}, 1), 'has-background-warning') self.assertEqual(view.grid_row_class(item, {}, 1), 'has-background-warning')
def test_configure_form(self): def test_common_configure_form(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
view = self.make_view() view = self.make_view()
@ -1343,7 +1347,7 @@ class TestOrderItemView(WebTestCase):
self.assertIsInstance(schema['order'].typ, OrderRef) self.assertIsInstance(schema['order'].typ, OrderRef)
self.assertNotIn('pending_product', form) self.assertNotIn('pending_product', form)
def test_get_template_context(self): def test_common_get_template_context(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
view = self.make_view() view = self.make_view()
@ -1363,7 +1367,7 @@ class TestOrderItemView(WebTestCase):
self.assertIn('order_qty_uom_text', context) self.assertIn('order_qty_uom_text', context)
self.assertEqual(context['order_qty_uom_text'], "2 Cases (&times; 8 = 16 Units)") self.assertEqual(context['order_qty_uom_text'], "2 Cases (&times; 8 = 16 Units)")
def test_render_event_note(self): def test_common_render_event_note(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
view = self.make_view() view = self.make_view()
@ -1381,7 +1385,7 @@ class TestOrderItemView(WebTestCase):
self.assertIn('class="has-background-info-light"', result) self.assertIn('class="has-background-info-light"', result)
self.assertIn('testing2', result) self.assertIn('testing2', result)
def test_get_xref_buttons(self): def test_common_get_xref_buttons(self):
self.pyramid_config.add_route('orders.view', '/orders/{uuid}') self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
@ -1404,11 +1408,12 @@ class TestOrderItemView(WebTestCase):
self.assertEqual(len(buttons), 1) self.assertEqual(len(buttons), 1)
self.assertIn("View the Order", buttons[0]) self.assertIn("View the Order", buttons[0])
def test_add_note(self): def test_common_add_note(self):
self.pyramid_config.add_route('order_items.view', '/order-items/{uuid}')
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
view = self.make_view() 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') user = model.User(username='barney')
self.session.add(user) self.session.add(user)
@ -1429,11 +1434,12 @@ class TestOrderItemView(WebTestCase):
self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED) self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
self.assertEqual(item.events[0].note, 'testing') self.assertEqual(item.events[0].note, 'testing')
def test_change_status(self): def test_common_change_status(self):
self.pyramid_config.add_route('order_items.view', '/order-items/{uuid}')
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
view = self.make_view() 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') user = model.User(username='barney')
self.session.add(user) self.session.add(user)
@ -1459,7 +1465,7 @@ class TestOrderItemView(WebTestCase):
self.assertEqual(len(item.events), 1) self.assertEqual(len(item.events), 1)
self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE) self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
self.assertEqual(item.events[0].note, self.assertEqual(item.events[0].note,
"status changed from 'initiated' to 'placed'") 'status changed from "initiated" to "placed"')
# status change plus note # status change plus note
with patch.object(self.request, 'POST', new={ with patch.object(self.request, 'POST', new={
@ -1472,10 +1478,10 @@ class TestOrderItemView(WebTestCase):
self.assertEqual(len(item.events), 3) self.assertEqual(len(item.events), 3)
self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE) self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
self.assertEqual(item.events[0].note, self.assertEqual(item.events[0].note,
"status changed from 'initiated' to 'placed'") 'status changed from "initiated" to "placed"')
self.assertEqual(item.events[1].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE) self.assertEqual(item.events[1].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
self.assertEqual(item.events[1].note, self.assertEqual(item.events[1].note,
"status changed from 'placed' to 'received'") 'status changed from "placed" to "received"')
self.assertEqual(item.events[2].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED) self.assertEqual(item.events[2].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
self.assertEqual(item.events[2].note, "check it out") self.assertEqual(item.events[2].note, "check it out")
@ -1486,3 +1492,510 @@ class TestOrderItemView(WebTestCase):
self.assertIsInstance(result, HTTPFound) self.assertIsInstance(result, HTTPFound)
self.assertTrue(self.request.session.peek_flash('error')) self.assertTrue(self.request.session.peek_flash('error'))
self.assertEqual(len(item.events), 3) 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)