Initial support for adding items to, executing customer order batch

This commit is contained in:
Lance Edgar 2021-01-26 20:10:05 -06:00
parent 475ab3013f
commit 480d878db8
6 changed files with 701 additions and 37 deletions

View file

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

View file

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

View file

@ -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>
&nbsp;
<a href="#" class="grid-action has-text-danger"
@click.prevent="deleteItem(props.index)">
<i class="fas fa-trash"></i>
Delete
</a>
&nbsp;
</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
})
},
}, },
} }

View file

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

View file

@ -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 = "{} (&times; ?? {})".format(
self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_CASE],
unit_name)
elif product.case_size > 1:
case_text = "{} (&times; {} {})".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': "{} {} (&times; {} {} = {} {})".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):

View file

@ -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")