Compare commits

...

19 commits

Author SHA1 Message Date
Lance Edgar 3d02e46f2a Update changelog 2024-06-03 11:40:09 -05:00
Lance Edgar f21a7298fe Optionally allow decimal quantities for receiving 2024-05-31 10:58:44 -05:00
Lance Edgar b04fa3eb72 Update changelog 2024-05-29 09:43:53 -05:00
Lance Edgar 32d1bd430f Show actual error text if applicable, for receiving failure 2024-04-11 14:14:01 -05:00
Lance Edgar b6e8b74eef Update changelog 2024-03-26 12:57:49 -05:00
Lance Edgar 18936d9efd Add delay when fetching rows data for model CRUD component
hoping this actually works, but can't reproduce the problem locally so
will just have to wait and see
2023-12-29 19:38:41 -06:00
Lance Edgar 86fbf8f164 Update changelog 2023-12-26 20:22:43 -06:00
Lance Edgar 7bc9991be0 Keep row filters "raw" until encoding for actual request
so there is no need to use `JSON.stringify()` everywhere, keep that
part in just one place
2023-12-20 11:48:09 -06:00
Lance Edgar 0609fdebf7 Improve focus behavior for inventory count view 2023-12-11 13:16:59 -06:00
Lance Edgar 0f56b478fc Update changelog 2023-10-23 13:13:08 -05:00
Lance Edgar e6012bddb8 Center receiving buttons; add "12 EA" quick receive button 2023-10-19 15:16:31 -05:00
Lance Edgar 4f355fc58f Show invoice number when receiving row
also fix spacing for quantities display
2023-10-19 14:56:03 -05:00
Lance Edgar 95d6e41c81 Use columns instead of table, for row receiving quantities 2023-10-19 14:39:01 -05:00
Lance Edgar daac09d1e4 Update changelog 2023-10-06 10:02:37 -05:00
Lance Edgar a1bf81aa56 Auto-focus case/unit field when showing inventory count page 2023-10-05 13:44:08 -05:00
Lance Edgar 8c6fe828ad Auto-trim username field in login form
to avoid confusion
2023-09-18 18:45:55 -05:00
Lance Edgar 8edbe48293 Update changelog 2023-09-17 21:28:32 -05:00
Lance Edgar 8421785aff Show "missing" quantities for item receiving 2023-09-17 18:30:58 -05:00
Lance Edgar 6fd233da87 Validate amount when adding receiving quantity 2023-09-17 17:18:33 -05:00
9 changed files with 360 additions and 84 deletions

View file

@ -5,6 +5,41 @@ All notable changes to 'byjove' will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## Unreleased
## [0.1.26] - 2024-06-03
### Changed
- Optionally allow decimal quantities for receiving.
## [0.1.25] - 2024-05-29
### Changed
- Show actual error text if applicable, for receiving failure.
## [0.1.24] - 2024-03-26
### Changed
- Add delay when fetching rows data for model CRUD component.
## [0.1.23] - 2023-12-26
### Changed
- Improve focus behavior for inventory count view.
- Keep row filters "raw" until encoding for actual request.
## [0.1.22] - 2023-10-23
### Changed
- Use columns instead of table, for row receiving quantities.
- Show invoice number when receiving row.
- Center receiving buttons; add "12 EA" quick receive button.
## [0.1.21] - 2023-10-06
### Changed
- Auto-trim username field in login form.
- Auto-focus case/unit field when showing inventory count page.
## [0.1.20] - 2023-09-17
### Changed
- Validate amount when adding receiving quantity.
- Show "missing" quantities for item receiving.
## [0.1.19] - 2023-08-29 ## [0.1.19] - 2023-08-29
### Changed ### Changed
- Add support for "missing" credit in mobile receiving. - Add support for "missing" credit in mobile receiving.

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "byjove", "name": "byjove",
"version": "0.1.19", "version": "0.1.26",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "byjove", "name": "byjove",
"version": "0.1.19", "version": "0.1.26",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"vue": "^2.7.14" "vue": "^2.7.14"

View file

@ -1,6 +1,6 @@
{ {
"name": "byjove", "name": "byjove",
"version": "0.1.19", "version": "0.1.26",
"description": "Generic-ish app components for Vue.js frontend to Tailbone API backend", "description": "Generic-ish app components for Vue.js frontend to Tailbone API backend",
"keywords": [ "keywords": [
"rattail", "rattail",

View file

@ -44,13 +44,15 @@
<b-input v-model="row.cases" <b-input v-model="row.cases"
type="number" type="number"
step="any" step="any"
ref="cases"
:disabled="!shouldAllowCases()"> :disabled="!shouldAllowCases()">
</b-input> </b-input>
</b-field> </b-field>
<b-field label="Units" expanded> <b-field label="Units" expanded>
<b-input v-model="row.units" <b-input v-model="row.units"
type="number" type="number"
step="any"> step="any"
ref="units">
</b-input> </b-input>
</b-field> </b-field>
</b-field> </b-field>
@ -81,6 +83,10 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
focusCases: {
type: Boolean,
default: false,
},
allowEdit: { allowEdit: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -90,6 +96,7 @@ export default {
default: false, default: false,
}, },
}, },
data() { data() {
return { return {
row: {}, row: {},
@ -153,6 +160,14 @@ export default {
if (this.row.batch_executed || this.row.batch_complete) { if (this.row.batch_executed || this.row.batch_complete) {
// cannot edit this row, so view it instead // cannot edit this row, so view it instead
this.$router.push(this.getViewUrl()) this.$router.push(this.getViewUrl())
} else {
this.$nextTick(() => {
if (this.shouldAllowCases() && this.focusCases) {
this.$refs.cases.focus()
} else {
this.$refs.units.focus()
}
})
} }
}, response => { }, response => {
if (response.status == 403) { // forbidden; redirect to model index if (response.status == 403) { // forbidden; redirect to model index

View file

@ -6,7 +6,7 @@
</byjove-logo> </byjove-logo>
<b-field label="Username"> <b-field label="Username">
<b-input v-model="username" /> <b-input v-model.trim="username" />
</b-field> </b-field>
<b-field label="Password"> <b-field label="Password">

View file

@ -189,10 +189,10 @@ export default {
rowFilters: { rowFilters: {
type: Function, type: Function,
default: (uuid) => { default: (uuid) => {
return JSON.stringify([ return [
{field: 'batch_uuid', op: 'eq', value: uuid}, {field: 'batch_uuid', op: 'eq', value: uuid},
{field: 'removed', op: 'eq', value: false}, {field: 'removed', op: 'eq', value: false},
]) ]
}, },
}, },
rowOrderBy: { rowOrderBy: {
@ -511,7 +511,14 @@ export default {
this.record = response.data.data this.record = response.data.data
this.$emit('refresh', this.record) this.$emit('refresh', this.record)
if (this.hasRows) { if (this.hasRows) {
this.fetchRows(uuid) // TODO: was seeing occasional errors when a batch
// view was loaded, and it tried to fetch rows but
// somehow the uuid was not passed along and
// server failed to build a query filter. so
// hoping the nextTick() delay fixes it..?
this.$nextTick(() => {
this.fetchRows(uuid)
})
} }
}, response => { }, response => {
if (response.status == 403) { // forbidden if (response.status == 403) { // forbidden
@ -538,7 +545,7 @@ export default {
fetchRows(uuid) { fetchRows(uuid) {
let params = { let params = {
filters: this.rowFilters(uuid), filters: JSON.stringify(this.rowFilters(uuid)),
orderBy: this.rowOrderBy, orderBy: this.rowOrderBy,
ascending: this.rowOrderAscending ? 1 : 0, ascending: this.rowOrderAscending ? 1 : 0,
} }

View file

@ -0,0 +1,152 @@
<template>
<b-input :name="name"
:value="value"
ref="input"
:placeholder="placeholder"
:size="size"
:icon-pack="iconPack"
:icon="icon"
:disabled="disabled"
:custom-class="customClass"
@focus="notifyFocus"
@blur="notifyBlur"
@keydown.native="keyDown"
@input="valueChanged"
/>
</template>
<script>
export default {
name: 'NumericInput',
props: {
name: String,
value: [Number, String],
placeholder: String,
iconPack: String,
icon: String,
size: String,
disabled: Boolean,
allowEnter: Boolean,
customClass: String,
},
methods: {
focus() {
this.$refs.input.focus()
},
notifyFocus(event) {
this.$emit('focus', event)
},
notifyBlur(event) {
this.$emit('blur', event)
},
keyDown(event) {
// by default we only allow numeric keys, and general navigation
// keys, but we might also allow Enter key
if (!this.key_modifies(event) && !this.key_allowed(event)) {
if (!this.allowEnter || event.which != 13) {
event.preventDefault()
}
}
},
/*
* Determine if a keypress would modify the value of a textbox.
*
* Note that this implies that the keypress is also *valid* in the context of a
* numeric textbox.
*
* Returns `true` if the keypress is valid and would modify the textbox value,
* or `false` otherwise.
*/
key_modifies(event) {
if (event.which >= 48 && event.which <= 57) { // Numeric (QWERTY)
if (! event.shiftKey) { // shift key means punctuation instead of numeric
return true;
}
} else if (event.which >= 96 && event.which <= 105) { // Numeric (10-Key)
return true;
} else if (event.which == 109 || event.which == 173) { // hyphen (negative sign)
return true;
} else if (event.which == 110 || event.which == 190) { // period/decimal
return true;
} else if (event.which == 8) { // Backspace
return true;
} else if (event.which == 46) { // Delete
return true;
} else if (event.ctrlKey && event.which == 86) { // Ctrl+V
return true;
} else if (event.ctrlKey && event.which == 88) { // Ctrl+X
return true;
}
return false;
},
/*
* Determine if a keypress is allowed in the context of a textbox.
*
* The purpose of this function is to let certain "special" keys (e.g. function
* and navigational keys) to pass through, so they may be processed as they
* would for a normal textbox.
*
* Note that this function does *not* check for keys which would actually
* modify the value of the textbox. It is assumed that the caller will have
* already used `key_modifies()` for that.
*
* Returns `true` if the keypress is allowed, or `false` otherwise.
*/
key_allowed(event) {
// Allow anything with modifiers (except Shift).
if (event.altKey || event.ctrlKey || event.metaKey) {
// ...but don't allow Ctrl+X or Ctrl+V
return event.which != 86 && event.which != 88;
}
// Allow function keys.
if (event.which >= 112 && event.which <= 123) {
return true;
}
// Allow Home/End/arrow keys.
if (event.which >= 35 && event.which <= 40) {
return true;
}
// Allow Tab key.
if (event.which == 9) {
return true;
}
// allow Escape key
if (event.which == 27) {
return true;
}
return false;
},
select() {
this.$el.children[0].select()
},
valueChanged(value) {
this.$emit('input', value)
}
},
}
</script>

View file

@ -0,0 +1,28 @@
// Import vue component
import NumericInput from './NumericInput.vue'
// Declare install function executed by Vue.use()
export function install(Vue) {
if (install.installed) return;
install.installed = true;
Vue.component('NumericInput', NumericInput);
}
// Create module definition for Vue.use()
const plugin = {
install,
};
// Auto-install when vue is found (eg. in browser via <script> tag)
let GlobalVue = null;
if (typeof window !== 'undefined') {
GlobalVue = window.Vue;
} else if (typeof global !== 'undefined') {
GlobalVue = global.Vue;
}
if (GlobalVue) {
GlobalVue.use(plugin);
}
// To allow use as module (npm/webpack/etc.) export component
export default NumericInput

View file

@ -25,58 +25,16 @@
{{ row.description }} {{ row.size }} {{ row.description }} {{ row.size }}
</p> </p>
<p v-if="row.invoice_number"
class="has-text-weight-bold">
Invoice # {{ row.invoice_number }}
</p>
<p v-if="allowCases" <p v-if="allowCases"
class="has-text-weight-bold"> class="has-text-weight-bold">
1 CS = {{ row.case_quantity || 1 }} {{ row.unit_uom }} 1 CS = {{ row.case_quantity || 1 }} {{ row.unit_uom }}
</p> </p>
<table class="receiving-quantities">
<tr v-if="row.order_quantities_known">
<td>ordered</td>
<td>
<span v-if="allowCases">
{{ row.cases_ordered || 0}} /
</span>
{{ row.units_ordered || 0}}
</td>
</tr>
<tr v-if="row.order_quantities_known">
<td>shipped</td>
<td>
<span v-if="allowCases">
{{ row.cases_shipped || 0}} /
</span>
{{ row.units_shipped || 0}}
</td>
</tr>
<tr>
<td>received</td>
<td>
<span v-if="allowCases">
{{ row.cases_received || 0}} /
</span>
{{ row.units_received || 0}}
</td>
</tr>
<tr>
<td>damaged</td>
<td>
<span v-if="allowCases">
{{ row.cases_damaged || 0}} /
</span>
{{ row.units_damaged || 0}}
</td>
</tr>
<tr v-if="allowExpired">
<td>expired</td>
<td>
<span v-if="allowCases">
{{ row.cases_expired || 0}} /
</span>
{{ row.units_expired || 0}}
</td>
</tr>
</table>
</div> </div>
<div> <div>
@ -84,6 +42,56 @@
</div> </div>
</div> </div>
<div class="columns is-mobile">
<div class="column">
<div v-if="row.order_quantities_known"
style="display: flex; gap: 0.3rem;">
<span style="flex-grow: 1;">ordered</span>
<span v-if="allowCases">{{ row.cases_ordered || 0 }} /</span>
<span>{{ row.units_ordered || 0}}</span>
</div>
<div v-if="row.order_quantities_known"
style="display: flex; gap: 0.3rem;">
<span style="flex-grow: 1;">shipped</span>
<span v-if="allowCases">{{ row.cases_shipped || 0 }} /</span>
<span>{{ row.units_shipped || 0}}</span>
</div>
<div style="display: flex; gap: 0.3rem;">
<span style="flex-grow: 1;">received</span>
<span v-if="allowCases">{{ row.cases_received || 0 }} /</span>
<span>{{ row.units_received || 0}}</span>
</div>
</div>
<div class="column">
<div style="display: flex; gap: 0.3rem;">
<span style="flex-grow: 1;">damaged</span>
<span v-if="allowCases">{{ row.cases_damaged || 0 }} /</span>
<span>{{ row.units_damaged || 0}}</span>
</div>
<div v-if="allowExpired"
style="display: flex; gap: 0.3rem;">
<span style="flex-grow: 1;">expired</span>
<span v-if="allowCases">{{ row.cases_expired || 0 }} /</span>
<span>{{ row.units_expired || 0}}</span>
</div>
<div style="display: flex; gap: 0.3rem;">
<span style="flex-grow: 1;">missing</span>
<span v-if="allowCases">{{ row.cases_missing || 0 }} /</span>
<span>{{ row.units_missing || 0}}</span>
</div>
</div>
</div>
<div v-if="row.received_alert" <div v-if="row.received_alert"
class="has-text-danger"> class="has-text-danger">
<br /> <br />
@ -110,28 +118,36 @@
expanded> expanded>
{{ row.quick_receive_text }} {{ row.quick_receive_text }}
</b-button> </b-button>
<div v-if="!row.quick_receive_all"> <div v-if="!row.quick_receive_all"
<div class="buttons" style="margin: auto;">
v-if="allowCases"> <b-button v-if="allowCases"
<b-button type="is-primary" type="is-primary"
@click="addQuickAmount(1, 'CS')" @click="addQuickAmount(1, 'CS')"
expanded> expanded
Receive 1 CS style="margin-bottom: 1rem;">
</b-button> Receive 1 CS
</div> </b-button>
<div class="buttons has-text-centered" style="width: 100%;"> <div>
<b-button type="is-primary" <b-button type="is-primary"
size="is-small"
@click="addQuickAmount(1, row.unit_uom)"> @click="addQuickAmount(1, row.unit_uom)">
1 {{ row.unit_uom }} 1 {{ row.unit_uom }}
</b-button> </b-button>
<b-button type="is-primary" <b-button type="is-primary"
size="is-small"
@click="addQuickAmount(3, row.unit_uom)"> @click="addQuickAmount(3, row.unit_uom)">
3 {{ row.unit_uom }} 3 {{ row.unit_uom }}
</b-button> </b-button>
<b-button type="is-primary" <b-button type="is-primary"
size="is-small"
@click="addQuickAmount(6, row.unit_uom)"> @click="addQuickAmount(6, row.unit_uom)">
6 {{ row.unit_uom }} 6 {{ row.unit_uom }}
</b-button> </b-button>
<b-button type="is-primary"
size="is-small"
@click="addQuickAmount(12, row.unit_uom)">
12 {{ row.unit_uom }}
</b-button>
</div> </div>
</div> </div>
</div> </div>
@ -143,10 +159,13 @@
<b-field grouped> <b-field grouped>
<b-field class="control"> <b-field class="control">
<b-input v-model="inputQuantity" <numeric-input v-if="allowDecimalQuantities"
v-model="inputQuantity"
custom-class="receiving-quantity-input" />
<b-input v-else
v-model="inputQuantity"
type="number" type="number"
custom-class="receiving-quantity-input"> custom-class="receiving-quantity-input" />
</b-input>
</b-field> </b-field>
<b-field class="control"> <b-field class="control">
<b-radio-button v-model="inputUOM" <b-radio-button v-model="inputUOM"
@ -195,15 +214,17 @@
</b-field> </b-field>
<div class="buttons"> <div class="buttons">
<b-button type="is-primary" <div style="margin: auto;">
@click="addAmount()" <b-button type="is-primary"
:disabled="addAmountDisabled"> @click="addAmount()"
Add Amount :disabled="addAmountDisabled">
</b-button> Add Amount
<b-button type="is-warning" </b-button>
@click="subtractAmount()"> <b-button type="is-warning"
Subtract Amount @click="subtractAmount()">
</b-button> Subtract
</b-button>
</div>
</div> </div>
<p class="has-text-centered is-italic">OR</p> <p class="has-text-centered is-italic">OR</p>
@ -228,8 +249,11 @@
</template> </template>
<script> <script>
import NumericInput from '../numeric-input/NumericInput.vue'
export default { export default {
name: 'ByjoveReceiving', name: 'ByjoveReceiving',
components: {NumericInput},
props: { props: {
productKey: { productKey: {
type: String, type: String,
@ -239,6 +263,10 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
allowDecimalQuantities: {
type: Boolean,
default: false,
},
allowExpired: { allowExpired: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -314,6 +342,17 @@ export default {
}, },
addAmount() { addAmount() {
let amount = this.inputQuantity
amount = this.allowDecimalQuantities ? parseFloat(amount) : parseInt(amount)
if (!amount) {
this.$buefy.toast.open({
message: "Please specify an amount",
type: 'is-info',
})
return
}
let url = `/api/receiving-batch-row/${this.row.uuid}/receive` let url = `/api/receiving-batch-row/${this.row.uuid}/receive`
let params = { let params = {
row: this.row.uuid, row: this.row.uuid,
@ -325,23 +364,23 @@ export default {
} }
if (this.inputUOM == 'CS') { if (this.inputUOM == 'CS') {
params.cases = this.inputQuantity params.cases = amount
} else { } else {
params.units = this.inputQuantity params.units = amount
} }
this.$http.post(url, params).then(response => { this.$http.post(url, params).then(response => {
if (response.data.data) { if (!response.data.error) {
this.$router.push(`/receiving/${this.row.batch_uuid}`) this.$router.push(`/receiving/${this.row.batch_uuid}`)
} else { } else {
this.$buefy.toast.open({ this.$buefy.toast.open({
message: response.data.error || "Failed to post receiving!", message: response.data.error,
type: 'is-danger', type: 'is-danger',
}) })
} }
}, response => { }, response => {
this.$buefy.toast.open({ this.$buefy.toast.open({
message: "Failed to post receiving!", message: "Save failed: unknown error",
type: 'is-danger', type: 'is-danger',
}) })
}) })
@ -365,6 +404,6 @@ export default {
padding: 0px 10px 0px 0px; padding: 0px 10px 0px 0px;
} }
.input.receiving-quantity-input { .input.receiving-quantity-input {
width: 10rem; width: 6rem;
} }
</style> </style>