diff --git a/tailbone/forms/custorders.py b/tailbone/forms/custorders.py
new file mode 100644
index 00000000..f749714c
--- /dev/null
+++ b/tailbone/forms/custorders.py
@@ -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 .
+#
+################################################################################
+"""
+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()]
diff --git a/tailbone/forms/renderers/__init__.py b/tailbone/forms/renderers/__init__.py
index 984e40ca..e1ad83fe 100644
--- a/tailbone/forms/renderers/__init__.py
+++ b/tailbone/forms/renderers/__init__.py
@@ -46,4 +46,6 @@ from .products import (GPCFieldRenderer, ScancodeFieldRenderer,
BrandFieldRenderer, ProductFieldRenderer,
PriceFieldRenderer, PriceWithExpirationFieldRenderer)
+from .custorders import CustomerOrderFieldRenderer
+
from .batch import BatchIDFieldRenderer, HandheldBatchFieldRenderer
diff --git a/tailbone/forms/renderers/core.py b/tailbone/forms/renderers/core.py
index ac80404a..f64e33f1 100644
--- a/tailbone/forms/renderers/core.py
+++ b/tailbone/forms/renderers/core.py
@@ -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
diff --git a/tailbone/forms/renderers/custorders.py b/tailbone/forms/renderers/custorders.py
new file mode 100644
index 00000000..5b5794f6
--- /dev/null
+++ b/tailbone/forms/renderers/custorders.py
@@ -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 .
+#
+################################################################################
+"""
+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))
diff --git a/tailbone/newgrids/core.py b/tailbone/newgrids/core.py
index 0beba6de..ad2985ce 100644
--- a/tailbone/newgrids/core.py
+++ b/tailbone/newgrids/core.py
@@ -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)
diff --git a/tailbone/static/css/custorders.new.css b/tailbone/static/css/custorders.new.css
new file mode 100644
index 00000000..bf6122ed
--- /dev/null
+++ b/tailbone/static/css/custorders.new.css
@@ -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%;
+}
diff --git a/tailbone/static/js/custorders.new.js b/tailbone/static/js/custorders.new.js
new file mode 100644
index 00000000..464e9dfa
--- /dev/null
+++ b/tailbone/static/js/custorders.new.js
@@ -0,0 +1,1037 @@
+
+/**********************************************************************
+ * JS for New Customer Order page
+ **********************************************************************/
+
+/**********************************************************************
+ * Global Variables / Config
+ **********************************************************************/
+
+/*
+ * Keep global references to some Backbone objects. These will be instantiated
+ * on document load.
+ */
+var App, ItemDialog, OrderItems;
+
+/*
+ * Keep track of which phone number was provided by customer's database record,
+ * so we can determine if the user has truly provided a different one.
+ */
+var customer_phone_stored = null;
+
+/*
+ * The default template syntax for underscore.js conflicts with Mako, so we
+ * must override it here.
+ */
+_.templateSettings = {
+ interpolate: /\{\{(.+?)\}\}/g
+};
+
+/*
+ * Prompt user if they attempt to leave this page before creating the order.
+ */
+var okay_to_leave = true; // FIXME
+
+window.onbeforeunload = function() {
+ if (! okay_to_leave) {
+ return "You have unsaved changes, are you sure you want to leave this page?";
+ }
+}
+
+
+/**********************************************************************
+ * Functions
+ **********************************************************************/
+
+/*
+ * Convenience function which updates the customer phone autocomplete elements,
+ * whenever a customer is identified via either name or phone autocomplete.
+ */
+function update_customer_phone(data) {
+ $('#customer_phone_known-textbox').autocomplete('option', 'disabled', true);
+ $('#customer_phone_known').val(data.phone_number);
+ $('#customer_phone_known-textbox').val(data.phone_number);
+ $('#customer_phone_known-textbox').select();
+ $('#customer_phone_known-textbox').focus();
+ customer_phone_stored = data.phone_number;
+ if (customer_phone_stored == '') {
+ $('div.field-wrapper.update-phone').show();
+ $('#update-phone').prop('checked', true);
+ $('#update-phone').attr('disabled', true);
+ }
+ if (data.note) {
+ $('#customer-notes').text(data.note);
+ $('div.field-wrapper.customer-notes').show();
+ }
+}
+
+
+/*
+ * This is the callback function for the customer name autocomplete field.
+ */
+function customer_selected(uuid, name) {
+ // FIXME url
+ $.get(urls.customer_info, {'uuid': uuid}, function(data) {
+ update_customer_phone(data);
+ });
+}
+
+
+/*
+ * This is the callback function for when the user clicks the "change" button
+ * next to the customer name autocomplete field.
+ */
+function clear_customer() {
+ $('#customer_phone_known').val('');
+ $('#customer_phone_known-textbox').val('');
+ $('#customer_phone_known-textbox').autocomplete('option', 'disabled', false);
+ $('div.field-wrapper.update-phone').hide();
+ $('#update-phone').prop('checked', false);
+ $('#update-phone').attr('disabled', false);
+ customer_phone_stored = null;
+ $('div.field-wrapper.customer-notes').hide();
+ $('#customer-notes').text('');
+}
+
+
+/*
+ * This is the callback function for the customer phone autocomplete field.
+ */
+function customer_phone_selected(event, ui) {
+ var uuid = ui.item.value;
+ // FIXME url
+ $.get(urls.customer_info, {'uuid': uuid}, function(data) {
+ update_customer_phone(data);
+ $('#customer_uuid').val(uuid);
+ $('#customer_uuid-display span:first').text(data.name);
+ $('#customer_uuid-textbox').hide();
+ $('#customer_uuid-display').show();
+ });
+ return false;
+}
+
+
+/*
+ * This function determines if a given phone number is valid, i.e. contains
+ * exactly 7 or 10 digits.
+ */
+function validate_phone_number(text) {
+ var digits = text.replace(/\D/g, '');
+ return (digits.length == 7) || (digits.length == 10);
+}
+
+
+// /*
+// * This function prompts an employee to login, if there is no user logged in.
+// */
+// function authenticate_employee() {
+// var dialog = $('#employee-login-dialog');
+// if (! dialog.length) {
+// dialog = $('
', {
+// id: 'employee-login-dialog'
+// });
+// }
+
+// function init_dialog() {
+// dialog.find('input[type="submit"]').button();
+// dialog.find('button').button();
+
+// dialog.find('form').submit(function() {
+// $.post($(this).attr('action'), $(this).serialize(), function(data) {
+// if (typeof(data) == 'object') {
+// if (data.result == 'success') {
+// current_user = data.user_uuid;
+// dialog.dialog('close');
+// submit_form();
+// } else if (data.result == 'no_user') {
+// var msg = "Hello, " + data.employee_name + ".\n\n"
+// + "I see you are an employee, but you do not yet have a Dtail user account.\n\n"
+// + "You will not be able to create special orders until you have this. Please\n"
+// + "talk to the IT department so they can create your account.\n\n";
+// alert(msg);
+// dialog.load(urls.employee_login, function() {
+// init_dialog();
+// });
+// } else {
+// alert("Unexpected result:\n\n" + data.result);
+// }
+// } else {
+// dialog.html(data);
+// init_dialog();
+// }
+// });
+// return false;
+// });
+
+// dialog.find('button.cancel').click(function() {
+// dialog.dialog('close');
+// });
+
+// with (dialog.find('#employee')) {
+// select();
+// focus();
+// }
+// }
+
+// dialog.load(urls.employee_login, function() {
+// init_dialog();
+// dialog.dialog({
+// title: "Employee Login",
+// width: 300,
+// height: 225
+// });
+// });
+// }
+
+
+/*
+ * This function assembles the data hash used to submit the special order(s) to
+ * the server for creation.
+ */
+function collect_data() {
+ var data = {user: current_user};
+
+ if ($('#customer_exists_true').is(':checked')) {
+ data.customer = $('#customer_uuid').val();
+ data.customer_name = null;
+ data.customer_phone = $('#customer_phone_known-textbox').val();
+ if ($('#update-phone').is(':checked')) {
+ data.update_customer_phone = 'true';
+ }
+ } else {
+ data.customer = null;
+ data.customer_name = $('#customer_name').val();
+ data.customer_phone = $('#customer_phone_unknown').val();
+ }
+
+ // We must use FormEncode-compatible keys for the product data. It would
+ // be nice if we could just send nested JSON data structures, but oh well.
+ var i = 0;
+ OrderItems.each(function(item) {
+ data['item-' + i + '.product'] = item.get('product_uuid');
+ data['item-' + i + '.product_brand'] = item.get('product_brand');
+ data['item-' + i + '.product_description'] = item.get('product_description');
+ data['item-' + i + '.product_size'] = item.get('product_size');
+ data['item-' + i + '.quantity'] = item.get('quantity');
+ data['item-' + i + '.unit_of_measure'] = item.get('unit_of_measure');
+ data['item-' + i + '.discount'] = item.get('discount');
+ data['item-' + i + '.note'] = item.get('note');
+ ++i;
+ });
+
+ return data;
+}
+
+
+/*
+ * This function finally submits the form data to create the new customer order.
+ */
+function submit_form() {
+ $('body').mask("Creating Order...");
+ // FIXME url
+ $.post(urls.create_orders, collect_data(), function(data) {
+ if (data.result == 'success') {
+ okay_to_leave = true;
+ // location.href = urls.orders_created; // FIXME url
+ location.href = urls.create_orders; // FIXME url
+ } else {
+ alert("Unexpected result:\n\n" + data.result.toString());
+ }
+ });
+}
+
+
+/**********************************************************************
+ * Document Load Event
+ **********************************************************************/
+
+$(function() {
+
+ /************************************************************
+ * OrderItem model
+ ************************************************************/
+
+ var OrderItem = Backbone.Model.extend({
+
+ defaults: function() {
+ return {
+ product_exists: true,
+ product_uuid: null,
+ product_brand: '',
+ // product_description: default_ambiguous_product_description,
+ product_description: '', // FIXME
+ product_size: '',
+ product_unit_of_measure: null,
+ quantity: 1,
+ unit_of_measure: null,
+ // discount: default_discount,
+ discount: 0, // FIXME
+ note: ''
+ };
+ },
+
+ validate: function(attrs, options) {
+ var value;
+
+ if (attrs.product_exists !== undefined) {
+
+ if (attrs.product_exists) {
+ if (! attrs.product_uuid) {
+ return {
+ attr: 'product_uuid',
+ error: "Product is required."
+ };
+ }
+
+ } else {
+ if (! attrs.product_description) {
+ return {
+ attr: 'product_description',
+ error: "Product description is required."
+ };
+ }
+ }
+ }
+
+ if (attrs.quantity !== undefined) {
+ value = parseInt(attrs.quantity);
+ if (isNaN(value) || value != attrs.quantity) {
+ return {
+ attr: 'quantity',
+ error: "Quantity must be numeric."
+ };
+ }
+ attrs.quantity = value;
+ }
+
+ if (attrs.discount !== undefined) {
+ if (attrs.discount) {
+ value = parseInt(attrs.discount);
+ if (isNaN(value) || value != attrs.discount || value < 0 || value > 100) {
+ return {
+ attr: 'discount',
+ error: "Discount must be a number between 0 and 100."
+ };
+ }
+ }
+ attrs.discount = value;
+ }
+ }
+
+ });
+
+
+ /*
+ * We use local storage for the OrderItem collection; this avoids
+ * cluttering the user's session (or worse, the database) on the server.
+ */
+ var OrderItemList = Backbone.Collection.extend({
+
+ model: OrderItem,
+
+ localStorage: new Backbone.LocalStorage('tailbone.new_customer_order')
+
+ });
+
+ // Create global instance of item list.
+ OrderItems = new OrderItemList();
+
+
+ /*
+ * The editable view of an OrderItem takes the form of a dialog.
+ */
+ var OrderItemDialogView = Backbone.View.extend({
+
+ el: $('#item-dialog'),
+
+ /*
+ * Cached product attributes; fetched whenever a product is selected in
+ * the autocomplete field.
+ */
+ productAttrs: {},
+
+ isVisible: function() {
+ return this.$el.is(':visible');
+ },
+
+ setModel: function(model) {
+ if (this.model) {
+ this.stopListening(this.model);
+ }
+ if (model === undefined) {
+ model = new OrderItem();
+ }
+ this.model = model;
+ this.listenTo(this.model, 'invalid', this.showValidationError);
+ },
+
+ showValidationError: function(model) {
+ var attr = model.validationError.attr;
+ if (attr == 'product_uuid') {
+ attr = 'product_uuid-textbox';
+ }
+ alert(model.validationError.error);
+ with (this.$('#' + attr)) {
+ select();
+ focus();
+ }
+ },
+
+ updateDisplay: function() {
+ var that = this;
+
+ this.$('#product_uuid-textbox').val('');
+ this.$('#upc-textbox').val('');
+
+ if (this.model.get('product_exists')) {
+ this.$('#product_exists_true').click();
+
+ if (this.model.get('product_uuid')) {
+ this.$('#product_uuid').val(this.model.get('product_uuid'));
+
+ // Mimic the pretty description returned by autocomplete.
+ var words = [];
+ if (this.model.get('product_brand')) {
+ words.push(this.model.get('product_brand'));
+ }
+ if (this.model.get('product_description')) {
+ words.push(this.model.get('product_description'));
+ }
+ if (this.model.get('product_size')) {
+ words.push(this.model.get('product_size'));
+ }
+ this.$('#product_uuid-display span:first').text(words.join(' '));
+
+ this.$('#product_uuid-textbox').hide();
+ this.$('#product_uuid-display').show();
+
+ this.$('#upc-input').hide();
+ var upc = this.model.get('product_upc');
+ upc = upc.slice(0, -1) + '-' + upc.slice(-1);
+ this.$('#upc-display span:first').text(upc);
+ this.$('#upc-display').show();
+
+ } else {
+ this.$('#product_uuid').val('');
+ this.$('#product_uuid-display span:first').text('');
+ this.$('#product_uuid-display').hide();
+ this.$('#product_uuid-textbox').show();
+ this.$('#upc-display span:first').text('');
+ this.$('#upc-display').hide();
+ this.$('#upc-input').show();
+ }
+
+ // this.$('#product_description').val(default_ambiguous_product_description);
+
+ } else {
+ this.$('#product_exists_false').click();
+
+ this.$('#product_uuid').val('');
+ this.$('#product_uuid-display span:first').text('');
+ this.$('#product_uuid-display').hide();
+ this.$('#product_uuid-textbox').show();
+
+ this.$('#upc-display span:first').text('');
+ this.$('#upc-display').hide();
+ this.$('#upc-input').show();
+
+ this.$('#product_description').val(this.model.get('product_description'));
+ }
+
+ this.$('#quantity').val(this.model.get('quantity'));
+ this.$('#discount').val(this.model.get('discount'));
+ this.$('#note').val(this.model.get('note'));
+
+ var updateMoreStuff = function() {
+ that.updateUOMChoices();
+
+ // Unit of measure selection must happen after we're certain
+ // the available choices are accurate.
+ that.$('#unit_of_measure').val(that.model.get('unit_of_measure'));
+
+ that.updateDiscountField();
+ that.updatePriceFields();
+ }
+
+ if (this.model.get('product_exists') && this.model.get('product_uuid')) {
+ this.fetchProductAttrs(this.model.get('product_uuid'), updateMoreStuff);
+ } else {
+ updateMoreStuff();
+ }
+ },
+
+ updateModel: function() {
+ var attrs = {};
+
+ if (this.$('#product_exists_true').is(':checked')) {
+ attrs.product_exists = true;
+ attrs.product_uuid = this.$('#product_uuid').val();
+ if (attrs.product_uuid) {
+ _.extend(attrs, this.productAttrs);
+ }
+ } else {
+ attrs.product_exists = false;
+ attrs.product_uuid = null;
+ attrs.product_brand = '';
+ attrs.product_description = $.trim(this.$('#product_description').val());
+ attrs.product_size = '';
+ }
+
+ attrs.quantity = this.$('#quantity').val();
+ attrs.unit_of_measure = this.$('#unit_of_measure').val();
+ attrs.discount = this.$('#discount').val();
+
+ // Set core attributes requiring validation first.
+ if (! this.model.set(attrs, {validate: true})) {
+ return false;
+ }
+
+ this.model.set({
+ quantity_display: attrs.quantity + " " + this.$('#unit_of_measure option:selected').text(),
+ price_display: this.$('#price_after_discount').val(),
+ note: this.$('#note').val()
+ });
+
+ return true;
+ },
+
+ showDialog: function(model) {
+ var that = this;
+
+ this.setModel(model);
+ this.updateDisplay();
+
+ options = {
+ title: this.model.isNew() ? "Add Item" : "Edit Item",
+ width: 700,
+ height: 500,
+ buttons: [
+ {
+ text: "OK",
+ click: function() {
+ if (that.updateModel()) {
+ if (that.model.isNew()) {
+ OrderItems.create(that.model);
+ }
+ that.$el.dialog('close');
+ }
+ }
+ },
+ {
+ text: "Cancel",
+ click: function() {
+ that.$el.dialog('close');
+ }
+ }
+ ]
+ };
+
+ this.$el.dialog(options);
+ this.$('#item-tabs').tabs('option', 'active', 0);
+ this.setFocus();
+ },
+
+ setFocus: function(panel) {
+ if (panel == undefined) {
+ panel = this.$('#item-product-tab');
+ if (! panel.is(':visible')) {
+ panel = this.$('#item-quantity-tab');
+ if (! panel.is(':visible')) {
+ panel = this.$('#item-note-tab');
+ }
+ }
+ }
+
+ if (panel.attr('id') == 'item-product-tab') {
+ if (this.$('#product_exists_true').is(':checked')) {
+ if (! this.$('#product_uuid').val()) {
+ with (this.$('#product_uuid-textbox')) {
+ select();
+ focus();
+ }
+ }
+ } else {
+ with (this.$('#product_description')) {
+ select();
+ focus();
+ }
+ }
+
+ } else if (panel.attr('id') == 'item-quantity-tab') {
+ with (this.$('#quantity')) {
+ select();
+ focus();
+ }
+ } else { // item-note-tab
+ with (this.$('#note')) {
+ select();
+ focus();
+ }
+ }
+ },
+
+ fetchProductAttrs: function(uuid, success) {
+ var that = this;
+ // FIXME url
+ $.get(urls.product_info, {uuid: uuid}, function(data) {
+ that.productAttrs = data;
+ if (success !== undefined) {
+ success(data);
+ }
+ });
+ },
+
+ productSelected: function(uuid) {
+ var that = this;
+ this.fetchProductAttrs(uuid, function(data) {
+ var upc = data.product_upc;
+ upc = upc.slice(0, -1) + '-' + upc.slice(-1);
+ that.$('#upc-display span:first').text(upc);
+ that.$('#upc-input').hide();
+ that.$('#upc-display').show();
+ that.updateUOMChoices();
+ that.updateDiscountField();
+ that.updatePriceFields();
+ });
+ },
+
+ productCleared: function() {
+ this.$('#upc-textbox').val('');
+ this.$('#upc-display').hide();
+ this.$('#upc-input').show();
+ },
+
+ updateUOMChoices: function(product_exists, product_uuid) {
+ var uom = this.$('#unit_of_measure');
+ uom.empty();
+
+ if (product_exists === undefined) {
+ product_exists = this.$('#product_exists_true').is(':checked');
+ }
+ if (product_exists && product_uuid === undefined) {
+ product_uuid = this.$('#product_uuid').val();
+ }
+
+ var uoms = null;
+ if (! product_exists) {
+ uoms = default_units_of_measure;
+ } else if (product_uuid) {
+ uoms = this.productAttrs.units_of_measure;
+ }
+
+ if (uoms) {
+ _.each(uoms, function(pair) {
+ uom.append($('', {
+ value: pair[0],
+ text: pair[1]
+ }));
+ });
+ }
+ },
+
+ updateDiscountField: function() {
+ if (this.$('#product_exists_true').is(':checked') && this.$('#product_uuid').val()) {
+ this.$('#discount').val(this.productAttrs.default_discount.toString());
+ } else {
+ this.$('#discount').val(default_discount.toString());
+ }
+ },
+
+ updatePriceFields: function() {
+ var product_exists = this.$('#product_exists_true').is(':checked');
+ var product_uuid = this.$('#product_uuid').val();
+ var unit_of_measure = this.$('#unit_of_measure').val();
+
+ this.$('#price_per_uom').val('');
+ this.$('#price_before_discount').val('');
+ this.$('#price_after_discount').val('');
+
+ var price = null;
+ if (product_exists && product_uuid) {
+ price = this.productAttrs.prices[unit_of_measure];
+ }
+
+ if (price) {
+ this.$('#price_per_uom').val('$ ' + price);
+
+ var quantity = this.$('#quantity').val();
+ if (! this.model.validate({quantity: quantity})) {
+ var total = (price * parseInt(quantity)).toFixed(2);
+ this.$('#price_before_discount').val('$ ' + total);
+
+ var discount = this.$('#discount').val();
+ if (! this.model.validate({discount: discount})) {
+ total = total * (100 - parseInt(discount)) / 100;
+
+ // This prevents rounding, e.g. 20.196 becomes 20.19
+ // instead of 20.20 (see #1475).
+ total = Math.floor(total * 100) / 100;
+
+ total = total.toFixed(2);
+ this.$('#price_after_discount').val('$ ' + total);
+ }
+ }
+ }
+ }
+
+ });
+
+ ItemDialog = new OrderItemDialogView();
+
+
+ /*
+ * The readonly view for an OrderItem is a table row, containing cells
+ * with edit and delete icons.
+ */
+ var OrderItemView = Backbone.View.extend({
+
+ tagName: 'tr',
+
+ template: _.template($('#product-template').html()),
+
+ events: {
+ 'click td.edit': 'editProduct',
+ 'click td.delete': 'promptToDeleteProduct'
+ },
+
+ initialize: function() {
+ this.listenTo(this.model, 'change', this.render);
+ this.listenTo(this.model, 'destroy', this.remove);
+ },
+
+ render: function() {
+ this.$el.html(this.template(this.model.toJSON()));
+ return this;
+ },
+
+ editProduct: function() {
+ if (! ItemDialog.isVisible()) {
+ ItemDialog.showDialog(this.model);
+ }
+ },
+
+ promptToDeleteProduct: function() {
+ if (! ItemDialog.isVisible()) {
+ if (confirm("Are you sure you wish to remove this product from the order?")) {
+ this.model.destroy();
+ }
+ }
+ }
+
+ });
+
+
+ /*
+ * The main Backbone application. This coordinates things which can't be
+ * handled at the model level.
+ */
+ var AppView = Backbone.View.extend({
+
+ el: $('#order-tabs'),
+
+ initialize: function() {
+ this.items_tbody = this.$('#order-items-tab #items tbody');
+ // this.dialog = $('#item-dialog');
+ this.listenTo(OrderItems, 'add', this.addRow);
+ this.listenTo(OrderItems, 'remove', this.fixRowHighlights);
+ },
+
+ setMainFocus: function(panel) {
+ if (panel == undefined) {
+ panel = $('#order-customer-tab');
+ if (! panel.is(':visible')) {
+ panel = $('#order-items-tab');
+ }
+ if (! panel.is(':visible')) {
+ panel = $('#order-note-tab');
+ }
+ }
+
+ if (panel.attr('id') == 'order-customer-tab') {
+ if ($('#customer_exists_true').is(':checked')) {
+ if (! $('#customer_uuid').val()) {
+ with ($('#customer_uuid-textbox')) {
+ select();
+ focus();
+ }
+ } else {
+ with ($('#customer_phone_known-textbox')) {
+ select();
+ focus();
+ }
+ }
+ } else {
+ with ($('#customer_name')) {
+ select();
+ focus();
+ }
+ }
+
+ } else if (panel.attr('id') == 'order-note-tab') {
+ $('#order-note-text').focus();
+ }
+ },
+
+ addRow: function(product) {
+ var view = new OrderItemView({model: product});
+ var tr = $(view.render().el);
+ tr.addClass((this.items_tbody.find('tr').length % 2) ? 'even' : 'odd');
+ this.items_tbody.append(tr);
+ },
+
+ fixRowHighlights: function() {
+ var odd = true;
+ this.items_tbody.find('tr').each(function() {
+ var wanted = odd ? 'odd' : 'even';
+ var unwanted = odd ? 'even' : 'odd';
+ if ($(this).hasClass(unwanted)) {
+ $(this).removeClass(unwanted);
+ }
+ $(this).addClass(wanted);
+ odd = ! odd;
+ });
+ }
+
+ });
+
+ App = new AppView();
+
+
+ /*
+ * Set up the order tabs within the main screen.
+ */
+ $('#order-tabs').tabs({
+ activate: function(event, ui) {
+ App.setMainFocus(ui.newPanel);
+ }
+ });
+
+ /*
+ * When user edits the phone number after having selected an existing
+ * customer account, show the "please update" checkbox.
+ */
+ $('#customer_phone_known-textbox').keypress(function(event) {
+ if (customer_phone_stored !== null && event.which != 0) {
+ $('div.field-wrapper.update-phone').show();
+ }
+ });
+
+ /*
+ * When user changes the option indicating whether the customer is already
+ * in the system, toggle visibility of appropriate sections and set focus.
+ */
+ $('input[name="customer_exists"]').click(function() {
+ if ($(this).val() == 'true') {
+ $('#customer-info-unknown').hide();
+ $('#customer-info-known').show();
+ } else {
+ $('#customer-info-known').hide();
+ $('#customer-info-unknown').show();
+ }
+ App.setMainFocus();
+ });
+
+
+ /*
+ * Set up the tabs within the item dialog.
+ */
+ $('#item-dialog #item-tabs').tabs({
+ activate: function(event, ui) {
+ ItemDialog.setFocus(ui.newPanel);
+ }
+ });
+
+
+ /*
+ * When user clicks the Add Product button, show dialog for new product.
+ */
+ $('#add-item').click(function() {
+ if (! ItemDialog.isVisible()) {
+ ItemDialog.showDialog();
+ }
+ });
+
+
+ /*
+ * When user changes the option indicating whether the product is already
+ * in the system, toggle visibility of appropriate sections and set focus.
+ */
+ $('#item-dialog input[name="product_exists"]').click(function() {
+ if ($(this).val() == 'true') {
+ $('#product-info-unknown').hide();
+ $('#product-info-known').show();
+ } else {
+ $('#product-info-known').hide();
+ $('#product-info-unknown').show();
+ }
+ ItemDialog.updateUOMChoices();
+ ItemDialog.updateDiscountField();
+ ItemDialog.updatePriceFields();
+ ItemDialog.setFocus();
+ });
+
+
+ /*
+ * When user clicks Change button for product autocomplete, remove the UOM
+ * choices since the new state is "product supposedly exists, but none has
+ * been selected." Also reset the UPC search.
+ */
+ $('#item-dialog #product_uuid-change').click(function() {
+ $('#item-dialog #upc').val('');
+ ItemDialog.updateUOMChoices(true, null);
+ ItemDialog.updateDiscountField();
+ ItemDialog.updatePriceFields();
+ });
+
+
+ /*
+ * When user clicks UPC Fetch button, locate product via AJAX and then load
+ * product data as if from autocomplete.
+ */
+ $('#item-dialog #upc-fetch').click(function() {
+ var upc = $.trim($('#item-dialog #upc-textbox').val());
+ if (! upc) {
+ alert("You must provide a UPC to fetch.");
+ with ($('#item-dialog #upc-textbox')) {
+ select();
+ focus();
+ }
+ return;
+ }
+ $('#item-dialog #product-info-known').mask("Fetching...");
+ // FIXME url
+ $.get(urls.product_search, {upc: upc}, function(data) {
+ $('#item-dialog #product-info-known').unmask();
+ if (data.product) {
+ $('#item-dialog #product_uuid').val(data.product.uuid);
+ $('#item-dialog #product_uuid-display span:first').text(data.product.full_description);
+ $('#item-dialog #product_uuid-textbox').hide();
+ $('#item-dialog #product_uuid-display').show();
+ ItemDialog.productSelected(data.product.uuid);
+ } else {
+ alert("No product found with that UPC!");
+ with ($('#item-dialog #upc-textbox')) {
+ select();
+ focus();
+ }
+ }
+ });
+ });
+
+
+ /*
+ * When user presses Enter while focused on the product UPC textbox,
+ * simulate click of the Fetch button.
+ */
+ $('#item-dialog #upc-textbox').keydown(function(event) {
+ if (event.which == 13) {
+ $('#item-dialog #upc-fetch').click();
+ }
+ });
+
+
+ /*
+ * When user clicks Change button for product UPC, simulate clicking the
+ * autocomplete Change button since that already does everything we want in
+ * this situation. But then set focus to the UPC field.
+ */
+ $('#item-dialog #upc-change').click(function() {
+ $('#item-dialog #product_uuid-change').click();
+ $('#item-dialog #upc-textbox').focus();
+ });
+
+
+ /*
+ * When user changes the various fields which affect pricing, update the
+ * displayed prices.
+ */
+ $('#item-dialog #quantity').change(function() {
+ ItemDialog.updatePriceFields();
+ });
+
+ $('#item-dialog #unit_of_measure').change(function() {
+ ItemDialog.updatePriceFields();
+ });
+
+ $('#item-dialog #discount').change(function() {
+ ItemDialog.updatePriceFields();
+ });
+
+
+ /*
+ * When user clicks the final submit button, validate things and then
+ * prompt one last time just to be sure.
+ */
+ $('form[name="new_order"]').submit(function() {
+ if (ItemDialog.isVisible()) {
+ return false;
+ }
+
+ // if ($('#customer_exists_true').is(':checked')) {
+ // if (! $('#customer_uuid').val()) {
+ // alert("Customer is required.");
+ // $('#order-tabs').tabs('option', 'active', 0);
+ // with ($('#customer_uuid-textbox')) {
+ // select();
+ // focus();
+ // }
+ // return false;
+ // }
+ // if (! $('#customer_phone_known-textbox').val()) {
+ // alert("Customer phone number is required.");
+ // $('#order-tabs').tabs('option', 'active', 0);
+ // with ($('#customer_phone_known-textbox')) {
+ // select();
+ // focus();
+ // }
+ // return false;
+ // }
+ // if (! validate_phone_number($('#customer_phone_known-textbox').val())) {
+ // alert("Customer phone must contain exactly 7 or 10 numeric digits.");
+ // $('#order-tabs').tabs('option', 'active', 0);
+ // with ($('#customer_phone_known-textbox')) {
+ // select();
+ // focus();
+ // }
+ // return false;
+ // }
+ // } else {
+ // if (! $('#customer_name').val()) {
+ // alert("Customer name is required.");
+ // with ($('#customer_name')) {
+ // select();
+ // focus();
+ // }
+ // return false;
+ // }
+ // if (! $('#customer_phone_unknown').val()) {
+ // alert("Customer phone number is required.");
+ // with ($('#customer_phone_unknown')) {
+ // select();
+ // focus();
+ // }
+ // return false;
+ // }
+ // }
+
+ // if (! OrderProducts.length) {
+ // alert("At least one product is required.");
+ // $('#order-tabs').tabs('option', 'active', 1);
+ // ItemDialog.showDialog();
+ // return false;
+ // }
+
+ if (! confirm("Are you sure you're ready to create the order?")) {
+ return false;
+ }
+
+ // if (current_user) {
+ // submit_form();
+ // } else {
+ // authenticate_employee();
+ // }
+
+ return false;
+ });
+
+});
diff --git a/tailbone/static/js/lib/backbone-1.0.0.min.js b/tailbone/static/js/lib/backbone-1.0.0.min.js
new file mode 100644
index 00000000..3541019c
--- /dev/null
+++ b/tailbone/static/js/lib/backbone-1.0.0.min.js
@@ -0,0 +1,4 @@
+(function(){var t=this;var e=t.Backbone;var i=[];var r=i.push;var s=i.slice;var n=i.splice;var a;if(typeof exports!=="undefined"){a=exports}else{a=t.Backbone={}}a.VERSION="1.0.0";var h=t._;if(!h&&typeof require!=="undefined")h=require("underscore");a.$=t.jQuery||t.Zepto||t.ender||t.$;a.noConflict=function(){t.Backbone=e;return this};a.emulateHTTP=false;a.emulateJSON=false;var o=a.Events={on:function(t,e,i){if(!l(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,i){if(!l(this,"once",t,[e,i])||!e)return this;var r=this;var s=h.once(function(){r.off(t,s);e.apply(this,arguments)});s._callback=e;return this.on(t,s,i)},off:function(t,e,i){var r,s,n,a,o,u,c,f;if(!this._events||!l(this,"off",t,[e,i]))return this;if(!t&&!e&&!i){this._events={};return this}a=t?[t]:h.keys(this._events);for(o=0,u=a.length;o").attr(t);this.setElement(e,false)}else{this.setElement(h.result(this,"el"),false)}}});a.sync=function(t,e,i){var r=k[t];h.defaults(i||(i={}),{emulateHTTP:a.emulateHTTP,emulateJSON:a.emulateJSON});var s={type:r,dataType:"json"};if(!i.url){s.url=h.result(e,"url")||U()}if(i.data==null&&e&&(t==="create"||t==="update"||t==="patch")){s.contentType="application/json";s.data=JSON.stringify(i.attrs||e.toJSON(i))}if(i.emulateJSON){s.contentType="application/x-www-form-urlencoded";s.data=s.data?{model:s.data}:{}}if(i.emulateHTTP&&(r==="PUT"||r==="DELETE"||r==="PATCH")){s.type="POST";if(i.emulateJSON)s.data._method=r;var n=i.beforeSend;i.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",r);if(n)return n.apply(this,arguments)}}if(s.type!=="GET"&&!i.emulateJSON){s.processData=false}if(s.type==="PATCH"&&window.ActiveXObject&&!(window.external&&window.external.msActiveXFilteringEnabled)){s.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var o=i.xhr=a.ajax(h.extend(s,i));e.trigger("request",e,o,i);return o};var k={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};a.ajax=function(){return a.$.ajax.apply(a.$,arguments)};var S=a.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var $=/\((.*?)\)/g;var T=/(\(\?)?:\w+/g;var H=/\*\w+/g;var A=/[\-{}\[\]+?.,\\\^$|#\s]/g;h.extend(S.prototype,o,{initialize:function(){},route:function(t,e,i){if(!h.isRegExp(t))t=this._routeToRegExp(t);if(h.isFunction(e)){i=e;e=""}if(!i)i=this[e];var r=this;a.history.route(t,function(s){var n=r._extractParameters(t,s);i&&i.apply(r,n);r.trigger.apply(r,["route:"+e].concat(n));r.trigger("route",e,n);a.history.trigger("route",r,e,n)});return this},navigate:function(t,e){a.history.navigate(t,e);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=h.result(this,"routes");var t,e=h.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(A,"\\$&").replace($,"(?:$1)?").replace(T,function(t,e){return e?t:"([^/]+)"}).replace(H,"(.*?)");return new RegExp("^"+t+"$")},_extractParameters:function(t,e){var i=t.exec(e).slice(1);return h.map(i,function(t){return t?decodeURIComponent(t):null})}});var I=a.History=function(){this.handlers=[];h.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var N=/^[#\/]|\s+$/g;var P=/^\/+|\/+$/g;var O=/msie [\w.]+/;var C=/\/$/;I.started=false;h.extend(I.prototype,o,{interval:50,getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=this.location.pathname;var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.substr(i.length)}else{t=this.getHash()}}return t.replace(N,"")},start:function(t){if(I.started)throw new Error("Backbone.history has already been started");I.started=true;this.options=h.extend({},{root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var e=this.getFragment();var i=document.documentMode;var r=O.exec(navigator.userAgent.toLowerCase())&&(!i||i<=7);this.root=("/"+this.root+"/").replace(P,"/");if(r&&this._wantsHashChange){this.iframe=a.$('').hide().appendTo("body")[0].contentWindow;this.navigate(e)}if(this._hasPushState){a.$(window).on("popstate",this.checkUrl)}else if(this._wantsHashChange&&"onhashchange"in window&&!r){a.$(window).on("hashchange",this.checkUrl)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}this.fragment=e;var s=this.location;var n=s.pathname.replace(/[^\/]$/,"$&/")===this.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!n){this.fragment=this.getFragment(null,true);this.location.replace(this.root+this.location.search+"#"+this.fragment);return true}else if(this._wantsPushState&&this._hasPushState&&n&&s.hash){this.fragment=this.getHash().replace(N,"");this.history.replaceState({},document.title,this.root+this.fragment+s.search)}if(!this.options.silent)return this.loadUrl()},stop:function(){a.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);I.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getFragment(this.getHash(this.iframe))}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(t){var e=this.fragment=this.getFragment(t);var i=h.any(this.handlers,function(t){if(t.route.test(e)){t.callback(e);return true}});return i},navigate:function(t,e){if(!I.started)return false;if(!e||e===true)e={trigger:e};t=this.getFragment(t||"");if(this.fragment===t)return;this.fragment=t;var i=this.root+t;if(this._hasPushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,i)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getFragment(this.getHash(this.iframe))){if(!e.replace)this.iframe.document.open().close();this._updateHash(this.iframe.location,t,e.replace)}}else{return this.location.assign(i)}if(e.trigger)this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});a.history=new I;var j=function(t,e){var i=this;var r;if(t&&h.has(t,"constructor")){r=t.constructor}else{r=function(){return i.apply(this,arguments)}}h.extend(r,i,e);var s=function(){this.constructor=r};s.prototype=i.prototype;r.prototype=new s;if(t)h.extend(r.prototype,t);r.__super__=i.prototype;return r};d.extend=g.extend=S.extend=b.extend=I.extend=j;var U=function(){throw new Error('A "url" property or function must be specified')};var R=function(t,e){var i=e.error;e.error=function(r){if(i)i(t,r,e);t.trigger("error",t,r,e)}}}).call(this);
+/*
+//@ sourceMappingURL=backbone-min.map
+*/
\ No newline at end of file
diff --git a/tailbone/static/js/lib/backbone.localStorage-1.1.0.min.js b/tailbone/static/js/lib/backbone.localStorage-1.1.0.min.js
new file mode 100644
index 00000000..467030f3
--- /dev/null
+++ b/tailbone/static/js/lib/backbone.localStorage-1.1.0.min.js
@@ -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});
\ No newline at end of file
diff --git a/tailbone/static/js/lib/underscore-1.4.4.min.js b/tailbone/static/js/lib/underscore-1.4.4.min.js
new file mode 100644
index 00000000..c1d9d3ae
--- /dev/null
+++ b/tailbone/static/js/lib/underscore-1.4.4.min.js
@@ -0,0 +1 @@
+(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.4";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2),e=w.isFunction(t);return w.map(n,function(n){return(e?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t,r){return w.isEmpty(t)?r?null:[]:w[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.findWhere=function(n,t){return w.where(n,t,!0)},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var k=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=k(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i},w.bind=function(n,t){if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));var r=o.call(arguments,2);return function(){return n.apply(t,r.concat(o.call(arguments)))}},w.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},w.bindAll=function(n){var t=o.call(arguments,1);return 0===t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var I=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=I(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&I(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return I(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),"function"!=typeof/./&&(w.isFunction=function(n){return"function"==typeof n}),w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return n===void 0},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var M={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};M.unescape=w.invert(M.escape);var S={escape:RegExp("["+w.keys(M.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(M.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(S[n],function(t){return M[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=++N+"";return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,q={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},B=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){var e;r=w.defaults({},r,w.templateSettings);var u=RegExp([(r.escape||T).source,(r.interpolate||T).source,(r.evaluate||T).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(B,function(n){return"\\"+q[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,w);var c=function(n){return e.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},w.chain=function(n){return w(n).chain()};var D=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);
\ No newline at end of file
diff --git a/tailbone/templates/custorders/config.mako b/tailbone/templates/custorders/config.mako
new file mode 100644
index 00000000..ddd14f2c
--- /dev/null
+++ b/tailbone/templates/custorders/config.mako
@@ -0,0 +1,41 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base.mako" />
+
+<%def name="title()">Customer Orders Configuration%def>
+
+
diff --git a/tailbone/templates/custorders/index.mako b/tailbone/templates/custorders/index.mako
new file mode 100644
index 00000000..e84f9427
--- /dev/null
+++ b/tailbone/templates/custorders/index.mako
@@ -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'):
+ ${h.link_to("Create a new Customer Order", url('custorders.new'))}
+ % endif
+%def>
+
+${parent.body()}
diff --git a/tailbone/templates/custorders/new.itemdialog.mako b/tailbone/templates/custorders/new.itemdialog.mako
new file mode 100644
index 00000000..1b76503c
--- /dev/null
+++ b/tailbone/templates/custorders/new.itemdialog.mako
@@ -0,0 +1,107 @@
+## -*- coding: utf-8 -*-
+<%namespace file="/autocomplete.mako" import="autocomplete" />
+
+<%def name="item_dialog()">
+
+
+
+
+
+
+
+
+ ${h.radio('product_exists', 'true', label="Product is already in the system.")}
+
+
+
+
+
+
+
${autocomplete('product_uuid', url('products.autocomplete'), selected='ItemDialog.productSelected', cleared='ItemDialog.productCleared')}
+
+
+
+
+
+
+ ${h.text('upc-textbox')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${h.radio('product_exists', 'false', label="Product cannot be located in the system.")}
+
+
+
+
+
+
${h.textarea('product_description')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
${h.text('quantity')}
+
+
+
+
+
${h.select('unit_of_measure', None, [])}
+
+
+
+
+
${h.text('discount')}
+
+
+
+
+
${h.text('price_per_uom', disabled=True)}
+
+
+
+
+
${h.text('price_before_discount', disabled=True)}
+
+
+
+
+
${h.text('price_after_discount', disabled=True)}
+
+
+
+
+
+
+
This note will apply only to this particular item.
+
Please see the main screen to attach a note to the order as a whole.
+ ${h.textarea('note')}
+
+
+
+
+
+%def>
diff --git a/tailbone/templates/custorders/new.mako b/tailbone/templates/custorders/new.mako
new file mode 100644
index 00000000..0e61db34
--- /dev/null
+++ b/tailbone/templates/custorders/new.mako
@@ -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'))}
+
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/custorders.new.css'))}
+%def>
+
+${form.begin(name='new_order')}
+
+
+
+
+
+
+
+
+ ${form.radio('customer_exists', value='true', label="Customer is already in the system.", checked=True)}
+
+
+
+
+
+
${autocomplete('customer_uuid', url('customers.autocomplete'), selected='customer_selected', cleared='clear_customer')}
+
+
+
+
+
${autocomplete('customer_phone_known', url('customers.autocomplete.phone'), select='customer_phone_selected', options={'minLength': 4})}
+
+
+
+ ${h.checkbox('update-phone', label="Please update this customer's phone number in the system.", checked=False)}
+
+
+
+
+
+
+
+
+
+ ${form.radio('customer_exists', value='false', label="Customer cannot be located in the system.", checked=False)}
+
+
+
+
+
+
${form.text('customer_name')}
+
+
+
+
+
${form.text('customer_phone_unknown')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Brand |
+ Description |
+ Size |
+ Quantity |
+ Price |
+ |
+ |
+
+
+
+
+ No items have been added yet. |
+
+
+
+
+
+
+
+
+
+
+
This note will apply to the order as a whole.
+
Please edit individual items to attach item-specific notes.
+ ${h.textarea('order-note-text')}
+
+
+
+
+
+ ${form.submit('submit', "Create Order")}
+
+
+${form.end()}
+
+${item_dialog()}
+
+
diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako
index fcddff05..19a68eb6 100644
--- a/tailbone/templates/master/view.mako
+++ b/tailbone/templates/master/view.mako
@@ -16,6 +16,12 @@
.newgrid-wrapper {
margin-top: 10px;
}
+ .rows-title {
+ margin-bottom: 0;
+ }
+ .rows-grid {
+ margin-top: 0;
+ }
% endif
%def>
@@ -43,5 +49,8 @@
% if master.has_rows:
+ % if rows_title:
+ ${rows_title}
+ % endif
${rows_grid|n}
% endif
diff --git a/tailbone/templates/newgrids/complete.mako b/tailbone/templates/newgrids/complete.mako
index 86e9ee90..1cd2a911 100644
--- a/tailbone/templates/newgrids/complete.mako
+++ b/tailbone/templates/newgrids/complete.mako
@@ -1,5 +1,5 @@
## -*- coding: utf-8 -*-
-