savepoint
This commit is contained in:
parent
35126e8e5b
commit
1150e6f7a6
82
tailbone/forms/custorders.py
Normal file
82
tailbone/forms/custorders.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2015 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Form Objects for Customer Orders
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rattail import enum
|
||||||
|
from rattail.db import model
|
||||||
|
|
||||||
|
import formencode
|
||||||
|
from formencode import validators
|
||||||
|
|
||||||
|
from tailbone.forms.validators import ValidCustomer, ValidProduct, ValidUser
|
||||||
|
|
||||||
|
|
||||||
|
class ValidCustomerInfo(validators.FormValidator):
|
||||||
|
"""
|
||||||
|
Custom validator to ensure we have either a proper customer reference, or
|
||||||
|
at least a customer name, when creating a new customer order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def validate_python(self, field_dict, state):
|
||||||
|
if not field_dict['customer'] and not field_dict['customer_name']:
|
||||||
|
raise formencode.Invalid("Customer name is required", field_dict, state)
|
||||||
|
|
||||||
|
|
||||||
|
class NewCustomerOrderItem(formencode.Schema):
|
||||||
|
"""
|
||||||
|
Form schema to which individual items on a new customer order must adhere.
|
||||||
|
"""
|
||||||
|
allow_extra_fields = True
|
||||||
|
|
||||||
|
product = ValidProduct()
|
||||||
|
product_description = validators.NotEmpty()
|
||||||
|
|
||||||
|
quantity = validators.Int()
|
||||||
|
unit_of_measure = validators.OneOf(enum.UNIT_OF_MEASURE)
|
||||||
|
discount = validators.Int()
|
||||||
|
|
||||||
|
notes = validators.String()
|
||||||
|
|
||||||
|
|
||||||
|
class NewCustomerOrder(formencode.Schema):
|
||||||
|
"""
|
||||||
|
Form schema for creating a new customer order.
|
||||||
|
"""
|
||||||
|
allow_extra_fields = True
|
||||||
|
pre_validators = [formencode.NestedVariables()]
|
||||||
|
|
||||||
|
user = formencode.Pipe(validators=[
|
||||||
|
validators.NotEmpty(),
|
||||||
|
ValidUser()])
|
||||||
|
|
||||||
|
customer = ValidCustomer()
|
||||||
|
customer_name = validators.String()
|
||||||
|
customer_phone = validators.NotEmpty()
|
||||||
|
|
||||||
|
products = formencode.ForEach(NewCustomerOrderItem())
|
||||||
|
|
||||||
|
chained_validators = [ValidCustomerInfo()]
|
|
@ -46,4 +46,6 @@ from .products import (GPCFieldRenderer, ScancodeFieldRenderer,
|
||||||
BrandFieldRenderer, ProductFieldRenderer,
|
BrandFieldRenderer, ProductFieldRenderer,
|
||||||
PriceFieldRenderer, PriceWithExpirationFieldRenderer)
|
PriceFieldRenderer, PriceWithExpirationFieldRenderer)
|
||||||
|
|
||||||
|
from .custorders import CustomerOrderFieldRenderer
|
||||||
|
|
||||||
from .batch import BatchIDFieldRenderer, HandheldBatchFieldRenderer
|
from .batch import BatchIDFieldRenderer, HandheldBatchFieldRenderer
|
||||||
|
|
|
@ -60,16 +60,24 @@ class CustomFieldRenderer(fa.FieldRenderer):
|
||||||
return self.request.rattail_config
|
return self.request.rattail_config
|
||||||
|
|
||||||
|
|
||||||
class DateFieldRenderer(CustomFieldRenderer, fa.DateFieldRenderer):
|
class DateFieldRenderer(CustomFieldRenderer):
|
||||||
"""
|
"""
|
||||||
Date field renderer which uses jQuery UI datepicker widget when rendering
|
Date field renderer which uses jQuery UI datepicker widget when rendering
|
||||||
in edit mode.
|
in edit mode.
|
||||||
"""
|
"""
|
||||||
|
date_format = None
|
||||||
change_year = False
|
change_year = False
|
||||||
|
|
||||||
def init(self, change_year=False):
|
def init(self, date_format=None, change_year=False):
|
||||||
|
self.date_format = date_format
|
||||||
self.change_year = change_year
|
self.change_year = change_year
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
value = self.raw_value
|
||||||
|
if value is None:
|
||||||
|
return ''
|
||||||
|
return value.strftime(self.date_format)
|
||||||
|
|
||||||
def render(self, **kwargs):
|
def render(self, **kwargs):
|
||||||
kwargs['name'] = self.name
|
kwargs['name'] = self.name
|
||||||
kwargs['value'] = self.value
|
kwargs['value'] = self.value
|
||||||
|
|
42
tailbone/forms/renderers/custorders.py
Normal file
42
tailbone/forms/renderers/custorders.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2016 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Customer order field renderers
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import formalchemy as fa
|
||||||
|
from webhelpers.html import tags
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerOrderFieldRenderer(fa.fields.SelectFieldRenderer):
|
||||||
|
"""
|
||||||
|
Renders a link to the customer order
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
order = self.raw_value
|
||||||
|
if not order:
|
||||||
|
return ''
|
||||||
|
return tags.link_to(order, self.request.route_url('custorders.view', uuid=order.uuid))
|
|
@ -517,6 +517,7 @@ class Grid(object):
|
||||||
Render the complete grid, including filters.
|
Render the complete grid, including filters.
|
||||||
"""
|
"""
|
||||||
kwargs['grid'] = self
|
kwargs['grid'] = self
|
||||||
|
kwargs.setdefault('div_class', '')
|
||||||
kwargs.setdefault('allow_save_defaults', True)
|
kwargs.setdefault('allow_save_defaults', True)
|
||||||
return render(template, kwargs)
|
return render(template, kwargs)
|
||||||
|
|
||||||
|
|
56
tailbone/static/css/custorders.new.css
Normal file
56
tailbone/static/css/custorders.new.css
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
|
||||||
|
#order-tabs {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrapper.update-phone {
|
||||||
|
display: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrapper.customer-notes {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#customer-notes {
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 5px;
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#order-note-text {
|
||||||
|
height: 200px;
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-info,
|
||||||
|
.product-info {
|
||||||
|
margin: 15px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-info > div,
|
||||||
|
.product-info > div {
|
||||||
|
padding-left: 15px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#items {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#items tbody td.price {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-dialog #item-quantity-tab {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-dialog #item-note-tab textarea {
|
||||||
|
height: 210px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
1037
tailbone/static/js/custorders.new.js
Normal file
1037
tailbone/static/js/custorders.new.js
Normal file
File diff suppressed because it is too large
Load diff
4
tailbone/static/js/lib/backbone-1.0.0.min.js
vendored
Normal file
4
tailbone/static/js/lib/backbone-1.0.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tailbone/static/js/lib/backbone.localStorage-1.1.0.min.js
vendored
Normal file
1
tailbone/static/js/lib/backbone.localStorage-1.1.0.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(root,factory){if(typeof define==="function"&&define.amd){define(["underscore","backbone"],function(_,Backbone){return factory(_||root._,Backbone||root.Backbone)})}else{factory(_,Backbone)}})(this,function(_,Backbone){function S4(){return((1+Math.random())*65536|0).toString(16).substring(1)}function guid(){return S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()}Backbone.LocalStorage=window.Store=function(name){this.name=name;var store=this.localStorage().getItem(this.name);this.records=store&&store.split(",")||[]};_.extend(Backbone.LocalStorage.prototype,{save:function(){this.localStorage().setItem(this.name,this.records.join(","))},create:function(model){if(!model.id){model.id=guid();model.set(model.idAttribute,model.id)}this.localStorage().setItem(this.name+"-"+model.id,JSON.stringify(model));this.records.push(model.id.toString());this.save();return this.find(model)},update:function(model){this.localStorage().setItem(this.name+"-"+model.id,JSON.stringify(model));if(!_.include(this.records,model.id.toString()))this.records.push(model.id.toString());this.save();return this.find(model)},find:function(model){return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id))},findAll:function(){return _(this.records).chain().map(function(id){return this.jsonData(this.localStorage().getItem(this.name+"-"+id))},this).compact().value()},destroy:function(model){if(model.isNew())return false;this.localStorage().removeItem(this.name+"-"+model.id);this.records=_.reject(this.records,function(id){return id===model.id.toString()});this.save();return model},localStorage:function(){return localStorage},jsonData:function(data){return data&&JSON.parse(data)}});Backbone.LocalStorage.sync=window.Store.sync=Backbone.localSync=function(method,model,options){var store=model.localStorage||model.collection.localStorage;var resp,errorMessage,syncDfd=$.Deferred&&$.Deferred();try{switch(method){case"read":resp=model.id!=undefined?store.find(model):store.findAll();break;case"create":resp=store.create(model);break;case"update":resp=store.update(model);break;case"delete":resp=store.destroy(model);break}}catch(error){if(error.code===DOMException.QUOTA_EXCEEDED_ERR&&window.localStorage.length===0)errorMessage="Private browsing is unsupported";else errorMessage=error.message}if(resp){if(options&&options.success)if(Backbone.VERSION==="0.9.10"){options.success(model,resp,options)}else{options.success(resp)}if(syncDfd)syncDfd.resolve(resp)}else{errorMessage=errorMessage?errorMessage:"Record Not Found";if(options&&options.error)if(Backbone.VERSION==="0.9.10"){options.error(model,errorMessage,options)}else{options.error(errorMessage)}if(syncDfd)syncDfd.reject(errorMessage)}if(options&&options.complete)options.complete(resp);return syncDfd&&syncDfd.promise()};Backbone.ajaxSync=Backbone.sync;Backbone.getSyncMethod=function(model){if(model.localStorage||model.collection&&model.collection.localStorage){return Backbone.localSync}return Backbone.ajaxSync};Backbone.sync=function(method,model,options){return Backbone.getSyncMethod(model).apply(this,[method,model,options])};return Backbone.LocalStorage});
|
1
tailbone/static/js/lib/underscore-1.4.4.min.js
vendored
Normal file
1
tailbone/static/js/lib/underscore-1.4.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
41
tailbone/templates/custorders/config.mako
Normal file
41
tailbone/templates/custorders/config.mako
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">Customer Orders Configuration</%def>
|
||||||
|
|
||||||
|
<div class="form">
|
||||||
|
${h.form(url('custorders.config'))}
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="handler">Order Handler</label>
|
||||||
|
<div class="field">
|
||||||
|
<div class="grid hoverable">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
% for i, handler in enumerate(sorted(registered_handlers.itervalues(), key=lambda h: h.name), 1):
|
||||||
|
<tr class="${'odd' if i % 2 else 'even'}">
|
||||||
|
<td>${h.radio('handler', handler.key, checked=current_handler.key == handler.key)}</td>
|
||||||
|
<td>${handler.name}</td>
|
||||||
|
<td>${handler.description}</td>
|
||||||
|
</tr>
|
||||||
|
% endfor
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
${h.submit('save', "Save")}
|
||||||
|
<a class="button" href="${url('custorders.config')}">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
12
tailbone/templates/custorders/index.mako
Normal file
12
tailbone/templates/custorders/index.mako
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/grid.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">Customer Orders</%def>
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
% if request.has_perm('custorders.create'):
|
||||||
|
<li>${h.link_to("Create a new Customer Order", url('custorders.new'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
${parent.body()}
|
107
tailbone/templates/custorders/new.itemdialog.mako
Normal file
107
tailbone/templates/custorders/new.itemdialog.mako
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%namespace file="/autocomplete.mako" import="autocomplete" />
|
||||||
|
|
||||||
|
<%def name="item_dialog()">
|
||||||
|
<div id="item-dialog" style="display: none;">
|
||||||
|
|
||||||
|
<div id="item-tabs">
|
||||||
|
<ul>
|
||||||
|
<li><a href="#item-product-tab">Product</a></li>
|
||||||
|
<li><a href="#item-quantity-tab">Quantity</a></li>
|
||||||
|
<li><a href="#item-note-tab">Note</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div id="item-product-tab">
|
||||||
|
|
||||||
|
<div class="product-info">
|
||||||
|
${h.radio('product_exists', 'true', label="Product is already in the system.")}
|
||||||
|
<div id="product-info-known">
|
||||||
|
|
||||||
|
<div class="fieldset">
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="product_uuid-textbox">Description</label>
|
||||||
|
<div class="field">${autocomplete('product_uuid', url('products.autocomplete'), selected='ItemDialog.productSelected', cleared='ItemDialog.productCleared')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="upc">UPC</label>
|
||||||
|
<div class="field" id="upc-container">
|
||||||
|
<div id="upc-input">
|
||||||
|
${h.text('upc-textbox')}
|
||||||
|
<button type="button" id="upc-fetch">Fetch</button>
|
||||||
|
</div>
|
||||||
|
<div id="upc-display" style="display: none;">
|
||||||
|
<span> </span>
|
||||||
|
<button type="button" id="upc-change">Change</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-info">
|
||||||
|
${h.radio('product_exists', 'false', label="Product cannot be located in the system.")}
|
||||||
|
<div id="product-info-unknown">
|
||||||
|
<div class="fieldset">
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="product_description">Description</label>
|
||||||
|
<div class="field">${h.textarea('product_description')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- item-product-tab -->
|
||||||
|
|
||||||
|
<div id="item-quantity-tab">
|
||||||
|
<div class="fieldset">
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="quantity">Quantity</label>
|
||||||
|
<div class="field">${h.text('quantity')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="unit_of_measure">Unit of Measure</label>
|
||||||
|
<div class="field">${h.select('unit_of_measure', None, [])}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="discount">Discount %</label>
|
||||||
|
<div class="field">${h.text('discount')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="price_per_uom">Price / UoM</label>
|
||||||
|
<div class="field">${h.text('price_per_uom', disabled=True)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="price_before_discount">Before Discount</label>
|
||||||
|
<div class="field">${h.text('price_before_discount', disabled=True)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="price_after_discount">After Discount</label>
|
||||||
|
<div class="field">${h.text('price_after_discount', disabled=True)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- item-quantity-tab -->
|
||||||
|
|
||||||
|
<div id="item-note-tab">
|
||||||
|
<p><strong>This note will apply only to this particular item.</strong></p>
|
||||||
|
<p>Please see the main screen to attach a note to the order as a whole.</p>
|
||||||
|
${h.textarea('note')}
|
||||||
|
</div><!-- item-note-tab -->
|
||||||
|
|
||||||
|
</div><!-- item-tabs -->
|
||||||
|
|
||||||
|
</div><!-- item-dialog -->
|
||||||
|
</%def>
|
160
tailbone/templates/custorders/new.mako
Normal file
160
tailbone/templates/custorders/new.mako
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
<%namespace file="/autocomplete.mako" import="autocomplete" />
|
||||||
|
<%namespace file="/custorders/new.itemdialog.mako" import="item_dialog" />
|
||||||
|
|
||||||
|
<%def name="title()">New Customer Order</%def>
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${h.javascript_link(request.static_url('tailbone:static/js/lib/underscore-1.4.4.min.js'))}
|
||||||
|
${h.javascript_link(request.static_url('tailbone:static/js/lib/backbone-1.0.0.min.js'))}
|
||||||
|
${h.javascript_link(request.static_url('tailbone:static/js/lib/backbone.localStorage-1.1.0.min.js'))}
|
||||||
|
${h.javascript_link(request.static_url('tailbone:static/js/custorders.new.js'))}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
## FIXME: These URLs must be generated from within a Mako template.
|
||||||
|
var urls = {
|
||||||
|
## home: '${url('home')}',
|
||||||
|
customer_info: '${url('customer.info')}',
|
||||||
|
product_search: '${url('products.search')}',
|
||||||
|
product_info: '${url('custorders.product_info')}',
|
||||||
|
## employee_login: '${url('employee_login')}',
|
||||||
|
create_orders: '${url('custorders.new')}'
|
||||||
|
## orders_created: '${url('specialorders.created')}'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some global defaults for customer orders.
|
||||||
|
## var default_discount = ${default_discount};
|
||||||
|
var default_discount = 0;
|
||||||
|
var default_ambiguous_product_description = '';
|
||||||
|
## % for field in ambiguous_product_fields:
|
||||||
|
## default_ambiguous_product_description += '${field}: \n';
|
||||||
|
## % endfor
|
||||||
|
|
||||||
|
## Default units of measure must leverage Rattail enumeration values.
|
||||||
|
var default_units_of_measure = [
|
||||||
|
['${rattail.enum.UNIT_OF_MEASURE_EACH}', "${rattail.enum.UNIT_OF_MEASURE[rattail.enum.UNIT_OF_MEASURE_EACH]}"],
|
||||||
|
['${rattail.enum.UNIT_OF_MEASURE_POUND}', "${rattail.enum.UNIT_OF_MEASURE[rattail.enum.UNIT_OF_MEASURE_POUND]}"],
|
||||||
|
['${rattail.enum.UNIT_OF_MEASURE_CASE}', "${rattail.enum.UNIT_OF_MEASURE[rattail.enum.UNIT_OF_MEASURE_CASE]}"]
|
||||||
|
];
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
App.setMainFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
${h.stylesheet_link(request.static_url('tailbone:static/css/custorders.new.css'))}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
${form.begin(name='new_order')}
|
||||||
|
|
||||||
|
<div id="order-tabs">
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="#order-customer-tab">Customer</a></li>
|
||||||
|
<li><a href="#order-items-tab">Item(s)</a></li>
|
||||||
|
<li><a href="#order-note-tab">Note</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div id="order-customer-tab">
|
||||||
|
|
||||||
|
<div class="customer-info">
|
||||||
|
${form.radio('customer_exists', value='true', label="Customer is already in the system.", checked=True)}
|
||||||
|
<div id="customer-info-known">
|
||||||
|
<div class="fieldset">
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="customer_uuid-textbox">Customer</label>
|
||||||
|
<div class="field">${autocomplete('customer_uuid', url('customers.autocomplete'), selected='customer_selected', cleared='clear_customer')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="customer_phone_known-textbox">Phone Number</label>
|
||||||
|
<div class="field">${autocomplete('customer_phone_known', url('customers.autocomplete.phone'), select='customer_phone_selected', options={'minLength': 4})}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper update-phone">
|
||||||
|
${h.checkbox('update-phone', label="Please update this customer's phone number in the system.", checked=False)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper customer-notes">
|
||||||
|
<label for="customer-notes">Customer Notes</label>
|
||||||
|
<div class="field" id="customer-notes"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="customer-info">
|
||||||
|
${form.radio('customer_exists', value='false', label="Customer cannot be located in the system.", checked=False)}
|
||||||
|
<div id="customer-info-unknown" style="display: none;">
|
||||||
|
<div class="fieldset">
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="customer_name">Customer Name</label>
|
||||||
|
<div class="field">${form.text('customer_name')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="customer_phone_unknown">Phone Number</label>
|
||||||
|
<div class="field">${form.text('customer_phone_unknown')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- order-customer-tab -->
|
||||||
|
|
||||||
|
<div id="order-items-tab">
|
||||||
|
<div class="grid hoverable full">
|
||||||
|
<table id="items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Brand</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th class="edit"></th>
|
||||||
|
<th class="delete"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr id="no-items">
|
||||||
|
<td colspan="7"><em>No items have been added yet.</em></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<button type="button" id="add-item">Add...</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- order-items-tab -->
|
||||||
|
|
||||||
|
<div id="order-note-tab">
|
||||||
|
<p><strong>This note will apply to the order as a whole.</strong></p>
|
||||||
|
<p>Please edit individual items to attach item-specific notes.</p>
|
||||||
|
${h.textarea('order-note-text')}
|
||||||
|
</div><!-- order-note-tab -->
|
||||||
|
|
||||||
|
</div><!-- order-tabs -->
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
${form.submit('submit', "Create Order")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${form.end()}
|
||||||
|
|
||||||
|
${item_dialog()}
|
||||||
|
|
||||||
|
<script type="text/template" id="product-template">
|
||||||
|
<td class="brand">{{ product_brand }}</td>
|
||||||
|
<td class="description">{{ product_description }}</td>
|
||||||
|
<td class="size">{{ product_size }}</td>
|
||||||
|
<td class="quantity">{{ quantity_display }}</td>
|
||||||
|
<td class="price">{{ price_display }}</td>
|
||||||
|
<td class="edit"></td>
|
||||||
|
<td class="delete"></td>
|
||||||
|
</script>
|
|
@ -16,6 +16,12 @@
|
||||||
.newgrid-wrapper {
|
.newgrid-wrapper {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
.rows-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.rows-grid {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
@ -43,5 +49,8 @@
|
||||||
</div><!-- form-wrapper -->
|
</div><!-- form-wrapper -->
|
||||||
|
|
||||||
% if master.has_rows:
|
% if master.has_rows:
|
||||||
|
% if rows_title:
|
||||||
|
<h2 class="rows-title">${rows_title}</h2>
|
||||||
|
% endif
|
||||||
${rows_grid|n}
|
${rows_grid|n}
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
<div class="newgrid-wrapper">
|
<div class="newgrid-wrapper ${div_class}">
|
||||||
|
|
||||||
<table class="grid-header">
|
<table class="grid-header">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -50,6 +50,7 @@ def includeme(config):
|
||||||
config.include('tailbone.views.categories')
|
config.include('tailbone.views.categories')
|
||||||
config.include('tailbone.views.customergroups')
|
config.include('tailbone.views.customergroups')
|
||||||
config.include('tailbone.views.customers')
|
config.include('tailbone.views.customers')
|
||||||
|
config.include('tailbone.views.custorders')
|
||||||
config.include('tailbone.views.datasync')
|
config.include('tailbone.views.datasync')
|
||||||
config.include('tailbone.views.departments')
|
config.include('tailbone.views.departments')
|
||||||
config.include('tailbone.views.depositlinks')
|
config.include('tailbone.views.depositlinks')
|
||||||
|
|
34
tailbone/views/custorders/__init__.py
Normal file
34
tailbone/views/custorders/__init__.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2015 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Customer Order Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
# config.include('tailbone.views.custorders.config')
|
||||||
|
# config.include('tailbone.views.custorders.creating')
|
||||||
|
config.include('tailbone.views.custorders.orders')
|
||||||
|
config.include('tailbone.views.custorders.items')
|
88
tailbone/views/custorders/config.py
Normal file
88
tailbone/views/custorders/config.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2015 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Customer Order System Configuration Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rattail.db import custorders
|
||||||
|
|
||||||
|
import formencode
|
||||||
|
from pyramid_simpleform import Form
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
|
from tailbone.db import Session
|
||||||
|
|
||||||
|
|
||||||
|
class ValidHandler(formencode.validators.FancyValidator):
|
||||||
|
"""
|
||||||
|
Validator/converter for the customer order handler choice field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ValidHandler, self).__init__(*args, **kwargs)
|
||||||
|
self.registered_handlers = custorders.get_registered_handlers()
|
||||||
|
|
||||||
|
def _to_python(self, value, state):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
handler = self.registered_handlers.get(value)
|
||||||
|
if not handler:
|
||||||
|
raise formencode.Invalid("No such customer order handler", value, state)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
|
||||||
|
class RootConfig(formencode.Schema):
|
||||||
|
"""
|
||||||
|
Schema for root system configuration form.
|
||||||
|
"""
|
||||||
|
allow_extra_fields = True
|
||||||
|
filter_extra_fields = True
|
||||||
|
|
||||||
|
handler = formencode.Pipe(validators=[
|
||||||
|
formencode.validators.NotEmpty(),
|
||||||
|
ValidHandler()])
|
||||||
|
|
||||||
|
|
||||||
|
def config_root(request):
|
||||||
|
"""
|
||||||
|
Primary (root) view for customer order system configuration.
|
||||||
|
"""
|
||||||
|
handlers = custorders.get_registered_handlers()
|
||||||
|
handler = custorders.get_current_handler(Session(), handlers)
|
||||||
|
form = Form(request, schema=RootConfig)
|
||||||
|
if form.validate():
|
||||||
|
custorders.set_current_handler(Session(), form.data['handler'])
|
||||||
|
request.session.flash("Customer order system configuration has been updated.")
|
||||||
|
return HTTPFound(location=request.route_url('custorders.config'))
|
||||||
|
return {
|
||||||
|
'registered_handlers': handlers,
|
||||||
|
'current_handler': handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
config.add_route('custorders.config', '/custorders/config/')
|
||||||
|
config.add_view(config_root, route_name='custorders.config',
|
||||||
|
renderer='/custorders/config.mako', permission='admin')
|
208
tailbone/views/custorders/creating.py
Normal file
208
tailbone/views/custorders/creating.py
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2015 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Views for creating new Customer Orders
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rattail import enum
|
||||||
|
from rattail.db import model
|
||||||
|
from rattail.db import custorders
|
||||||
|
|
||||||
|
from pyramid.renderers import render_to_response
|
||||||
|
from pyramid_simpleform import Form
|
||||||
|
|
||||||
|
from tailbone.db import Session
|
||||||
|
from tailbone.forms import renderers, FormRenderer
|
||||||
|
from tailbone.forms.custorders import NewCustomerOrder
|
||||||
|
from tailbone.views import View
|
||||||
|
|
||||||
|
|
||||||
|
class NewCustomerOrder(View):
|
||||||
|
"""
|
||||||
|
View for creating a new customer order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
super(NewCustomerOrder, self).__init__(request)
|
||||||
|
self.handler = custorders.get_current_handler(Session())
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
"""
|
||||||
|
Use custom view instead of normal CRUD when creating new customer orders.
|
||||||
|
"""
|
||||||
|
form = Form(self.request, schema=NewCustomerOrder)
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
if form.validate():
|
||||||
|
|
||||||
|
user = form.data['user']
|
||||||
|
|
||||||
|
order = model.CustomerOrder()
|
||||||
|
order.customer = form.data['customer']
|
||||||
|
if order.customer:
|
||||||
|
order.customer_name = order.customer.name
|
||||||
|
else:
|
||||||
|
order.customer_name = form.data['customer_name']
|
||||||
|
order.customer_phone = form.data['customer_phone']
|
||||||
|
|
||||||
|
# user = form.data['user']
|
||||||
|
# customer = form.data['customer']
|
||||||
|
# if customer:
|
||||||
|
# customer_name = customer.name
|
||||||
|
# else:
|
||||||
|
# customer_name = form.data['customer_name']
|
||||||
|
# customer_phone = form.data['customer_phone']
|
||||||
|
|
||||||
|
# if form.data.get('update_customer_phone') == 'true':
|
||||||
|
# data = {'user': user, 'customer': customer, 'phone': customer_phone}
|
||||||
|
# send_email(self.rattail_config, 'specialorders_customer_phone_update', data)
|
||||||
|
|
||||||
|
# order_uuids = []
|
||||||
|
for data in form.data['products']:
|
||||||
|
# order = model.CustomerOrder()
|
||||||
|
# order.customer = customer
|
||||||
|
# order.customer_name = customer_name
|
||||||
|
# order.customer_phone = customer_phone
|
||||||
|
|
||||||
|
item = model.CustomerOrderItem()
|
||||||
|
|
||||||
|
# Set these first so extended price may be calculated below.
|
||||||
|
item.quantity = data['quantity']
|
||||||
|
item.unit_of_measure = data['unit_of_measure']
|
||||||
|
item.discount = data['discount']
|
||||||
|
|
||||||
|
item.product = data['product']
|
||||||
|
if item.product:
|
||||||
|
if item.product.brand:
|
||||||
|
item.product_brand = item.product.brand.name
|
||||||
|
item.product_description = item.product.description
|
||||||
|
item.product_size = item.product.size
|
||||||
|
item.product_unit_of_measure = item.product.unit_of_measure
|
||||||
|
item.product_case_pack = item.product.case_pack
|
||||||
|
if item.product.cost:
|
||||||
|
item.product_unit_cost = item.product.cost.unit_cost
|
||||||
|
# item.unit_price = order.product.regular_price.price / order.product.regular_price.multiple
|
||||||
|
# item.extended_price = order.calculate_extended_price()
|
||||||
|
else:
|
||||||
|
item.product_description = data['product_description']
|
||||||
|
|
||||||
|
item.status = self.handler.default_created_order_item_status
|
||||||
|
order.items.append(item)
|
||||||
|
|
||||||
|
Session.add(order)
|
||||||
|
return render_to_response('json', {'result': 'success'}, request=self.request)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_to_response('json', {'result': form.errors}, request=self.request)
|
||||||
|
|
||||||
|
# ambiguous_product_fields = parse_list(self.rattail_config.require(
|
||||||
|
# u'dtail', u'specialorders.ambiguous_product_fields'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'form': FormRenderer(form),
|
||||||
|
# 'default_discount': get_default_discount(self.rattail_config),
|
||||||
|
# 'ambiguous_product_fields': ambiguous_product_fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def uom_for_product(product):
|
||||||
|
"""
|
||||||
|
Determine the default "unit" UOM for the given product. Will always be
|
||||||
|
either "Pound" or "Each".
|
||||||
|
"""
|
||||||
|
if product.weighed:
|
||||||
|
return enum.UNIT_OF_MEASURE_POUND
|
||||||
|
return enum.UNIT_OF_MEASURE_EACH
|
||||||
|
|
||||||
|
|
||||||
|
def uom_choices_for_product(product=None):
|
||||||
|
"""
|
||||||
|
Generate a UOM enumeration customized for the given product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if product:
|
||||||
|
uom = uom_for_product(product)
|
||||||
|
uoms = [(uom, enum.UNIT_OF_MEASURE[uom])]
|
||||||
|
if product.case_pack and product.case_pack > 1:
|
||||||
|
uoms.append((
|
||||||
|
enum.UNIT_OF_MEASURE_CASE,
|
||||||
|
"{0} ({1} {2})".format(
|
||||||
|
enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_CASE],
|
||||||
|
product.case_pack,
|
||||||
|
enum.UNIT_OF_MEASURE[uom])))
|
||||||
|
|
||||||
|
else:
|
||||||
|
uoms = [
|
||||||
|
(enum.UNIT_OF_MEASURE_EACH, enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_EACH]),
|
||||||
|
(enum.UNIT_OF_MEASURE_POUND, enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_POUND]),
|
||||||
|
(enum.UNIT_OF_MEASURE_CASE, enum.UNIT_OF_MEASURE[enum.UNIT_OF_MEASURE_CASE]),
|
||||||
|
]
|
||||||
|
|
||||||
|
return uoms
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_info(config, uuid):
|
||||||
|
"""
|
||||||
|
Generic, sort of, function to retrieve a dictionary of info for the product
|
||||||
|
specified by ``uuid``.
|
||||||
|
"""
|
||||||
|
product = Session.query(model.Product).get(uuid) if uuid else None
|
||||||
|
if not product:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
info = {
|
||||||
|
'product_upc': unicode(product.upc),
|
||||||
|
'product_brand': product.brand.name if product.brand else '',
|
||||||
|
'product_description': product.description,
|
||||||
|
'product_size': product.size,
|
||||||
|
'product_case_pack': product.case_pack or 1,
|
||||||
|
# 'units_of_measure': uom_choices_for_product(product),
|
||||||
|
# u'default_discount': get_default_discount(config, product),
|
||||||
|
}
|
||||||
|
|
||||||
|
prices = {}
|
||||||
|
if product.regular_price:
|
||||||
|
price = product.regular_price.price
|
||||||
|
prices[uom_for_product(product)] = "{0:0.2f}".format(price)
|
||||||
|
if info['product_case_pack'] > 1:
|
||||||
|
prices[enum.UNIT_OF_MEASURE_CASE] = "{0:0.2f}".format(
|
||||||
|
price * info['product_case_pack'])
|
||||||
|
info['prices'] = prices
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def product_info(request):
|
||||||
|
return get_product_info(request, request.GET.get('uuid'))
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
|
||||||
|
config.add_route('custorders.new', '/custorders/new')
|
||||||
|
config.add_view(NewCustomerOrder, route_name='custorders.new',
|
||||||
|
renderer='/custorders/new.mako',
|
||||||
|
permission='custorders.create')
|
||||||
|
|
||||||
|
config.add_route('custorders.product_info', '/custorders/product-info')
|
||||||
|
config.add_view(product_info, route_name='custorders.product_info',
|
||||||
|
renderer='json')
|
175
tailbone/views/custorders/items.py
Normal file
175
tailbone/views/custorders/items.py
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2016 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Customer order item views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from rattail.db import model
|
||||||
|
from rattail.time import localtime
|
||||||
|
|
||||||
|
import formalchemy as fa
|
||||||
|
|
||||||
|
from tailbone import forms
|
||||||
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerOrderItemsView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for customer order items
|
||||||
|
"""
|
||||||
|
model_class = model.CustomerOrderItem
|
||||||
|
route_prefix = 'custorders.items'
|
||||||
|
url_prefix = '/custorders/items'
|
||||||
|
creatable = False
|
||||||
|
editable = False
|
||||||
|
deletable = False
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
model_row_class = model.CustomerOrderItemEvent
|
||||||
|
rows_title = "Event History"
|
||||||
|
rows_filterable = False
|
||||||
|
rows_sortable = False
|
||||||
|
rows_pageable = False
|
||||||
|
rows_viewable = False
|
||||||
|
|
||||||
|
def query(self, session):
|
||||||
|
return session.query(model.CustomerOrderItem)\
|
||||||
|
.join(model.CustomerOrder)\
|
||||||
|
.options(orm.joinedload(model.CustomerOrderItem.order)\
|
||||||
|
.joinedload(model.CustomerOrder.person))
|
||||||
|
|
||||||
|
def _preconfigure_grid(self, g):
|
||||||
|
|
||||||
|
g.joiners['person'] = lambda q: q.outerjoin(model.Person) #, model.Person.uuid == model.CustomerOrder.person_uuid)
|
||||||
|
g.filters['person'] = g.make_filter('person', model.Person.display_name, label="Person Name",
|
||||||
|
default_active=True, default_verb='contains')
|
||||||
|
g.sorters['person'] = g.make_sorter(model.Person.display_name)
|
||||||
|
|
||||||
|
g.filters['product_brand'].label = "Brand"
|
||||||
|
g.product_brand.set(label="Brand")
|
||||||
|
|
||||||
|
g.filters['product_description'].label = "Description"
|
||||||
|
g.product_description.set(label="Description")
|
||||||
|
|
||||||
|
g.filters['product_size'].label = "Size"
|
||||||
|
g.product_size.set(label="Size")
|
||||||
|
|
||||||
|
g.case_quantity.set(renderer=forms.renderers.QuantityFieldRenderer)
|
||||||
|
g.cases_ordered.set(renderer=forms.renderers.QuantityFieldRenderer)
|
||||||
|
g.units_ordered.set(renderer=forms.renderers.QuantityFieldRenderer)
|
||||||
|
|
||||||
|
g.total_price.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
||||||
|
|
||||||
|
g.joiners['status'] = lambda q: q.join(model.CustomerOrderItemStatus)
|
||||||
|
g.sorters['status'] = g.make_sorter(model.CustomerOrderItemStatus.code)
|
||||||
|
|
||||||
|
g.append(fa.Field('person', value=lambda i: i.order.person))
|
||||||
|
|
||||||
|
g.sorters['order_created'] = g.make_sorter(model.CustomerOrder.created)
|
||||||
|
g.append(fa.Field('order_created',
|
||||||
|
value=lambda i: localtime(self.rattail_config, i.order.created, from_utc=True),
|
||||||
|
renderer=forms.renderers.DateTimeFieldRenderer))
|
||||||
|
|
||||||
|
g.default_sortkey = 'order_created'
|
||||||
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
|
def configure_grid(self, g):
|
||||||
|
g.configure(
|
||||||
|
include=[
|
||||||
|
g.person,
|
||||||
|
g.product_brand,
|
||||||
|
g.product_description,
|
||||||
|
g.product_size,
|
||||||
|
g.case_quantity,
|
||||||
|
g.cases_ordered,
|
||||||
|
g.units_ordered,
|
||||||
|
g.order_created,
|
||||||
|
g.status,
|
||||||
|
],
|
||||||
|
readonly=True)
|
||||||
|
|
||||||
|
def _preconfigure_fieldset(self, fs):
|
||||||
|
fs.order.set(renderer=forms.renderers.CustomerOrderFieldRenderer)
|
||||||
|
fs.product.set(renderer=forms.renderers.ProductFieldRenderer)
|
||||||
|
fs.product_unit_of_measure.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.UNIT_OF_MEASURE))
|
||||||
|
fs.case_quantity.set(renderer=forms.renderers.QuantityFieldRenderer)
|
||||||
|
fs.cases_ordered.set(renderer=forms.renderers.QuantityFieldRenderer)
|
||||||
|
fs.units_ordered.set(renderer=forms.renderers.QuantityFieldRenderer)
|
||||||
|
fs.unit_price.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
||||||
|
fs.total_price.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
||||||
|
fs.paid_amount.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
||||||
|
fs.append(fa.Field('person', value=lambda i: i.order.person,
|
||||||
|
renderer=forms.renderers.PersonFieldRenderer))
|
||||||
|
|
||||||
|
def configure_fieldset(self, fs):
|
||||||
|
fs.configure(
|
||||||
|
include=[
|
||||||
|
fs.person,
|
||||||
|
fs.product,
|
||||||
|
fs.product_brand,
|
||||||
|
fs.product_description,
|
||||||
|
fs.product_size,
|
||||||
|
fs.case_quantity,
|
||||||
|
fs.cases_ordered,
|
||||||
|
fs.units_ordered,
|
||||||
|
fs.unit_price,
|
||||||
|
fs.total_price,
|
||||||
|
fs.paid_amount,
|
||||||
|
fs.status,
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_row_data(self, item):
|
||||||
|
return self.Session.query(model.CustomerOrderItemEvent)\
|
||||||
|
.filter(model.CustomerOrderItemEvent.item == item)\
|
||||||
|
.order_by(model.CustomerOrderItemEvent.occurred,
|
||||||
|
model.CustomerOrderItemEvent.type_code)
|
||||||
|
|
||||||
|
def _preconfigure_row_grid(self, g):
|
||||||
|
g.occurred.set(label="When")
|
||||||
|
g.type.set(label="What")
|
||||||
|
g.user.set(label="Who")
|
||||||
|
g.note.set(label="Notes")
|
||||||
|
|
||||||
|
def configure_row_grid(self, g):
|
||||||
|
g.configure(
|
||||||
|
include=[
|
||||||
|
g.occurred,
|
||||||
|
g.type,
|
||||||
|
g.user,
|
||||||
|
g.note,
|
||||||
|
],
|
||||||
|
readonly=True)
|
||||||
|
|
||||||
|
# def render_row_grid(self, item, grid, **kwargs):
|
||||||
|
# kwargs['div_class'] = 'event-history'
|
||||||
|
# return super(CustomerOrderItemsView, self).render_row_grid(item, grid, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
CustomerOrderItemsView.defaults(config)
|
107
tailbone/views/custorders/orders.py
Normal file
107
tailbone/views/custorders/orders.py
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2016 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Customer Order Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from rattail.db import model
|
||||||
|
|
||||||
|
from tailbone import forms
|
||||||
|
from tailbone.db import Session
|
||||||
|
from tailbone.views import MasterView
|
||||||
|
from tailbone.newgrids.filters import ChoiceValueRenderer
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerOrdersView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for customer orders
|
||||||
|
"""
|
||||||
|
model_class = model.CustomerOrder
|
||||||
|
route_prefix = 'custorders'
|
||||||
|
creatable = False
|
||||||
|
editable = False
|
||||||
|
deletable = False
|
||||||
|
|
||||||
|
def query(self, session):
|
||||||
|
return session.query(model.CustomerOrder)\
|
||||||
|
.options(orm.joinedload(model.CustomerOrder.customer))
|
||||||
|
|
||||||
|
def _preconfigure_grid(self, g):
|
||||||
|
g.joiners['customer'] = lambda q: q.outerjoin(model.Customer)
|
||||||
|
g.sorters['customer'] = g.make_sorter(model.Customer.name)
|
||||||
|
g.filters['customer'] = g.make_filter('customer', model.Customer.name,
|
||||||
|
label="Customer Name",
|
||||||
|
default_active=True,
|
||||||
|
default_verb='contains')
|
||||||
|
|
||||||
|
g.joiners['person'] = lambda q: q.outerjoin(model.Person)
|
||||||
|
g.sorters['person'] = g.make_sorter(model.Person.display_name)
|
||||||
|
g.filters['person'] = g.make_filter('person', model.Person.display_name,
|
||||||
|
label="Person Name",
|
||||||
|
default_active=True,
|
||||||
|
default_verb='contains')
|
||||||
|
|
||||||
|
query = Session.query(model.CustomerOrderStatus)\
|
||||||
|
.order_by(model.CustomerOrderStatus.code)
|
||||||
|
status_choices = [(s.code, s.description) for s in query]
|
||||||
|
g.filters['status_code'].set_value_renderer(ChoiceValueRenderer(status_choices))
|
||||||
|
g.filters['status_code'].label = "Status"
|
||||||
|
|
||||||
|
g.id.set(label="ID")
|
||||||
|
g.default_sortkey = 'created'
|
||||||
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
|
def configure_grid(self, g):
|
||||||
|
g.configure(
|
||||||
|
include=[
|
||||||
|
g.id,
|
||||||
|
g.customer,
|
||||||
|
g.person,
|
||||||
|
g.created,
|
||||||
|
g.status,
|
||||||
|
],
|
||||||
|
readonly=True)
|
||||||
|
|
||||||
|
def _preconfigure_fieldset(self, fs):
|
||||||
|
fs.customer.set(options=[])
|
||||||
|
fs.id.set(label="ID", readonly=True)
|
||||||
|
fs.person.set(renderer=forms.renderers.PersonFieldRenderer)
|
||||||
|
fs.created.set(readonly=True)
|
||||||
|
|
||||||
|
def configure_fieldset(self, fs):
|
||||||
|
fs.configure(
|
||||||
|
include=[
|
||||||
|
fs.id,
|
||||||
|
fs.customer,
|
||||||
|
fs.person,
|
||||||
|
fs.created,
|
||||||
|
fs.status,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
CustomerOrdersView.defaults(config)
|
|
@ -75,6 +75,7 @@ class MasterView(View):
|
||||||
model_row_class = None
|
model_row_class = None
|
||||||
rows_filterable = True
|
rows_filterable = True
|
||||||
rows_sortable = True
|
rows_sortable = True
|
||||||
|
rows_pageable = True
|
||||||
rows_viewable = True
|
rows_viewable = True
|
||||||
rows_creatable = False
|
rows_creatable = False
|
||||||
rows_editable = False
|
rows_editable = False
|
||||||
|
@ -180,10 +181,15 @@ class MasterView(View):
|
||||||
'form': form,
|
'form': form,
|
||||||
}
|
}
|
||||||
if self.has_rows:
|
if self.has_rows:
|
||||||
context['rows_grid'] = grid.render_complete(allow_save_defaults=False,
|
context['rows_grid'] = self.render_row_grid(instance, grid)
|
||||||
tools=self.make_row_grid_tools(instance))
|
|
||||||
return self.render_to_response('view', context)
|
return self.render_to_response('view', context)
|
||||||
|
|
||||||
|
def render_row_grid(self, instance, grid, **kwargs):
|
||||||
|
kwargs.setdefault('div_class', 'rows-grid')
|
||||||
|
kwargs.setdefault('allow_save_defaults', False)
|
||||||
|
kwargs.setdefault('tools', self.make_row_grid_tools(instance))
|
||||||
|
return grid.render_complete(**kwargs)
|
||||||
|
|
||||||
def make_default_row_grid_tools(self, obj):
|
def make_default_row_grid_tools(self, obj):
|
||||||
if self.rows_creatable:
|
if self.rows_creatable:
|
||||||
link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
|
link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
|
||||||
|
@ -267,7 +273,7 @@ class MasterView(View):
|
||||||
'width': 'full',
|
'width': 'full',
|
||||||
'filterable': self.rows_filterable,
|
'filterable': self.rows_filterable,
|
||||||
'sortable': self.rows_sortable,
|
'sortable': self.rows_sortable,
|
||||||
'pageable': True,
|
'pageable': self.rows_pageable,
|
||||||
'row_attrs': self.row_grid_row_attrs,
|
'row_attrs': self.row_grid_row_attrs,
|
||||||
'model_title': self.get_row_model_title(),
|
'model_title': self.get_row_model_title(),
|
||||||
'model_title_plural': self.get_row_model_title_plural(),
|
'model_title_plural': self.get_row_model_title_plural(),
|
||||||
|
@ -620,6 +626,7 @@ class MasterView(View):
|
||||||
context['grid_count'] = self.grid_count
|
context['grid_count'] = self.grid_count
|
||||||
|
|
||||||
if self.has_rows:
|
if self.has_rows:
|
||||||
|
context['rows_title'] = getattr(self, 'rows_title', '')
|
||||||
context['row_route_prefix'] = self.get_row_route_prefix()
|
context['row_route_prefix'] = self.get_row_route_prefix()
|
||||||
context['row_permission_prefix'] = self.get_row_permission_prefix()
|
context['row_permission_prefix'] = self.get_row_permission_prefix()
|
||||||
context['row_model_title'] = self.get_row_model_title()
|
context['row_model_title'] = self.get_row_model_title()
|
||||||
|
|
Loading…
Reference in a new issue