[MERGE] point_of_sale js part manual merge from ~openerp-dev/openerp-web/trunk-pos
bzr revid: al@openerp.com-20110927153203-22mt6akllb4xvk25
|
@ -0,0 +1,33 @@
|
|||
// Backbone.js 0.5.3
|
||||
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Backbone may be freely distributed under the MIT license.
|
||||
// For all details and documentation:
|
||||
// http://documentcloud.github.com/backbone
|
||||
(function(){var h=this,p=h.Backbone,e;e=typeof exports!=="undefined"?exports:h.Backbone={};e.VERSION="0.5.3";var f=h._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var g=h.jQuery||h.Zepto;e.noConflict=function(){h.Backbone=p;return this};e.emulateHTTP=!1;e.emulateJSON=!1;e.Events={bind:function(a,b,c){var d=this._callbacks||(this._callbacks={});(d[a]||(d[a]=[])).push([b,c]);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=
|
||||
0,e=c.length;d<e;d++)if(c[d]&&b===c[d][0]){c[d]=null;break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,e,f=2;if(!(c=this._callbacks))return this;for(;f--;)if(b=f?a:"all",b=c[b])for(var g=0,h=b.length;g<h;g++)(d=b[g])?(e=f?Array.prototype.slice.call(arguments,1):arguments,d[0].apply(d[1]||this,e)):(b.splice(g,1),g--,h--);return this}};e.Model=function(a,b){var c;a||(a={});if(c=this.defaults)f.isFunction(c)&&(c=c.call(this)),a=f.extend({},c,a);this.attributes={};
|
||||
this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:!0});this._changed=!1;this._previousAttributes=f.clone(this.attributes);if(b&&b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:!1,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];
|
||||
return this._escapedAttributes[a]=(b==null?"":""+b).replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},has:function(a){return this.attributes[a]!=null},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute];
|
||||
var e=this._changing;this._changing=!0;for(var g in a){var h=a[g];if(!f.isEqual(c[g],h))c[g]=h,delete d[g],this._changed=!0,b.silent||this.trigger("change:"+g,this,h,b)}!e&&!b.silent&&this._changed&&this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&&delete this.id;this._changed=
|
||||
!0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,d={};for(b in c)d[b]=void 0;if(!a.silent&&this.validate&&!this._performValidation(d,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&
|
||||
c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return!1;var c=this,d=b.success;b.success=function(a,e,f){if(!c.set(c.parse(a,f),b))return!1;d&&d(c,a,f)};b.error=i(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||e.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(d){b.trigger("destroy",
|
||||
b,b.collection,a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"delete",this,a)},url:function(){var a=k(this.collection)||this.urlRoot||l();if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return this.id==null},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){if(a)return this._previousAttributes[a]!=
|
||||
this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,d;for(d in a)f.isEqual(b[d],a[d])||(c=c||{},c[d]=a[d]);return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c)return b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1;return!0}});
|
||||
e.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&&this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,b){if(f.isArray(a))for(var c=
|
||||
0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){if(a==null)return null;return this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},
|
||||
reset:function(a,b){a||(a=[]);b||(b={});this.each(this._removeReference);this._reset();this.add(a,{silent:!0});b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,f,e){b[a.add?"add":"reset"](b.parse(d,e),a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},create:function(a,b){var c=this;b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var d=b.success;b.success=function(a,e,f){c.add(a,b);
|
||||
d&&d(a,e,f)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_prepareModel:function(a,b){if(a instanceof e.Model){if(!a.collection)a.collection=this}else{var c=a;a=new this.model(c,{collection:this});a.validate&&!a._performValidation(c,b)&&(a=!1)}return a},_add:function(a,b){b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",
|
||||
c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;this.models.splice(b.at!=null?b.at:this.comparator?this.sortedIndex(a,this.comparator):this.length,0,a);a.bind("all",this._onModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);this._removeReference(a);return a},
|
||||
_removeReference:function(a){this==a.collection&&delete a.collection;a.unbind("all",this._onModelEvent)},_onModelEvent:function(a,b,c,d){(a=="add"||a=="remove")&&c!=this||(a=="destroy"&&this._remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,arguments))}});f.each(["forEach","each","map","reduce","reduceRight","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max",
|
||||
"min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty","groupBy"],function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Router=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var q=/:([\w\d]+)/g,r=/\*([\w\d]+)/g,s=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(e.Router.prototype,e.Events,{initialize:function(){},route:function(a,
|
||||
b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},navigate:function(a,b){e.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(s,"\\$&").replace(q,
|
||||
"([^/]*)").replace(r,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")};var j=/^#*/,t=/msie [\w.]+/,m=!1;f.extend(e.History.prototype,{interval:50,getFragment:function(a,b){if(a==null)if(this._hasPushState||b){a=window.location.pathname;var c=window.location.search;c&&(a+=c);a.indexOf(this.options.root)==0&&(a=a.substr(this.options.root.length))}else a=window.location.hash;return decodeURIComponent(a.replace(j,
|
||||
""))},start:function(a){if(m)throw Error("Backbone.history has already been started");this.options=f.extend({},{root:"/"},this.options,a);this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||!window.history||!window.history.pushState);a=this.getFragment();var b=document.documentMode;if(b=t.exec(navigator.userAgent.toLowerCase())&&(!b||b<=7))this.iframe=g('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);
|
||||
this._hasPushState?g(window).bind("popstate",this.checkUrl):"onhashchange"in window&&!b?g(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;m=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;else if(this._wantsPushState&&this._hasPushState&&b&&a.hash)this.fragment=a.hash.replace(j,""),window.history.replaceState({},
|
||||
document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a);
|
||||
return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(j,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var d=window.location;c.indexOf(this.options.root)!=0&&(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,d.protocol+"//"+d.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&&c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(),
|
||||
this.iframe.location.hash=c;b&&this.loadUrl(a)}}});e.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var u=/^(\S+)\s*(.*)$/,n=["model","collection","el","id","attributes","className","tagName"];f.extend(e.View.prototype,e.Events,{tagName:"div",$:function(a){return g(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){g(this.el).remove();return this},make:function(a,
|
||||
b,c){a=document.createElement(a);b&&g(a).attr(b);c&&g(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events))for(var b in f.isFunction(a)&&(a=a.call(this)),g(this.el).unbind(".delegateEvents"+this.cid),a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(u),e=d[1];d=d[2];c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?g(this.el).bind(e,c):g(this.el).delegate(d,e,c)}},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=
|
||||
0,c=n.length;b<c;b++){var d=n[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el){if(f.isString(this.el))this.el=g(this.el).get(0)}else{var a=this.attributes||{};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});e.Model.extend=e.Collection.extend=e.Router.extend=e.View.extend=function(a,b){var c=v(this,a,b);c.extend=this.extend;return c};var w={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,
|
||||
b,c){var d=w[a];c=f.extend({type:d,dataType:"json"},c);if(!c.url)c.url=k(b)||l();if(!c.data&&b&&(a=="create"||a=="update"))c.contentType="application/json",c.data=JSON.stringify(b.toJSON());if(e.emulateJSON)c.contentType="application/x-www-form-urlencoded",c.data=c.data?{model:c.data}:{};if(e.emulateHTTP&&(d==="PUT"||d==="DELETE")){if(e.emulateJSON)c.data._method=d;c.type="POST";c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)}}if(c.type!=="GET"&&!e.emulateJSON)c.processData=
|
||||
!1;return g.ajax(c)};var o=function(){},v=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};f.extend(d,a);o.prototype=a.prototype;d.prototype=new o;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){if(!a||!a.url)return null;return f.isFunction(a.url)?a.url():a.url},l=function(){throw Error('A "url" property or function must be specified');},i=function(a,b,c){return function(d){a?
|
||||
a(b,d,c):b.trigger("error",b,d,c)}}}).call(this);
|
|
@ -0,0 +1,11 @@
|
|||
{spawn, exec} = require 'child_process'
|
||||
|
||||
task 'watch', 'Watch source files and build JS & CSS', (options) ->
|
||||
runCommand = (name, args...) ->
|
||||
proc = spawn name, args
|
||||
proc.stderr.on 'data', (buffer) -> console.log buffer.toString()
|
||||
proc.stdout.on 'data', (buffer) -> console.log buffer.toString()
|
||||
proc.on 'exit', (status) -> process.exit(1) if status isnt 0
|
||||
runCommand 'coffee', '-o', 'js', '-wc', 'js'
|
||||
runCommand 'sass', '--watch', 'src:css'
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
body, html {
|
||||
height: 100%; }
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f0eeee;
|
||||
font-family: "Lucida Grande", Helvetica, Verdana, Arial;
|
||||
color: #555555;
|
||||
font-size: 12px; }
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse; }
|
||||
|
||||
td {
|
||||
border: 1px solid #e9eaec; }
|
||||
|
||||
input {
|
||||
color: #555555; }
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #555555; }
|
||||
|
||||
button, a.button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
border: 1px solid #cacaca;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
background: #e2e2e2;
|
||||
background: -moz-linear-gradient(#f0f0f0, #e2e2e2);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#f0f0f0), to(#e2e2e2)); }
|
||||
|
||||
ul, ol {
|
||||
padding: 0;
|
||||
margin: 0; }
|
||||
|
||||
li {
|
||||
list-style-type: none; }
|
||||
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%; }
|
||||
|
||||
#topheader {
|
||||
width: 100%;
|
||||
height: 54px;
|
||||
color: gray;
|
||||
border-top: solid 1px #d3d3d3;
|
||||
border-bottom: solid 1px black;
|
||||
background: #393939;
|
||||
background: -moz-linear-gradient(#7b7979, #393939);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#7b7979), to(#393939)); }
|
||||
#topheader button {
|
||||
color: black;
|
||||
border: 1px solid black;
|
||||
background: #7f82ac;
|
||||
background: -moz-linear-gradient(#b2b3d7, #7f82ac);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac)); }
|
||||
|
||||
#branding, #steps, #rightheader {
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
height: 35px;
|
||||
padding: 10px; }
|
||||
|
||||
#branding {
|
||||
border-right: 1px solid #373737;
|
||||
text-align: center; }
|
||||
#branding img {
|
||||
height: 32px;
|
||||
width: 116px; }
|
||||
|
||||
#steps {
|
||||
padding: 10px 17px;
|
||||
border-right: solid 1px #3b3b3b;
|
||||
vertical-align: top; }
|
||||
#steps label {
|
||||
width: 80px;
|
||||
background-color: "";
|
||||
background-image: url("../img/steps-bg.png");
|
||||
border-bottom: solid 1px #5c5c5c;
|
||||
border-top: solid 1px #373737;
|
||||
vertical-align: top; }
|
||||
#steps label:first-child {
|
||||
border-left: solid 1px #373737; }
|
||||
#steps label:last-child {
|
||||
border-right: solid 1px #373737; }
|
||||
#steps span {
|
||||
padding: 2px 6px; }
|
||||
#steps img {
|
||||
height: 32px; }
|
||||
#steps .ui-button, #steps .ui-button-text-only {
|
||||
height: 30px;
|
||||
margin: 0 -4px; }
|
||||
|
||||
#neworder-button {
|
||||
width: 32px;
|
||||
padding: 1px;
|
||||
font-size: 23px; }
|
||||
|
||||
#loggedas {
|
||||
float: right;
|
||||
padding: 5px 9px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
border-left: 1px solid #373737; }
|
||||
#loggedas p {
|
||||
margin: 0 0 3px 0; }
|
||||
|
||||
#content {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
bottom: 0; }
|
||||
|
||||
#leftpane {
|
||||
height: 100%;
|
||||
width: 440px;
|
||||
position: relative;
|
||||
border-right: solid 1px #afafb6;
|
||||
background-color: white; }
|
||||
#leftpane footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #e0e0e0;
|
||||
background-image: url(../img/headerbackground.jpg); }
|
||||
|
||||
#current-order {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 271px;
|
||||
overflow: auto; }
|
||||
#current-order thead {
|
||||
background-color: #cccccc;
|
||||
background-image: url(../img/headerbackground.jpg);
|
||||
border: 0px;
|
||||
font-size: 12px;
|
||||
width: 440px; }
|
||||
#current-order thead td {
|
||||
text-align: center;
|
||||
padding: 8px 0px;
|
||||
min-width: 40px;
|
||||
font-size: 12px; }
|
||||
#current-order td {
|
||||
padding: 6px 4px;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
min-width: 40px;
|
||||
white-space: nowrap; }
|
||||
#current-order td:first-child {
|
||||
width: 320px;
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis; }
|
||||
#current-order td:last-child {
|
||||
border-right: none; }
|
||||
#current-order tr.selected {
|
||||
background-color: #e9eaf2; }
|
||||
#current-order tr.selected td {
|
||||
border-top: 2px solid #d5d6e0;
|
||||
border-bottom: 1px solid #d5d6e0;
|
||||
padding-top: 5px;
|
||||
color: #555555; }
|
||||
|
||||
#amounts {
|
||||
background: white;
|
||||
border-bottom: solid 1px #d2d2d2;
|
||||
border-top: solid 1px #e9eaec;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
-webkit-margin-before: 0;
|
||||
-webkit-margin-after: 0; }
|
||||
#amounts li {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
width: 29%; }
|
||||
|
||||
#paypad {
|
||||
padding: 9px;
|
||||
float: left;
|
||||
text-align: center; }
|
||||
#paypad button {
|
||||
height: 54px;
|
||||
width: 208px;
|
||||
margin: 0 -3px;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
color: #555555;
|
||||
border-top: 1px solid #efefef; }
|
||||
#paypad button:hover {
|
||||
color: white;
|
||||
background: #7f82ac;
|
||||
background: -moz-linear-gradient(#9d9fc5, #7f82ac);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac)); }
|
||||
|
||||
#numpad {
|
||||
padding: 9px;
|
||||
float: right;
|
||||
text-align: center; }
|
||||
#numpad button {
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
margin: 0 -3px;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
color: #555555;
|
||||
border-top: 1px solid #efefef; }
|
||||
#numpad button:hover {
|
||||
color: white;
|
||||
background: #7f82ac;
|
||||
background: -moz-linear-gradient(#9d9fc5, #7f82ac);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac)); }
|
||||
#numpad .selected-mode {
|
||||
color: white;
|
||||
background: #7f82ac;
|
||||
background: -moz-linear-gradient(#9d9fc5, #7f82ac);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac)); }
|
||||
|
||||
.payment-button {
|
||||
font-size: 14px; }
|
||||
|
||||
.input-button {
|
||||
font-size: 24px; }
|
||||
|
||||
.mode-button, #numpad-delete, #numpad-minus {
|
||||
font-size: 14px; }
|
||||
|
||||
#rightpane {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 441px;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
vertical-align: top; }
|
||||
#rightpane header {
|
||||
padding: 0;
|
||||
height: 32px;
|
||||
border-bottom: 1px solid #cecbcb;
|
||||
background: #d3d3d3;
|
||||
background: -moz-linear-gradient(white, #d3d3d3);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(white), to(#d3d3d3)); }
|
||||
|
||||
.product-list {
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
bottom: 0; }
|
||||
|
||||
.breadcrumb li {
|
||||
float: left;
|
||||
line-height: 32px;
|
||||
height: 32px; }
|
||||
.breadcrumb li:last-child {
|
||||
padding-right: 3px;
|
||||
border-right: 1px solid #c5c5c5; }
|
||||
.breadcrumb a {
|
||||
display: inline-block;
|
||||
padding: 0 9px;
|
||||
vertical-align: top;
|
||||
text-shadow: #f7f7f7 0 1px 1px;
|
||||
color: #555555;
|
||||
font-weight: bold; }
|
||||
|
||||
.bc-arrow {
|
||||
height: 33px; }
|
||||
|
||||
.homeimg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
margin: 6px 0; }
|
||||
|
||||
.searchbox {
|
||||
position: absolute;
|
||||
right: 2px; }
|
||||
.searchbox input {
|
||||
width: 130px;
|
||||
-moz-border-radius: 11px;
|
||||
-webkit-border-radius: 11px;
|
||||
border-radius: 11px;
|
||||
border: 1px solid #cecbcb;
|
||||
padding: 3px 19px;
|
||||
margin: 6px;
|
||||
background: url("../img/search.png") no-repeat 5px;
|
||||
background-color: white; }
|
||||
|
||||
.search-clear {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 11px;
|
||||
cursor: pointer;
|
||||
display: none; }
|
||||
|
||||
#categories {
|
||||
border-bottom: 1px solid #cecbcb; }
|
||||
#categories h4 {
|
||||
display: inline-block;
|
||||
margin: 9px 5px; }
|
||||
#categories ol {
|
||||
display: inline; }
|
||||
#categories li {
|
||||
display: inline-block; }
|
||||
#categories .button {
|
||||
padding: 6px 14px;
|
||||
margin: 4px 0;
|
||||
font-size: 12px; }
|
||||
|
||||
.product {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
margin: 5px;
|
||||
max-width: 120px;
|
||||
border: 1px solid lightgray;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
-moz-box-shadow: 0px 1px 4px #777777;
|
||||
-webkit-box-shadow: 0px 1px 4px #777777;
|
||||
-box-shadow: 0px 1px 4px #777777; }
|
||||
|
||||
.product-img {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 100px;
|
||||
background: white;
|
||||
text-align: center; }
|
||||
|
||||
.price-tag {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
vertical-align: top;
|
||||
color: white;
|
||||
background: #7f82ac;
|
||||
padding: 2px 5px;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px; }
|
||||
|
||||
.product-name {
|
||||
padding: 3px; }
|
||||
|
||||
#login-form label, #login-form input {
|
||||
display: block; }
|
||||
#login-form input {
|
||||
margin: 4px 0 12px;
|
||||
padding: 4px;
|
||||
width: 96%; }
|
||||
|
||||
div#order-selector {
|
||||
display: inline; }
|
||||
|
||||
ol#orders {
|
||||
display: inline; }
|
||||
|
||||
li.order-selector-button {
|
||||
display: inline; }
|
||||
|
||||
li.selected-order button {
|
||||
font-weight: 900; }
|
|
@ -0,0 +1,360 @@
|
|||
@mixin vertical-gradient($startColor: #555, $endColor: #333)
|
||||
background: $endColor
|
||||
background: -moz-linear-gradient($startColor, $endColor)
|
||||
background: -webkit-gradient(linear, left top, left bottom, from($startColor), to($endColor))
|
||||
|
||||
@mixin radius($radius: 5px)
|
||||
-moz-border-radius: $radius
|
||||
-webkit-border-radius: $radius
|
||||
border-radius: $radius
|
||||
|
||||
@mixin box-shadow($bsval: 0px 1px 4px #777)
|
||||
-moz-box-shadow: $bsval
|
||||
-webkit-box-shadow: $bsval
|
||||
-box-shadow: $bsval
|
||||
|
||||
body, html
|
||||
height: 100%
|
||||
|
||||
body
|
||||
padding: 0
|
||||
margin: 0
|
||||
background-color: #f0eeee
|
||||
font-family: "Lucida Grande", Helvetica, Verdana, Arial
|
||||
color: #555
|
||||
font-size: 12px
|
||||
|
||||
table
|
||||
border-spacing: 0
|
||||
border-collapse: collapse
|
||||
|
||||
td
|
||||
border: 1px solid #e9eaec
|
||||
|
||||
input
|
||||
color: #555
|
||||
|
||||
a
|
||||
text-decoration: none
|
||||
color: #555
|
||||
|
||||
button, a.button
|
||||
display: inline-block
|
||||
cursor: pointer
|
||||
padding: 4px 10px
|
||||
font-size: 11px
|
||||
border: 1px solid #cacaca
|
||||
@include radius(4px)
|
||||
@include vertical-gradient(#f0f0f0, #e2e2e2)
|
||||
|
||||
ul, ol
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
li
|
||||
list-style-type: none
|
||||
|
||||
#container
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
#topheader
|
||||
width: 100%
|
||||
height: 54px
|
||||
color: gray
|
||||
border-top: solid 1px #d3d3d3
|
||||
border-bottom: solid 1px black
|
||||
@include vertical-gradient(#7b7979, #393939)
|
||||
button
|
||||
color: black
|
||||
border: 1px solid black
|
||||
@include vertical-gradient(#b2b3d7, #7f82ac)
|
||||
|
||||
#branding, #steps, #rightheader
|
||||
float: left
|
||||
overflow: hidden
|
||||
height: 35px
|
||||
padding: 10px
|
||||
|
||||
#branding
|
||||
border-right: 1px solid #373737
|
||||
text-align: center
|
||||
img
|
||||
height: 32px
|
||||
width: 116px
|
||||
|
||||
#steps
|
||||
padding: 10px 17px
|
||||
border-right: solid 1px #3b3b3b
|
||||
vertical-align: top
|
||||
label
|
||||
width: 80px
|
||||
background-color: ''
|
||||
background-image: url('../img/steps-bg.png')
|
||||
border-bottom: solid 1px #5c5c5c
|
||||
border-top: solid 1px #373737
|
||||
vertical-align: top
|
||||
&:first-child
|
||||
border-left: solid 1px #373737
|
||||
&:last-child
|
||||
border-right: solid 1px #373737
|
||||
span
|
||||
padding: 2px 6px
|
||||
img
|
||||
height: 32px
|
||||
.ui-button, .ui-button-text-only
|
||||
height: 30px
|
||||
margin: 0 -4px
|
||||
|
||||
#neworder-button
|
||||
width: 32px
|
||||
padding: 1px
|
||||
font-size: 23px
|
||||
|
||||
#loggedas
|
||||
float: right
|
||||
padding: 5px 9px
|
||||
text-align: center
|
||||
color: white
|
||||
border-left: 1px solid #373737
|
||||
p
|
||||
margin: 0 0 3px 0
|
||||
|
||||
#content
|
||||
width: 100%
|
||||
position: absolute
|
||||
top: 56px
|
||||
bottom: 0
|
||||
|
||||
#leftpane
|
||||
height: 100%
|
||||
width: 440px
|
||||
position: relative
|
||||
border-right: solid 1px #afafb6
|
||||
background-color: white
|
||||
footer
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
background-color: #e0e0e0
|
||||
background-image: url(../img/headerbackground.jpg)
|
||||
|
||||
#current-order
|
||||
width: 100%
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 271px
|
||||
overflow: auto
|
||||
thead
|
||||
background-color: #ccc
|
||||
background-image: url(../img/headerbackground.jpg)
|
||||
border: 0px
|
||||
font-size: 12px
|
||||
width: 440px
|
||||
td
|
||||
text-align: center
|
||||
padding: 8px 0px
|
||||
min-width: 40px
|
||||
font-size: 12px
|
||||
td
|
||||
padding: 6px 4px
|
||||
font-size: 11px
|
||||
text-align: right
|
||||
min-width: 40px
|
||||
white-space: nowrap
|
||||
&:first-child
|
||||
width: 320px
|
||||
padding: 6px
|
||||
text-align: left
|
||||
text-overflow: ellipsis
|
||||
&:last-child
|
||||
border-right: none
|
||||
tr.selected
|
||||
background-color: #e9eaf2
|
||||
td
|
||||
border-top: 2px solid #d5d6e0
|
||||
border-bottom: 1px solid #d5d6e0
|
||||
padding-top: 5px
|
||||
color: #555
|
||||
|
||||
#amounts
|
||||
background: white
|
||||
border-bottom: solid 1px #d2d2d2
|
||||
border-top: solid 1px #e9eaec
|
||||
font-weight: bold
|
||||
text-align: right
|
||||
-webkit-margin-before: 0
|
||||
-webkit-margin-after: 0
|
||||
li
|
||||
display: inline-block
|
||||
padding: 8px
|
||||
width: 29%
|
||||
|
||||
@mixin highlighted-button
|
||||
color: white
|
||||
@include vertical-gradient(#9d9fc5, #7f82ac)
|
||||
|
||||
@mixin pad-button($height, $width)
|
||||
height: $height
|
||||
width: $width
|
||||
margin: 0 -3px
|
||||
font-weight: bold
|
||||
vertical-align: middle
|
||||
color: #555
|
||||
border-top: 1px solid #efefef
|
||||
&:hover
|
||||
@include highlighted-button
|
||||
|
||||
#paypad
|
||||
padding: 9px
|
||||
float: left
|
||||
text-align: center
|
||||
button
|
||||
@include pad-button(54px, 208px)
|
||||
|
||||
#numpad
|
||||
padding: 9px
|
||||
float: right
|
||||
text-align: center
|
||||
button
|
||||
@include pad-button(54px, 54px)
|
||||
.selected-mode
|
||||
@include highlighted-button
|
||||
|
||||
.payment-button
|
||||
font-size: 14px
|
||||
|
||||
.input-button
|
||||
font-size: 24px
|
||||
|
||||
.mode-button, #numpad-delete, #numpad-minus
|
||||
font-size: 14px
|
||||
|
||||
#rightpane
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 441px
|
||||
right: 0
|
||||
height: 100%
|
||||
vertical-align: top
|
||||
header
|
||||
padding: 0
|
||||
height: 32px
|
||||
border-bottom: 1px solid #cecbcb
|
||||
@include vertical-gradient(#fff, #d3d3d3)
|
||||
|
||||
.product-list
|
||||
overflow: auto
|
||||
position: absolute
|
||||
top: 72px
|
||||
bottom: 0
|
||||
|
||||
.breadcrumb
|
||||
li
|
||||
float: left
|
||||
line-height: 32px
|
||||
height: 32px
|
||||
&:last-child
|
||||
padding-right: 3px
|
||||
border-right: 1px solid #c5c5c5
|
||||
a
|
||||
display: inline-block
|
||||
padding: 0 9px
|
||||
vertical-align: top
|
||||
text-shadow: #F7F7F7 0 1px 1px
|
||||
color: #555
|
||||
font-weight: bold
|
||||
|
||||
.bc-arrow
|
||||
height: 33px
|
||||
|
||||
.homeimg
|
||||
width: 19px
|
||||
height: 19px
|
||||
margin: 6px 0
|
||||
|
||||
.searchbox
|
||||
position: absolute
|
||||
right: 2px
|
||||
input
|
||||
width: 130px
|
||||
@include radius(11px)
|
||||
border: 1px solid #cecbcb
|
||||
padding: 3px 19px
|
||||
margin: 6px
|
||||
background: url('../img/search.png') no-repeat 5px
|
||||
background-color: white
|
||||
|
||||
.search-clear
|
||||
position: absolute
|
||||
top: 11px
|
||||
right: 11px
|
||||
cursor: pointer
|
||||
display: none
|
||||
|
||||
#categories
|
||||
border-bottom: 1px solid #cecbcb
|
||||
h4
|
||||
display: inline-block
|
||||
margin: 9px 5px
|
||||
ol
|
||||
display: inline
|
||||
li
|
||||
display: inline-block
|
||||
.button
|
||||
padding: 6px 14px
|
||||
margin: 4px 0
|
||||
font-size: 12px
|
||||
|
||||
.product
|
||||
vertical-align: top
|
||||
display: inline-block
|
||||
font-size: 11px
|
||||
margin: 5px
|
||||
max-width: 120px
|
||||
border: 1px solid lightgray
|
||||
@include radius(4px)
|
||||
@include box-shadow()
|
||||
|
||||
.product-img
|
||||
position: relative
|
||||
width: 120px
|
||||
height: 100px
|
||||
background: white
|
||||
text-align: center
|
||||
|
||||
.price-tag
|
||||
position: absolute
|
||||
top: 2px
|
||||
right: 2px
|
||||
vertical-align: top
|
||||
color: white
|
||||
background: #7F82AC
|
||||
padding: 2px 5px
|
||||
@include radius(3px)
|
||||
|
||||
.product-name
|
||||
padding: 3px
|
||||
|
||||
#login-form
|
||||
label, input
|
||||
display: block
|
||||
input
|
||||
margin: 4px 0 12px
|
||||
padding: 4px
|
||||
width: 96%
|
||||
|
||||
div#order-selector
|
||||
display: inline
|
||||
|
||||
ol#orders
|
||||
display: inline
|
||||
|
||||
li.order-selector-button
|
||||
display: inline
|
||||
|
||||
li.selected-order
|
||||
button
|
||||
font-weight: 900
|
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 916 B |
After Width: | Height: | Size: 361 B |
After Width: | Height: | Size: 293 B |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 505 B |
After Width: | Height: | Size: 195 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 186 B |
After Width: | Height: | Size: 760 B |
|
@ -0,0 +1,641 @@
|
|||
db = openerp.init()
|
||||
|
||||
###
|
||||
Local store access. Read once from localStorage upon construction and persist on every change.
|
||||
There should only be one store active at any given time to ensure data consistency.
|
||||
###
|
||||
class Store
|
||||
constructor: ->
|
||||
store = localStorage['pos']
|
||||
@data = (store && JSON.parse store) || {}
|
||||
get: (key) ->
|
||||
@data[key]
|
||||
set: (key, value) ->
|
||||
@data[key] = value
|
||||
localStorage['pos'] = JSON.stringify @data
|
||||
|
||||
###
|
||||
Gets all the necessary data from the OpenERP web client (session, shop data etc.)
|
||||
###
|
||||
class Pos
|
||||
constructor: ->
|
||||
@session.session_login 'web-trunk-pos', 'admin', 'admin', =>
|
||||
$.when(
|
||||
@fetch(
|
||||
'pos.category',
|
||||
['name','parent_id','child_id']
|
||||
),
|
||||
@fetch(
|
||||
'product.product',
|
||||
['name','list_price','pos_categ_id','taxes_id','img'],
|
||||
[['pos_categ_id','!=','false']]
|
||||
),
|
||||
@fetch(
|
||||
'account.bank.statement',
|
||||
['account_id', 'currency', 'journal_id', 'state', 'name']
|
||||
),
|
||||
@fetch(
|
||||
'account.journal',
|
||||
['auto_cash', 'check_dtls', 'currency', 'name', 'type']
|
||||
)
|
||||
).then @build_tree
|
||||
ready: $.Deferred()
|
||||
session: new db.base.Session 'DEBUG'
|
||||
store: new Store
|
||||
fetch: (osvModel, fields, domain, callback, errorCallback) ->
|
||||
callback = callback || (result) => @store.set osvModel, result
|
||||
dataSetSearch = new db.base.DataSetSearch this, osvModel, null, domain
|
||||
dataSetSearch.read_slice fields, 0, null, callback
|
||||
push: (osvModel, record, callback, errorCallback) ->
|
||||
dataSet = new db.base.DataSet(this, osvModel, null)
|
||||
dataSet.create record, callback, errorCallback
|
||||
categories: {}
|
||||
build_tree: =>
|
||||
for c in @store.get 'pos.category'
|
||||
@categories[c.id] = id:c.id, name:c.name, children:c.child_id,
|
||||
parent:c.parent_id[0], ancestors:[c.id], subtree:[c.id]
|
||||
for id, c of @categories
|
||||
@current_category = c
|
||||
@build_ancestors c.parent
|
||||
@build_subtree c
|
||||
@categories[0] =
|
||||
ancestors: []
|
||||
children: c.id for c in @store.get 'pos.category' when not c.parent_id[0]?
|
||||
subtree: c.id for c in @store.get 'pos.category'
|
||||
@ready.resolve()
|
||||
build_ancestors: (parent) ->
|
||||
if parent?
|
||||
@current_category.ancestors.unshift parent
|
||||
@build_ancestors @categories[parent].parent
|
||||
build_subtree: (category) ->
|
||||
for c in category.children
|
||||
@current_category.subtree.push c
|
||||
@build_subtree @categories[c]
|
||||
|
||||
window.pos = new Pos
|
||||
|
||||
$ ->
|
||||
$('#steps').buttonset() # jQuery UI buttonset
|
||||
|
||||
###
|
||||
---
|
||||
Models
|
||||
---
|
||||
###
|
||||
|
||||
class CashRegister extends Backbone.Model
|
||||
|
||||
class CashRegisterCollection extends Backbone.Collection
|
||||
model: CashRegister
|
||||
|
||||
class Product extends Backbone.Model
|
||||
|
||||
class ProductCollection extends Backbone.Collection
|
||||
model: Product
|
||||
|
||||
class Category extends Backbone.Model
|
||||
|
||||
class CategoryCollection extends Backbone.Collection
|
||||
model: Category
|
||||
|
||||
###
|
||||
Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
|
||||
There should only ever be one Orderline per distinct product in an Order.
|
||||
To add more of the same product, just update the quantity accordingly.
|
||||
The Order also contains payment information.
|
||||
###
|
||||
class Orderline extends Backbone.Model
|
||||
defaults: {
|
||||
quantity: 1,
|
||||
list_price: 0,
|
||||
discount: 0
|
||||
}
|
||||
incrementQuantity: -> @set quantity: (@get 'quantity') + 1
|
||||
getTotal: -> (@get 'quantity') * (@get 'list_price') * (1 - (@get 'discount')/100)
|
||||
exportAsJSON: ->
|
||||
result = {
|
||||
qty: (@get 'quantity'),
|
||||
price_unit: (@get 'list_price'),
|
||||
discount: (@get 'discount'),
|
||||
product_id: (@get 'id')
|
||||
}
|
||||
return result
|
||||
|
||||
class OrderlineCollection extends Backbone.Collection
|
||||
model: Orderline
|
||||
|
||||
###
|
||||
Every PaymentLine has all the attributes of the corresponding CashRegister.
|
||||
###
|
||||
class Paymentline extends Backbone.Model
|
||||
defaults: {
|
||||
amount: 0
|
||||
}
|
||||
getAmount: -> @get 'amount'
|
||||
exportAsJSON: ->
|
||||
result = {
|
||||
name: "Payment line",
|
||||
statement_id: (@get 'id'),
|
||||
account_id: (@get 'account_id')[0],
|
||||
journal_id: (@get 'journal_id')[0],
|
||||
amount: @getAmount()
|
||||
}
|
||||
return result
|
||||
|
||||
class PaymentlineCollection extends Backbone.Collection
|
||||
model: Paymentline
|
||||
|
||||
class Order extends Backbone.Model
|
||||
defaults: {
|
||||
validated: false
|
||||
}
|
||||
initialize: ->
|
||||
@set orderLines: new OrderlineCollection
|
||||
@set paymentLines: new PaymentlineCollection
|
||||
@set name: "Order " + @generateUniqueId()
|
||||
generateUniqueId: ->
|
||||
new Date().getTime()
|
||||
addProduct: (product) ->
|
||||
existing = (@get 'orderLines').get product.id
|
||||
if existing?
|
||||
existing.incrementQuantity()
|
||||
else
|
||||
(@get 'orderLines').add new Orderline product.toJSON()
|
||||
addPaymentLine: (cashRegister) ->
|
||||
newPaymentline = new Paymentline cashRegister
|
||||
### TODO: Should be 0 for cash-like accounts ###
|
||||
newPaymentline.set amount: @getDueLeft()
|
||||
(@get 'paymentLines').add newPaymentline
|
||||
getName: ->
|
||||
return @get 'name'
|
||||
getTotal: ->
|
||||
return (@get 'orderLines').reduce ((sum, orderLine) -> sum + orderLine.getTotal()), 0
|
||||
getTotalTaxExcluded: ->
|
||||
return @getTotal()/1.21
|
||||
getTax: ->
|
||||
return @getTotal()/1.21*0.21
|
||||
getPaidTotal: ->
|
||||
return (@get 'paymentLines').reduce ((sum, paymentLine) -> sum + paymentLine.getAmount()), 0
|
||||
getChange: ->
|
||||
return @getPaidTotal() - @getTotal()
|
||||
getDueLeft: ->
|
||||
return @getTotal() - @getPaidTotal()
|
||||
exportAsJSON: ->
|
||||
orderLines = []
|
||||
(@get 'orderLines').each (item) => orderLines.push [0, 0, item.exportAsJSON()]
|
||||
paymentLines = []
|
||||
(@get 'paymentLines').each (item) => paymentLines.push [0, 0, item.exportAsJSON()]
|
||||
result = {
|
||||
name: @getName(),
|
||||
amount_paid: @getPaidTotal(),
|
||||
amount_total: @getTotal(),
|
||||
amount_tax: @getTax(),
|
||||
amount_return: @getChange(),
|
||||
lines: orderLines,
|
||||
statement_ids: paymentLines
|
||||
}
|
||||
return result
|
||||
|
||||
class OrderCollection extends Backbone.Collection
|
||||
model: Order
|
||||
|
||||
class Shop extends Backbone.Model
|
||||
defaults: {
|
||||
cashRegisters: (new CashRegisterCollection pos.store.get('account.bank.statement')),
|
||||
orders: new OrderCollection,
|
||||
products: new ProductCollection
|
||||
}
|
||||
initialize: ->
|
||||
(@get 'orders').bind 'remove', (removedOrder) =>
|
||||
if (@get 'orders').isEmpty()
|
||||
@addAndSelectOrder new Order
|
||||
if (@get 'selectedOrder') is removedOrder
|
||||
@set selectedOrder: (@get 'orders').last()
|
||||
addAndSelectOrder: (newOrder) ->
|
||||
(@get 'orders').add newOrder
|
||||
@set selectedOrder: newOrder
|
||||
|
||||
|
||||
###
|
||||
The numpad handles both the choice of the property currently being modified
|
||||
(quantity, price or discount) and the edition of the corresponding numeric value.
|
||||
###
|
||||
class NumpadState extends Backbone.Model
|
||||
defaults: {
|
||||
buffer: "0"
|
||||
mode: "quantity"
|
||||
}
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
@shop.bind 'change:selectedOrder', @reset, this
|
||||
appendNewChar: (newChar) ->
|
||||
oldBuffer = @get 'buffer'
|
||||
if oldBuffer is '0'
|
||||
@set buffer: newChar
|
||||
else if oldBuffer is '-0'
|
||||
@set buffer: "-" + newChar
|
||||
else
|
||||
@set buffer: (@get 'buffer') + newChar
|
||||
@updateTarget()
|
||||
deleteLastChar: ->
|
||||
tempNewBuffer = (@get 'buffer').slice(0, -1) || "0"
|
||||
if isNaN tempNewBuffer
|
||||
tempNewBuffer = "0"
|
||||
@set buffer: tempNewBuffer
|
||||
@updateTarget()
|
||||
switchSign: ->
|
||||
oldBuffer = @get 'buffer'
|
||||
@set buffer: if oldBuffer[0] is '-' then oldBuffer.substr 1 else "-" + oldBuffer
|
||||
@updateTarget()
|
||||
changeMode: (newMode) ->
|
||||
@set buffer: "0", mode: newMode
|
||||
reset: ->
|
||||
@set buffer: "0"
|
||||
updateTarget: ->
|
||||
bufferContent = @get 'buffer'
|
||||
if bufferContent && !isNaN bufferContent
|
||||
params = {}
|
||||
params[@get 'mode'] = parseFloat bufferContent
|
||||
(@shop.get 'selectedOrder').selected.set params
|
||||
|
||||
###
|
||||
---
|
||||
Views
|
||||
---
|
||||
###
|
||||
|
||||
class NumpadView extends Backbone.View
|
||||
initialize: (options) ->
|
||||
@state = options.state
|
||||
events: {
|
||||
'click button#numpad-backspace': 'clickDeleteLastChar',
|
||||
'click button#numpad-minus': 'clickSwitchSign',
|
||||
'click button.number-char': 'clickAppendNewChar',
|
||||
'click button.mode-button': 'clickChangeMode'
|
||||
}
|
||||
clickDeleteLastChar: ->
|
||||
@state.deleteLastChar()
|
||||
clickSwitchSign: ->
|
||||
@state.switchSign()
|
||||
clickAppendNewChar: (event) ->
|
||||
newChar = event.currentTarget.innerText
|
||||
@state.appendNewChar newChar
|
||||
clickChangeMode: (event) ->
|
||||
$('.selected-mode').removeClass 'selected-mode'
|
||||
$(event.currentTarget).addClass 'selected-mode'
|
||||
newMode = event.currentTarget.attributes['data-mode'].nodeValue
|
||||
@state.changeMode newMode
|
||||
|
||||
###
|
||||
Gives access to the payment methods (aka. 'cash registers')
|
||||
###
|
||||
class PaypadView extends Backbone.View
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
events: {
|
||||
'click button': 'performPayment'
|
||||
}
|
||||
performPayment: (event) ->
|
||||
cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue
|
||||
cashRegisterCollection = (@shop.get 'cashRegisters')
|
||||
cashRegister = cashRegisterCollection.find (item) => (item.get 'id') is parseInt cashRegisterId, 10
|
||||
(@shop.get 'selectedOrder').addPaymentLine cashRegister
|
||||
render: ->
|
||||
$(@el).empty()
|
||||
(@shop.get 'cashRegisters').each (cashRegister) => $(@el).append (new PaymentButtonView model: cashRegister).render()
|
||||
|
||||
class PaymentButtonView extends Backbone.View
|
||||
template: _.template $('#payment-button-template').html()
|
||||
render: ->
|
||||
$(@el).html @template {id: (@model.get 'id'), name: (@model.get 'journal_id')[1]}
|
||||
|
||||
|
||||
###
|
||||
There are 3 steps in a POS workflow:
|
||||
1. prepare the order (i.e. chose products, quantities etc.)
|
||||
2. choose payment method(s) and amount(s)
|
||||
3. validae order and print receipt
|
||||
It should be possible to go back to any step as long as step 3 hasn't been completed.
|
||||
Modifying an order after validation shouldn't be allowed.
|
||||
###
|
||||
class StepsView extends Backbone.View
|
||||
initialize: (options) ->
|
||||
@step = "products"
|
||||
events: {
|
||||
'click input.step-button': 'clickChangeStep'
|
||||
}
|
||||
clickChangeStep: (event) ->
|
||||
newStep = event.currentTarget.attributes['data-step'].nodeValue
|
||||
$('.step-screen').hide()
|
||||
$('#' + newStep + '-screen').show()
|
||||
@step = newStep
|
||||
|
||||
###
|
||||
Shopping carts.
|
||||
###
|
||||
class OrderlineView extends Backbone.View
|
||||
tagName: 'tr'
|
||||
template: _.template $('#orderline-template').html()
|
||||
initialize: (options) ->
|
||||
@model.bind 'change', => $(@el).hide(); @render()
|
||||
@model.bind 'remove', => $(@el).remove()
|
||||
@order = options.order
|
||||
@numpadState = options.numpadState
|
||||
events: {
|
||||
'click': 'clickHandler'
|
||||
}
|
||||
clickHandler: ->
|
||||
@numpadState.reset()
|
||||
@select()
|
||||
render: ->
|
||||
@select()
|
||||
$(@el).html(@template @model.toJSON()).fadeIn 400, -> $('#current-order').scrollTop $(@).offset().top
|
||||
select: ->
|
||||
$('tr.selected').removeClass 'selected'
|
||||
$(@el).addClass 'selected'
|
||||
@order.selected = @model
|
||||
|
||||
class OrderView extends Backbone.View
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
@numpadState = options.numpadState
|
||||
@shop.bind 'change:selectedOrder', @changeSelectedOrder, this
|
||||
@bindOrderLineEvents()
|
||||
changeSelectedOrder: ->
|
||||
@currentOrderLines.unbind()
|
||||
@bindOrderLineEvents()
|
||||
@render()
|
||||
bindOrderLineEvents: ->
|
||||
@currentOrderLines = (@shop.get 'selectedOrder' ).get 'orderLines'
|
||||
@currentOrderLines.bind 'add', @addLine, this
|
||||
@currentOrderLines.bind 'change', @render, this
|
||||
@currentOrderLines.bind 'remove', @render, this
|
||||
addLine: (newLine) ->
|
||||
$(@el).append (new OrderlineView model: newLine, order: (@shop.get 'selectedOrder'), numpadState: @numpadState).render()
|
||||
@updateSummary()
|
||||
render: ->
|
||||
$(@el).empty()
|
||||
@currentOrderLines.each (orderLine) =>
|
||||
$(@el).append (new OrderlineView model: orderLine, order: (@shop.get 'selectedOrder'), numpadState: @numpadState).render()
|
||||
@updateSummary()
|
||||
updateSummary: ->
|
||||
currentOrder = @shop.get 'selectedOrder'
|
||||
total = currentOrder.getTotal()
|
||||
totalTaxExcluded = currentOrder.getTotalTaxExcluded()
|
||||
tax = currentOrder.getTax()
|
||||
$('#subtotal').html(totalTaxExcluded.toFixed 2).hide().fadeIn()
|
||||
$('#tax').html(tax.toFixed 2).hide().fadeIn()
|
||||
$('#total').html(total.toFixed 2).hide().fadeIn()
|
||||
|
||||
###
|
||||
"Products" step.
|
||||
###
|
||||
class CategoryView extends Backbone.View
|
||||
template: _.template $('#category-template').html()
|
||||
render: (ancestors, children) ->
|
||||
$(@el).html @template
|
||||
breadcrumb: pos.categories[c] for c in ancestors
|
||||
categories: pos.categories[c] for c in children
|
||||
|
||||
class ProductView extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'product'
|
||||
template: _.template $('#product-template').html()
|
||||
events: {
|
||||
'click a': 'addToOrder'
|
||||
}
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
addToOrder: (event) ->
|
||||
### Preserve the category URL ###
|
||||
event.preventDefault()
|
||||
(@shop.get 'selectedOrder').addProduct @model
|
||||
render: ->
|
||||
$(@el).html @template @model.toJSON()
|
||||
|
||||
class ProductListView extends Backbone.View
|
||||
tagName: 'ol'
|
||||
className: 'product-list'
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
(@shop.get 'products').bind 'reset', @render, this
|
||||
render: ->
|
||||
$(@el).empty()
|
||||
(@shop.get 'products').each (product) => $(@el).append (new ProductView model: product, shop: @shop).render()
|
||||
$('#products-screen').append @el
|
||||
|
||||
###
|
||||
"Payment" step.
|
||||
###
|
||||
class PaymentlineView extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'paymentline'
|
||||
template: _.template $('#paymentline-template').html()
|
||||
initialize: ->
|
||||
@model.bind 'change', @render, this
|
||||
events: {
|
||||
'keyup input': 'changeAmount'
|
||||
}
|
||||
changeAmount: (event) ->
|
||||
newAmount = event.currentTarget.value
|
||||
if newAmount && !isNaN(newAmount)
|
||||
@model.set amount: parseFloat(newAmount)
|
||||
render: ->
|
||||
$(@el).html @template {name: (@model.get 'journal_id')[1], amount: (@model.get 'amount')}
|
||||
|
||||
class PaymentView extends Backbone.View
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
@shop.bind 'change:selectedOrder', @changeSelectedOrder, this
|
||||
@bindPaymentLineEvents()
|
||||
@bindOrderLineEvents()
|
||||
paymentLineList: ->
|
||||
$(@el).find '#paymentlines'
|
||||
events: {
|
||||
'click button#validate-order': 'validateCurrentOrder'
|
||||
}
|
||||
validateCurrentOrder: ->
|
||||
currentOrder = @shop.get 'selectedOrder'
|
||||
callback = => currentOrder.set validated: true
|
||||
pos.push 'pos.order', currentOrder.exportAsJSON(), callback
|
||||
bindPaymentLineEvents: ->
|
||||
@currentPaymentLines = (@shop.get 'selectedOrder').get 'paymentLines'
|
||||
@currentPaymentLines.bind 'add', @addPaymentLine, this
|
||||
@currentPaymentLines.bind 'change', @render, this
|
||||
@currentPaymentLines.bind 'remove', @render, this
|
||||
@currentPaymentLines.bind 'all', @updatePaymentSummary, this
|
||||
bindOrderLineEvents: ->
|
||||
@currentOrderLines = (@shop.get 'selectedOrder').get 'orderLines'
|
||||
@currentOrderLines.bind 'all', @updatePaymentSummary, this
|
||||
changeSelectedOrder: ->
|
||||
@currentPaymentLines.unbind()
|
||||
@bindPaymentLineEvents()
|
||||
@currentOrderLines.unbind()
|
||||
@bindOrderLineEvents()
|
||||
@render()
|
||||
addPaymentLine: (newPaymentLine) ->
|
||||
@paymentLineList().append (new PaymentlineView model: newPaymentLine).render()
|
||||
render: ->
|
||||
@paymentLineList().empty()
|
||||
@currentPaymentLines.each (paymentLine) => @paymentLineList().append (new PaymentlineView model: paymentLine).render()
|
||||
@updatePaymentSummary()
|
||||
updatePaymentSummary: ->
|
||||
currentOrder = @shop.get 'selectedOrder'
|
||||
paidTotal = currentOrder.getPaidTotal()
|
||||
dueTotal = currentOrder.getTotal()
|
||||
$(@el).find('#payment-due-total').html dueTotal.toFixed 2
|
||||
$(@el).find('#payment-paid-total').html paidTotal.toFixed 2
|
||||
remainingAmount = dueTotal-paidTotal
|
||||
remaining = if remainingAmount > 0 then "Due left: " + remainingAmount.toFixed 2 else "Change: " + (-remainingAmount).toFixed 2
|
||||
$('#payment-remaining').html remaining
|
||||
|
||||
###
|
||||
"Receipt" step.
|
||||
###
|
||||
class ReceiptLineView extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'receiptline'
|
||||
template: _.template $('#receiptline-template').html()
|
||||
initialize: ->
|
||||
@model.bind 'change', @render, this
|
||||
render: ->
|
||||
$(@el).html @template @model.toJSON()
|
||||
|
||||
class ReceiptView extends Backbone.View
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
@shop.bind 'change:selectedOrder', @changeSelectedOrder, this
|
||||
@bindOrderLineEvents()
|
||||
@bindPaymentLineEvents()
|
||||
receiptLineList: ->
|
||||
$(@el).find('#receiptlines')
|
||||
bindOrderLineEvents: ->
|
||||
@currentOrderLines = (@shop.get 'selectedOrder').get 'orderLines'
|
||||
@currentOrderLines.bind 'add', @addReceiptLine, this
|
||||
@currentOrderLines.bind 'change', @render, this
|
||||
@currentOrderLines.bind 'remove', @render, this
|
||||
bindPaymentLineEvents: ->
|
||||
@currentPaymentLines = (@shop.get 'selectedOrder').get 'paymentLines'
|
||||
@currentPaymentLines.bind 'all', @updateReceiptSummary, this
|
||||
changeSelectedOrder: ->
|
||||
@currentOrderLines.unbind()
|
||||
@bindOrderLineEvents()
|
||||
@currentPaymentLines.unbind()
|
||||
@bindPaymentLineEvents()
|
||||
@render()
|
||||
addReceiptLine: (newOrderItem) ->
|
||||
@receiptLineList().append (new ReceiptLineView model: newOrderItem).render()
|
||||
@updateReceiptSummary()
|
||||
render: ->
|
||||
@receiptLineList().empty()
|
||||
@currentOrderLines.each (orderItem) => @receiptLineList().append (new ReceiptLineView model: orderItem).render()
|
||||
@updateReceiptSummary()
|
||||
updateReceiptSummary: ->
|
||||
currentOrder = @shop.get 'selectedOrder'
|
||||
total = currentOrder.getTotal()
|
||||
tax = currentOrder.getTax()
|
||||
change = currentOrder.getPaidTotal() - total
|
||||
$('#receipt-summary-tax').html tax.toFixed 2
|
||||
$('#receipt-summary-total').html total.toFixed 2
|
||||
$('#receipt-summary-change').html change.toFixed 2
|
||||
|
||||
class OrderButtonView extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'order-selector-button'
|
||||
template: _.template $('#order-selector-button-template').html()
|
||||
initialize: (options) ->
|
||||
@order = options.order
|
||||
@shop = options.shop
|
||||
@order.bind 'destroy', => $(@el).remove()
|
||||
@shop.bind 'change:selectedOrder', (shop) =>
|
||||
selectedOrder = shop.get 'selectedOrder'
|
||||
if @order is selectedOrder
|
||||
@setButtonSelected()
|
||||
events: {
|
||||
'click button.select-order': 'selectOrder',
|
||||
'click button.close-order': 'closeOrder'
|
||||
}
|
||||
selectOrder: (event) ->
|
||||
@shop.set selectedOrder: @order
|
||||
setButtonSelected: ->
|
||||
$('.selected-order').removeClass 'selected-order'
|
||||
$(@el).addClass 'selected-order'
|
||||
closeOrder: (event) ->
|
||||
@order.destroy()
|
||||
render: ->
|
||||
$(@el).html @template @order.toJSON()
|
||||
|
||||
class ShopView extends Backbone.View
|
||||
initialize: (options) ->
|
||||
@shop = options.shop
|
||||
(@shop.get 'orders').bind 'add', @orderAdded, this
|
||||
(@shop.get 'orders').add new Order
|
||||
@numpadState = new NumpadState
|
||||
shop: @shop
|
||||
@productListView = new ProductListView
|
||||
shop: @shop
|
||||
@paypadView = new PaypadView
|
||||
shop: @shop
|
||||
el: $('#paypad')
|
||||
@paypadView.render()
|
||||
@orderView = new OrderView
|
||||
shop: @shop,
|
||||
numpadState: @numpadState
|
||||
el: $('#current-order-content')
|
||||
@paymentView = new PaymentView
|
||||
shop: @shop,
|
||||
el: $('#payment-screen')
|
||||
@receiptView = new ReceiptView
|
||||
shop: @shop,
|
||||
el: $('#receipt-screen')
|
||||
@numpadView = new NumpadView
|
||||
state: @numpadState,
|
||||
el: $('#numpad')
|
||||
@stepsView = new StepsView
|
||||
el: $('#steps')
|
||||
events: {
|
||||
'click button#neworder-button': 'createNewOrder'
|
||||
}
|
||||
createNewOrder: ->
|
||||
newOrder = new Order
|
||||
(@shop.get 'orders').add newOrder
|
||||
@shop.set selectedOrder: newOrder
|
||||
orderAdded: (newOrder) ->
|
||||
newOrderButton = new OrderButtonView
|
||||
order: newOrder,
|
||||
shop: @shop
|
||||
$('#orders').append (newOrderButton).render()
|
||||
newOrderButton.selectOrder()
|
||||
|
||||
class App extends Backbone.Router
|
||||
routes:
|
||||
'': 'category'
|
||||
'category/:id': 'category'
|
||||
initialize: ->
|
||||
@shop = new Shop
|
||||
@shopView = new ShopView
|
||||
shop: @shop
|
||||
el: $('body')
|
||||
@categoryView = new CategoryView
|
||||
category: (id = 0) ->
|
||||
c = pos.categories[id]
|
||||
$('#products-screen').html(@categoryView.render c.ancestors, c.children)
|
||||
products = pos.store.get('product.product').filter (p) -> p.pos_categ_id[0] in c.subtree
|
||||
(@shop.get 'products').reset products
|
||||
$('.searchbox input').keyup ->
|
||||
s = $(@).val().toLowerCase()
|
||||
if s
|
||||
m = products.filter (p) -> p.name.toLowerCase().indexOf s
|
||||
$('.search-clear').fadeIn()
|
||||
else
|
||||
m = products
|
||||
$('.search-clear').fadeOut()
|
||||
(@shop.get 'products').reset m
|
||||
$('.search-clear').click ->
|
||||
(@shop.get 'products').reset products
|
||||
$('.searchbox input').val('').focus()
|
||||
$('.search-clear').fadeOut()
|
||||
|
||||
pos.ready.then ->
|
||||
pos.app = new App
|
||||
Backbone.history.start()
|
|
@ -0,0 +1,200 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Point of Sale</title>
|
||||
<link rel="stylesheet" href="/web/static/lib/jquery.ui/css/smoothness/jquery-ui-1.8.9.custom.css">
|
||||
<link rel="stylesheet" href="/point_of_sale/static/src/css/pos.css">
|
||||
<script type="text/javascript" src="/web/static/lib/jquery/jquery-1.6.2.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/lib/underscore/underscore.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/qweb/qweb2.js"></script>
|
||||
|
||||
<script src="/web/static/src/js/boot.js"></script>
|
||||
<script src="/web/static/src/js/core.js"></script>
|
||||
<script src="/web/static/src/js/formats.js"></script>
|
||||
<script src="/web/static/src/js/chrome.js"></script>
|
||||
<script src="/web/static/src/js/data.js"></script>
|
||||
<script src="/web/static/src/js/dates.js"></script>
|
||||
<script src="/point_of_sale/static/lib/backbone/backbone-0.5.3.min.js"></script>
|
||||
<script src="/point_of_sale/static/src/js/pos.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="topheader">
|
||||
<div id="branding"><img src="/point_of_sale2/static/img/logo.png" /></div>
|
||||
<div id="steps">
|
||||
<input type="radio" id="products-step-button" class="step-button" data-step='products' name="radio" checked="checked" />
|
||||
<label for="products-step-button">Products</label>
|
||||
<img src="/point_of_sale2/static/img/steps-arrow.png">
|
||||
<input type="radio" id="payment-step-button" class="step-button" data-step='payment' name="radio" />
|
||||
<label for="payment-step-button">Payment</label>
|
||||
<img src="/point_of_sale2/static/img/steps-arrow.png">
|
||||
<input type="radio" id="receipt-step-button" class="step-button" data-step='receipt' name="radio" />
|
||||
<label for="receipt-step-button">Receipt</label>
|
||||
</div>
|
||||
<div id="rightheader">
|
||||
<div id="order-selector">
|
||||
<button id="neworder-button">+</button>
|
||||
<ol id="orders">
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loggedas">
|
||||
<p>Minh Tran</p>
|
||||
<button>Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<div id="leftpane">
|
||||
<div id="current-order">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Product</td>
|
||||
<td>Price</td>
|
||||
<td>Disc (%)</td>
|
||||
<td>Qty</td>
|
||||
<td>Total</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="current-order-content">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<footer>
|
||||
<ul id="amounts">
|
||||
<li> Subtotal: <span id="subtotal">0</span> €</li>
|
||||
<li> Tax: <span id="tax">0</span> €</li>
|
||||
<li> Total: <span id="total">0</span> €</li>
|
||||
</ul>
|
||||
<div id="paypad">
|
||||
</div>
|
||||
<div id="numpad">
|
||||
<button class="input-button number-char">1</button>
|
||||
<button class="input-button number-char">2</button>
|
||||
<button class="input-button number-char">3</button>
|
||||
<button class="mode-button selected-mode" data-mode='quantity'>Qty</button><br>
|
||||
<button class="input-button number-char">4</button>
|
||||
<button class="input-button number-char">5</button>
|
||||
<button class="input-button number-char">6</button>
|
||||
<button class="mode-button" data-mode='discount'>Disc</button><br>
|
||||
<button class="input-button number-char">7</button>
|
||||
<button class="input-button number-char">8</button>
|
||||
<button class="input-button number-char">9</button>
|
||||
<button class="mode-button" data-mode='list_price'>Price</button><br>
|
||||
<button class="input-button" id="numpad-minus" >+/-</button>
|
||||
<button class="input-button number-char">0</button>
|
||||
<button class="input-button number-char">.</button>
|
||||
<button class="input-button" id="numpad-backspace">
|
||||
<img src="/point_of_sale2/static/img/backspace.png" width="24" height="21">
|
||||
</button><br>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div id="rightpane">
|
||||
<div id="products-screen" class="step-screen selected-step"></div>
|
||||
<div id="payment-screen" class="step-screen" style="display:none">
|
||||
<header>Payment</header>
|
||||
<section>
|
||||
<p>Due: <span id="payment-due-total"></span> €</p>
|
||||
<p>Paid: </p>
|
||||
<ol id="paymentlines"></ol>
|
||||
</section>
|
||||
<footer>
|
||||
<p>Total paid: <span id="payment-paid-total"></span> €</p>
|
||||
<p><span id="payment-remaining"></span> €</p>
|
||||
<p><button id="validate-order">Validate</button></p>
|
||||
</footer>
|
||||
</div>
|
||||
<div id="receipt-screen" class="step-screen" style="display:none">
|
||||
<header>Receipt</header>
|
||||
<section>
|
||||
<ol id="receiptlines"></ol>
|
||||
</section>
|
||||
<footer id="receipt-summary">
|
||||
<ul>
|
||||
<li>Total: <span id="receipt-summary-total"></span> €</li>
|
||||
<li>Tax: <span id="receipt-summary-tax"></span> €</li>
|
||||
<li>Change: <span id="receipt-summary-change"></span> €</li>
|
||||
</ul>
|
||||
</footer>
|
||||
<p><button id="print-receipt">Print receipt</button></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/template" id="category-template">
|
||||
<header>
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a href="#"><img src="/point_of_sale2/static/img/home.png" class="homeimg"></a>
|
||||
</li>
|
||||
<% _.each(breadcrumb, function(category) { %>
|
||||
<li>
|
||||
<img src="/point_of_sale2/static/img/bc-arrow.png" class="bc-arrow">
|
||||
<a href="#category/<%= category.id %>"><%= category.name %></a>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ol>
|
||||
<div class="searchbox">
|
||||
<input placeholder="Search Products">
|
||||
<img class="search-clear" src="/point_of_sale2/static/img/search_reset.gif">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="categories">
|
||||
<h4>Categories:</h4>
|
||||
<ol>
|
||||
<% _.each(categories, function(category) { %>
|
||||
<li><a href="#category/<%= category.id %>" class="button"><%= category.name %></a></li>
|
||||
<% }); %>
|
||||
</ol>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="product-template">
|
||||
<a href="#">
|
||||
<div class="product-img">
|
||||
<img src="data:image/gif;base64,<%= img %>">
|
||||
<span class="price-tag"><%= list_price %> €</span>
|
||||
</div>
|
||||
<div class="product-name"><%= name %></div>
|
||||
</a>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="orderline-template">
|
||||
<td><%= name %></td>
|
||||
<td><%= list_price.toFixed(2) %> €</td>
|
||||
<td><%= discount.toFixed(2) %></td>
|
||||
<td><%= quantity.toFixed(0) %></td>
|
||||
<td><%= (list_price * (1 - discount/100) * quantity).toFixed(2) %> €</td>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="paymentline-template">
|
||||
<span class="paymentline-type"><%= name %></span>
|
||||
<span class="paymentline-amount"><input type="text" value="<%= amount.toFixed(2) %>" /></span>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="receiptline-template">
|
||||
<span class="receiptline-quantity"><%= quantity.toFixed(0) %></span>
|
||||
<span class="receiptline-name"><%= name %></span>
|
||||
<span class="receiptline-amount"><%= (list_price * (1 - discount/100) * quantity).toFixed(2) %> €</span>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="test/template" id="payment-button-template">
|
||||
<button class="payment-button" cash-register-id="<%= id %>"><%= name %></button><br>
|
||||
</script>
|
||||
|
||||
<script type="test/template" id="order-selector-button-template">
|
||||
<button class="select-order">Order</button><button class="close-order">X</button>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|