Add basic "receiving" support for theo mobile

This commit is contained in:
Lance Edgar 2021-12-07 20:05:49 -06:00
parent 3779d7b9f4
commit 22aad5e31f
8 changed files with 917 additions and 0 deletions

View file

@ -11,6 +11,12 @@
<router-link to="/ordering/">Ordering</router-link>
</b-dropdown-item>
<b-dropdown-item v-if="$hasPerm('receiving.list')"
aria-role="menuitem"
has-link>
<router-link to="/receiving/">Receiving</router-link>
</b-dropdown-item>
</byjove-menu>
</template>

View file

@ -3,6 +3,7 @@ import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'
import {OrderingBatches, OrderingBatch, OrderingBatchRow, OrderingBatchWorksheet} from '../views/ordering'
import {ReceivingBatches, ReceivingBatch, ReceivingBatchRow, ReceivingBatchRowReceive} from '../views/receiving'
Vue.use(VueRouter)
@ -75,6 +76,44 @@ const routes = [
name: 'ordering.worksheet',
component: OrderingBatchWorksheet,
},
//////////////////////////////
// Receiving
//////////////////////////////
{
path: '/receiving/',
name: 'receiving',
component: ReceivingBatches,
},
{
path: '/receiving/new',
name: 'receiving.new',
component: ReceivingBatch,
props: {mode: 'creating'},
},
{
path: '/receiving/:uuid',
name: 'receiving.view',
component: ReceivingBatch,
props: {mode: 'viewing'},
},
{
path: '/receiving/:uuid/edit',
name: 'receiving.edit',
component: ReceivingBatch,
props: {mode: 'editing'},
},
{
path: '/receiving/rows/:uuid',
name: 'receiving.rows.view',
component: ReceivingBatchRow,
props: {mode: 'viewing'},
},
{
path: '/receiving/rows/:uuid/receive',
name: 'receiving.rows.receive',
component: ReceivingBatchRowReceive,
},
]
const router = new VueRouter({

View file

@ -0,0 +1,608 @@
<template>
<byjove-model-crud model-name="PurchaseBatch"
model-index-title="Receiving"
model-title="Receiving Batch"
model-title-plural="Receiving Batches"
model-path-prefix="/receiving"
model-permission-prefix="receiving"
row-path-prefix="/receiving/rows"
:mode="mode"
@refresh="refreshData"
api-index-url="/api/receiving-batches"
api-object-url="/api/receiving-batch/"
:header-label-renderer="renderHeaderLabel"
:row-label-renderer="renderRowLabel"
:allow-edit="false"
has-rows
api-rows-url="/api/receiving-batch-rows"
@save="save"
:save-disabled="saveDisabled()"
:hide-buttons="hideButtons"
:row-filters="rowFilters"
:row-route-getter="getRowRoute"
ref="modelCrud"
>
<div v-if="mode == 'creating'">
<b-field label="Vendor"
:type="{'is-danger': !batch.vendor_uuid}">
<byjove-autocomplete v-model="batch.vendor_uuid"
service-url="/api/vendors/autocomplete">
</byjove-autocomplete>
</b-field>
<div v-show="batch.vendor_uuid">
<div v-show="!createMode"
class="buttons">
<b-button type="is-primary"
@click="receiveFromPO()">
Receive from PO
</b-button>
<b-button type="is-primary"
@click="receiveFromScratch()">
Receive from Scratch
</b-button>
</div>
<div v-show="createMode == 'from_po'">
<p>Please choose a Purchase Order to receive:</p>
<div>
<b-menu>
<b-menu-list>
<b-menu-item v-for="po in purchaseOrders"
:key="po.uuid"
tag="a" href="#"
@click.prevent="receivePO(po)">
<template slot="label" slot-scope="props">
<span>{{ po.display }}</span>
</template>
</b-menu-item>
</b-menu-list>
</b-menu>
<div v-if="!fetchingPurchaseOrders && !purchaseOrders.length"
class="has-text-info">
(no purchase orders found)
</div>
<b-loading :active="fetchingPurchaseOrders || postingFormData"></b-loading>
</div>
<br />
<b-button type="is-primary"
@click="receiveFromScratch()">
Receive from Scratch
</b-button>
</div>
<!-- <b-field label="Description"> -->
<!-- <b-input v-model="batch.description"> -->
<!-- </b-input> -->
<!-- </b-field> -->
<!-- <b-field label="Notes"> -->
<!-- <b-input v-model="batch.notes" -->
<!-- type="textarea"> -->
<!-- </b-input> -->
<!-- </b-field> -->
</div>
<br />
</div> <!-- end creating -->
<div v-if="mode == 'viewing'">
<b-field label="Vendor">
<span>
{{ batch.vendor_display }}
</span>
</b-field>
<b-field label="Department">
<span>
{{ batch.department_display }}
</span>
</b-field>
<!-- <div v-if="!batch.executed && unprintedSpecialOrders" -->
<!-- class="buttons"> -->
<!-- <b-button type="is-primary" -->
<!-- @click="printSpecialOrderTickets()"> -->
<!-- Print {{ batch.special_orders.length }} Special Order Tickets -->
<!-- </b-button> -->
<!-- </div> -->
<div v-if="!batch.executed && batch.complete">
<b-field label="Description">
<span>
{{ batch.description }}
</span>
</b-field>
<b-field label="Notes"
v-show="batch.notes">
<span>
{{ batch.notes }}
</span>
</b-field>
</div>
<br v-if="!batch.executed" />
<div v-if="batch.executed">
<b-field label="Created">
<span>
{{ batch.created }}
</span>
</b-field>
<b-field label="Created By">
<span>
{{ batch.created_by_display }}
</span>
</b-field>
<b-field label="Row Count">
<span>
{{ batch.rowcount }}
</span>
</b-field>
<b-field label="Executed">
<span>
{{ batch.executed }}
</span>
</b-field>
<b-field label="Executed By">
<span>
{{ batch.executed_by_display }}
</span>
</b-field>
</div> <!-- end executed -->
</div> <!-- end viewing -->
<!-- <b-field label="Vendor" -->
<!-- :type="{'is-danger': mode == 'creating' && !batch.vendor_uuid}"> -->
<!-- <byjove-autocomplete v-if="mode == 'creating'" -->
<!-- v-model="batch.vendor_uuid" -->
<!-- service-url="/api/vendors/autocomplete"> -->
<!-- </byjove-autocomplete> -->
<!-- <span v-if="mode != 'creating'"> -->
<!-- {{ batch.vendor_display }} -->
<!-- </span> -->
<!-- </b-field> -->
<!-- <b-field label="Description"> -->
<!-- <b-input v-if="mode == 'creating'" -->
<!-- v-model="batch.description"> -->
<!-- </b-input> -->
<!-- <span v-if="mode != 'creating'"> -->
<!-- {{ batch.description }} -->
<!-- </span> -->
<!-- </b-field> -->
<!-- <b-field label="Notes" -->
<!-- > -->
<!-- <\!-- v-show="mode == 'creating' || batch.notes" -\-> -->
<!-- <b-input v-if="mode == 'creating'" -->
<!-- v-model="batch.notes" -->
<!-- type="textarea"> -->
<!-- </b-input> -->
<!-- <span v-if="mode != 'creating'"> -->
<!-- {{ batch.notes }} -->
<!-- </span> -->
<!-- </b-field> -->
<!-- <b-field label="Created" -->
<!-- v-if="mode != 'creating' && batch.executed"> -->
<!-- <span> -->
<!-- {{ batch.created }} -->
<!-- </span> -->
<!-- </b-field> -->
<!-- <b-field label="Created By" -->
<!-- v-if="mode != 'creating' && batch.executed"> -->
<!-- <span> -->
<!-- {{ batch.created_by_display }} -->
<!-- </span> -->
<!-- </b-field> -->
<!-- <b-field label="Row Count" -->
<!-- v-if="mode != 'creating' && batch.executed"> -->
<!-- <span> -->
<!-- {{ batch.rowcount }} -->
<!-- </span> -->
<!-- </b-field> -->
<!-- <b-field label="Executed" -->
<!-- v-if="mode != 'creating' && batch.executed"> -->
<!-- <span> -->
<!-- {{ batch.executed }} -->
<!-- </span> -->
<!-- </b-field> -->
<!-- <b-field label="Executed By" -->
<!-- v-if="mode != 'creating' && batch.executed"> -->
<!-- <span> -->
<!-- {{ batch.executed_by_display }} -->
<!-- </span> -->
<!-- </b-field> -->
<template slot="quick-entry">
<div v-if="mode == 'viewing' && !batch.executed && !batch.complete">
<b-input v-model="quickEntry"
placeholder="Enter UPC"
icon="search"
@keypress.native="quickKey">
</b-input>
<br />
</div>
</template>
<template slot="row-filters">
<b-field grouped group-multiline>
<b-radio-button v-model="selectedRowFilter"
native-value="is_incomplete">
<span>incomplete</span>
</b-radio-button>
<b-radio-button v-model="selectedRowFilter"
native-value="is_unexpected">
<span>unexpected</span>
</b-radio-button>
<b-radio-button v-model="selectedRowFilter"
native-value="is_damaged">
<span>damaged</span>
</b-radio-button>
<b-radio-button v-model="selectedRowFilter"
native-value="is_invalid">
<span>invalid</span>
</b-radio-button>
<b-radio-button v-model="selectedRowFilter"
native-value="all">
<span>all</span>
</b-radio-button>
</b-field>
</template>
<template slot="footer"
v-if="mode == 'viewing' && !batch.executed">
<br />
<div class="buttons is-centered">
<b-button v-if="!batch.complete"
type="is-primary"
@click="markReceivingComplete()">
Receiving is Complete!
</b-button>
<!-- <b-button v-if="batch.special_orders && (ticketsPrinted || !unprintedSpecialOrders)" -->
<!-- type="is-primary" -->
<!-- @click="printSpecialOrderTickets()"> -->
<!-- Re-print {{ batch.special_orders.length }} Special Order Tickets -->
<!-- </b-button> -->
</div>
</template>
</byjove-model-crud>
</template>
<script>
import {ByjoveModelCrud, ByjoveAutocomplete} from 'byjove'
export default {
name: 'ReceivingBatch',
props: {
mode: String,
},
components: {
ByjoveModelCrud,
ByjoveAutocomplete,
},
data() {
return {
createMode: null,
purchaseOrders: [],
fetchingPurchaseOrders: false,
postingFormData: false,
batch: {},
quickEntry: '',
ticketsPrinted: false,
selectedRowFilter: 'is_incomplete',
possibleRowFilters: {
'is_incomplete': [
{field: 'is_incomplete', op: 'eq', value: true},
],
'is_unexpected': [
{field: 'is_unexpected', op: 'eq', value: true},
],
'is_damaged': [
{field: 'is_damaged', op: 'eq', value: true},
],
'is_invalid': [
{field: 'is_invalid', op: 'eq', value: true},
],
'all': [],
},
}
},
computed: {
hideButtons: function() {
if (this.mode == 'creating') {
return true
}
return false
},
// unprintedSpecialOrders() {
// if (!this.batch.special_orders) {
// return false
// }
// if (!this.batch.special_orders.length) {
// return false
// }
// if (!this.batch.params) {
// return true
// }
// if (this.batch.params.special_order_tickets_printed) {
// return false
// }
// return true
// },
},
watch: {
'batch.vendor_uuid' (to, from) {
// when vendor selection is cleared, we must also "forget" our
// batch creation mode, so UI behaves as expected
if (!to) {
this.createMode = null
}
},
'selectedRowFilter' (to, from) {
// when user changes row filter, tell model-crud to fetch rows
this.$refs.modelCrud.fetchRows(this.batch.uuid)
},
},
mounted() {
window.addEventListener('keypress', this.globalKey)
},
beforeDestroy() {
window.removeEventListener('keypress', this.globalKey)
},
methods: {
renderHeaderLabel(batch) {
let label = batch.id_str
if (batch.executed) {
label += " (executed)"
} else if (batch.complete) {
label += " (complete)"
} else {
label += " (pending)"
}
return label
},
renderRowLabel(row) {
return `(${row.cases_received || 0} / ${row.units_received || 0}) ${row.full_description}`
},
globalKey(event) {
if (event.target.tagName == 'BODY') {
// mimic keyboard wedge
if (event.charCode >= 48 && event.charCode <= 57) { // numeric (qwerty)
this.$nextTick(function() {
this.quickEntry += event.key
})
} else if (event.keyCode == 13) { // enter
this.submitQuickEntry()
}
}
},
quickKey(event) {
if (event.keyCode == 13) { // enter
this.submitQuickEntry()
}
},
submitQuickEntry() {
if (!this.quickEntry) {
return
}
let params = {
batch_uuid: this.batch.uuid,
quick_entry: this.quickEntry,
}
let url = '/api/receiving-batch-rows/quick-entry'
this.$http.post(url, params).then(response => {
if (response.data.data) {
this.$router.push(`/receiving/rows/${response.data.data.uuid}/receive`)
} else {
this.$buefy.toast.open({
message: response.data.error || "Failed to post quick entry!",
type: 'is-danger',
position: 'is-bottom',
})
}
}, response => {
this.$buefy.toast.open({
message: "Failed to post quick entry!",
type: 'is-danger',
position: 'is-bottom',
})
})
},
markReceivingComplete() {
this.$http.post(`/api/receiving-batch/${this.batch.uuid}/mark-receiving-complete`).then(response => {
if (response.data.data) {
this.batch = response.data.data
this.$router.push('/receiving')
} else {
this.$buefy.toast.open({
message: response.data.error || "Failed to mark receiving complete!",
type: 'is-danger',
position: 'is-bottom',
})
}
}, response => {
this.$buefy.toast.open({
message: "Failed to mark receiving complete!",
type: 'is-danger',
position: 'is-bottom',
})
})
},
receiveFromPO() {
// here we will fetch all "eligible" purchase orders from backend,
// so user can then choose one to receive from/against
this.purchaseOrders = []
this.fetchingPurchaseOrders = true
this.createMode = 'from_po'
let url = '/api/receiving-batches/eligible-purchases'
let params = {
'vendor_uuid': this.batch.vendor_uuid,
}
this.$http.get(url, {params: params}).then(response => {
if (response.data.purchases) {
this.purchaseOrders = response.data.purchases
this.fetchingPurchaseOrders = false
} else {
this.$buefy.toast.open({
message: response.data.error || "Failed to fetch available purchases!",
type: 'is-danger',
position: 'is-bottom',
})
this.fetchingPurchaseOrders = false
}
}, response => {
this.$buefy.toast.open({
message: "Failed to fetch available purchases!",
type: 'is-danger',
position: 'is-bottom',
})
this.fetchingPurchaseOrders = false
})
},
receivePO(po) {
// user has chosen a purchase order (PO) for which they wish to do
// receiving. so we create a new batch from that PO
this.batch.purchase_key = po.key
this.batch.description = "(from PO)"
this.postingFormData = true
this.save()
},
receiveFromScratch() {
// if user intends to receive "from scratch" then we can
// immediately create a new (empty) batch and send them to it
this.batch.description = "(from scratch)"
this.save()
},
getRowRoute(row) {
return `/receiving/rows/${row.uuid}/receive`
},
rowFilters(uuid) {
let filters = [{field: 'batch_uuid', op: 'eq', value: uuid}]
filters = filters.concat(this.possibleRowFilters[this.selectedRowFilter])
return JSON.stringify(filters)
},
refreshData(record) {
this.batch = record
if (this.batch.complete) {
this.selectedRowFilter = 'all'
}
},
saveDisabled() {
// vendor is required when creating
if (this.mode == 'creating' && !this.batch.vendor_uuid) {
return true
}
// save is allowed by default
return false
},
save(url) {
if (url === undefined) {
url = '/api/receiving-batches'
}
let params = {
vendor_uuid: this.batch.vendor_uuid,
description: this.batch.description,
notes: this.batch.notes,
}
if (this.mode == 'creating') {
params.purchase_key = this.batch.purchase_key
}
this.$http.post(url, params).then(response => {
if (response.data.data) {
this.$router.push('/receiving/' + response.data.data.uuid)
} else {
this.$buefy.toast.open({
message: response.data.error || "Failed to save batch!",
type: 'is-danger',
position: 'is-bottom',
})
this.postingFormData = false
}
}, response => {
this.$buefy.toast.open({
message: "Failed to save batch!",
type: 'is-danger',
position: 'is-bottom',
})
this.postingFormData = false
})
},
// printSpecialOrderTickets() {
// let url = `/api/receiving-batch/${this.batch.uuid}/print-special-order-tickets`
// this.$http.post(url).then(response => {
// if (response.data.ok) {
// // must update our batch, so we know tickets were printed
// this.batch = response.data.batch
// this.$buefy.toast.open({
// message: `${response.data.printed} tickets were sent to the printer`,
// type: 'is-success',
// })
// } else {
// this.$buefy.toast.open({
// message: response.data.error || "Something went wrong!",
// type: 'is-danger',
// })
// }
// }, response => {
// this.$buefy.toast.open({
// message: response.data.error,
// type: 'is-danger',
// })
// })
// },
},
}
</script>

View file

@ -0,0 +1,193 @@
<template>
<byjove-model-crud model-name="ReceivingBatchRow"
model-index-title="Receiving"
:mode="mode"
:header-label-renderer="renderHeaderLabel"
:parent-header-label-renderer="renderParentHeaderLabel"
api-object-url="/api/receiving-batch-row/"
model-path-prefix="/receiving"
@refresh="record => { row = record }"
is-row
:allow-edit="false"
>
<!-- TODO: allow-edit should be be true if batch still open -->
<!-- model-title="Ordering Batch" -->
<!-- model-title-plural="Ordering Batches" -->
<!-- api-index-url="/api/ordering-batches" -->
<!-- :row-label-renderer="renderRowLabel" -->
<!-- @save="save" -->
<b-field label="UPC">
<span>
{{ row.upc_pretty }}
</span>
</b-field>
<b-field label="Brand">
<span>
{{ row.brand_name }}
</span>
</b-field>
<b-field label="Description">
<span>
{{ row.description }}
</span>
</b-field>
<b-field label="Size">
<span>
{{ row.size }}
</span>
</b-field>
<b-field label="Case Size">
<span>
{{ row.case_quantity }}
</span>
</b-field>
<b-field label="Cases Ordered">
<span>
{{ row.cases_ordered }}
</span>
</b-field>
<b-field label="Units Ordered">
<span>
{{ row.units_ordered }}
</span>
</b-field>
<b-field label="Cases Shipped">
<span>
{{ row.cases_shipped }}
</span>
</b-field>
<b-field label="Units Shipped">
<span>
{{ row.units_shipped }}
</span>
</b-field>
<b-field label="Cases Received">
<span>
{{ row.cases_received }}
</span>
</b-field>
<b-field label="Units Received">
<span>
{{ row.units_received }}
</span>
</b-field>
<b-field label="Cases Damaged">
<span>
{{ row.cases_damaged }}
</span>
</b-field>
<b-field label="Units Damaged">
<span>
{{ row.units_damaged }}
</span>
</b-field>
<b-field label="Cases Expired">
<span>
{{ row.cases_expired }}
</span>
</b-field>
<b-field label="Units Expired">
<span>
{{ row.units_expired }}
</span>
</b-field>
<b-field label="Status">
<span>
{{ row.status_display }}
</span>
</b-field>
<br />
<div class="buttons">
<b-button type="is-primary"
tag="router-link"
:to="`/receiving/rows/${row.uuid}/receive`">
Receive against this Row
</b-button>
<b-button type="is-primary"
tag="router-link"
:to="`/receiving/rows/${row.uuid}/edit`"
disabled>
Edit this Row
</b-button>
<b-button type="is-danger"
disabled>
Delete this Row
</b-button> </div>
</byjove-model-crud>
</template>
<script>
import {ByjoveModelCrud} from 'byjove'
export default {
name: 'ReceivingBatchRow',
props: {
mode: String,
allowCases: { // TODO
type: Boolean,
default: true,
},
allowExpired: { // TODO
type: Boolean,
default: true,
},
orderQuantitiesKnown: { // TODO
type: Boolean,
default: true,
},
},
components: {
ByjoveModelCrud,
},
data: function() {
return {
row: {},
}
},
methods: {
renderParentHeaderLabel(row) {
return row.batch_id_str
},
renderHeaderLabel(row) {
return row.upc_pretty
},
// save(url) {
// let params = {
// item_id: this.product.item_id,
// description: this.product.description,
// }
// this.$http.post(url, params).then(response => {
// this.$router.push('/products/' + response.data.data.uuid)
// })
// },
},
}
</script>
<!-- <style> -->
<!-- table.receiving-quantities td { -->
<!-- padding: 0px 10px 0px 0px; -->
<!-- } -->
<!-- </style> -->

View file

@ -0,0 +1,15 @@
<template>
<byjove-receiving>
</byjove-receiving>
</template>
<script>
import {ByjoveReceiving} from 'byjove'
export default {
name: 'ReceivingBatchRowReceive',
components: {
ByjoveReceiving,
},
}
</script>

View file

@ -0,0 +1,44 @@
<template>
<byjove-model-index model-name="PurchaseBatch"
model-index-title="Receiving"
model-title="Receiving Batch"
model-title-plural="Receiving Batches"
model-path-prefix="/receiving"
model-permission-prefix="receiving"
api-index-url="/api/receiving-batches"
:api-index-sort="{field: 'id', dir: 'desc'}"
:api-index-filters="filters"
:label-renderer="renderLabel">
</byjove-model-index>
</template>
<script>
import {ByjoveModelIndex} from 'byjove'
export default {
name: 'ReceivingBatches',
components: {
ByjoveModelIndex,
},
data() {
return {
filters: [
{field: 'executed', op: 'is_null'},
{or: [
{field: 'receiving_complete', op: 'is_null'},
{field: 'receiving_complete', op: 'eq', value: false},
]},
{or: [
{field: 'complete', op: 'is_null'},
{field: 'complete', op: 'eq', value: false},
]},
],
}
},
methods: {
renderLabel(batch) {
return `(${batch.id_str}) ${batch.vendor_display}; ${batch.description || ''} (${batch.rowcount} rows)`
},
},
}
</script>

View file

@ -0,0 +1,11 @@
import ReceivingBatches from './ReceivingBatches'
import ReceivingBatch from './ReceivingBatch'
import ReceivingBatchRow from './ReceivingBatchRow'
import ReceivingBatchRowReceive from './ReceivingBatchRowReceive'
export {
ReceivingBatches,
ReceivingBatch,
ReceivingBatchRow,
ReceivingBatchRowReceive,
}

View file

@ -36,3 +36,4 @@ def includeme(config):
# batches
config.include('tailbone.api.batch.ordering')
config.include('tailbone.api.batch.receiving')