spree_advanced_reporting 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +5 -0
  3. data/README.md +23 -0
  4. data/Rakefile +20 -0
  5. data/TODO.txt +3 -0
  6. data/app/assets/images/admin/advanced_reporting/asc.gif +0 -0
  7. data/app/assets/images/admin/advanced_reporting/barchart.png +0 -0
  8. data/app/assets/images/admin/advanced_reporting/bg.gif +0 -0
  9. data/app/assets/images/admin/advanced_reporting/close.png +0 -0
  10. data/app/assets/images/admin/advanced_reporting/desc.gif +0 -0
  11. data/app/assets/images/admin/advanced_reporting/menu-current-opposite.png +0 -0
  12. data/app/assets/images/admin/advanced_reporting/open.png +0 -0
  13. data/app/assets/images/admin/advanced_reporting/save.png +0 -0
  14. data/app/assets/images/admin/advanced_reporting/search.png +0 -0
  15. data/app/assets/images/admin/advanced_reporting/usa.png +0 -0
  16. data/app/assets/images/admin/advanced_reporting/world.png +0 -0
  17. data/app/assets/javascripts/admin/advanced_reporting/advanced_reporting.js +46 -0
  18. data/app/assets/javascripts/admin/advanced_reporting/jquery.tablesorter.min.js +4 -0
  19. data/app/assets/stylesheets/admin/advanced_reporting/advanced_reporting.css.erb +18 -0
  20. data/app/assets/stylesheets/pdf.css +15 -0
  21. data/app/controllers/spree/admin/advanced_report_overview_controller.rb +21 -0
  22. data/app/controllers/spree/admin/reports_controller_decorator.rb +127 -0
  23. data/app/helpers/advanced_report_helper.rb +13 -0
  24. data/app/models/ruport/formatter/html_decorator.rb +29 -0
  25. data/app/models/ruport/formatter/wicked_pdf_decorator.rb +12 -0
  26. data/app/views/spree/admin/advanced_report_overview/index.html.erb +117 -0
  27. data/app/views/spree/admin/reports/_advanced_report_criteria.html.erb +64 -0
  28. data/app/views/spree/admin/reports/geo_base.html.erb +61 -0
  29. data/app/views/spree/admin/reports/increment_base.html.erb +63 -0
  30. data/app/views/spree/admin/reports/outstanding.html.erb +16 -0
  31. data/app/views/spree/admin/reports/top_base.html.erb +17 -0
  32. data/config/locales/en.yml +96 -0
  33. data/config/locales/pt-BR.yml +92 -0
  34. data/config/routes.rb +37 -0
  35. data/lib/spree/advanced_report.rb +225 -0
  36. data/lib/spree/advanced_report/geo_report.rb +2 -0
  37. data/lib/spree/advanced_report/geo_report/geo_profit.rb +44 -0
  38. data/lib/spree/advanced_report/geo_report/geo_revenue.rb +44 -0
  39. data/lib/spree/advanced_report/geo_report/geo_units.rb +42 -0
  40. data/lib/spree/advanced_report/increment_report.rb +78 -0
  41. data/lib/spree/advanced_report/increment_report/count.rb +33 -0
  42. data/lib/spree/advanced_report/increment_report/profit.rb +41 -0
  43. data/lib/spree/advanced_report/increment_report/revenue.rb +40 -0
  44. data/lib/spree/advanced_report/increment_report/units.rb +33 -0
  45. data/lib/spree/advanced_report/top_report.rb +2 -0
  46. data/lib/spree/advanced_report/top_report/top_customers.rb +32 -0
  47. data/lib/spree/advanced_report/top_report/top_products.rb +35 -0
  48. data/lib/spree/advanced_report/transaction_report.rb +79 -0
  49. data/lib/spree_advanced_reporting.rb +25 -0
  50. data/spree_advanced_reporting.gemspec +22 -0
  51. metadata +133 -0
