Compare commits

...

1 commit

Author SHA1 Message Date
Lance Edgar 1150e6f7a6 savepoint 2017-02-16 21:33:54 -06:00
23 changed files with 2189 additions and 6 deletions

View 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()]

View file

@ -46,4 +46,6 @@ from .products import (GPCFieldRenderer, ScancodeFieldRenderer,
BrandFieldRenderer, ProductFieldRenderer,
PriceFieldRenderer, PriceWithExpirationFieldRenderer)
from .custorders import CustomerOrderFieldRenderer
from .batch import BatchIDFieldRenderer, HandheldBatchFieldRenderer

View file

@ -60,16 +60,24 @@ class CustomFieldRenderer(fa.FieldRenderer):
return self.request.rattail_config
class DateFieldRenderer(CustomFieldRenderer, fa.DateFieldRenderer):
class DateFieldRenderer(CustomFieldRenderer):
"""
Date field renderer which uses jQuery UI datepicker widget when rendering
in edit mode.
"""
date_format = None
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
def render_readonly(self, **kwargs):
value = self.raw_value
if value is None:
return ''
return value.strftime(self.date_format)
def render(self, **kwargs):
kwargs['name'] = self.name
kwargs['value'] = self.value

View 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))

View file

@ -517,6 +517,7 @@ class Grid(object):
Render the complete grid, including filters.
"""
kwargs['grid'] = self
kwargs.setdefault('div_class', '')
kwargs.setdefault('allow_save_defaults', True)
return render(template, kwargs)

View 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%;
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View 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});

File diff suppressed because one or more lines are too long

View 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>&nbsp;</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>

View 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()}

View 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>&nbsp;</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>

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

View file

@ -16,6 +16,12 @@
.newgrid-wrapper {
margin-top: 10px;
}
.rows-title {
margin-bottom: 0;
}
.rows-grid {
margin-top: 0;
}
</style>
% endif
</%def>
@ -43,5 +49,8 @@
</div><!-- form-wrapper -->
% if master.has_rows:
% if rows_title:
<h2 class="rows-title">${rows_title}</h2>
% endif
${rows_grid|n}
% endif

View file

@ -1,5 +1,5 @@
## -*- coding: utf-8 -*-
<div class="newgrid-wrapper">
<div class="newgrid-wrapper ${div_class}">
<table class="grid-header">
<tbody>

View file

@ -50,6 +50,7 @@ def includeme(config):
config.include('tailbone.views.categories')
config.include('tailbone.views.customergroups')
config.include('tailbone.views.customers')
config.include('tailbone.views.custorders')
config.include('tailbone.views.datasync')
config.include('tailbone.views.departments')
config.include('tailbone.views.depositlinks')

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

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

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

View 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)

View 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)

View file

@ -75,6 +75,7 @@ class MasterView(View):
model_row_class = None
rows_filterable = True
rows_sortable = True
rows_pageable = True
rows_viewable = True
rows_creatable = False
rows_editable = False
@ -180,10 +181,15 @@ class MasterView(View):
'form': form,
}
if self.has_rows:
context['rows_grid'] = grid.render_complete(allow_save_defaults=False,
tools=self.make_row_grid_tools(instance))
context['rows_grid'] = self.render_row_grid(instance, grid)
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):
if self.rows_creatable:
link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
@ -267,7 +273,7 @@ class MasterView(View):
'width': 'full',
'filterable': self.rows_filterable,
'sortable': self.rows_sortable,
'pageable': True,
'pageable': self.rows_pageable,
'row_attrs': self.row_grid_row_attrs,
'model_title': self.get_row_model_title(),
'model_title_plural': self.get_row_model_title_plural(),
@ -620,6 +626,7 @@ class MasterView(View):
context['grid_count'] = self.grid_count
if self.has_rows:
context['rows_title'] = getattr(self, 'rows_title', '')
context['row_route_prefix'] = self.get_row_route_prefix()
context['row_permission_prefix'] = self.get_row_permission_prefix()
context['row_model_title'] = self.get_row_model_title()