Initial support for adding items to, executing customer order batch
This commit is contained in:
parent
475ab3013f
commit
480d878db8
|
@ -8,6 +8,8 @@ const TailboneAutocomplete = {
|
||||||
serviceUrl: String,
|
serviceUrl: String,
|
||||||
value: String,
|
value: String,
|
||||||
initialLabel: String,
|
initialLabel: String,
|
||||||
|
assignedValue: String,
|
||||||
|
assignedLabel: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -43,7 +45,7 @@ const TailboneAutocomplete = {
|
||||||
this.value = null
|
this.value = null
|
||||||
if (focus) {
|
if (focus) {
|
||||||
this.$nextTick(function() {
|
this.$nextTick(function() {
|
||||||
this.$refs.autocomplete.focus()
|
this.focus()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +53,10 @@ const TailboneAutocomplete = {
|
||||||
// $('#' + oid + '-textbox').trigger('autocompletevaluecleared');
|
// $('#' + oid + '-textbox').trigger('autocompletevaluecleared');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.$refs.autocomplete.focus()
|
||||||
|
},
|
||||||
|
|
||||||
getDisplayText() {
|
getDisplayText() {
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
return this.selected.label
|
return this.selected.label
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
|
|
||||||
<b-autocomplete ref="autocomplete"
|
<b-autocomplete ref="autocomplete"
|
||||||
:name="name"
|
:name="name"
|
||||||
v-show="!selected"
|
v-show="!assignedValue && !selected"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
:data="data"
|
:data="data"
|
||||||
@typing="getAsyncData"
|
@typing="getAsyncData"
|
||||||
|
@ -76,10 +76,10 @@
|
||||||
</template>
|
</template>
|
||||||
</b-autocomplete>
|
</b-autocomplete>
|
||||||
|
|
||||||
<b-button v-if="selected"
|
<b-button v-if="assignedValue || selected"
|
||||||
style="width: 100%; justify-content: left;"
|
style="width: 100%; justify-content: left;"
|
||||||
@click="clearSelection()">
|
@click="clearSelection()">
|
||||||
{{ selected.label }} (click to change)
|
{{ assignedLabel || selected.label }} (click to change)
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,24 +22,32 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="order_form_buttons()">
|
<%def name="order_form_buttons()">
|
||||||
<div class="buttons">
|
<div class="level">
|
||||||
<b-button type="is-primary"
|
<div class="level-left">
|
||||||
@click="submitOrder()"
|
</div>
|
||||||
icon-pack="fas"
|
<div class="level-right">
|
||||||
icon-left="fas fa-upload">
|
<div class="level-item">
|
||||||
Submit this Order
|
<div class="buttons">
|
||||||
</b-button>
|
<b-button type="is-primary"
|
||||||
<b-button @click="startOverEntirely()"
|
@click="submitOrder()"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-redo">
|
icon-left="fas fa-upload">
|
||||||
Start Over Entirely
|
Submit this Order
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button @click="cancelOrder()"
|
<b-button @click="startOverEntirely()"
|
||||||
type="is-danger"
|
icon-pack="fas"
|
||||||
icon-pack="fas"
|
icon-left="fas fa-redo">
|
||||||
icon-left="fas fa-trash">
|
Start Over Entirely
|
||||||
Cancel this Order
|
</b-button>
|
||||||
</b-button>
|
<b-button @click="cancelOrder()"
|
||||||
|
type="is-danger"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-trash">
|
||||||
|
Cancel this Order
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
@ -163,12 +171,157 @@
|
||||||
## (for now we just always show caret-right instead)
|
## (for now we just always show caret-right instead)
|
||||||
icon="caret-right">
|
icon="caret-right">
|
||||||
</b-icon>
|
</b-icon>
|
||||||
<strong>Items</strong>
|
<strong v-html="itemsPanelHeader"></strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-block">
|
<div class="panel-block">
|
||||||
<div>
|
<div>
|
||||||
TODO: items go here
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-plus"
|
||||||
|
@click="showAddItemDialog()">
|
||||||
|
Add Item
|
||||||
|
</b-button>
|
||||||
|
<b-modal :active.sync="showingItemDialog">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<b-tabs type="is-boxed is-toggle"
|
||||||
|
:animated="false">
|
||||||
|
|
||||||
|
<b-tab-item label="Product">
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="productIsKnown"
|
||||||
|
:native-value="true">
|
||||||
|
Product is already in the system.
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="productIsKnown">
|
||||||
|
|
||||||
|
<b-field grouped>
|
||||||
|
<b-field label="Description" horizontal expanded>
|
||||||
|
<tailbone-autocomplete
|
||||||
|
ref="productDescriptionAutocomplete"
|
||||||
|
v-model="productUUID"
|
||||||
|
:assigned-value="productUUID"
|
||||||
|
:assigned-label="productDisplay"
|
||||||
|
serviceUrl="${url('products.autocomplete')}"
|
||||||
|
@input="productChanged">
|
||||||
|
</tailbone-autocomplete>
|
||||||
|
</b-field>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field grouped>
|
||||||
|
<b-field label="UPC" horizontal expanded>
|
||||||
|
<b-input v-if="!productUUID"
|
||||||
|
v-model="productUPC"
|
||||||
|
ref="productUPCInput">
|
||||||
|
</b-input>
|
||||||
|
<b-button v-if="!productUUID"
|
||||||
|
@click="fetchProductByUPC()">
|
||||||
|
Fetch
|
||||||
|
</b-button>
|
||||||
|
<b-button v-if="productUUID"
|
||||||
|
@click="clearProduct(true)">
|
||||||
|
{{ productUPC }} (click to change)
|
||||||
|
</b-button>
|
||||||
|
</b-field>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="productIsKnown" disabled
|
||||||
|
:native-value="false">
|
||||||
|
Product is not yet in the system.
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</b-tab-item>
|
||||||
|
<b-tab-item label="Quantity">
|
||||||
|
|
||||||
|
<b-field grouped>
|
||||||
|
|
||||||
|
<b-field label="Quantity" horizontal>
|
||||||
|
<b-input v-model="productQuantity"></b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-select v-model="productUOM">
|
||||||
|
<option v-for="choice in productUnitChoices"
|
||||||
|
:key="choice.key"
|
||||||
|
:value="choice.key"
|
||||||
|
v-html="choice.value">
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
|
||||||
|
</b-field>
|
||||||
|
</b-tab-item>
|
||||||
|
</b-tabs>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<b-button @click="showingItemDialog = false">
|
||||||
|
Cancel
|
||||||
|
</b-button>
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-save"
|
||||||
|
@click="itemDialogSave()">
|
||||||
|
{{ itemDialogSaveButtonText }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
|
||||||
|
<b-table
|
||||||
|
:data="items">
|
||||||
|
<template slot-scope="props">
|
||||||
|
|
||||||
|
<b-table-column field="product_upc_pretty" label="UPC">
|
||||||
|
{{ props.row.product_upc_pretty }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column field="product_brand" label="Brand">
|
||||||
|
{{ props.row.product_brand }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column field="product_description" label="Description">
|
||||||
|
{{ props.row.product_description }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column field="product_size" label="Size">
|
||||||
|
{{ props.row.product_size }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column field="order_quantity_display" label="Quantity">
|
||||||
|
<span v-html="props.row.order_quantity_display"></span>
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column field="total_price_display" label="Total">
|
||||||
|
{{ props.row.total_price_display }}
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column field="actions" label="Actions">
|
||||||
|
<a href="#" class="grid-action"
|
||||||
|
@click.prevent="showEditItemDialog(props.index)">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<a href="#" class="grid-action has-text-danger"
|
||||||
|
@click.prevent="deleteItem(props.index)">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-collapse>
|
</b-collapse>
|
||||||
|
@ -191,10 +344,20 @@
|
||||||
const CustomerOrderCreator = {
|
const CustomerOrderCreator = {
|
||||||
template: '#customer-order-creator-template',
|
template: '#customer-order-creator-template',
|
||||||
data() {
|
data() {
|
||||||
|
|
||||||
|
## TODO: these should come from handler
|
||||||
|
let defaultUnitChoices = [
|
||||||
|
{key: '${enum.UNIT_OF_MEASURE_EACH}', value: "Each"},
|
||||||
|
{key: '${enum.UNIT_OF_MEASURE_POUND}', value: "Pound"},
|
||||||
|
{key: '${enum.UNIT_OF_MEASURE_CASE}', value: "Case"},
|
||||||
|
]
|
||||||
|
let defaultUOM = '${enum.UNIT_OF_MEASURE_CASE}'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
batchAction: null,
|
batchAction: null,
|
||||||
|
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
|
||||||
|
|
||||||
customerPanelOpen: true,
|
customerPanelOpen: false,
|
||||||
customerIsKnown: true,
|
customerIsKnown: true,
|
||||||
customerUUID: ${json.dumps(batch.customer_uuid)|n},
|
customerUUID: ${json.dumps(batch.customer_uuid)|n},
|
||||||
customerDisplay: ${json.dumps(six.text_type(batch.customer or ''))|n},
|
customerDisplay: ${json.dumps(six.text_type(batch.customer or ''))|n},
|
||||||
|
@ -204,6 +367,20 @@
|
||||||
customerName: null,
|
customerName: null,
|
||||||
phoneNumber: null,
|
phoneNumber: null,
|
||||||
|
|
||||||
|
items: ${json.dumps(order_items)|n},
|
||||||
|
editingItem: null,
|
||||||
|
showingItemDialog: false,
|
||||||
|
productIsKnown: true,
|
||||||
|
productUUID: null,
|
||||||
|
productDisplay: null,
|
||||||
|
productUPC: null,
|
||||||
|
productQuantity: null,
|
||||||
|
defaultUnitChoices: defaultUnitChoices,
|
||||||
|
productUnitChoices: defaultUnitChoices,
|
||||||
|
defaultUOM: defaultUOM,
|
||||||
|
productUOM: defaultUOM,
|
||||||
|
productCaseSize: null,
|
||||||
|
|
||||||
## TODO: should find a better way to handle CSRF token
|
## 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},
|
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
|
||||||
}
|
}
|
||||||
|
@ -301,9 +478,28 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: null,
|
type: null,
|
||||||
text: "Everything seems to be okay here.",
|
text: "Customer info looks okay.",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
itemsPanelHeader() {
|
||||||
|
let text = "Items"
|
||||||
|
|
||||||
|
if (this.items.length) {
|
||||||
|
text = "Items: " + this.items.length.toString() + " for " + this.batchTotalPriceDisplay
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
},
|
||||||
|
|
||||||
|
itemDialogSaveButtonText() {
|
||||||
|
return this.editingItem ? "Update Item" : "Add Item"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.customerStatusType) {
|
||||||
|
this.customerPanelOpen = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
@ -369,6 +565,12 @@
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(response)
|
callback(response)
|
||||||
}
|
}
|
||||||
|
}, response => {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Unexpected error occurred",
|
||||||
|
type: 'is-danger',
|
||||||
|
duration: 2000, // 2 seconds
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -385,7 +587,24 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
submitOrder() {
|
submitOrder() {
|
||||||
alert("okay then!")
|
let params = {
|
||||||
|
action: 'submit_new_order',
|
||||||
|
}
|
||||||
|
this.submitBatchData(params, response => {
|
||||||
|
if (response.data.error) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Submit failed: " + response.data.error,
|
||||||
|
type: 'is-danger',
|
||||||
|
duration: 2000, // 2 seconds
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (response.data.next_url) {
|
||||||
|
location.href = response.data.next_url
|
||||||
|
} else {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
customerChanged(uuid) {
|
customerChanged(uuid) {
|
||||||
|
@ -414,6 +633,155 @@
|
||||||
this.setCustomerData()
|
this.setCustomerData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showAddItemDialog() {
|
||||||
|
this.editingItem = null
|
||||||
|
this.productIsKnown = true
|
||||||
|
this.productUUID = null
|
||||||
|
this.productDisplay = null
|
||||||
|
this.productUPC = null
|
||||||
|
this.productQuantity = 1
|
||||||
|
this.productUnitChoices = this.defaultUnitChoices
|
||||||
|
this.productUOM = this.defaultUOM
|
||||||
|
this.showingItemDialog = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.productDescriptionAutocomplete.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
showEditItemDialog(index) {
|
||||||
|
row = this.items[index]
|
||||||
|
this.editingItem = row
|
||||||
|
this.productIsKnown = true // TODO
|
||||||
|
this.productUUID = row.product_uuid
|
||||||
|
this.productDisplay = row.product_full_description
|
||||||
|
this.productUPC = row.product_upc_pretty || row.product_upc
|
||||||
|
this.productQuantity = row.order_quantity
|
||||||
|
this.productUnitChoices = row.order_uom_choices
|
||||||
|
this.productUOM = row.order_uom
|
||||||
|
|
||||||
|
this.showingItemDialog = true
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteItem(index) {
|
||||||
|
if (!confirm("Are you sure you want to delete this item?")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
action: 'delete_item',
|
||||||
|
uuid: this.items[index].uuid,
|
||||||
|
}
|
||||||
|
this.submitBatchData(params, response => {
|
||||||
|
if (response.data.error) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Delete failed: " + response.data.error,
|
||||||
|
type: 'is-warning',
|
||||||
|
duration: 2000, // 2 seconds
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.items.splice(index, 1)
|
||||||
|
this.batchTotalPriceDisplay = response.data.batch.total_price_display
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
clearProduct(autofocus) {
|
||||||
|
this.productUUID = null
|
||||||
|
this.productDisplay = null
|
||||||
|
this.productUPC = null
|
||||||
|
this.productUnitChoices = this.defaultUnitChoices
|
||||||
|
if (autofocus) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.productUPCInput.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchProductByUPC() {
|
||||||
|
let params = {
|
||||||
|
action: 'find_product_by_upc',
|
||||||
|
upc: this.productUPC,
|
||||||
|
}
|
||||||
|
this.submitBatchData(params, response => {
|
||||||
|
if (response.data.error) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Fetch failed: " + response.data.error,
|
||||||
|
type: 'is-warning',
|
||||||
|
duration: 2000, // 2 seconds
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.productUUID = response.data.uuid
|
||||||
|
this.productUPC = response.data.upc_pretty
|
||||||
|
this.productDisplay = response.data.full_description
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
productChanged(uuid) {
|
||||||
|
if (uuid) {
|
||||||
|
this.productUUID = uuid
|
||||||
|
let params = {
|
||||||
|
action: 'get_product_info',
|
||||||
|
uuid: this.productUUID,
|
||||||
|
}
|
||||||
|
this.submitBatchData(params, response => {
|
||||||
|
this.productUPC = response.data.upc_pretty
|
||||||
|
this.productDisplay = response.data.full_description
|
||||||
|
this.productUnitChoices = response.data.uom_choices
|
||||||
|
|
||||||
|
let found = false
|
||||||
|
for (let uom of this.productUnitChoices) {
|
||||||
|
if (this.productUOM == uom.key) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
this.productUOM = this.productUnitChoices[0].key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.clearProduct()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
itemDialogSave() {
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
product_is_known: this.productIsKnown,
|
||||||
|
product_uuid: this.productUUID,
|
||||||
|
order_quantity: this.productQuantity,
|
||||||
|
order_uom: this.productUOM,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingItem) {
|
||||||
|
params.action = 'update_item'
|
||||||
|
params.uuid = this.editingItem.uuid
|
||||||
|
} else {
|
||||||
|
params.action = 'add_item'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitBatchData(params, response => {
|
||||||
|
|
||||||
|
if (params.action == 'add_item') {
|
||||||
|
this.items.push(response.data.row)
|
||||||
|
|
||||||
|
} else { // update_item
|
||||||
|
// must update each value separately, instead of
|
||||||
|
// overwriting the item record, or else display will
|
||||||
|
// not update properly
|
||||||
|
for (let [key, value] of Object.entries(response.data.row)) {
|
||||||
|
this.editingItem[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// also update the batch total price
|
||||||
|
this.batchTotalPriceDisplay = response.data.batch.total_price_display
|
||||||
|
|
||||||
|
this.showingItemDialog = false
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,8 @@ class CustomerOrderBatchView(BatchMasterView):
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'id',
|
'id',
|
||||||
'customer',
|
'customer',
|
||||||
'rows',
|
'rowcount',
|
||||||
|
'total_price',
|
||||||
'created',
|
'created',
|
||||||
'created_by',
|
'created_by',
|
||||||
]
|
]
|
||||||
|
@ -61,13 +62,35 @@ class CustomerOrderBatchView(BatchMasterView):
|
||||||
'email_address',
|
'email_address',
|
||||||
'created',
|
'created',
|
||||||
'created_by',
|
'created_by',
|
||||||
'rows',
|
'rowcount',
|
||||||
|
'total_price',
|
||||||
|
]
|
||||||
|
|
||||||
|
row_labels = {
|
||||||
|
'product_upc': "UPC",
|
||||||
|
'product_brand': "Brand",
|
||||||
|
'product_description': "Description",
|
||||||
|
'product_size': "Size",
|
||||||
|
'order_uom': "Order UOM",
|
||||||
|
}
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
'sequence',
|
||||||
|
'product_upc',
|
||||||
|
'product_brand',
|
||||||
|
'product_description',
|
||||||
|
'product_size',
|
||||||
|
'order_quantity',
|
||||||
|
'order_uom',
|
||||||
|
'total_price',
|
||||||
'status_code',
|
'status_code',
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(CustomerOrderBatchView, self).configure_grid(g)
|
super(CustomerOrderBatchView, self).configure_grid(g)
|
||||||
|
|
||||||
|
g.set_type('total_price', 'currency')
|
||||||
|
|
||||||
g.set_link('customer')
|
g.set_link('customer')
|
||||||
g.set_link('created')
|
g.set_link('created')
|
||||||
g.set_link('created_by')
|
g.set_link('created_by')
|
||||||
|
@ -120,3 +143,36 @@ class CustomerOrderBatchView(BatchMasterView):
|
||||||
f.set_label('person_uuid', "Person")
|
f.set_label('person_uuid', "Person")
|
||||||
else:
|
else:
|
||||||
f.set_renderer('person', self.render_person)
|
f.set_renderer('person', self.render_person)
|
||||||
|
|
||||||
|
f.set_type('total_price', 'currency')
|
||||||
|
|
||||||
|
def row_grid_extra_class(self, row, i):
|
||||||
|
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
||||||
|
return 'warning'
|
||||||
|
|
||||||
|
def configure_row_grid(self, g):
|
||||||
|
super(CustomerOrderBatchView, self).configure_row_grid(g)
|
||||||
|
|
||||||
|
g.set_type('case_quantity', 'quantity')
|
||||||
|
g.set_type('cases_ordered', 'quantity')
|
||||||
|
g.set_type('units_ordered', 'quantity')
|
||||||
|
g.set_type('order_quantity', 'quantity')
|
||||||
|
g.set_enum('order_uom', self.enum.UNIT_OF_MEASURE)
|
||||||
|
g.set_type('unit_price', 'currency')
|
||||||
|
g.set_type('total_price', 'currency')
|
||||||
|
|
||||||
|
g.set_link('product_upc')
|
||||||
|
g.set_link('product_description')
|
||||||
|
|
||||||
|
def configure_row_form(self, f):
|
||||||
|
super(CustomerOrderBatchView, self).configure_row_form(f)
|
||||||
|
|
||||||
|
f.set_renderer('product', self.render_product)
|
||||||
|
|
||||||
|
f.set_type('case_quantity', 'quantity')
|
||||||
|
f.set_type('cases_ordered', 'quantity')
|
||||||
|
f.set_type('units_ordered', 'quantity')
|
||||||
|
f.set_type('order_quantity', 'quantity')
|
||||||
|
f.set_enum('order_uom', self.enum.UNIT_OF_MEASURE)
|
||||||
|
f.set_type('unit_price', 'currency')
|
||||||
|
f.set_type('total_price', 'currency')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2021 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -26,10 +26,15 @@ Customer Order Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import decimal
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail import pod
|
||||||
|
from rattail.db import api, model
|
||||||
|
from rattail.util import pretty_quantity
|
||||||
|
from rattail.batch import get_batch_handler
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
|
@ -123,6 +128,10 @@ class CustomerOrdersView(MasterView):
|
||||||
submits the order, at which point the batch is converted to a proper
|
submits the order, at which point the batch is converted to a proper
|
||||||
order.
|
order.
|
||||||
"""
|
"""
|
||||||
|
self.handler = get_batch_handler(
|
||||||
|
self.rattail_config, 'custorder',
|
||||||
|
default='rattail.batch.custorder:CustomerOrderBatchHandler')
|
||||||
|
|
||||||
batch = self.get_current_batch()
|
batch = self.get_current_batch()
|
||||||
|
|
||||||
if self.request.method == 'POST':
|
if self.request.method == 'POST':
|
||||||
|
@ -142,13 +151,22 @@ class CustomerOrdersView(MasterView):
|
||||||
json_actions = [
|
json_actions = [
|
||||||
'get_customer_info',
|
'get_customer_info',
|
||||||
'set_customer_data',
|
'set_customer_data',
|
||||||
|
'find_product_by_upc',
|
||||||
|
'get_product_info',
|
||||||
|
'add_item',
|
||||||
|
'update_item',
|
||||||
|
'delete_item',
|
||||||
'submit_new_order',
|
'submit_new_order',
|
||||||
]
|
]
|
||||||
if action in json_actions:
|
if action in json_actions:
|
||||||
result = getattr(self, action)(batch, data)
|
result = getattr(self, action)(batch, data)
|
||||||
return self.json_response(result)
|
return self.json_response(result)
|
||||||
|
|
||||||
context = {'batch': batch}
|
items = [self.normalize_row(row)
|
||||||
|
for row in batch.active_rows()]
|
||||||
|
context = {'batch': batch,
|
||||||
|
'normalized_batch': self.normalize_batch(batch),
|
||||||
|
'order_items': items}
|
||||||
return self.render_to_response(template, context)
|
return self.render_to_response(template, context)
|
||||||
|
|
||||||
def get_current_batch(self):
|
def get_current_batch(self):
|
||||||
|
@ -161,13 +179,15 @@ class CustomerOrdersView(MasterView):
|
||||||
batch = self.Session.query(model.CustomerOrderBatch)\
|
batch = self.Session.query(model.CustomerOrderBatch)\
|
||||||
.filter(model.CustomerOrderBatch.mode == self.enum.CUSTORDER_BATCH_MODE_CREATING)\
|
.filter(model.CustomerOrderBatch.mode == self.enum.CUSTORDER_BATCH_MODE_CREATING)\
|
||||||
.filter(model.CustomerOrderBatch.created_by == user)\
|
.filter(model.CustomerOrderBatch.created_by == user)\
|
||||||
|
.filter(model.CustomerOrderBatch.executed == None)\
|
||||||
.one()
|
.one()
|
||||||
|
|
||||||
except orm.exc.NoResultFound:
|
except orm.exc.NoResultFound:
|
||||||
# no batch yet for this user, so make one
|
# no batch yet for this user, so make one
|
||||||
batch = model.CustomerOrderBatch()
|
|
||||||
batch.mode = self.enum.CUSTORDER_BATCH_MODE_CREATING
|
batch = self.handler.make_batch(
|
||||||
batch.created_by = user
|
self.Session(), created_by=user,
|
||||||
|
mode=self.enum.CUSTORDER_BATCH_MODE_CREATING)
|
||||||
self.Session.add(batch)
|
self.Session.add(batch)
|
||||||
self.Session.flush()
|
self.Session.flush()
|
||||||
|
|
||||||
|
@ -236,9 +256,220 @@ class CustomerOrdersView(MasterView):
|
||||||
self.Session.flush()
|
self.Session.flush()
|
||||||
return {'success': True}
|
return {'success': True}
|
||||||
|
|
||||||
|
def find_product_by_upc(self, batch, data):
|
||||||
|
upc = data.get('upc')
|
||||||
|
if not upc:
|
||||||
|
return {'error': "Must specify a product UPC"}
|
||||||
|
|
||||||
|
product = api.get_product_by_upc(self.Session(), upc)
|
||||||
|
if not product:
|
||||||
|
return {'error': "Product not found"}
|
||||||
|
|
||||||
|
return self.info_for_product(batch, data, product)
|
||||||
|
|
||||||
|
def get_product_info(self, batch, data):
|
||||||
|
uuid = data.get('uuid')
|
||||||
|
if not uuid:
|
||||||
|
return {'error': "Must specify a product UUID"}
|
||||||
|
|
||||||
|
product = self.Session.query(model.Product).get(uuid)
|
||||||
|
if not product:
|
||||||
|
return {'error': "Product not found"}
|
||||||
|
|
||||||
|
return self.info_for_product(batch, data, product)
|
||||||
|
|
||||||
|
def uom_choices_for_product(self, product):
|
||||||
|
choices = []
|
||||||
|
|
||||||
|
# Each
|
||||||
|
if not product or not product.weighed:
|
||||||
|
unit_name = self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_EACH]
|
||||||
|
choices.append({'key': self.enum.UNIT_OF_MEASURE_EACH,
|
||||||
|
'value': unit_name})
|
||||||
|
|
||||||
|
# Pound
|
||||||
|
if not product or product.weighed:
|
||||||
|
unit_name = self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_POUND]
|
||||||
|
choices.append({
|
||||||
|
'key': self.enum.UNIT_OF_MEASURE_POUND,
|
||||||
|
'value': unit_name,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Case
|
||||||
|
case_text = None
|
||||||
|
if product.case_size is None:
|
||||||
|
case_text = "{} (× ?? {})".format(
|
||||||
|
self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_CASE],
|
||||||
|
unit_name)
|
||||||
|
elif product.case_size > 1:
|
||||||
|
case_text = "{} (× {} {})".format(
|
||||||
|
self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_CASE],
|
||||||
|
pretty_quantity(product.case_size),
|
||||||
|
unit_name)
|
||||||
|
if case_text:
|
||||||
|
choices.append({'key': self.enum.UNIT_OF_MEASURE_CASE,
|
||||||
|
'value': case_text})
|
||||||
|
|
||||||
|
return choices
|
||||||
|
|
||||||
|
def info_for_product(self, batch, data, product):
|
||||||
|
return {
|
||||||
|
'uuid': product.uuid,
|
||||||
|
'upc': six.text_type(product.upc),
|
||||||
|
'upc_pretty': product.upc.pretty(),
|
||||||
|
'full_description': product.full_description,
|
||||||
|
'image_url': pod.get_image_url(self.rattail_config, product.upc),
|
||||||
|
'uom_choices': self.uom_choices_for_product(product),
|
||||||
|
}
|
||||||
|
|
||||||
|
def normalize_batch(self, batch):
|
||||||
|
return {
|
||||||
|
'uuid': batch.uuid,
|
||||||
|
'total_price': six.text_type(batch.total_price or 0),
|
||||||
|
'total_price_display': "${:0.2f}".format(batch.total_price or 0),
|
||||||
|
'status_code': batch.status_code,
|
||||||
|
'status_text': batch.status_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
def normalize_row(self, row):
|
||||||
|
data = {
|
||||||
|
'uuid': row.uuid,
|
||||||
|
'sequence': row.sequence,
|
||||||
|
'item_entry': row.item_entry,
|
||||||
|
'product_uuid': row.product_uuid,
|
||||||
|
'product_upc': six.text_type(row.product_upc or ''),
|
||||||
|
'product_upc_pretty': row.product_upc.pretty() if row.product_upc else None,
|
||||||
|
'product_brand': row.product_brand,
|
||||||
|
'product_description': row.product_description,
|
||||||
|
'product_size': row.product_size,
|
||||||
|
'product_full_description': row.product.full_description if row.product else row.product_description,
|
||||||
|
'product_weighed': row.product_weighed,
|
||||||
|
|
||||||
|
'case_quantity': pretty_quantity(row.case_quantity),
|
||||||
|
'cases_ordered': pretty_quantity(row.cases_ordered),
|
||||||
|
'units_ordered': pretty_quantity(row.units_ordered),
|
||||||
|
'order_quantity': pretty_quantity(row.order_quantity),
|
||||||
|
'order_uom': row.order_uom,
|
||||||
|
'order_uom_choices': self.uom_choices_for_product(row.product),
|
||||||
|
|
||||||
|
'unit_price': six.text_type(row.unit_price) if row.unit_price is not None else None,
|
||||||
|
'unit_price_display': "${:0.2f}".format(row.unit_price) if row.unit_price is not None else None,
|
||||||
|
'total_price': six.text_type(row.total_price) if row.total_price is not None else None,
|
||||||
|
'total_price_display': "${:0.2f}".format(row.total_price) if row.total_price is not None else None,
|
||||||
|
|
||||||
|
'status_code': row.status_code,
|
||||||
|
'status_text': row.status_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
unit_uom = self.enum.UNIT_OF_MEASURE_POUND if data['product_weighed'] else self.enum.UNIT_OF_MEASURE_EACH
|
||||||
|
if row.order_uom == self.enum.UNIT_OF_MEASURE_CASE:
|
||||||
|
data.update({
|
||||||
|
'order_quantity_display': "{} {} (× {} {} = {} {})".format(
|
||||||
|
data['order_quantity'],
|
||||||
|
self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_CASE],
|
||||||
|
data['case_quantity'],
|
||||||
|
self.enum.UNIT_OF_MEASURE[unit_uom],
|
||||||
|
pretty_quantity(row.order_quantity * row.case_quantity),
|
||||||
|
self.enum.UNIT_OF_MEASURE[unit_uom]),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
data.update({
|
||||||
|
'order_quantity_display': "{} {}".format(
|
||||||
|
pretty_quantity(row.order_quantity),
|
||||||
|
self.enum.UNIT_OF_MEASURE[unit_uom]),
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def add_item(self, batch, data):
|
||||||
|
if data.get('product_is_known'):
|
||||||
|
|
||||||
|
uuid = data.get('product_uuid')
|
||||||
|
if not uuid:
|
||||||
|
return {'error': "Must specify a product UUID"}
|
||||||
|
|
||||||
|
product = self.Session.query(model.Product).get(uuid)
|
||||||
|
if not product:
|
||||||
|
return {'error': "Product not found"}
|
||||||
|
|
||||||
|
row = self.handler.make_row()
|
||||||
|
row.item_entry = product.uuid
|
||||||
|
row.product = product
|
||||||
|
row.order_quantity = decimal.Decimal(data.get('order_quantity') or '0')
|
||||||
|
row.order_uom = data.get('order_uom')
|
||||||
|
self.handler.add_row(batch, row)
|
||||||
|
self.Session.flush()
|
||||||
|
self.Session.refresh(row)
|
||||||
|
|
||||||
|
else: # product is not known
|
||||||
|
raise NotImplementedError # TODO
|
||||||
|
|
||||||
|
return {'batch': self.normalize_batch(batch),
|
||||||
|
'row': self.normalize_row(row)}
|
||||||
|
|
||||||
|
def update_item(self, batch, data):
|
||||||
|
uuid = data.get('uuid')
|
||||||
|
if not uuid:
|
||||||
|
return {'error': "Must specify a row UUID"}
|
||||||
|
|
||||||
|
row = self.Session.query(model.CustomerOrderBatchRow).get(uuid)
|
||||||
|
if not row:
|
||||||
|
return {'error': "Row not found"}
|
||||||
|
|
||||||
|
if row not in batch.active_rows():
|
||||||
|
return {'error': "Row is not active for the batch"}
|
||||||
|
|
||||||
|
if data.get('product_is_known'):
|
||||||
|
|
||||||
|
uuid = data.get('product_uuid')
|
||||||
|
if not uuid:
|
||||||
|
return {'error': "Must specify a product UUID"}
|
||||||
|
|
||||||
|
product = self.Session.query(model.Product).get(uuid)
|
||||||
|
if not product:
|
||||||
|
return {'error': "Product not found"}
|
||||||
|
|
||||||
|
row.item_entry = product.uuid
|
||||||
|
row.product = product
|
||||||
|
row.order_quantity = decimal.Decimal(data.get('order_quantity') or '0')
|
||||||
|
row.order_uom = data.get('order_uom')
|
||||||
|
self.handler.refresh_row(row)
|
||||||
|
self.Session.flush()
|
||||||
|
self.Session.refresh(row)
|
||||||
|
|
||||||
|
else: # product is not known
|
||||||
|
raise NotImplementedError # TODO
|
||||||
|
|
||||||
|
return {'batch': self.normalize_batch(batch),
|
||||||
|
'row': self.normalize_row(row)}
|
||||||
|
|
||||||
|
def delete_item(self, batch, data):
|
||||||
|
|
||||||
|
uuid = data.get('uuid')
|
||||||
|
if not uuid:
|
||||||
|
return {'error': "Must specify a row UUID"}
|
||||||
|
|
||||||
|
row = self.Session.query(model.CustomerOrderBatchRow).get(uuid)
|
||||||
|
if not row:
|
||||||
|
return {'error': "Row not found"}
|
||||||
|
|
||||||
|
if row not in batch.active_rows():
|
||||||
|
return {'error': "Row is not active for this batch"}
|
||||||
|
|
||||||
|
self.handler.do_remove_row(row)
|
||||||
|
return {'ok': True,
|
||||||
|
'batch': self.normalize_batch(batch)}
|
||||||
|
|
||||||
def submit_new_order(self, batch, data):
|
def submit_new_order(self, batch, data):
|
||||||
# TODO
|
result = self.handler.do_execute(batch, self.request.user)
|
||||||
return {'success': True}
|
if not result:
|
||||||
|
return {'error': "Batch failed to execute"}
|
||||||
|
|
||||||
|
next_url = None
|
||||||
|
if isinstance(result, model.CustomerOrder):
|
||||||
|
next_url = self.get_action_url('view', result)
|
||||||
|
|
||||||
|
return {'ok': True, 'next_url': next_url}
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -862,6 +862,9 @@ class ProductsView(MasterView):
|
||||||
else:
|
else:
|
||||||
f.set_readonly('brand')
|
f.set_readonly('brand')
|
||||||
|
|
||||||
|
# case_size
|
||||||
|
f.set_type('case_size', 'quantity')
|
||||||
|
|
||||||
# status_code
|
# status_code
|
||||||
f.set_label('status_code', "Status")
|
f.set_label('status_code', "Status")
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue