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.
"""
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
@ -366,6 +372,7 @@ ORDER_ITEM_EVENT = OrderedDict([
(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"),

View file

@ -96,3 +96,215 @@ class OrderHandler(GenericHandler):
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

View file

@ -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',

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 colander
import sqlalchemy as sa
from sqlalchemy import orm
from webhelpers2.html import tags, HTML
@ -977,6 +978,15 @@ 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.
@ -986,7 +996,8 @@ class OrderItemView(MasterView):
: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
@ -1072,6 +1083,12 @@ class OrderItemView(MasterView):
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)
@ -1304,13 +1321,56 @@ class OrderItemView(MasterView):
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()
@ -1347,6 +1407,536 @@ class OrderItemView(MasterView):
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()
@ -1356,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)

View file

@ -9,6 +9,7 @@ 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
@ -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_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)

View file

@ -1265,24 +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_order_handler(self):
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)
@ -1290,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)
@ -1298,13 +1302,13 @@ 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_grid_row_class(self):
def test_common_grid_row_class(self):
model = self.app.model
enum = self.app.enum
view = self.make_view()
@ -1317,7 +1321,7 @@ class TestOrderItemView(WebTestCase):
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_CANCELED)
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
enum = self.app.enum
view = self.make_view()
@ -1343,7 +1347,7 @@ class TestOrderItemView(WebTestCase):
self.assertIsInstance(schema['order'].typ, OrderRef)
self.assertNotIn('pending_product', form)
def test_get_template_context(self):
def test_common_get_template_context(self):
model = self.app.model
enum = self.app.enum
view = self.make_view()
@ -1363,7 +1367,7 @@ class TestOrderItemView(WebTestCase):
self.assertIn('order_qty_uom_text', context)
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
enum = self.app.enum
view = self.make_view()
@ -1381,7 +1385,7 @@ class TestOrderItemView(WebTestCase):
self.assertIn('class="has-background-info-light"', 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}')
model = self.app.model
enum = self.app.enum
@ -1404,11 +1408,12 @@ class TestOrderItemView(WebTestCase):
self.assertEqual(len(buttons), 1)
self.assertIn("View the Order", buttons[0])
def test_add_note(self):
self.pyramid_config.add_route('order_items.view', '/order-items/{uuid}')
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)
@ -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].note, 'testing')
def test_change_status(self):
self.pyramid_config.add_route('order_items.view', '/order-items/{uuid}')
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)
@ -1459,7 +1465,7 @@ class TestOrderItemView(WebTestCase):
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 changed from "initiated" to "placed"')
# status change plus note
with patch.object(self.request, 'POST', new={
@ -1472,10 +1478,10 @@ class TestOrderItemView(WebTestCase):
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'")
'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'")
'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")
@ -1486,3 +1492,510 @@ class TestOrderItemView(WebTestCase):
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)