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, BrandFieldRenderer, ProductFieldRenderer,
PriceFieldRenderer, PriceWithExpirationFieldRenderer) PriceFieldRenderer, PriceWithExpirationFieldRenderer)
from .custorders import CustomerOrderFieldRenderer
from .batch import BatchIDFieldRenderer, HandheldBatchFieldRenderer from .batch import BatchIDFieldRenderer, HandheldBatchFieldRenderer

View file

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

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

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 { .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

View file

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

View file

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

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