@@ -0,0 +1 @@
1
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'ruport', :git => 'git://github.com/iloveitaly/ruport.git', :branch => 'wicked-pdf'
4
+
5
+ gemspec
@@ -0,0 +1,23 @@
1
+ # NOTES:
2
+
3
+ This branch is for use with Spree 1.1.0 and later.
4
+
5
+ For Spree 1.0.x, use the spree-1-0 branch.
6
+
7
+ Forked from what appeared to the be the most up to date for, and made the following general changes:
8
+
9
+ 1. Removed the route that overrides the main admin overview page
10
+ 2. Fixed a warning about ```ADVANCED_REPORTS``` being redefined
11
+ 3. Improved PDF generation ([custom ruport](http://github.com/iloveitaly/ruport/tree/wicked-pdf) uses wicked_pdf instead of the ancient PDF::Writer)
12
+
13
+ ## Includes:
14
+ * Base reports of Revenue, Units, Profit into Daily, Weekly, Monthly, and Yearly increments
15
+ * Geo reports of Revenue, Units divided into states and countries
16
+ * Two "top" reports for top products and top customers
17
+ * The ability to limit reports by order date, "store" (multi-store extension), product, and taxon.
18
+ * The ability to export data in PDF or CSV format.
19
+ * Transaction reports
20
+
21
+ ## Dependencies:
22
+ * Ruport and Ruport-util
23
+ * Google Visualization
@@ -0,0 +1,20 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/packagetask'
4
+ require 'rubygems/package_task'
5
+ require 'rspec/core/rake_task'
6
+ require 'cucumber/rake/task'
7
+ require 'spree_core/testing_support/common_rake'
8
+
9
+ RSpec::Core::RakeTask.new
10
+ Cucumber::Rake::Task.new
11
+
12
+ task :default => [:spec, :cucumber ]
13
+
14
+ spec = eval(File.read('advanced_reporting.gemspec'))
15
+
16
+ desc "Generates a dummy app for testing"
17
+ task :test_app do
18
+ ENV['LIB_NAME'] = 'advanced_reporting'
19
+ Rake::Task['common:test_app'].invoke
20
+ end
@@ -0,0 +1,3 @@
1
+ # Add UI for limiting products / store on frontend
2
+ # Add localization
3
+ # Add SVG formatting? or another plotting tool?
@@ -0,0 +1,46 @@
1
+ // TODO this would look alot cleaner in coffeescript
2
+
3
+ $(function() {
4
+ $('ul#show_data li').click(function() {
5
+ $('ul#show_data li').not(this).removeClass('selected');
6
+ $(this).addClass('selected');
7
+ var id = 'div#' + $(this).attr('id') + '_data';
8
+ $('div.advanced_reporting_data').not($(id)).hide();
9
+ $(id).show();
10
+ });
11
+
12
+ if($('table.tablesorter tbody').length) {
13
+ $('table.tablesorter').tablesorter();
14
+ $('table.tablesorter').bind("sortEnd", function() {
15
+ var section = $(this).parent().attr('id');
16
+ var even = true;
17
+ $.each($('div#' + section + ' table tr'), function(i, j) {
18
+ $(j).removeClass('even').removeClass('odd');
19
+ $(j).addClass(even ? 'even' : 'odd');
20
+ even = !even;
21
+ });
22
+ });
23
+ }
24
+
25
+ if($('input#product_id').length > 0) {
26
+ $('select#advanced_reporting_product_id').val($('input#product_id').val());
27
+ }
28
+ if($('input#taxon_id').length > 0) {
29
+ $('select#advanced_reporting_taxon_id').val($('input#taxon_id').val());
30
+ }
31
+ $('div#advanced_report_search form').submit(function() {
32
+ $('div#advanced_report_search form').attr('action', $('select#report').val());
33
+ });
34
+
35
+ $('select#report').change(function() {
36
+ var value = $(this).val()
37
+ $('div#advanced_report > form').action = value
38
+
39
+ if(value.match(/\/count$/) || value.match(/\/top_products$/)) {
40
+ $('select#advanced_reporting_product_id,select#advanced_reporting_taxon_id').val('');
41
+ $('div#taxon_products').hide();
42
+ } else {
43
+ $('div#taxon_products').show();
44
+ }
45
+ }).trigger('change')
46
+ })
@@ -0,0 +1,4 @@
1
+
2
+ (function($){$.extend({tablesorter:new
3
+ function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,rows,-1,i);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,rows,rowIndex,cellIndex){var l=parsers.length,node=false,nodeValue=false,keepLooking=true;while(nodeValue==''&&keepLooking){rowIndex++;if(rows[rowIndex]){node=getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex);nodeValue=trimAndGetNodeText(table.config,node);if(table.config.debug){log('Checking if value was empty on row:'+rowIndex);}}else{keepLooking=false;}}for(var i=1;i<l;i++){if(parsers[i].is(nodeValue,table,node)){return parsers[i];}}return parsers[0];}function getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex){return rows[rowIndex].cells[cellIndex];}function trimAndGetNodeText(config,node){return $.trim(getElementText(config,node));}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=$(table.tBodies[0].rows[i]),cols=[];if(c.hasClass(table.config.cssChildRow)){cache.row[cache.row.length-1]=cache.row[cache.row.length-1].add(c);continue;}cache.row.push(c);for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c[0].cells[j]),table,c[0].cells[j]));}cols.push(cache.normalized.length);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){var text="";if(!node)return"";if(!config.supportsTextContent)config.supportsTextContent=node.textContent||false;if(config.textExtraction=="simple"){if(config.supportsTextContent){text=node.textContent;}else{if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){text=node.childNodes[0].innerHTML;}else{text=node.innerHTML;}}}else{if(typeof(config.textExtraction)=="function"){text=config.textExtraction(node);}else{text=$(node).text();}}return text;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){var pos=n[i][checkCell];rows.push(r[pos]);if(!table.config.appender){var l=r[pos].length;for(var j=0;j<l;j++){tableBody[0].appendChild(r[pos][j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false;var header_index=computeTableHeaderCellIndexes(table);$tableHeaders=$(table.config.selectorHeaders,table).each(function(index){this.column=header_index[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(table.config.sortInitialOrder);this.count=this.order;if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(checkHeaderOptionsSortingLocked(table,index))this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(table,index);if(!this.sortDisabled){var $th=$(this).addClass(table.config.cssHeader);if(table.config.onRenderHeader)table.config.onRenderHeader.apply($th);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function computeTableHeaderCellIndexes(t){var matrix=[];var lookup={};var thead=t.getElementsByTagName('THEAD')[0];var trs=thead.getElementsByTagName('TR');for(var i=0;i<trs.length;i++){var cells=trs[i].cells;for(var j=0;j<cells.length;j++){var c=cells[j];var rowIndex=c.parentNode.rowIndex;var cellId=rowIndex+"-"+c.cellIndex;var rowSpan=c.rowSpan||1;var colSpan=c.colSpan||1
4
+ var firstAvailCol;if(typeof(matrix[rowIndex])=="undefined"){matrix[rowIndex]=[];}for(var k=0;k<matrix[rowIndex].length+1;k++){if(typeof(matrix[rowIndex][k])=="undefined"){firstAvailCol=k;break;}}lookup[cellId]=firstAvailCol;for(var k=rowIndex;k<rowIndex+rowSpan;k++){if(typeof(matrix[k])=="undefined"){matrix[k]=[];}var matrixrow=matrix[k];for(var l=firstAvailCol;l<firstAvailCol+colSpan;l++){matrixrow[l]="x";}}}}return lookup;}function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){return(v.toLowerCase()=="desc")?1:0;}else{return(v==1)?1:0;}}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(table.config.parsers[c].type=="text")?((order==0)?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c)):((order==0)?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c));var e="e"+i;dynamicExp+="var "+e+" = "+s;dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";if(table.config.debug){benchmark("Evaling expression:"+dynamicExp,new Date());}eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function makeSortFunction(type,direction,index){var a="a["+index+"]",b="b["+index+"]";if(type=='text'&&direction=='asc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+a+" < "+b+") ? -1 : 1 )));";}else if(type=='text'&&direction=='desc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+b+" < "+a+") ? -1 : 1 )));";}else if(type=='numeric'&&direction=='asc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+a+" - "+b+"));";}else if(type=='numeric'&&direction=='desc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+b+" - "+a+"));";}};function makeSortText(i){return"((a["+i+"] < b["+i+"]) ? -1 : ((a["+i+"] > b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){var me=this;setTimeout(function(){me.config.parsers=buildParserCache(me,$headers);cache=buildCache(me);},1);}).bind("updateCell",function(e,cell){var config=this.config;var pos=[(cell.parentNode.rowIndex-1),cell.cellIndex];cache.normalized[pos[0]][pos[1]]=config.parsers[pos[1]].format(getElementText(config,cell),cell);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){return/^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g,'')));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLocaleLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}var $tr,row=-1,odd;$("tr:visible",table.tBodies[0]).each(function(i){$tr=$(this);if(!$tr.hasClass(table.config.cssChildRow))row++;odd=(row%2==0);$tr.removeClass(table.config.widgetZebra.css[odd?0:1]).addClass(table.config.widgetZebra.css[odd?1:0])});if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery);
@@ -0,0 +1,18 @@
1
+ /* without overriding the default spree_core in spree admin you can't include CSS */
2
+ /* easier to precompile this asset and include manually via stylesheet tag */
3
+
4
+ div.canvas { padding-bottom: 20px; }
5
+
6
+ table.tablesorter th.header { background-image: url(<%= asset_path 'admin/advanced_reporting/bg.gif'%>); cursor: pointer; font-weight: bold; background-repeat: no-repeat; background-position: center left; padding-left: 20px; border-right: 1px solid #dad9c7; margin-left: -1px; }
7
+ table.tablesorter th.headerSortUp { background-image: url(<%= asset_path 'admin/advanced_reporting/asc.gif'%>); background-color: #3399FF; }
8
+ table.tablesorter th.headerSortDown { background-image: url(<%= asset_path 'admin/advanced_reporting/desc.gif'%>); background-color: #3399FF; }
9
+
10
+ #sidebar #advanced_report_search h1,
11
+ #sidebar #advanced_report_search h2,
12
+ #sidebar #advanced_report_search h3 {
13
+ margin-left:0;
14
+ }
15
+
16
+ #sidebar #advanced_report_search select {
17
+ max-width: 100%;
18
+ }
@@ -0,0 +1,15 @@
1
+ body { font:normal 100%/1.25 Helvetica, Arial, Verdana, sans-serif; }
2
+
3
+ .odd { background-color: #e6e6e6; }
4
+
5
+ /*
6
+ * Page breaking seems like a tricky issue...
7
+ * http://stackoverflow.com/questions/1763639/how-to-deal-with-page-breaks-when-printing-a-large-html-table
8
+ * http://stackoverflow.com/questions/8786755/wkhtmltopdf-characters-in-single-line-partially-cut-between-pages
9
+ * http://stackoverflow.com/questions/1539876/controlling-css-page-breaks-when-printing-in-webkit
10
+ *
11
+ * as long as vertical-align:bottom is set on all tds, and a recent version of the binary is installed,
12
+ * things seem to work well enough (not clean breaks, but nothing is cut off)
13
+ */
14
+
15
+ td { vertical-align: bottom; padding:5px; }
@@ -0,0 +1,21 @@
1
+ class Spree::Admin::AdvancedReportOverviewController < Spree::Admin::BaseController
2
+ def index
3
+ @reports = Spree::Admin::ReportsController::ADVANCED_REPORTS
4
+ @products = Spree::Product.all
5
+ @taxons = Spree::Taxon.all
6
+ if defined?(MultiDomainExtension)
7
+ @stores = Store.all
8
+ end
9
+ @report = Spree::AdvancedReport::IncrementReport::Revenue.new({ :search => {} })
10
+ @top_products_report = Spree::AdvancedReport::TopReport::TopProducts.new({ :search => {} }, 5)
11
+ @top_customers_report = Spree::AdvancedReport::TopReport::TopCustomers.new({ :search => {} }, 5)
12
+ @top_customers_report.ruportdata.remove_column(I18n.t("adv_report.units"))
13
+
14
+ # From overview_dashboard, Cleanup eventually
15
+ orders = Spree::Order.find(:all, :order => "completed_at DESC", :limit => 10, :include => :line_items, :conditions => "completed_at is not null")
16
+ @last_orders = orders.inject([]) { |arr, o| arr << [o.bill_address.firstname, o.line_items.sum(:quantity), o.total, o.number]; arr }
17
+ @best_taxons = Spree::Taxon.connection.select_rows("select t.name, count(li.quantity) from spree_line_items li inner join spree_variants v on
18
+ li.variant_id = v.id inner join spree_products p on v.product_id = p.id inner join spree_products_taxons pt on p.id = pt.product_id
19
+ inner join spree_taxons t on pt.taxon_id = t.id where t.taxonomy_id = #{Spree::Taxonomy.last.id} group by t.name order by count(li.quantity) desc limit 5;")
20
+ end
21
+ end
@@ -0,0 +1,127 @@
1
+ require_dependency 'spree/admin/reports_controller'
2
+
3
+ Spree::Admin::ReportsController.class_eval do
4
+ # until https://github.com/spree/spree/issues/1863 is taken care of
5
+ # this is a workaround hack to get the report definitions to load
6
+
7
+ I18n.load_path << Spree::AdvancedReporting::Engine.config.paths["config/locales"].first
8
+ I18n.locale = Spree::Config[:default_locale]
9
+ I18n.reload!
10
+
11
+ # TODO there has got to be a more ruby way to do this...
12
+ ADVANCED_REPORTS ||= {}
13
+ [ :outstanding, :revenue, :units, :profit, :count, :top_products, :top_customers, :geo_revenue, :geo_units, :geo_profit, :transactions].each do |x|
14
+ # TODO we should pull the name + description for the report models themselves rather than redefining them as I18n definitions
15
+ ADVANCED_REPORTS[x]= {name: I18n.t("adv_report.#{x}"), :description => I18n.t("adv_report.#{x}")}
16
+ end
17
+
18
+ Spree::Admin::ReportsController::AVAILABLE_REPORTS.merge!(ADVANCED_REPORTS)
19
+
20
+ before_filter :basic_report_setup, :actions => ADVANCED_REPORTS.keys
21
+
22
+ def basic_report_setup
23
+ @reports = ADVANCED_REPORTS
24
+ @products = Spree::Product.all
25
+ @taxons = Spree::Taxon.all
26
+ if defined?(MultiDomainExtension)
27
+ @stores = Store.all
28
+ end
29
+ end
30
+
31
+ def geo_report_render(filename)
32
+ params[:advanced_reporting] ||= {}
33
+ params[:advanced_reporting]["report_type"] = params[:advanced_reporting]["report_type"].to_sym if params[:advanced_reporting]["report_type"]
34
+ params[:advanced_reporting]["report_type"] ||= :state
35
+ respond_to do |format|
36
+ format.html { render :template => "spree/admin/reports/geo_base" }
37
+ format.pdf { send_data @report.ruportdata[params[:advanced_reporting]['report_type']].to_pdf }
38
+ format.csv { send_data @report.ruportdata[params[:advanced_reporting]['report_type']].to_csv }
39
+ end
40
+ end
41
+
42
+ def base_report_top_render(filename)
43
+ respond_to do |format|
44
+ format.html { render :template => "spree/admin/reports/top_base" }
45
+ format.pdf { send_data @report.ruportdata.to_pdf }
46
+ format.csv { send_data view_context.strip_tags(@report.ruportdata.to_csv) }
47
+ end
48
+ end
49
+
50
+ def base_report_render(filename)
51
+ params[:advanced_reporting] ||= {}
52
+ params[:advanced_reporting]["report_type"] = params[:advanced_reporting]["report_type"].to_sym if params[:advanced_reporting]["report_type"]
53
+ params[:advanced_reporting]["report_type"] ||= I18n.t("adv_report.daily").downcase.to_sym
54
+ respond_to do |format|
55
+ format.html { render :template => "spree/admin/reports/increment_base" }
56
+ format.pdf do
57
+ if params[:advanced_reporting]["report_type"] == :all
58
+ send_data @report.all_data.to_pdf
59
+ else
60
+ send_data @report.ruportdata[params[:advanced_reporting]["report_type"]].to_pdf
61
+ end
62
+ end
63
+ format.csv do
64
+ if params[:advanced_reporting]["report_type"] == :all
65
+ send_data @report.all_data.to_csv
66
+ else
67
+ send_data @report.ruportdata[params[:advanced_reporting]['report_type']].to_csv
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def outstanding
74
+ @orders = Spree::Order.complete.where("state != 'canceled'").select{ |o| o.outstanding_balance? }
75
+ @outstanding_balance = @orders.inject(0){ |outstanding, o| outstanding += o.outstanding_balance }
76
+ end
77
+
78
+ def revenue
79
+ @report = Spree::AdvancedReport::IncrementReport::Revenue.new(params)
80
+ base_report_render("revenue")
81
+ end
82
+
83
+ def units
84
+ @report = Spree::AdvancedReport::IncrementReport::Units.new(params)
85
+ base_report_render("units")
86
+ end
87
+
88
+ def profit
89
+ @report = Spree::AdvancedReport::IncrementReport::Profit.new(params)
90
+ base_report_render("profit")
91
+ end
92
+
93
+ def count
94
+ @report = Spree::AdvancedReport::IncrementReport::Count.new(params)
95
+ base_report_render("profit")
96
+ end
97
+
98
+ def top_products
99
+ @report = Spree::AdvancedReport::TopReport::TopProducts.new(params, 4)
100
+ base_report_top_render("top_products")
101
+ end
102
+
103
+ def top_customers
104
+ @report = Spree::AdvancedReport::TopReport::TopCustomers.new(params, 4)
105
+ base_report_top_render("top_customers")
106
+ end
107
+
108
+ def geo_revenue
109
+ @report = Spree::AdvancedReport::GeoReport::GeoRevenue.new(params)
110
+ geo_report_render("geo_revenue")
111
+ end
112
+
113
+ def geo_units
114
+ @report = Spree::AdvancedReport::GeoReport::GeoUnits.new(params)
115
+ geo_report_render("geo_units")
116
+ end
117
+
118
+ def geo_profit
119
+ @report = Spree::AdvancedReport::GeoReport::GeoProfit.new(params)
120
+ geo_report_render("geo_profit")
121
+ end
122
+
123
+ def transactions
124
+ @report = Spree::AdvancedReport::TransactionReport.new(params)
125
+ base_report_top_render("transactions")
126
+ end
127
+ end
@@ -0,0 +1,13 @@
1
+ module AdvancedReportHelper
2
+ def order_states_options
3
+ order_states.inject([]){ |acc, value| acc << [t("order_state.#{value}"), value]; acc}
4
+ end
5
+
6
+ def order_states
7
+ if Spree::Order.respond_to? :progress_states
8
+ Spree::Order.progress_states.unshift "complete"
9
+ else
10
+ Spree::Order.state_machines[:state].states.map(&:name)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ Ruport::Formatter::HTML.class_eval do
2
+ # Renders individual rows for the table.
3
+ def build_row(data = self.data)
4
+ @odd = !@odd
5
+ klass = @odd ? "odd" : "even"
6
+ output <<
7
+ "\t\t<tr class=\"#{klass}\">\n\t\t\t<td>" +
8
+ data.to_a.join("</td>\n\t\t\t<td>") +
9
+ "</td>\n\t\t</tr>\n"
10
+ end
11
+
12
+ def html_table
13
+ @odd = false
14
+ "<table class=\"tablesorter\">\n" << yield << "</table>\n"
15
+ end
16
+
17
+ def build_table_header
18
+ output << "\t<table class=\"tablesorter\">\n"
19
+ unless data.column_names.empty? || !options.show_table_headers
20
+ output << "\t\t<thead><tr>\n\t\t\t<th>" +
21
+ data.column_names.join("</th>\n\t\t\t<th>") +
22
+ "</th>\n\t\t</tr></thead>\n"
23
+ end
24
+ end
25
+
26
+ def build_group_header
27
+ output << "\t<h1>#{data.name}</h1>\n"
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ Ruport::Formatter::WickedPDF.class_eval do
2
+ def finalize_table
3
+ # TODO would be great to eliminate this hack
4
+
5
+ # I think the reason the helper doesn't work is because of the pdf_from_string call vs the standard render :pdf
6
+ # <%= wicked_pdf_stylesheet_link_tag "pdf" %>
7
+
8
+ output.replace(WickedPdf.new.pdf_from_string("<style>" + Rails.application.assets.find_asset("pdf").body + "</style>" + output,
9
+ :print_media_type => false
10
+ ))
11
+ end
12
+ end
@@ -0,0 +1,117 @@
1
+ <table width="100%">
2
+ <tr>
3
+ <td colspan="3">
4
+ <% @report.increments.each do |type| -%>
5
+ <div id="<%= type.to_s %>_data" <%= type.to_s == 'daily' ? '' : 'style=display:none;' %> class="advanced_reporting_data">
6
+ <h3><%=t "adv_report."+ type.to_s %> <%=t "adv_report."+@report.name.downcase %></h3>
7
+ <div id="<%= type.to_s %>_chart_revenue"></div>
8
+ </div>
9
+ <% end -%>
10
+ </td>
11
+ </tr>
12
+ <tr>
13
+ <td width="33%">
14
+ <h3><%=t("adv_report.top_products_by_revenue")%></h3>
15
+ <div id="top_products_chart"></div>
16
+ <h3>Top Taxons by Quantity</h3>
17
+ <div id="top_taxons_chart"></div>
18
+ </td>
19
+ <td width="33%">
20
+ <h3><%=t("adv_report.top_customers_by_revenue")%></h3>
21
+ <div id="top_customers">
22
+ <%= raw @top_customers_report.ruportdata.to_html %>
23
+ </div>
24
+ </td>
25
+ <td width="33%">
26
+ <h3><%=t("adv_report.last_10_orders")%></h3>
27
+ <div id="recent_orders">
28
+ <table class="tablesorter">
29
+ <thead><tr>
30
+ <th><%=t(:order_number)%></th>
31
+ <th><%=t(:order)%></th>
32
+ <th><%=t("adv_report.items_count")%></th>
33
+ <th><%=t(:total)%></th>
34
+ </tr></thead>
35
+ <% @last_orders.each do |order| %>
36
+ <tr class="odd">
37
+ <td><%=link_to order[3], admin_order_path(:id => order[3]) %></td>
38
+ <td><%= order[0] %></td>
39
+ <td><%= order[1] %></td>
40
+ <td><%= number_to_currency order[2] %></td>
41
+ </tr>
42
+ <% end -%>
43
+ </table>
44
+ </div>
45
+ </td>
46
+ </tr>
47
+ </table>
48
+
49
+ <% content_for :sidebar do %>
50
+ <div class="report_details">
51
+ <h1><%= t("adv_report.dashboard")%></h1>
52
+ </div>
53
+
54
+ <ul id="show_data">
55
+ <% @report.increments.each do |inc| %>
56
+ <li <%= inc == :daily ? 'class=selected' : '' %> id="<%= inc %>">
57
+ <label><%= t("adv_report.#{inc}") %></label><br />
58
+ <a href="<%= @report.download_url('/admin/reports/revenue', 'csv', inc) %>">CSV</a>
59
+ <a href="<%= @report.download_url('/admin/reports/revenue', 'pdf', inc) %>">PDF</a>
60
+ </li>
61
+ <% end -%>
62
+ </ul>
63
+ <%= render :partial => 'spree/admin/reports/advanced_report_criteria', :locals => {} %>
64
+ <% end %>
65
+
66
+ <% content_for :head do -%>
67
+ <script src="http://www.google.com/jsapi"></script>
68
+ <script >
69
+ google.load('visualization', '1', {'packages': ['corechart']});
70
+ </script>
71
+ <script >
72
+ google.setOnLoadCallback(drawChart);
73
+
74
+ function drawChart() {
75
+ <% @report.increments.each do |type| -%>
76
+ var data_<%= type.to_s %> = new google.visualization.DataTable();
77
+ data_<%= type.to_s %>.addColumn('string', 'Display');
78
+ data_<%= type.to_s %>.addColumn('number', '<%= @report.name %>');
79
+ data_<%= type.to_s %>.addRows(<%= @report.ruportdata[type].size %>);
80
+ <% @report.ruportdata[type].each_with_index do |p, i| %>
81
+ data_<%= type.to_s %>.setValue(<%= i.to_s %>, 0, "<%= p.data[type.to_s.capitalize] %>");
82
+ data_<%= type.to_s %>.setValue(<%= i.to_s %>, 1, <%= p.data[@report.column].to_s.gsub(/^\$/, '') %>);
83
+ <% end -%>
84
+ var chart_<%= type.to_s %> = new google.visualization.ColumnChart(document.getElementById('<%= type.to_s %>_chart_revenue'));
85
+ chart_<%= type.to_s %>.draw(data_<%= type.to_s %>, {
86
+ width: 800,
87
+ height: 400,
88
+ legend : 'none',
89
+ title: '' });
90
+ <% end -%>
91
+
92
+ var product_data = new google.visualization.DataTable();
93
+ product_data.addColumn('string', 'Product');
94
+ product_data.addColumn('number', 'Revenue');
95
+ product_data.addRows(<%= @top_products_report.ruportdata.size %>);
96
+ <% @top_products_report.ruportdata.each_with_index do |p, i| %>
97
+ product_data.setValue(<%= i.to_s %>, 0, "<%= p.data["Product Name"].gsub('"', '\"') %>");
98
+ product_data.setValue(<%= i.to_s %>, 1, <%= p.data["Revenue"].to_s.gsub(/^\$/, '') %>);
99
+ <% end -%>
100
+
101
+ var chart = new google.visualization.PieChart(document.getElementById('top_products_chart'));
102
+ chart.draw(product_data, {width: "100%", height: 300, title: ''});
103
+
104
+ var taxon_data = new google.visualization.DataTable();
105
+ taxon_data.addColumn('string', 'Taxon');
106
+ taxon_data.addColumn('number', 'Orders');
107
+ taxon_data.addRows(<%= @best_taxons.size %>);
108
+ <% @best_taxons.each_with_index do |p, i| %>
109
+ taxon_data.setValue(<%= i.to_s %>, 0, "<%= p[0] %>");
110
+ taxon_data.setValue(<%= i.to_s %>, 1, <%= p[1] %>);
111
+ <% end -%>
112
+
113
+ var taxon_chart = new google.visualization.PieChart(document.getElementById('top_taxons_chart'));
114
+ taxon_chart.draw(taxon_data, {width: "100%", height: 300, title: ''});
115
+ }
116
+ </script>
117
+ <% end -%>