Add initial "scanning" feature for Ordering Batches
This commit is contained in:
parent
801c56f06e
commit
329e75ee82
|
@ -31,7 +31,6 @@ import logging
|
||||||
import six
|
import six
|
||||||
import humanize
|
import humanize
|
||||||
|
|
||||||
from rattail import pod
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.time import make_utc
|
from rattail.time import make_utc
|
||||||
from rattail.util import pretty_quantity
|
from rattail.util import pretty_quantity
|
||||||
|
@ -268,9 +267,12 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def normalize(self, row):
|
def normalize(self, row):
|
||||||
batch = row.batch
|
|
||||||
data = super(ReceivingBatchRowViews, self).normalize(row)
|
data = super(ReceivingBatchRowViews, self).normalize(row)
|
||||||
|
|
||||||
|
batch = row.batch
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
prodder = app.get_products_handler()
|
||||||
|
|
||||||
data['product_uuid'] = row.product_uuid
|
data['product_uuid'] = row.product_uuid
|
||||||
data['item_id'] = row.item_id
|
data['item_id'] = row.item_id
|
||||||
data['upc'] = six.text_type(row.upc)
|
data['upc'] = six.text_type(row.upc)
|
||||||
|
@ -282,7 +284,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
# only provide image url if so configured
|
# only provide image url if so configured
|
||||||
if self.rattail_config.getbool('rattail.batch', 'purchase.mobile_images', default=True):
|
if self.rattail_config.getbool('rattail.batch', 'purchase.mobile_images', default=True):
|
||||||
data['image_url'] = pod.get_image_url(self.rattail_config, row.upc) if row.upc else None
|
data['image_url'] = prodder.get_image_url(product=row.product, upc=row.upc)
|
||||||
|
|
||||||
# unit_uom can vary by product
|
# unit_uom can vary by product
|
||||||
data['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA'
|
data['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA'
|
||||||
|
|
|
@ -4,8 +4,14 @@ const NumericInput = {
|
||||||
'<b-input',
|
'<b-input',
|
||||||
':name="name"',
|
':name="name"',
|
||||||
':value="value"',
|
':value="value"',
|
||||||
'@focus="focus"',
|
'ref="input"',
|
||||||
'@blur="blur"',
|
':placeholder="placeholder"',
|
||||||
|
':size="size"',
|
||||||
|
':icon-pack="iconPack"',
|
||||||
|
':icon="icon"',
|
||||||
|
':disabled="disabled"',
|
||||||
|
'@focus="notifyFocus"',
|
||||||
|
'@blur="notifyBlur"',
|
||||||
'@keydown.native="keyDown"',
|
'@keydown.native="keyDown"',
|
||||||
'@input="valueChanged"',
|
'@input="valueChanged"',
|
||||||
'>',
|
'>',
|
||||||
|
@ -15,16 +21,25 @@ const NumericInput = {
|
||||||
props: {
|
props: {
|
||||||
name: String,
|
name: String,
|
||||||
value: String,
|
value: String,
|
||||||
|
placeholder: String,
|
||||||
|
iconPack: String,
|
||||||
|
icon: String,
|
||||||
|
size: String,
|
||||||
|
disabled: Boolean,
|
||||||
allowEnter: Boolean
|
allowEnter: Boolean
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
focus(event) {
|
focus() {
|
||||||
|
this.$refs.input.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
notifyFocus(event) {
|
||||||
this.$emit('focus', event)
|
this.$emit('focus', event)
|
||||||
},
|
},
|
||||||
|
|
||||||
blur(event) {
|
notifyBlur(event) {
|
||||||
this.$emit('blur', event)
|
this.$emit('blur', event)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -96,8 +96,9 @@
|
||||||
<%def name="leading_buttons()">
|
<%def name="leading_buttons()">
|
||||||
% if master.has_worksheet and master.allow_worksheet(batch) and master.has_perm('worksheet'):
|
% if master.has_worksheet and master.allow_worksheet(batch) and master.has_perm('worksheet'):
|
||||||
% if use_buefy:
|
% if use_buefy:
|
||||||
<once-button tag="a"
|
<once-button type="is-primary"
|
||||||
href="${url('{}.worksheet'.format(route_prefix), uuid=batch.uuid)}"
|
tag="a" href="${url('{}.worksheet'.format(route_prefix), uuid=batch.uuid)}"
|
||||||
|
icon-left="edit"
|
||||||
text="Edit as Worksheet">
|
text="Edit as Worksheet">
|
||||||
</once-button>
|
</once-button>
|
||||||
% else:
|
% else:
|
||||||
|
@ -110,10 +111,10 @@
|
||||||
% if master.batch_refreshable(batch) and master.has_perm('refresh'):
|
% if master.batch_refreshable(batch) and master.has_perm('refresh'):
|
||||||
% if use_buefy:
|
% if use_buefy:
|
||||||
## TODO: this should surely use a POST request?
|
## TODO: this should surely use a POST request?
|
||||||
<once-button tag="a"
|
<once-button type="is-primary"
|
||||||
href="${url('{}.refresh'.format(route_prefix), uuid=batch.uuid)}"
|
tag="a" href="${url('{}.refresh'.format(route_prefix), uuid=batch.uuid)}"
|
||||||
text="Refresh Data"
|
text="Refresh Data"
|
||||||
icon-left="fas fa-redo">
|
icon-left="redo">
|
||||||
</once-button>
|
</once-button>
|
||||||
% else:
|
% else:
|
||||||
<button type="button" class="button" id="refresh-data">Refresh Data</button>
|
<button type="button" class="button" id="refresh-data">Refresh Data</button>
|
||||||
|
|
|
@ -13,4 +13,411 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_row_grid_tools()">
|
||||||
|
${parent.render_row_grid_tools()}
|
||||||
|
% if not batch.executed and not batch.complete and master.has_perm('edit_row'):
|
||||||
|
<ordering-scanner numeric-only>
|
||||||
|
</ordering-scanner>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_this_page_template()">
|
||||||
|
${parent.render_this_page_template()}
|
||||||
|
% if not batch.executed and not batch.complete and master.has_perm('edit_row'):
|
||||||
|
<script type="text/x-template" id="ordering-scanner-template">
|
||||||
|
<div>
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-play"
|
||||||
|
@click="startScanning()">
|
||||||
|
Start Scanning
|
||||||
|
</b-button>
|
||||||
|
<b-modal :active.sync="showScanningDialog"
|
||||||
|
:can-cancel="false">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<section style="min-height: 400px;">
|
||||||
|
<div class="columns">
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<b-field grouped>
|
||||||
|
|
||||||
|
<numeric-input v-if="numericOnly"
|
||||||
|
v-model="itemEntry"
|
||||||
|
allow-enter
|
||||||
|
placeholder="Enter UPC"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon="fas fa-search"
|
||||||
|
ref="itemEntryInput"
|
||||||
|
:disabled="currentRow"
|
||||||
|
@keydown.native="itemEntryKeydown">
|
||||||
|
</numeric-input>
|
||||||
|
|
||||||
|
<b-input v-if="!numericOnly"
|
||||||
|
v-model="itemEntry"
|
||||||
|
placeholder="Enter UPC"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon="fas fa-search"
|
||||||
|
ref="itemEntryInput"
|
||||||
|
:disabled="currentRow">
|
||||||
|
</b-input>
|
||||||
|
|
||||||
|
<b-button @click="fetchEntry()"
|
||||||
|
:disabled="currentRow">
|
||||||
|
Fetch
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div v-if="currentRow">
|
||||||
|
<b-field grouped>
|
||||||
|
|
||||||
|
<b-field label="${enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_CASE]}" horizontal>
|
||||||
|
<numeric-input v-model="currentRow.cases_ordered"
|
||||||
|
ref="casesInput"
|
||||||
|
@keydown.native="casesKeydown"
|
||||||
|
style="width: 60px; margin-right: 1rem;">
|
||||||
|
</numeric-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="currentRow.unit_of_measure_display" horizontal>
|
||||||
|
<numeric-input v-model="currentRow.units_ordered"
|
||||||
|
ref="unitsInput"
|
||||||
|
@keydown.native="unitsKeydown"
|
||||||
|
style="width: 60px;">
|
||||||
|
</numeric-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<p class="block has-text-weight-bold">
|
||||||
|
1 ${enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_CASE]}
|
||||||
|
= {{ currentRow.case_quantity || '??' }}
|
||||||
|
{{ currentRow.unit_of_measure_display }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="block has-text-weight-bold">
|
||||||
|
{{ currentRow.po_case_cost_display || '$?.??' }}
|
||||||
|
per ${enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_CASE]};
|
||||||
|
{{ currentRow.po_unit_cost_display || '$?.??' }}
|
||||||
|
per {{ currentRow.unit_of_measure_display }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="block has-text-weight-bold">
|
||||||
|
Total is
|
||||||
|
{{ totalCostDisplay }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-save"
|
||||||
|
@click="saveCurrentRow()">
|
||||||
|
Save
|
||||||
|
</b-button>
|
||||||
|
<b-button @click="cancelCurrentRow()">
|
||||||
|
Cancel
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-three-fifths">
|
||||||
|
<div v-if="currentRow">
|
||||||
|
|
||||||
|
<b-field label="UPC" horizontal>
|
||||||
|
{{ currentRow.upc_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Brand" horizontal>
|
||||||
|
{{ currentRow.brand_name }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Description" horizontal>
|
||||||
|
{{ currentRow.description }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Size" horizontal>
|
||||||
|
{{ currentRow.size }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Reg. Price" horizontal>
|
||||||
|
{{ currentRow.product_price_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<img :src="currentRow.image_url"></img>
|
||||||
|
<b-button v-if="currentRow.product_url"
|
||||||
|
type="is-primary"
|
||||||
|
tag="a" :href="currentRow.product_url"
|
||||||
|
target="_blank">
|
||||||
|
View Full Product
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <!-- columns -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item buttons">
|
||||||
|
<once-button type="is-primary"
|
||||||
|
@click="stopScanning()"
|
||||||
|
text="Stop Scanning"
|
||||||
|
icon-left="stop"
|
||||||
|
:disabled="currentRow"
|
||||||
|
:title="currentRow ? 'Please save or cancel first' : null">
|
||||||
|
</once-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <!-- card-content -->
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="modify_this_page_vars()">
|
||||||
|
${parent.modify_this_page_vars()}
|
||||||
|
% if not batch.executed and not batch.complete and master.has_perm('edit_row'):
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
let OrderingScanner = {
|
||||||
|
template: '#ordering-scanner-template',
|
||||||
|
props: {
|
||||||
|
numericOnly: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showScanningDialog: false,
|
||||||
|
itemEntry: null,
|
||||||
|
fetching: false,
|
||||||
|
currentRow: null,
|
||||||
|
saving: false,
|
||||||
|
|
||||||
|
## TODO: should find a better way to handle CSRF token
|
||||||
|
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
totalUnits() {
|
||||||
|
let cases = parseFloat(this.currentRow.cases_ordered || 0)
|
||||||
|
let units = parseFloat(this.currentRow.units_ordered || 0)
|
||||||
|
if (cases) {
|
||||||
|
units += cases * (this.currentRow.case_quantity || 1)
|
||||||
|
}
|
||||||
|
return units
|
||||||
|
},
|
||||||
|
|
||||||
|
totalUnitsDisplay() {
|
||||||
|
let cases = parseFloat(this.currentRow.cases_ordered || 0)
|
||||||
|
let units = parseFloat(this.currentRow.units_ordered || 0)
|
||||||
|
let casesTotal = ""
|
||||||
|
if (cases) {
|
||||||
|
casesTotal = cases.toString() + " ${enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_CASE]}"
|
||||||
|
}
|
||||||
|
let unitsTotal = ""
|
||||||
|
if (units) {
|
||||||
|
unitsTotal = units.toString() + " " + this.currentRow.unit_of_measure_display
|
||||||
|
}
|
||||||
|
if (casesTotal.length && unitsTotal.length) {
|
||||||
|
return casesTotal + " + " + unitsTotal
|
||||||
|
} else if (casesTotal.length) {
|
||||||
|
return casesTotal
|
||||||
|
} else if (unitsTotal.length) {
|
||||||
|
return unitsTotal
|
||||||
|
}
|
||||||
|
return "??"
|
||||||
|
},
|
||||||
|
|
||||||
|
totalCost() {
|
||||||
|
if (this.currentRow.po_case_cost === null
|
||||||
|
&& this.currentRow.po_unit_cost === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let cases = parseFloat(this.currentRow.cases_ordered || 0)
|
||||||
|
let units = parseFloat(this.currentRow.units_ordered || 0)
|
||||||
|
let total = cases * this.currentRow.po_case_cost
|
||||||
|
total += units * this.currentRow.po_unit_cost
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
|
||||||
|
totalCostDisplay() {
|
||||||
|
if (this.totalCost === null) {
|
||||||
|
return '$?.??'
|
||||||
|
}
|
||||||
|
return '$' + this.totalCost.toFixed(2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
startScanning() {
|
||||||
|
this.showScanningDialog = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.itemEntryInput.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
itemEntryKeydown(event) {
|
||||||
|
if (event.which == 13) {
|
||||||
|
this.fetchEntry()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchEntry() {
|
||||||
|
if (this.fetching) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.itemEntry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetching = true
|
||||||
|
|
||||||
|
let url = '${url('{}.scanning_entry'.format(route_prefix), uuid=batch.uuid)}'
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
entry: this.itemEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = {
|
||||||
|
## TODO: should find a better way to handle CSRF token
|
||||||
|
'X-CSRF-TOKEN': this.csrftoken,
|
||||||
|
}
|
||||||
|
|
||||||
|
## TODO: should find a better way to handle CSRF token
|
||||||
|
this.$http.post(url, params, {headers: headers}).then(({ data }) => {
|
||||||
|
if (data.error) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Fetch failed: " + data.error,
|
||||||
|
type: 'is-danger',
|
||||||
|
duration: 4000, // 4 seconds
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.currentRow = data.row
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.casesInput.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.fetching = false
|
||||||
|
}, response => {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Fetch failed: (unknown error)",
|
||||||
|
type: 'is-danger',
|
||||||
|
duration: 4000, // 4 seconds
|
||||||
|
})
|
||||||
|
this.fetching = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
casesKeydown(event) {
|
||||||
|
if (event.which == 13) {
|
||||||
|
this.$refs.unitsInput.focus()
|
||||||
|
} else if (event.which == 27) {
|
||||||
|
this.cancelCurrentRow()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unitsKeydown(event) {
|
||||||
|
if (event.which == 13) {
|
||||||
|
this.saveCurrentRow()
|
||||||
|
} else if (event.which == 27) {
|
||||||
|
this.cancelCurrentRow()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveCurrentRow() {
|
||||||
|
if (this.saving) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saving = true
|
||||||
|
|
||||||
|
let url = '${url('{}.scanning_update'.format(route_prefix), uuid=batch.uuid)}'
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
row_uuid: this.currentRow.uuid,
|
||||||
|
cases_ordered: this.currentRow.cases_ordered,
|
||||||
|
units_ordered: this.currentRow.units_ordered,
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = {
|
||||||
|
## TODO: should find a better way to handle CSRF token
|
||||||
|
'X-CSRF-TOKEN': this.csrftoken,
|
||||||
|
}
|
||||||
|
|
||||||
|
## TODO: should find a better way to handle CSRF token
|
||||||
|
this.$http.post(url, params, {headers: headers}).then(({ data }) => {
|
||||||
|
if (data.error) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Save failed: " + data.error,
|
||||||
|
type: 'is-danger',
|
||||||
|
duration: 4000, // 4 seconds
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Item was saved",
|
||||||
|
type: 'is-success',
|
||||||
|
})
|
||||||
|
this.itemEntry = null
|
||||||
|
this.currentRow = null
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.itemEntryInput.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.saving = false
|
||||||
|
}, response => {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Save failed: (unknown error)",
|
||||||
|
type: 'is-danger',
|
||||||
|
duration: 4000, // 4 seconds
|
||||||
|
})
|
||||||
|
this.saving = false
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelCurrentRow() {
|
||||||
|
this.itemEntry = null
|
||||||
|
this.currentRow = null
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Edit was cancelled",
|
||||||
|
type: 'is-warning',
|
||||||
|
})
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.itemEntryInput.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
stopScanning() {
|
||||||
|
location.reload()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_this_page_component()">
|
||||||
|
${parent.make_this_page_component()}
|
||||||
|
% if not batch.executed and not batch.complete and master.has_perm('edit_row'):
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
Vue.component('ordering-scanner', OrderingScanner)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
${parent.body()}
|
||||||
|
|
|
@ -412,10 +412,10 @@ class BatchMasterView(MasterView):
|
||||||
return text
|
return text
|
||||||
|
|
||||||
if batch.complete:
|
if batch.complete:
|
||||||
label = "Mark as NOT Complete"
|
label = "Mark Incomplete"
|
||||||
value = 'false'
|
value = 'false'
|
||||||
else:
|
else:
|
||||||
label = "Mark as Complete"
|
label = "Mark Complete"
|
||||||
value = 'true'
|
value = 'true'
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
|
@ -169,6 +169,89 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
if field not in editable_fields:
|
if field not in editable_fields:
|
||||||
f.set_readonly(field)
|
f.set_readonly(field)
|
||||||
|
|
||||||
|
def scanning_entry(self):
|
||||||
|
"""
|
||||||
|
AJAX view to handle user entry/fetch input for "scanning" feature.
|
||||||
|
"""
|
||||||
|
data = self.request.json_body
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
prodder = app.get_products_handler()
|
||||||
|
|
||||||
|
batch = self.get_instance()
|
||||||
|
entry = data['entry']
|
||||||
|
row = self.handler.quick_entry(self.Session(), batch, entry)
|
||||||
|
|
||||||
|
uom = self.enum.UNIT_OF_MEASURE_EACH
|
||||||
|
if row.product and row.product.weighed:
|
||||||
|
uom = self.enum.UNIT_OF_MEASURE_POUND
|
||||||
|
|
||||||
|
cases_ordered = None
|
||||||
|
if row.cases_ordered:
|
||||||
|
cases_ordered = float(row.cases_ordered)
|
||||||
|
|
||||||
|
units_ordered = None
|
||||||
|
if row.units_ordered:
|
||||||
|
units_ordered = float(row.units_ordered)
|
||||||
|
|
||||||
|
po_case_cost = None
|
||||||
|
if row.po_unit_cost is not None:
|
||||||
|
po_case_cost = row.po_unit_cost * (row.case_quantity or 1)
|
||||||
|
|
||||||
|
product_url = None
|
||||||
|
if row.product_uuid:
|
||||||
|
product_url = self.request.route_url('products.view', uuid=row.product_uuid)
|
||||||
|
|
||||||
|
product_price = None
|
||||||
|
if row.product and row.product.regular_price:
|
||||||
|
product_price = row.product.regular_price.price
|
||||||
|
|
||||||
|
product_price_display = None
|
||||||
|
if product_price is not None:
|
||||||
|
product_price_display = app.render_currency(product_price)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'ok': True,
|
||||||
|
'entry': entry,
|
||||||
|
'row': {
|
||||||
|
'uuid': row.uuid,
|
||||||
|
'item_id': row.item_id,
|
||||||
|
'upc_display': row.upc.pretty() if row.upc else None,
|
||||||
|
'brand_name': row.brand_name,
|
||||||
|
'description': row.description,
|
||||||
|
'size': row.size,
|
||||||
|
'unit_of_measure_display': self.enum.UNIT_OF_MEASURE[uom],
|
||||||
|
'case_quantity': float(row.case_quantity) if row.case_quantity is not None else None,
|
||||||
|
'cases_ordered': cases_ordered,
|
||||||
|
'units_ordered': units_ordered,
|
||||||
|
'po_unit_cost': float(row.po_unit_cost) if row.po_unit_cost is not None else None,
|
||||||
|
'po_unit_cost_display': app.render_currency(row.po_unit_cost),
|
||||||
|
'po_case_cost': float(po_case_cost) if po_case_cost is not None else None,
|
||||||
|
'po_case_cost_display': app.render_currency(po_case_cost),
|
||||||
|
'image_url': prodder.get_image_url(upc=row.upc),
|
||||||
|
'product_url': product_url,
|
||||||
|
'product_price_display': product_price_display,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def scanning_update(self):
|
||||||
|
"""
|
||||||
|
AJAX view to handle row data updates for "scanning" feature.
|
||||||
|
"""
|
||||||
|
data = self.request.json_body
|
||||||
|
batch = self.get_instance()
|
||||||
|
assert batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING
|
||||||
|
assert not (batch.executed or batch.complete)
|
||||||
|
|
||||||
|
uuid = data.get('row_uuid')
|
||||||
|
row = self.Session.query(self.model_row_class).get(uuid) if uuid else None
|
||||||
|
if not row:
|
||||||
|
return {'error': "Row not found"}
|
||||||
|
if row.batch is not batch or row.removed:
|
||||||
|
return {'error': "Row is not active for batch"}
|
||||||
|
|
||||||
|
self.handler.update_row_quantity(row, **data)
|
||||||
|
return {'ok': True}
|
||||||
|
|
||||||
def worksheet(self):
|
def worksheet(self):
|
||||||
"""
|
"""
|
||||||
View for editing batch row data as an order form worksheet.
|
View for editing batch row data as an order form worksheet.
|
||||||
|
@ -401,24 +484,6 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
return self.request.route_url('purchases.view', uuid=result.uuid)
|
return self.request.route_url('purchases.view', uuid=result.uuid)
|
||||||
return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs)
|
return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _ordering_defaults(cls, config):
|
|
||||||
route_prefix = cls.get_route_prefix()
|
|
||||||
url_prefix = cls.get_url_prefix()
|
|
||||||
permission_prefix = cls.get_permission_prefix()
|
|
||||||
model_title = cls.get_model_title()
|
|
||||||
model_title_plural = cls.get_model_title_plural()
|
|
||||||
|
|
||||||
# fix permission group label
|
|
||||||
config.add_tailbone_permission_group(permission_prefix, model_title_plural)
|
|
||||||
|
|
||||||
# download as Excel
|
|
||||||
config.add_route('{}.download_excel'.format(route_prefix), '{}/{{uuid}}/excel'.format(url_prefix))
|
|
||||||
config.add_view(cls, attr='download_excel', route_name='{}.download_excel'.format(route_prefix),
|
|
||||||
permission='{}.download_excel'.format(permission_prefix))
|
|
||||||
config.add_tailbone_permission(permission_prefix, '{}.download_excel'.format(permission_prefix),
|
|
||||||
"Download {} as Excel".format(model_title))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
cls._ordering_defaults(config)
|
cls._ordering_defaults(config)
|
||||||
|
@ -426,6 +491,37 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ordering_defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
model_title = cls.get_model_title()
|
||||||
|
model_title_plural = cls.get_model_title_plural()
|
||||||
|
|
||||||
|
# fix permission group label
|
||||||
|
config.add_tailbone_permission_group(permission_prefix, model_title_plural,
|
||||||
|
overwrite=False)
|
||||||
|
|
||||||
|
# scanning entry
|
||||||
|
config.add_route('{}.scanning_entry'.format(route_prefix), '{}/scanning-entry'.format(instance_url_prefix))
|
||||||
|
config.add_view(cls, attr='scanning_entry', route_name='{}.scanning_entry'.format(route_prefix),
|
||||||
|
permission='{}.edit_row'.format(permission_prefix),
|
||||||
|
renderer='json')
|
||||||
|
|
||||||
|
# scanning update
|
||||||
|
config.add_route('{}.scanning_update'.format(route_prefix), '{}/scanning-update'.format(instance_url_prefix))
|
||||||
|
config.add_view(cls, attr='scanning_update', route_name='{}.scanning_update'.format(route_prefix),
|
||||||
|
permission='{}.edit_row'.format(permission_prefix),
|
||||||
|
renderer='json')
|
||||||
|
|
||||||
|
# download as Excel
|
||||||
|
config.add_route('{}.download_excel'.format(route_prefix), '{}/excel'.format(instance_url_prefix))
|
||||||
|
config.add_view(cls, attr='download_excel', route_name='{}.download_excel'.format(route_prefix),
|
||||||
|
permission='{}.download_excel'.format(permission_prefix))
|
||||||
|
config.add_tailbone_permission(permission_prefix, '{}.download_excel'.format(permission_prefix),
|
||||||
|
"Download {} as Excel".format(model_title))
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
OrderingBatchView.defaults(config)
|
OrderingBatchView.defaults(config)
|
||||||
|
|
Loading…
Reference in a new issue