From eb1f4410b8abe44d29ffbc80e2281dcc170cc304 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 27 Sep 2011 17:32:03 +0200 Subject: [PATCH] [MERGE] point_of_sale js part manual merge from ~openerp-dev/openerp-web/trunk-pos bzr revid: al@openerp.com-20110927153203-22mt6akllb4xvk25 --- .../static/lib/backbone/backbone-0.5.3.min.js | 33 + addons/point_of_sale/static/src/Cakefile | 11 + addons/point_of_sale/static/src/css/pos.css | 370 ++++++ addons/point_of_sale/static/src/css/pos.sass | 360 ++++++ .../static/src/img/backspace.png | Bin 0 -> 5970 bytes .../point_of_sale/static/src/img/barcode.png | Bin 0 -> 916 bytes .../point_of_sale/static/src/img/bc-arrow.png | Bin 0 -> 361 bytes .../static/src/img/headerbackground.jpg | Bin 0 -> 293 bytes addons/point_of_sale/static/src/img/home.png | Bin 0 -> 2924 bytes addons/point_of_sale/static/src/img/logo.png | Bin 0 -> 7792 bytes .../point_of_sale/static/src/img/search.png | Bin 0 -> 505 bytes .../static/src/img/search_reset.gif | Bin 0 -> 195 bytes .../static/src/img/steps-arrow.png | Bin 0 -> 1210 bytes .../point_of_sale/static/src/img/steps-bg.png | Bin 0 -> 186 bytes .../static/src/img/validate-icon.png | Bin 0 -> 760 bytes addons/point_of_sale/static/src/js/pos.coffee | 641 ++++++++++ addons/point_of_sale/static/src/js/pos.js | 1097 +++++++++++++++++ addons/point_of_sale/static/src/pos.html | 200 +++ 18 files changed, 2712 insertions(+) create mode 100644 addons/point_of_sale/static/lib/backbone/backbone-0.5.3.min.js create mode 100644 addons/point_of_sale/static/src/Cakefile create mode 100644 addons/point_of_sale/static/src/css/pos.css create mode 100644 addons/point_of_sale/static/src/css/pos.sass create mode 100644 addons/point_of_sale/static/src/img/backspace.png create mode 100644 addons/point_of_sale/static/src/img/barcode.png create mode 100644 addons/point_of_sale/static/src/img/bc-arrow.png create mode 100644 addons/point_of_sale/static/src/img/headerbackground.jpg create mode 100644 addons/point_of_sale/static/src/img/home.png create mode 100644 addons/point_of_sale/static/src/img/logo.png create mode 100644 addons/point_of_sale/static/src/img/search.png create mode 100644 addons/point_of_sale/static/src/img/search_reset.gif create mode 100644 addons/point_of_sale/static/src/img/steps-arrow.png create mode 100644 addons/point_of_sale/static/src/img/steps-bg.png create mode 100644 addons/point_of_sale/static/src/img/validate-icon.png create mode 100644 addons/point_of_sale/static/src/js/pos.coffee create mode 100644 addons/point_of_sale/static/src/js/pos.js create mode 100644 addons/point_of_sale/static/src/pos.html diff --git a/addons/point_of_sale/static/lib/backbone/backbone-0.5.3.min.js b/addons/point_of_sale/static/lib/backbone/backbone-0.5.3.min.js new file mode 100644 index 00000000000..3f0d495dc39 --- /dev/null +++ b/addons/point_of_sale/static/lib/backbone/backbone-0.5.3.min.js @@ -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/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').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 + 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' + diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css new file mode 100644 index 00000000000..184eafe717c --- /dev/null +++ b/addons/point_of_sale/static/src/css/pos.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; } diff --git a/addons/point_of_sale/static/src/css/pos.sass b/addons/point_of_sale/static/src/css/pos.sass new file mode 100644 index 00000000000..c02daec43b2 --- /dev/null +++ b/addons/point_of_sale/static/src/css/pos.sass @@ -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 diff --git a/addons/point_of_sale/static/src/img/backspace.png b/addons/point_of_sale/static/src/img/backspace.png new file mode 100644 index 0000000000000000000000000000000000000000..705051d925e75da92078fcff3aeea61aad4694f1 GIT binary patch literal 5970 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 T*+JTOOASCpL0!H|4jJ|z#dk-> literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/barcode.png b/addons/point_of_sale/static/src/img/barcode.png new file mode 100644 index 0000000000000000000000000000000000000000..fc780b9346bc80d81ca06ac1e37d615b6b2cc23d GIT binary patch literal 916 zcmV;F18e+=P)~2d^aecv-ooi}Lc#!~ps}e5Jyt8^@F&TSr`)9ElUFo3!^Kuke%ODnp&2vAf+w@=OMgoWn3`tJcoqmctfMn)hZ z08`M~+KSGuP81iH@axTrlbOjmwQ@DU#&v^ZwnapSeXe!n06{rv$d q#*6>+hZ7$m$}!5<+vlIWW$^>&l{%%bL^+!P0000_t;do04Ndo6H4JQ4`9vT zFcS@cI2MTY2xTr%A}vGr0J_2`D0>;90>BuEa|tCOfB*vk*6b~$Z7;Cn00000NkvXX Hu0mjfDb{|8 literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/headerbackground.jpg b/addons/point_of_sale/static/src/img/headerbackground.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cec8fa7ccff9e7e7f7aff7c81bd9c782646a13e6 GIT binary patch literal 293 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 literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/search.png b/addons/point_of_sale/static/src/img/search.png new file mode 100644 index 0000000000000000000000000000000000000000..d1705e9a24ab07f65286a311cf2f41510dde7f4d GIT binary patch literal 505 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FZT01FZU(%pXi00007bV*G`2ipZ1 z4Gt$_joP0800DVPL_t(2&wY?FZ`xoKMbG<;1Exa=IDtqEjs!GY=)x~x59#lPouwHK zUqpoy#VNwjxI+vvO<#v9r0LKzUg=z2UCE!H*=)CJt>rd~SSAT4$HxR`XSCaG%AP0u zg`{b!UCPJ7L7)O#bUv1Hp5P*R4Nrdb-VkL zLKH;+bUGc{trqUVfm99;)fmHaxg?4rx)&FF!K{)51%e=;D)Q~& zLBCvI^K~|R_mGI7s#K1S_$(CU+s%g6dad*MocR8p)q0Izt5L7lxxKsNN19Tt zRv8QixUMU*$udNQJkNEv+wt?|1;-eS_^7B-^L_aZ(fX_D)Rz{800000NkvXXu0mjfr7qyI literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/search_reset.gif b/addons/point_of_sale/static/src/img/search_reset.gif new file mode 100644 index 0000000000000000000000000000000000000000..9d4253e018bde41cd581581df52d60fb435ba5ac GIT binary patch literal 195 zcmZ?wbhEHbj;>UFNmSM&} z#f$!Xz3BbUZr6?-+rd#l0Y%z;onh;;y2&|bFCdYaVARrc^Kw3(^LwA<=k@?C$d4-@k9q9u|w(%4Tmi zJ1&W1Div3D zt_)9WHB6>b{;8ig8c4?D-U$Hss%aRB#k>>GA`uwPrh8&gl5p8-xwjGp0oSJKhI7Wi zICpI+?Ck7tMN#m2a}%~F(=;JTQqLH+uV1NB!EJG`7QTJ!Z1KkGDgdC`xm!I&-QsG! z4ndTPpf=QO{4gK3%w z-z9iCF#)Ypyf0g=n`+ccF!ACAbX^Akw63mP^S-21G6_vp0RY#g>8*RW2gVW!=Y3*P z>YKWY6dxURJTT6CS6KVUh%7_xq$G%9@F^dWWyf@oK$0Yg4;7fE2_c`)b5&LG+xE7* z=EY+8Ykwb{b6A!IQ4|phg&y4(a=9G02Yw__$mjF?_JSV)bUE?^{<^j00q%YL*dIrJ z!0l|-4ICcE{=q?i9Qgq^*48}0gU_G)H{F;K=(Jv+nSmzOSpkN$puiBt;Fr%$1&Dj20;l;XTz$3LxB z5a51}#*;}zhK8VaIk@=p1^-@O-wP1vD3M5Da9{wcs)AAqN-4f73jQlEpR0mk|F0@|M^iFh2kuDbx1Wg$5}j?vCv)PA)8 Y0Zt+keVWv3rT_o{07*qoM6N<$g5JO*?*IS* literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/steps-bg.png b/addons/point_of_sale/static/src/img/steps-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe810a77a47273983ee41e4e4b5e2677d2616f4 GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm#=yXM@7uZ;AWpiYuOkD)#(wTUiL5|ATavfC z3xgd9avaN82^8Th@Q5sCVBi)4Va7{$>;3=**-JcqUD94KV&>Eal| zaozO9LCymXJj@4e1;f+$uGCvIS@%!4Z&30*sk;BI@1dRF*Quy<&iu)?U=>3}oeJXv ZR>Ac`O0#Xb)&h-S@O1TaS?83{1ORXpH7Ni9 literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/validate-icon.png b/addons/point_of_sale/static/src/img/validate-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d02604e43ae37d92d9786a2b115ce94712eac5 GIT binary patch literal 760 zcmVPx#24YJ`L;&6Z-T>Yy`kk%-000SaNLh0L01EH`01EH{Laa2H00007bV*G`2ipY% z2Rb!{`ckw200MYPL_t(I%dL~kOH^SP#(&>;&Y3gk%*>f_oN;c+R8SDnqO0H^(A7)r ztUse|`*tO@$Uw;wyIBxI_ZDqJN-VUP$QFhenYt)t%rxh`EgCH?4Z-i@Sw0Uxc-}`{ zxcHRyc%}&J6vWz-Lg)WPh_vtf z+3F46$l-%Kv2Ba9&6oM~d75-Ok49hxDFhcGilF_Hza^ImjWpEl!L}`0S~?jT8mBUz z-X5@mXyLDK31SFREy$c8QzgH&EtdmirjOgJ2E)=Go?(V8~=!TP!#_`VE0 zeDs>)^n4x|jYf^}#`?Wzt(lqm&ZYJ{RHgGoyb7(fX3<|Ja*I=wXeH#bm7Hj*C133g zTSh5$QklqJuq>OhZ&A7&SaRHmQ~U<4ZPyq3uBh+;C>E#by7h#rbRNeIcc9el z@{% + 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() diff --git a/addons/point_of_sale/static/src/js/pos.js b/addons/point_of_sale/static/src/js/pos.js new file mode 100644 index 00000000000..04b7d9991eb --- /dev/null +++ b/addons/point_of_sale/static/src/js/pos.js @@ -0,0 +1,1097 @@ +(function() { + var Pos, Store, db; + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { + for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor; + child.__super__ = parent.prototype; + return child; + }, __indexOf = Array.prototype.indexOf || function(item) { + for (var i = 0, l = this.length; i < l; i++) { + if (this[i] === item) return i; + } + return -1; + }; + 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. + */ + Store = (function() { + function Store() { + var store; + store = localStorage['pos']; + this.data = (store && JSON.parse(store)) || {}; + } + Store.prototype.get = function(key) { + return this.data[key]; + }; + Store.prototype.set = function(key, value) { + this.data[key] = value; + return localStorage['pos'] = JSON.stringify(this.data); + }; + return Store; + })(); + /* + Gets all the necessary data from the OpenERP web client (session, shop data etc.) + */ + Pos = (function() { + function Pos() { + this.build_tree = __bind(this.build_tree, this); this.session.session_login('web-trunk-pos', 'admin', 'admin', __bind(function() { + return $.when(this.fetch('pos.category', ['name', 'parent_id', 'child_id']), this.fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id', 'img'], [['pos_categ_id', '!=', 'false']]), this.fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name']), this.fetch('account.journal', ['auto_cash', 'check_dtls', 'currency', 'name', 'type'])).then(this.build_tree); + }, this)); + } + Pos.prototype.ready = $.Deferred(); + Pos.prototype.session = new db.base.Session('DEBUG'); + Pos.prototype.store = new Store; + Pos.prototype.fetch = function(osvModel, fields, domain, callback, errorCallback) { + var dataSetSearch; + callback = callback || __bind(function(result) { + return this.store.set(osvModel, result); + }, this); + dataSetSearch = new db.base.DataSetSearch(this, osvModel, null, domain); + return dataSetSearch.read_slice(fields, 0, null, callback); + }; + Pos.prototype.push = function(osvModel, record, callback, errorCallback) { + var dataSet; + dataSet = new db.base.DataSet(this, osvModel, null); + return dataSet.create(record, callback, errorCallback); + }; + Pos.prototype.categories = {}; + Pos.prototype.build_tree = function() { + var c, id, _i, _len, _ref, _ref2; + _ref = this.store.get('pos.category'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + c = _ref[_i]; + this.categories[c.id] = { + id: c.id, + name: c.name, + children: c.child_id, + parent: c.parent_id[0], + ancestors: [c.id], + subtree: [c.id] + }; + } + _ref2 = this.categories; + for (id in _ref2) { + c = _ref2[id]; + this.current_category = c; + this.build_ancestors(c.parent); + this.build_subtree(c); + } + this.categories[0] = { + ancestors: [], + children: (function() { + var _j, _len2, _ref3, _results; + _ref3 = this.store.get('pos.category'); + _results = []; + for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) { + c = _ref3[_j]; + if (!(c.parent_id[0] != null)) { + _results.push(c.id); + } + } + return _results; + }).call(this), + subtree: (function() { + var _j, _len2, _ref3, _results; + _ref3 = this.store.get('pos.category'); + _results = []; + for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) { + c = _ref3[_j]; + _results.push(c.id); + } + return _results; + }).call(this) + }; + return this.ready.resolve(); + }; + Pos.prototype.build_ancestors = function(parent) { + if (parent != null) { + this.current_category.ancestors.unshift(parent); + return this.build_ancestors(this.categories[parent].parent); + } + }; + Pos.prototype.build_subtree = function(category) { + var c, _i, _len, _ref, _results; + _ref = category.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + c = _ref[_i]; + this.current_category.subtree.push(c); + _results.push(this.build_subtree(this.categories[c])); + } + return _results; + }; + return Pos; + })(); + window.pos = new Pos; + $(function() { + var App, CashRegister, CashRegisterCollection, Category, CategoryCollection, CategoryView, NumpadState, NumpadView, Order, OrderButtonView, OrderCollection, OrderView, Orderline, OrderlineCollection, OrderlineView, PaymentButtonView, PaymentView, Paymentline, PaymentlineCollection, PaymentlineView, PaypadView, Product, ProductCollection, ProductListView, ProductView, ReceiptLineView, ReceiptView, Shop, ShopView, StepsView; + $('#steps').buttonset(); + /* + --- + Models + --- + */ + CashRegister = (function() { + __extends(CashRegister, Backbone.Model); + function CashRegister() { + CashRegister.__super__.constructor.apply(this, arguments); + } + return CashRegister; + })(); + CashRegisterCollection = (function() { + __extends(CashRegisterCollection, Backbone.Collection); + function CashRegisterCollection() { + CashRegisterCollection.__super__.constructor.apply(this, arguments); + } + CashRegisterCollection.prototype.model = CashRegister; + return CashRegisterCollection; + })(); + Product = (function() { + __extends(Product, Backbone.Model); + function Product() { + Product.__super__.constructor.apply(this, arguments); + } + return Product; + })(); + ProductCollection = (function() { + __extends(ProductCollection, Backbone.Collection); + function ProductCollection() { + ProductCollection.__super__.constructor.apply(this, arguments); + } + ProductCollection.prototype.model = Product; + return ProductCollection; + })(); + Category = (function() { + __extends(Category, Backbone.Model); + function Category() { + Category.__super__.constructor.apply(this, arguments); + } + return Category; + })(); + CategoryCollection = (function() { + __extends(CategoryCollection, Backbone.Collection); + function CategoryCollection() { + CategoryCollection.__super__.constructor.apply(this, arguments); + } + CategoryCollection.prototype.model = Category; + return CategoryCollection; + })(); + /* + 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. + */ + Orderline = (function() { + __extends(Orderline, Backbone.Model); + function Orderline() { + Orderline.__super__.constructor.apply(this, arguments); + } + Orderline.prototype.defaults = { + quantity: 1, + list_price: 0, + discount: 0 + }; + Orderline.prototype.incrementQuantity = function() { + return this.set({ + quantity: (this.get('quantity')) + 1 + }); + }; + Orderline.prototype.getTotal = function() { + return (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100); + }; + Orderline.prototype.exportAsJSON = function() { + var result; + result = { + qty: this.get('quantity'), + price_unit: this.get('list_price'), + discount: this.get('discount'), + product_id: this.get('id') + }; + return result; + }; + return Orderline; + })(); + OrderlineCollection = (function() { + __extends(OrderlineCollection, Backbone.Collection); + function OrderlineCollection() { + OrderlineCollection.__super__.constructor.apply(this, arguments); + } + OrderlineCollection.prototype.model = Orderline; + return OrderlineCollection; + })(); + /* + Every PaymentLine has all the attributes of the corresponding CashRegister. + */ + Paymentline = (function() { + __extends(Paymentline, Backbone.Model); + function Paymentline() { + Paymentline.__super__.constructor.apply(this, arguments); + } + Paymentline.prototype.defaults = { + amount: 0 + }; + Paymentline.prototype.getAmount = function() { + return this.get('amount'); + }; + Paymentline.prototype.exportAsJSON = function() { + var result; + result = { + name: "Payment line", + statement_id: this.get('id'), + account_id: (this.get('account_id'))[0], + journal_id: (this.get('journal_id'))[0], + amount: this.getAmount() + }; + return result; + }; + return Paymentline; + })(); + PaymentlineCollection = (function() { + __extends(PaymentlineCollection, Backbone.Collection); + function PaymentlineCollection() { + PaymentlineCollection.__super__.constructor.apply(this, arguments); + } + PaymentlineCollection.prototype.model = Paymentline; + return PaymentlineCollection; + })(); + Order = (function() { + __extends(Order, Backbone.Model); + function Order() { + Order.__super__.constructor.apply(this, arguments); + } + Order.prototype.defaults = { + validated: false + }; + Order.prototype.initialize = function() { + this.set({ + orderLines: new OrderlineCollection + }); + this.set({ + paymentLines: new PaymentlineCollection + }); + return this.set({ + name: "Order " + this.generateUniqueId() + }); + }; + Order.prototype.generateUniqueId = function() { + return new Date().getTime(); + }; + Order.prototype.addProduct = function(product) { + var existing; + existing = (this.get('orderLines')).get(product.id); + if (existing != null) { + return existing.incrementQuantity(); + } else { + return (this.get('orderLines')).add(new Orderline(product.toJSON())); + } + }; + Order.prototype.addPaymentLine = function(cashRegister) { + var newPaymentline; + newPaymentline = new Paymentline(cashRegister); + /* TODO: Should be 0 for cash-like accounts */ + newPaymentline.set({ + amount: this.getDueLeft() + }); + return (this.get('paymentLines')).add(newPaymentline); + }; + Order.prototype.getName = function() { + return this.get('name'); + }; + Order.prototype.getTotal = function() { + return (this.get('orderLines')).reduce((function(sum, orderLine) { + return sum + orderLine.getTotal(); + }), 0); + }; + Order.prototype.getTotalTaxExcluded = function() { + return this.getTotal() / 1.21; + }; + Order.prototype.getTax = function() { + return this.getTotal() / 1.21 * 0.21; + }; + Order.prototype.getPaidTotal = function() { + return (this.get('paymentLines')).reduce((function(sum, paymentLine) { + return sum + paymentLine.getAmount(); + }), 0); + }; + Order.prototype.getChange = function() { + return this.getPaidTotal() - this.getTotal(); + }; + Order.prototype.getDueLeft = function() { + return this.getTotal() - this.getPaidTotal(); + }; + Order.prototype.exportAsJSON = function() { + var orderLines, paymentLines, result; + orderLines = []; + (this.get('orderLines')).each(__bind(function(item) { + return orderLines.push([0, 0, item.exportAsJSON()]); + }, this)); + paymentLines = []; + (this.get('paymentLines')).each(__bind(function(item) { + return paymentLines.push([0, 0, item.exportAsJSON()]); + }, this)); + result = { + name: this.getName(), + amount_paid: this.getPaidTotal(), + amount_total: this.getTotal(), + amount_tax: this.getTax(), + amount_return: this.getChange(), + lines: orderLines, + statement_ids: paymentLines + }; + return result; + }; + return Order; + })(); + OrderCollection = (function() { + __extends(OrderCollection, Backbone.Collection); + function OrderCollection() { + OrderCollection.__super__.constructor.apply(this, arguments); + } + OrderCollection.prototype.model = Order; + return OrderCollection; + })(); + Shop = (function() { + __extends(Shop, Backbone.Model); + function Shop() { + Shop.__super__.constructor.apply(this, arguments); + } + Shop.prototype.defaults = { + cashRegisters: new CashRegisterCollection(pos.store.get('account.bank.statement')), + orders: new OrderCollection, + products: new ProductCollection + }; + Shop.prototype.initialize = function() { + return (this.get('orders')).bind('remove', __bind(function(removedOrder) { + if ((this.get('orders')).isEmpty()) { + this.addAndSelectOrder(new Order); + } + if ((this.get('selectedOrder')) === removedOrder) { + return this.set({ + selectedOrder: (this.get('orders')).last() + }); + } + }, this)); + }; + Shop.prototype.addAndSelectOrder = function(newOrder) { + (this.get('orders')).add(newOrder); + return this.set({ + selectedOrder: newOrder + }); + }; + return Shop; + })(); + /* + The numpad handles both the choice of the property currently being modified + (quantity, price or discount) and the edition of the corresponding numeric value. + */ + NumpadState = (function() { + __extends(NumpadState, Backbone.Model); + function NumpadState() { + NumpadState.__super__.constructor.apply(this, arguments); + } + NumpadState.prototype.defaults = { + buffer: "0", + mode: "quantity" + }; + NumpadState.prototype.initialize = function(options) { + this.shop = options.shop; + return this.shop.bind('change:selectedOrder', this.reset, this); + }; + NumpadState.prototype.appendNewChar = function(newChar) { + var oldBuffer; + oldBuffer = this.get('buffer'); + if (oldBuffer === '0') { + this.set({ + buffer: newChar + }); + } else if (oldBuffer === '-0') { + this.set({ + buffer: "-" + newChar + }); + } else { + this.set({ + buffer: (this.get('buffer')) + newChar + }); + } + return this.updateTarget(); + }; + NumpadState.prototype.deleteLastChar = function() { + var tempNewBuffer; + tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0"; + if (isNaN(tempNewBuffer)) { + tempNewBuffer = "0"; + } + this.set({ + buffer: tempNewBuffer + }); + return this.updateTarget(); + }; + NumpadState.prototype.switchSign = function() { + var oldBuffer; + oldBuffer = this.get('buffer'); + this.set({ + buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer + }); + return this.updateTarget(); + }; + NumpadState.prototype.changeMode = function(newMode) { + return this.set({ + buffer: "0", + mode: newMode + }); + }; + NumpadState.prototype.reset = function() { + return this.set({ + buffer: "0" + }); + }; + NumpadState.prototype.updateTarget = function() { + var bufferContent, params; + bufferContent = this.get('buffer'); + if (bufferContent && !isNaN(bufferContent)) { + params = {}; + params[this.get('mode')] = parseFloat(bufferContent); + return (this.shop.get('selectedOrder')).selected.set(params); + } + }; + return NumpadState; + })(); + /* + --- + Views + --- + */ + NumpadView = (function() { + __extends(NumpadView, Backbone.View); + function NumpadView() { + NumpadView.__super__.constructor.apply(this, arguments); + } + NumpadView.prototype.initialize = function(options) { + return this.state = options.state; + }; + NumpadView.prototype.events = { + 'click button#numpad-backspace': 'clickDeleteLastChar', + 'click button#numpad-minus': 'clickSwitchSign', + 'click button.number-char': 'clickAppendNewChar', + 'click button.mode-button': 'clickChangeMode' + }; + NumpadView.prototype.clickDeleteLastChar = function() { + return this.state.deleteLastChar(); + }; + NumpadView.prototype.clickSwitchSign = function() { + return this.state.switchSign(); + }; + NumpadView.prototype.clickAppendNewChar = function(event) { + var newChar; + newChar = event.currentTarget.innerText; + return this.state.appendNewChar(newChar); + }; + NumpadView.prototype.clickChangeMode = function(event) { + var newMode; + $('.selected-mode').removeClass('selected-mode'); + $(event.currentTarget).addClass('selected-mode'); + newMode = event.currentTarget.attributes['data-mode'].nodeValue; + return this.state.changeMode(newMode); + }; + return NumpadView; + })(); + /* + Gives access to the payment methods (aka. 'cash registers') + */ + PaypadView = (function() { + __extends(PaypadView, Backbone.View); + function PaypadView() { + PaypadView.__super__.constructor.apply(this, arguments); + } + PaypadView.prototype.initialize = function(options) { + return this.shop = options.shop; + }; + PaypadView.prototype.events = { + 'click button': 'performPayment' + }; + PaypadView.prototype.performPayment = function(event) { + var cashRegister, cashRegisterCollection, cashRegisterId; + cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue; + cashRegisterCollection = this.shop.get('cashRegisters'); + cashRegister = cashRegisterCollection.find(__bind(function(item) { + return (item.get('id')) === parseInt(cashRegisterId, 10); + }, this)); + return (this.shop.get('selectedOrder')).addPaymentLine(cashRegister); + }; + PaypadView.prototype.render = function() { + $(this.el).empty(); + return (this.shop.get('cashRegisters')).each(__bind(function(cashRegister) { + return $(this.el).append((new PaymentButtonView({ + model: cashRegister + })).render()); + }, this)); + }; + return PaypadView; + })(); + PaymentButtonView = (function() { + __extends(PaymentButtonView, Backbone.View); + function PaymentButtonView() { + PaymentButtonView.__super__.constructor.apply(this, arguments); + } + PaymentButtonView.prototype.template = _.template($('#payment-button-template').html()); + PaymentButtonView.prototype.render = function() { + return $(this.el).html(this.template({ + id: this.model.get('id'), + name: (this.model.get('journal_id'))[1] + })); + }; + return PaymentButtonView; + })(); + /* + 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. + */ + StepsView = (function() { + __extends(StepsView, Backbone.View); + function StepsView() { + StepsView.__super__.constructor.apply(this, arguments); + } + StepsView.prototype.initialize = function(options) { + return this.step = "products"; + }; + StepsView.prototype.events = { + 'click input.step-button': 'clickChangeStep' + }; + StepsView.prototype.clickChangeStep = function(event) { + var newStep; + newStep = event.currentTarget.attributes['data-step'].nodeValue; + $('.step-screen').hide(); + $('#' + newStep + '-screen').show(); + return this.step = newStep; + }; + return StepsView; + })(); + /* + Shopping carts. + */ + OrderlineView = (function() { + __extends(OrderlineView, Backbone.View); + function OrderlineView() { + OrderlineView.__super__.constructor.apply(this, arguments); + } + OrderlineView.prototype.tagName = 'tr'; + OrderlineView.prototype.template = _.template($('#orderline-template').html()); + OrderlineView.prototype.initialize = function(options) { + this.model.bind('change', __bind(function() { + $(this.el).hide(); + return this.render(); + }, this)); + this.model.bind('remove', __bind(function() { + return $(this.el).remove(); + }, this)); + this.order = options.order; + return this.numpadState = options.numpadState; + }; + OrderlineView.prototype.events = { + 'click': 'clickHandler' + }; + OrderlineView.prototype.clickHandler = function() { + this.numpadState.reset(); + return this.select(); + }; + OrderlineView.prototype.render = function() { + this.select(); + return $(this.el).html(this.template(this.model.toJSON())).fadeIn(400, function() { + return $('#current-order').scrollTop($(this).offset().top); + }); + }; + OrderlineView.prototype.select = function() { + $('tr.selected').removeClass('selected'); + $(this.el).addClass('selected'); + return this.order.selected = this.model; + }; + return OrderlineView; + })(); + OrderView = (function() { + __extends(OrderView, Backbone.View); + function OrderView() { + OrderView.__super__.constructor.apply(this, arguments); + } + OrderView.prototype.initialize = function(options) { + this.shop = options.shop; + this.numpadState = options.numpadState; + this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this); + return this.bindOrderLineEvents(); + }; + OrderView.prototype.changeSelectedOrder = function() { + this.currentOrderLines.unbind(); + this.bindOrderLineEvents(); + return this.render(); + }; + OrderView.prototype.bindOrderLineEvents = function() { + this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines'); + this.currentOrderLines.bind('add', this.addLine, this); + this.currentOrderLines.bind('change', this.render, this); + return this.currentOrderLines.bind('remove', this.render, this); + }; + OrderView.prototype.addLine = function(newLine) { + $(this.el).append((new OrderlineView({ + model: newLine, + order: this.shop.get('selectedOrder'), + numpadState: this.numpadState + })).render()); + return this.updateSummary(); + }; + OrderView.prototype.render = function() { + $(this.el).empty(); + this.currentOrderLines.each(__bind(function(orderLine) { + return $(this.el).append((new OrderlineView({ + model: orderLine, + order: this.shop.get('selectedOrder'), + numpadState: this.numpadState + })).render()); + }, this)); + return this.updateSummary(); + }; + OrderView.prototype.updateSummary = function() { + var currentOrder, tax, total, totalTaxExcluded; + currentOrder = this.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(); + return $('#total').html(total.toFixed(2)).hide().fadeIn(); + }; + return OrderView; + })(); + /* + "Products" step. + */ + CategoryView = (function() { + __extends(CategoryView, Backbone.View); + function CategoryView() { + CategoryView.__super__.constructor.apply(this, arguments); + } + CategoryView.prototype.template = _.template($('#category-template').html()); + CategoryView.prototype.render = function(ancestors, children) { + var c; + return $(this.el).html(this.template({ + breadcrumb: (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = ancestors.length; _i < _len; _i++) { + c = ancestors[_i]; + _results.push(pos.categories[c]); + } + return _results; + })(), + categories: (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = children.length; _i < _len; _i++) { + c = children[_i]; + _results.push(pos.categories[c]); + } + return _results; + })() + })); + }; + return CategoryView; + })(); + ProductView = (function() { + __extends(ProductView, Backbone.View); + function ProductView() { + ProductView.__super__.constructor.apply(this, arguments); + } + ProductView.prototype.tagName = 'li'; + ProductView.prototype.className = 'product'; + ProductView.prototype.template = _.template($('#product-template').html()); + ProductView.prototype.events = { + 'click a': 'addToOrder' + }; + ProductView.prototype.initialize = function(options) { + return this.shop = options.shop; + }; + ProductView.prototype.addToOrder = function(event) { + /* Preserve the category URL */ event.preventDefault(); + return (this.shop.get('selectedOrder')).addProduct(this.model); + }; + ProductView.prototype.render = function() { + return $(this.el).html(this.template(this.model.toJSON())); + }; + return ProductView; + })(); + ProductListView = (function() { + __extends(ProductListView, Backbone.View); + function ProductListView() { + ProductListView.__super__.constructor.apply(this, arguments); + } + ProductListView.prototype.tagName = 'ol'; + ProductListView.prototype.className = 'product-list'; + ProductListView.prototype.initialize = function(options) { + this.shop = options.shop; + return (this.shop.get('products')).bind('reset', this.render, this); + }; + ProductListView.prototype.render = function() { + $(this.el).empty(); + (this.shop.get('products')).each(__bind(function(product) { + return $(this.el).append((new ProductView({ + model: product, + shop: this.shop + })).render()); + }, this)); + return $('#products-screen').append(this.el); + }; + return ProductListView; + })(); + /* + "Payment" step. + */ + PaymentlineView = (function() { + __extends(PaymentlineView, Backbone.View); + function PaymentlineView() { + PaymentlineView.__super__.constructor.apply(this, arguments); + } + PaymentlineView.prototype.tagName = 'li'; + PaymentlineView.prototype.className = 'paymentline'; + PaymentlineView.prototype.template = _.template($('#paymentline-template').html()); + PaymentlineView.prototype.initialize = function() { + return this.model.bind('change', this.render, this); + }; + PaymentlineView.prototype.events = { + 'keyup input': 'changeAmount' + }; + PaymentlineView.prototype.changeAmount = function(event) { + var newAmount; + newAmount = event.currentTarget.value; + if (newAmount && !isNaN(newAmount)) { + return this.model.set({ + amount: parseFloat(newAmount) + }); + } + }; + PaymentlineView.prototype.render = function() { + return $(this.el).html(this.template({ + name: (this.model.get('journal_id'))[1], + amount: this.model.get('amount') + })); + }; + return PaymentlineView; + })(); + PaymentView = (function() { + __extends(PaymentView, Backbone.View); + function PaymentView() { + PaymentView.__super__.constructor.apply(this, arguments); + } + PaymentView.prototype.initialize = function(options) { + this.shop = options.shop; + this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this); + this.bindPaymentLineEvents(); + return this.bindOrderLineEvents(); + }; + PaymentView.prototype.paymentLineList = function() { + return $(this.el).find('#paymentlines'); + }; + PaymentView.prototype.events = { + 'click button#validate-order': 'validateCurrentOrder' + }; + PaymentView.prototype.validateCurrentOrder = function() { + var callback, currentOrder; + currentOrder = this.shop.get('selectedOrder'); + callback = __bind(function() { + return currentOrder.set({ + validated: true + }); + }, this); + return pos.push('pos.order', currentOrder.exportAsJSON(), callback); + }; + PaymentView.prototype.bindPaymentLineEvents = function() { + this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines'); + this.currentPaymentLines.bind('add', this.addPaymentLine, this); + this.currentPaymentLines.bind('change', this.render, this); + this.currentPaymentLines.bind('remove', this.render, this); + return this.currentPaymentLines.bind('all', this.updatePaymentSummary, this); + }; + PaymentView.prototype.bindOrderLineEvents = function() { + this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines'); + return this.currentOrderLines.bind('all', this.updatePaymentSummary, this); + }; + PaymentView.prototype.changeSelectedOrder = function() { + this.currentPaymentLines.unbind(); + this.bindPaymentLineEvents(); + this.currentOrderLines.unbind(); + this.bindOrderLineEvents(); + return this.render(); + }; + PaymentView.prototype.addPaymentLine = function(newPaymentLine) { + return this.paymentLineList().append((new PaymentlineView({ + model: newPaymentLine + })).render()); + }; + PaymentView.prototype.render = function() { + this.paymentLineList().empty(); + this.currentPaymentLines.each(__bind(function(paymentLine) { + return this.paymentLineList().append((new PaymentlineView({ + model: paymentLine + })).render()); + }, this)); + return this.updatePaymentSummary(); + }; + PaymentView.prototype.updatePaymentSummary = function() { + var currentOrder, dueTotal, paidTotal, remaining, remainingAmount; + currentOrder = this.shop.get('selectedOrder'); + paidTotal = currentOrder.getPaidTotal(); + dueTotal = currentOrder.getTotal(); + $(this.el).find('#payment-due-total').html(dueTotal.toFixed(2)); + $(this.el).find('#payment-paid-total').html(paidTotal.toFixed(2)); + remainingAmount = dueTotal - paidTotal; + remaining = remainingAmount > 0 ? "Due left: " + remainingAmount.toFixed(2) : "Change: " + (-remainingAmount).toFixed(2); + return $('#payment-remaining').html(remaining); + }; + return PaymentView; + })(); + /* + "Receipt" step. + */ + ReceiptLineView = (function() { + __extends(ReceiptLineView, Backbone.View); + function ReceiptLineView() { + ReceiptLineView.__super__.constructor.apply(this, arguments); + } + ReceiptLineView.prototype.tagName = 'li'; + ReceiptLineView.prototype.className = 'receiptline'; + ReceiptLineView.prototype.template = _.template($('#receiptline-template').html()); + ReceiptLineView.prototype.initialize = function() { + return this.model.bind('change', this.render, this); + }; + ReceiptLineView.prototype.render = function() { + return $(this.el).html(this.template(this.model.toJSON())); + }; + return ReceiptLineView; + })(); + ReceiptView = (function() { + __extends(ReceiptView, Backbone.View); + function ReceiptView() { + ReceiptView.__super__.constructor.apply(this, arguments); + } + ReceiptView.prototype.initialize = function(options) { + this.shop = options.shop; + this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this); + this.bindOrderLineEvents(); + return this.bindPaymentLineEvents(); + }; + ReceiptView.prototype.receiptLineList = function() { + return $(this.el).find('#receiptlines'); + }; + ReceiptView.prototype.bindOrderLineEvents = function() { + this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines'); + this.currentOrderLines.bind('add', this.addReceiptLine, this); + this.currentOrderLines.bind('change', this.render, this); + return this.currentOrderLines.bind('remove', this.render, this); + }; + ReceiptView.prototype.bindPaymentLineEvents = function() { + this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines'); + return this.currentPaymentLines.bind('all', this.updateReceiptSummary, this); + }; + ReceiptView.prototype.changeSelectedOrder = function() { + this.currentOrderLines.unbind(); + this.bindOrderLineEvents(); + this.currentPaymentLines.unbind(); + this.bindPaymentLineEvents(); + return this.render(); + }; + ReceiptView.prototype.addReceiptLine = function(newOrderItem) { + this.receiptLineList().append((new ReceiptLineView({ + model: newOrderItem + })).render()); + return this.updateReceiptSummary(); + }; + ReceiptView.prototype.render = function() { + this.receiptLineList().empty(); + this.currentOrderLines.each(__bind(function(orderItem) { + return this.receiptLineList().append((new ReceiptLineView({ + model: orderItem + })).render()); + }, this)); + return this.updateReceiptSummary(); + }; + ReceiptView.prototype.updateReceiptSummary = function() { + var change, currentOrder, tax, total; + currentOrder = this.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)); + return $('#receipt-summary-change').html(change.toFixed(2)); + }; + return ReceiptView; + })(); + OrderButtonView = (function() { + __extends(OrderButtonView, Backbone.View); + function OrderButtonView() { + OrderButtonView.__super__.constructor.apply(this, arguments); + } + OrderButtonView.prototype.tagName = 'li'; + OrderButtonView.prototype.className = 'order-selector-button'; + OrderButtonView.prototype.template = _.template($('#order-selector-button-template').html()); + OrderButtonView.prototype.initialize = function(options) { + this.order = options.order; + this.shop = options.shop; + this.order.bind('destroy', __bind(function() { + return $(this.el).remove(); + }, this)); + return this.shop.bind('change:selectedOrder', __bind(function(shop) { + var selectedOrder; + selectedOrder = shop.get('selectedOrder'); + if (this.order === selectedOrder) { + return this.setButtonSelected(); + } + }, this)); + }; + OrderButtonView.prototype.events = { + 'click button.select-order': 'selectOrder', + 'click button.close-order': 'closeOrder' + }; + OrderButtonView.prototype.selectOrder = function(event) { + return this.shop.set({ + selectedOrder: this.order + }); + }; + OrderButtonView.prototype.setButtonSelected = function() { + $('.selected-order').removeClass('selected-order'); + return $(this.el).addClass('selected-order'); + }; + OrderButtonView.prototype.closeOrder = function(event) { + return this.order.destroy(); + }; + OrderButtonView.prototype.render = function() { + return $(this.el).html(this.template(this.order.toJSON())); + }; + return OrderButtonView; + })(); + ShopView = (function() { + __extends(ShopView, Backbone.View); + function ShopView() { + ShopView.__super__.constructor.apply(this, arguments); + } + ShopView.prototype.initialize = function(options) { + this.shop = options.shop; + (this.shop.get('orders')).bind('add', this.orderAdded, this); + (this.shop.get('orders')).add(new Order); + this.numpadState = new NumpadState({ + shop: this.shop + }); + this.productListView = new ProductListView({ + shop: this.shop + }); + this.paypadView = new PaypadView({ + shop: this.shop, + el: $('#paypad') + }); + this.paypadView.render(); + this.orderView = new OrderView({ + shop: this.shop, + numpadState: this.numpadState, + el: $('#current-order-content') + }); + this.paymentView = new PaymentView({ + shop: this.shop, + el: $('#payment-screen') + }); + this.receiptView = new ReceiptView({ + shop: this.shop, + el: $('#receipt-screen') + }); + this.numpadView = new NumpadView({ + state: this.numpadState, + el: $('#numpad') + }); + return this.stepsView = new StepsView({ + el: $('#steps') + }); + }; + ShopView.prototype.events = { + 'click button#neworder-button': 'createNewOrder' + }; + ShopView.prototype.createNewOrder = function() { + var newOrder; + newOrder = new Order; + (this.shop.get('orders')).add(newOrder); + return this.shop.set({ + selectedOrder: newOrder + }); + }; + ShopView.prototype.orderAdded = function(newOrder) { + var newOrderButton; + newOrderButton = new OrderButtonView({ + order: newOrder, + shop: this.shop + }); + $('#orders').append(newOrderButton.render()); + return newOrderButton.selectOrder(); + }; + return ShopView; + })(); + App = (function() { + __extends(App, Backbone.Router); + function App() { + App.__super__.constructor.apply(this, arguments); + } + App.prototype.routes = { + '': 'category', + 'category/:id': 'category' + }; + App.prototype.initialize = function() { + this.shop = new Shop; + this.shopView = new ShopView({ + shop: this.shop, + el: $('body') + }); + return this.categoryView = new CategoryView; + }; + App.prototype.category = function(id) { + var c, products; + if (id == null) { + id = 0; + } + c = pos.categories[id]; + $('#products-screen').html(this.categoryView.render(c.ancestors, c.children)); + products = pos.store.get('product.product').filter(function(p) { + var _ref; + return _ref = p.pos_categ_id[0], __indexOf.call(c.subtree, _ref) >= 0; + }); + (this.shop.get('products')).reset(products); + $('.searchbox input').keyup(function() { + var m, s; + s = $(this).val().toLowerCase(); + if (s) { + m = products.filter(function(p) { + return p.name.toLowerCase().indexOf(s); + }); + $('.search-clear').fadeIn(); + } else { + m = products; + $('.search-clear').fadeOut(); + } + return (this.shop.get('products')).reset(m); + }); + return $('.search-clear').click(function() { + (this.shop.get('products')).reset(products); + $('.searchbox input').val('').focus(); + return $('.search-clear').fadeOut(); + }); + }; + return App; + })(); + return pos.ready.then(function() { + pos.app = new App; + return Backbone.history.start(); + }); + }); +}).call(this); diff --git a/addons/point_of_sale/static/src/pos.html b/addons/point_of_sale/static/src/pos.html new file mode 100644 index 00000000000..3d8cfab7ba1 --- /dev/null +++ b/addons/point_of_sale/static/src/pos.html @@ -0,0 +1,200 @@ + + + + + Point of Sale + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + +
+
+
+ +
    +
+
+
+
+

Minh Tran

+ +
+
+ +
+ +
+
+ + + + + + + + + + + + +
ProductPriceDisc (%)QtyTotal
+
+
+
    +
  • Subtotal: 0 €
  • +
  • Tax: 0 €
  • +
  • Total: 0 €
  • +
+
+
+
+ + + +
+ + + +
+ + + +
+ + + +
+
+
+
+ +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + +