Initial support for adding items to, executing customer order batch
This commit is contained in:
parent
475ab3013f
commit
480d878db8
6 changed files with 701 additions and 37 deletions
|
@ -48,7 +48,8 @@ class CustomerOrderBatchView(BatchMasterView):
|
|||
grid_columns = [
|
||||
'id',
|
||||
'customer',
|
||||
'rows',
|
||||
'rowcount',
|
||||
'total_price',
|
||||
'created',
|
||||
'created_by',
|
||||
]
|
||||
|
@ -61,13 +62,35 @@ class CustomerOrderBatchView(BatchMasterView):
|
|||
'email_address',
|
||||
'created',
|
||||
'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',
|
||||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(CustomerOrderBatchView, self).configure_grid(g)
|
||||
|
||||
g.set_type('total_price', 'currency')
|
||||
|
||||
g.set_link('customer')
|
||||
g.set_link('created')
|
||||
g.set_link('created_by')
|
||||
|
@ -120,3 +143,36 @@ class CustomerOrderBatchView(BatchMasterView):
|
|||
f.set_label('person_uuid', "Person")
|
||||
else:
|
||||
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
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -26,10 +26,15 @@ Customer Order Views
|
|||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import decimal
|
||||
|
||||
import six
|
||||
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
|
||||
|
||||
|
@ -123,6 +128,10 @@ class CustomerOrdersView(MasterView):
|
|||
submits the order, at which point the batch is converted to a proper
|
||||
order.
|
||||
"""
|
||||
self.handler = get_batch_handler(
|
||||
self.rattail_config, 'custorder',
|
||||
default='rattail.batch.custorder:CustomerOrderBatchHandler')
|
||||
|
||||
batch = self.get_current_batch()
|
||||
|
||||
if self.request.method == 'POST':
|
||||
|
@ -142,13 +151,22 @@ class CustomerOrdersView(MasterView):
|
|||
json_actions = [
|
||||
'get_customer_info',
|
||||
'set_customer_data',
|
||||
'find_product_by_upc',
|
||||
'get_product_info',
|
||||
'add_item',
|
||||
'update_item',
|
||||
'delete_item',
|
||||
'submit_new_order',
|
||||
]
|
||||
if action in json_actions:
|
||||
result = getattr(self, action)(batch, data)
|
||||
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)
|
||||
|
||||
def get_current_batch(self):
|
||||
|
@ -161,13 +179,15 @@ class CustomerOrdersView(MasterView):
|
|||
batch = self.Session.query(model.CustomerOrderBatch)\
|
||||
.filter(model.CustomerOrderBatch.mode == self.enum.CUSTORDER_BATCH_MODE_CREATING)\
|
||||
.filter(model.CustomerOrderBatch.created_by == user)\
|
||||
.filter(model.CustomerOrderBatch.executed == None)\
|
||||
.one()
|
||||
|
||||
except orm.exc.NoResultFound:
|
||||
# no batch yet for this user, so make one
|
||||
batch = model.CustomerOrderBatch()
|
||||
batch.mode = self.enum.CUSTORDER_BATCH_MODE_CREATING
|
||||
batch.created_by = user
|
||||
|
||||
batch = self.handler.make_batch(
|
||||
self.Session(), created_by=user,
|
||||
mode=self.enum.CUSTORDER_BATCH_MODE_CREATING)
|
||||
self.Session.add(batch)
|
||||
self.Session.flush()
|
||||
|
||||
|
@ -236,9 +256,220 @@ class CustomerOrdersView(MasterView):
|
|||
self.Session.flush()
|
||||
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):
|
||||
# TODO
|
||||
return {'success': True}
|
||||
result = self.handler.do_execute(batch, self.request.user)
|
||||
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):
|
||||
|
|
|
@ -862,6 +862,9 @@ class ProductsView(MasterView):
|
|||
else:
|
||||
f.set_readonly('brand')
|
||||
|
||||
# case_size
|
||||
f.set_type('case_size', 'quantity')
|
||||
|
||||
# status_code
|
||||
f.set_label('status_code', "Status")
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue