[MERGE] point_of_sale js part manual merge from ~openerp-dev/openerp-web/trunk-pos

bzr revid: al@openerp.com-20110927153203-22mt6akllb4xvk25
This commit is contained in:
Antony Lesuisse 2011-09-27 17:32:03 +02:00
parent 9a158a41eb
commit eb1f4410b8
18 changed files with 2712 additions and 0 deletions

View File

@ -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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")},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);

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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> &euro;</li>
<li> Tax: <span id="tax">0</span> &euro;</li>
<li> Total: <span id="total">0</span> &euro;</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> &euro;</p>
<p>Paid: </p>
<ol id="paymentlines"></ol>
</section>
<footer>
<p>Total paid: <span id="payment-paid-total"></span> &euro;</p>
<p><span id="payment-remaining"></span> &euro;</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> &euro;</li>
<li>Tax: <span id="receipt-summary-tax"></span> &euro;</li>
<li>Change: <span id="receipt-summary-change"></span> &euro;</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 %> &euro;</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) %> &euro;</td>
<td><%= discount.toFixed(2) %></td>
<td><%= quantity.toFixed(0) %></td>
<td><%= (list_price * (1 - discount/100) * quantity).toFixed(2) %> &euro;</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) %> &euro;</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>