diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index b4d6585ec2..60ed1a4e03 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -1,11 +1,14 @@ require 'csv' require 'open_food_web/order_and_distributor_report' require 'open_food_web/group_buy_report' +require 'open_food_web/order_grouper' Spree::Admin::ReportsController.class_eval do Spree::Admin::ReportsController::AVAILABLE_REPORTS.merge!({:orders_and_distributors => {:name => "Orders And Distributors", :description => "Orders with distributor details"}}) Spree::Admin::ReportsController::AVAILABLE_REPORTS.merge!({:group_buys => {:name => "Group Buys", :description => "Orders by supplier and variant"}}) + Spree::Admin::ReportsController::AVAILABLE_REPORTS.merge!({:bulk_coop => {:name => "Bulk Co-Op", :description => "Reports for Bulk Co-Op orders"}}) + Spree::Admin::ReportsController::AVAILABLE_REPORTS.merge!({:payments => {:name => "Payment Reports", :description => "Reports for Payments"}}) def orders_and_distributors params[:q] = {} unless params[:q] @@ -67,4 +70,244 @@ Spree::Admin::ReportsController.class_eval do end end + def bulk_coop + params[:q] = {} unless params[:q] + + if params[:q][:created_at_gt].blank? + params[:q][:created_at_gt] = Time.zone.now.beginning_of_month + else + params[:q][:created_at_gt] = Time.zone.parse(params[:q][:created_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month + end + + if params[:q] && !params[:q][:created_at_lt].blank? + params[:q][:created_at_lt] = Time.zone.parse(params[:q][:created_at_lt]).end_of_day rescue "" + end + params[:q][:meta_sort] ||= "created_at.desc" + + @search = Spree::Order.complete.search(params[:q]) + orders = @search.result + line_items = orders.map { |o| o.line_items }.flatten + + @distributors = Spree::Distributor.all + @report_type = params[:report_type] + + case params[:report_type] + when "bulk_coop_supplier_report" + + header = ["Supplier", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total"] + + columns = [ proc { |lis| lis.first.variant.product.supplier.name }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| "UNIT SIZE" }, + proc { |lis| lis.first.variant.options_text }, + proc { |lis| lis.first.variant.weight }, + proc { |lis| lis.sum { |li| li.quantity } }, + proc { |lis| lis.sum { |li| li.max_quantity || 0 } } ] + + rules = [ { group_by: proc { |li| li.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name }, + summary_columns: [ proc { |lis| lis.first.variant.product.supplier.name }, + proc { |lis| lis.first.variant.product.name }, proc { |lis| "UNIT SIZE" }, + proc { |lis| "" }, proc { |lis| "" }, + proc { |lis| lis.sum { |li| li.quantity * li.variant.weight } }, + proc { |lis| lis.sum { |li| (li.max_quantity || 0) * li.variant.weight } } ] }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.options_text } } ] + + when "bulk_coop_allocation" + + header = ["Customer", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total"] + + columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| "UNIT SIZE" }, + proc { |lis| lis.first.variant.options_text }, + proc { |lis| lis.first.variant.weight }, + proc { |lis| lis.sum { |li| li.quantity } }, + proc { |lis| lis.sum { |li| li.max_quantity || 0 } } ] + + rules = [ { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.options_text } }, + { group_by: proc { |li| li.order.user }, + sort_by: proc { |user| user.to_s } } ] + + when "bulk_coop_packing_sheets" + + header = ["Customer", "Product", "Variant", "Sum Total"] + + columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| lis.first.variant.options_text }, + proc { |lis| lis.sum { |li| li.quantity } } ] + + rules = [ { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.options_text } }, + { group_by: proc { |li| li.order.user }, + sort_by: proc { |user| user.to_s } } ] + + when "bulk_coop_customer_payments" + + header = ["Customer", "Date of Order", "Total Cost", "Amount Owing", "Amount Paid"] + + columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, + proc { |lis| lis.first.order.created_at.to_s }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.total } }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.outstanding_balance } }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.payment_total } } ] + + rules = [ { group_by: proc { |li| li.order.user }, + sort_by: proc { |user| user.to_s } }, + { group_by: proc { |li| li.order }, + sort_by: proc { |order| order.created_at } } ] + + else # List all line items + + header = ["Supplier", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total"] + + columns = [ proc { |lis| lis.first.variant.product.supplier.name }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| "UNIT SIZE" }, + proc { |lis| lis.first.variant.options_text }, + proc { |lis| lis.first.variant.weight }, + proc { |lis| lis.sum { |li| li.quantity } }, + proc { |lis| lis.sum { |li| li.max_quantity || 0 } } ] + + rules = [ { group_by: proc { |li| li.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.options_text } } ] + end + + order_grouper = OpenFoodWeb::OrderGrouper.new rules, columns + + @header = header + @table = order_grouper.table(line_items) + csv_file_name = "bulk_coop.csv" + + render_report(@header, @table, params[:csv], csv_file_name) + end + + def payments + params[:q] = {} unless params[:q] + + if params[:q][:created_at_gt].blank? + params[:q][:created_at_gt] = Time.zone.now.beginning_of_month + else + params[:q][:created_at_gt] = Time.zone.parse(params[:q][:created_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month + end + + if params[:q] && !params[:q][:created_at_lt].blank? + params[:q][:created_at_lt] = Time.zone.parse(params[:q][:created_at_lt]).end_of_day rescue "" + end + params[:q][:meta_sort] ||= "created_at.desc" + + @search = Spree::Order.complete.search(params[:q]) + orders = @search.result + payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments + + @distributors = Spree::Distributor.all + @report_type = params[:report_type] + + case params[:report_type] + when "payments_by_payment_type" + table_items = payments + + header = ["Payment State", "Distributor", "Payment Type", "Total ($)"] + + columns = [ proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum { |payment| payment.amount } } ] + + rules = [ { group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| payment.payment_method }, + sort_by: proc { |method| method.name } } ] + + when "itemised_payment_totals" + table_items = orders + + header = ["Payment State", "Distributor", "Product Total ($)", "Shipping Total ($)", "Outstanding Balance ($)", "Total ($)"] + + columns = [ proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.sum { |o| o.item_total } }, + proc { |orders| orders.sum { |o| o.ship_total } }, + proc { |orders| orders.sum { |o| o.outstanding_balance } }, + proc { |orders| orders.sum { |o| o.total } } ] + + rules = [ { group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + + when "payment_totals" + table_items = orders + + header = ["Payment State", "Distributor", "Product Total ($)", "Shipping Total ($)", "Total ($)", "EFT ($)", "PayPal ($)", "Outstanding Balance ($)"] + + columns = [ proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.sum { |o| o.item_total } }, + proc { |orders| orders.sum { |o| o.ship_total } }, + proc { |orders| orders.sum { |o| o.total } }, + proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "EFT") }.sum { |payment| payment.amount } } }, + proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "PayPal") }.sum{ |payment| payment.amount } } }, + proc { |orders| orders.sum { |o| o.outstanding_balance } } ] + + rules = [ { group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + + else + table_items = payments + + header = ["Payment State", "Distributor", "Payment Type", "Total ($)"] + + columns = [ proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum { |payment| payment.amount } } ] + + rules = [ { group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| payment.payment_method }, + sort_by: proc { |method| method.name } } ] + + end + + order_grouper = OpenFoodWeb::OrderGrouper.new rules, columns + + @header = header + @table = order_grouper.table(table_items) + csv_file_name = "payments.csv" + + render_report(@header, @table, params[:csv], csv_file_name) + + end + + def render_report (header, table, create_csv, csv_file_name) + unless create_csv + render :html => table + else + csv_string = CSV.generate do |csv| + csv << header + table.each { |row| csv << row } + end + send_data csv_string, :filename => csv_file_name + end + end end diff --git a/app/views/spree/admin/reports/bulk_coop.html.haml b/app/views/spree/admin/reports/bulk_coop.html.haml new file mode 100644 index 0000000000..8c570e8de1 --- /dev/null +++ b/app/views/spree/admin/reports/bulk_coop.html.haml @@ -0,0 +1,39 @@ += form_for @search, :url => spree.bulk_coop_admin_reports_path do |f| + = label_tag nil, t(:date_range) + %br + .date-range-filter + %div{"class" => "left sub-field"} + = f.text_field :created_at_gt, :class => 'datepicker' + %br + = label_tag nil, t(:start), :class => 'sub' + %div{"class" => "right sub-field"} + = f.text_field :created_at_lt, :class => 'datepicker' + %br + = label_tag nil, t(:stop) + %br + = label_tag nil, "Distributor: " + = f.collection_select(:distributor_id_eq, @distributors, :id, :name, :include_blank => 'All') + %br + = label_tag nil, "Report Type: " + = select_tag(:report_type, options_for_select([['Bulk Co-op - Totals by Supplier',:bulk_coop_supplier_report],['Bulk Co-op - Allocation',:bulk_coop_allocation],['Bulk Co-op - Packing Sheets',:bulk_coop_packing_sheets],['Bulk Co-op - Customer Payments',:bulk_coop_customer_payments]], @report_type)) + %br + %br + = check_box_tag :csv + = label_tag :csv, "Download as csv" + %br + = button t(:search) +%br +%br +%table#listing_orders.index + %thead + %tr{'data-hook' => "orders_header"} + - @header.each do |heading| + %th=heading + %tbody + - @table.each do |row| + %tr + - row.each do |column| + %td= column + - if @table.empty? + %tr + %td{:colspan => "2"}= t(:none) \ No newline at end of file diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml new file mode 100644 index 0000000000..898bba70ef --- /dev/null +++ b/app/views/spree/admin/reports/payments.html.haml @@ -0,0 +1,39 @@ += form_for @search, :url => spree.payments_admin_reports_path do |f| + = label_tag nil, t(:date_range) + %br + .date-range-filter + %div{"class" => "left sub-field"} + = f.text_field :created_at_gt, :class => 'datepicker' + %br + = label_tag nil, t(:start), :class => 'sub' + %div{"class" => "right sub-field"} + = f.text_field :created_at_lt, :class => 'datepicker' + %br + = label_tag nil, t(:stop) + %br + = label_tag nil, "Distributor: " + = f.collection_select(:distributor_id_eq, @distributors, :id, :name, :include_blank => 'All') + %br + = label_tag nil, "Report Type: " + = select_tag(:report_type, options_for_select([['Payments By Type',:payments_by_payment_type],['Itemised Payment Totals',:itemised_payment_totals],['Payment Totals',:payment_totals]], @report_type)) + %br + %br + = check_box_tag :csv + = label_tag :csv, "Download as csv" + %br + = button t(:search) +%br +%br +%table#listing_orders.index + %thead + %tr{'data-hook' => "orders_header"} + - @header.each do |heading| + %th=heading + %tbody + - @table.each do |row| + %tr + - row.each do |column| + %td= column + - if @table.empty? + %tr + %td{:colspan => "2"}= t(:none) \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 3d5a29e0bb..5b13ed1356 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,4 +22,6 @@ end Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_distributors' => 'admin/reports#orders_and_distributors', :as => "orders_and_distributors_admin_reports", :via => [:get, :post] match '/admin/reports/group_buys' => 'admin/reports#group_buys', :as => "group_buys_admin_reports", :via => [:get, :post] + match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] + match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] end diff --git a/db/schema.rb b/db/schema.rb index e23f9e59a8..09dc89c12e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -140,8 +140,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "abn" t.string "acn" t.string "description" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "pickup_address_id" t.string "next_collection_at" t.text "long_description" @@ -183,8 +183,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "alternative_phone" t.integer "state_id" t.integer "country_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "company" end @@ -193,12 +193,12 @@ ActiveRecord::Schema.define(:version => 20121018002907) do create_table "spree_adjustments", :force => true do |t| t.integer "source_id" - t.decimal "amount", :precision => 8, :scale => 2, :default => 0.0 + t.decimal "amount", :precision => 8, :scale => 2 t.string "label" t.string "source_type" t.integer "adjustable_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.boolean "mandatory" t.boolean "locked" t.integer "originator_id" @@ -230,15 +230,15 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "type" t.integer "calculable_id", :null => false t.string "calculable_type", :null => false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_configurations", :force => true do |t| t.string "name" t.string "type", :limit => 50 - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end add_index "spree_configurations", ["name", "type"], :name => "index_configurations_on_name_and_type" @@ -276,8 +276,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "environment", :default => "development" t.string "server", :default => "test" t.boolean "test_mode", :default => true - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_inventory_units", :force => true do |t| @@ -285,8 +285,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "state" t.integer "variant_id" t.integer "order_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "shipment_id" t.integer "return_authorization_id" end @@ -300,8 +300,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "variant_id" t.integer "quantity", :null => false t.decimal "price", :precision => 8, :scale => 2, :null => false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "max_quantity" t.integer "shipping_method_id" end @@ -313,22 +313,22 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "source_id" t.string "source_type" t.text "details" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_mail_methods", :force => true do |t| t.string "environment" t.boolean "active", :default => true - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_option_types", :force => true do |t| t.string "name", :limit => 100 t.string "presentation", :limit => 100 - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "position", :default => 0, :null => false end @@ -342,8 +342,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "name" t.string "presentation" t.integer "option_type_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_option_values_variants", :id => false, :force => true do |t| @@ -362,8 +362,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.decimal "adjustment_total", :precision => 8, :scale => 2, :default => 0.0, :null => false t.decimal "credit_total", :precision => 8, :scale => 2, :default => 0.0, :null => false t.integer "user_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.datetime "completed_at" t.integer "bill_address_id" t.integer "ship_address_id" @@ -384,8 +384,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.text "description" t.boolean "active", :default => true t.string "environment", :default => "development" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.datetime "deleted_at" t.string "display_on" end @@ -393,8 +393,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do create_table "spree_payments", :force => true do |t| t.decimal "amount", :precision => 8, :scale => 2, :default => 0.0, :null => false t.integer "order_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "source_id" t.string "source_type" t.integer "payment_method_id" @@ -423,8 +423,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "owner_id" t.string "owner_type" t.text "value" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "key" t.string "value_type" end @@ -449,16 +449,16 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "position" t.integer "product_id" t.integer "option_type_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_product_properties", :force => true do |t| t.string "value" t.integer "product_id" t.integer "property_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end add_index "spree_product_properties", ["product_id"], :name => "index_product_properties_on_product_id" @@ -482,8 +482,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "meta_keywords" t.integer "tax_category_id" t.integer "shipping_category_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "count_on_hand", :default => 0, :null => false t.integer "supplier_id" t.boolean "group_buy" @@ -528,8 +528,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "user_id" t.integer "product_group_id" t.string "type" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end add_index "spree_promotion_rules", ["product_group_id"], :name => "index_promotion_rules_on_product_group_id" @@ -546,8 +546,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do create_table "spree_properties", :force => true do |t| t.string "name" t.string "presentation", :null => false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_properties_prototypes", :id => false, :force => true do |t| @@ -557,8 +557,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do create_table "spree_prototypes", :force => true do |t| t.string "name" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_return_authorizations", :force => true do |t| @@ -567,8 +567,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.decimal "amount", :precision => 8, :scale => 2, :default => 0.0, :null => false t.integer "order_id" t.text "reason" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_roles", :force => true do |t| @@ -591,8 +591,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "order_id" t.integer "shipping_method_id" t.integer "address_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "state" end @@ -600,15 +600,15 @@ ActiveRecord::Schema.define(:version => 20121018002907) do create_table "spree_shipping_categories", :force => true do |t| t.string "name" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_shipping_methods", :force => true do |t| t.string "name" t.integer "zone_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "display_on" t.integer "shipping_category_id" t.boolean "match_none" @@ -624,8 +624,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "transaction_id" t.integer "customer_id" t.string "payment_type" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_state_changes", :force => true do |t| @@ -633,8 +633,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "previous_state" t.integer "stateful_id" t.integer "user_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "stateful_type" t.string "next_state" end @@ -648,8 +648,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do create_table "spree_tax_categories", :force => true do |t| t.string "name" t.string "description" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.boolean "is_default", :default => false t.datetime "deleted_at" end @@ -658,15 +658,15 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.decimal "amount", :precision => 8, :scale => 5 t.integer "zone_id" t.integer "tax_category_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.boolean "included_in_price", :default => false end create_table "spree_taxonomies", :force => true do |t| t.string "name", :null => false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_taxons", :force => true do |t| @@ -675,8 +675,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "name", :null => false t.string "permalink" t.integer "taxonomy_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "lft" t.integer "rgt" t.string "icon_file_name" @@ -694,8 +694,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "permissable_id" t.string "permissable_type" t.string "token" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end add_index "spree_tokenized_permissions", ["permissable_id", "permissable_type"], :name => "index_tokenized_name_and_type" @@ -704,8 +704,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "environment" t.string "analytics_id" t.boolean "active", :default => true - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_users", :force => true do |t| @@ -726,8 +726,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "login" t.integer "ship_address_id" t.integer "bill_address_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "authentication_token" t.string "unlock_token" t.datetime "locked_at" @@ -750,7 +750,7 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.boolean "is_master", :default => false t.integer "product_id" t.integer "count_on_hand", :default => 0, :null => false - t.decimal "cost_price", :precision => 8, :scale => 2, :default => 0.0 + t.decimal "cost_price", :precision => 8, :scale => 2 t.integer "position" end @@ -760,15 +760,15 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.integer "zoneable_id" t.string "zoneable_type" t.integer "zone_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "spree_zones", :force => true do |t| t.string "name" t.string "description" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.boolean "default_tax", :default => false t.integer "zone_members_count", :default => 0 end @@ -779,8 +779,8 @@ ActiveRecord::Schema.define(:version => 20121018002907) do t.string "email" t.string "twitter" t.string "website" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "address_id" t.text "long_description" end diff --git a/lib/open_food_web/order_grouper.rb b/lib/open_food_web/order_grouper.rb new file mode 100644 index 0000000000..d222a7cc13 --- /dev/null +++ b/lib/open_food_web/order_grouper.rb @@ -0,0 +1,58 @@ +module OpenFoodWeb + + class OrderGrouper + def initialize(rules, column_constructors) + @rules = rules + @column_constructors = column_constructors + end + + def build_tree(items, remaining_rules) + rules = remaining_rules.clone + unless rules.empty? + rule = rules.delete_at(0) # Remove current rule for subsequent groupings + group_and_sort(rule, rules, items) + else + items + end + end + + def group_and_sort(rule, remaining_rules, items) + branch = {} + groups = items.group_by { |item| rule[:group_by].call(item) } + sorted_groups = groups.sort_by { |key, value| rule[:sort_by].call(key) } + sorted_groups.each do |property,items_by_property| + branch[property] = build_tree(items_by_property, remaining_rules) + branch[property][:summary_row] = { items: items_by_property, columns: rule[:summary_columns] } unless rule[:summary_columns] == nil || is_leaf_node(branch[property]) + end + branch + end + + def build_table(groups) + rows = [] + unless is_leaf_node(groups) + groups.each do |key, group| + unless key == :summary_row + build_table(group).each { |g| rows << g } + else + rows << group[:columns].map { |cols| cols.call(group[:items]) } + end + end + else + rows << @column_constructors.map { |column_constructor| column_constructor.call(groups) } + end + rows + end + + def table(items) + tree = build_tree(items,@rules) + table = build_table(tree) + table + end + + private + + def is_leaf_node(node) + node.is_a? Array + end + end +end \ No newline at end of file diff --git a/spec/lib/open_food_web/order_grouper_spec.rb b/spec/lib/open_food_web/order_grouper_spec.rb new file mode 100644 index 0000000000..d734b78c7b --- /dev/null +++ b/spec/lib/open_food_web/order_grouper_spec.rb @@ -0,0 +1,152 @@ +require 'spec_helper' + +module OpenFoodWeb + describe OrderGrouper do + + before(:each) do + @items = [1, 2, 3, 4] + end + + context "constructing the table" do + it "should build a tree then build a table" do + rules = [ { group_by: Proc.new { |sentence| sentence.paragraph.chapter }, sort_by: Proc.new { |chapter| chapter.name }, summary_columns: [Proc.new { |is| is.first.paragraph.chapter.name }, Proc.new { |is| "TOTAL" }, Proc.new { |is| "" }, Proc.new { |is| is.sum {|i| i.property1 } } ] }, + { group_by: Proc.new { |sentence| sentence.paragraph }, sort_by: Proc.new { |paragraph| paragraph.name } } ] + columns = [Proc.new { |is| is.first.paragraph.chapter.name }, Proc.new { |is| is.first.paragraph.name }, Proc.new { |is| is.first.name }, Proc.new { |is| is.sum {|i| i.property1 } }] + + subject = OrderGrouper.new rules, columns + + tree = double(:tree) + subject.should_receive(:build_tree).with(@items, rules).and_return(tree) + subject.should_receive(:build_table).with(tree) + + subject.table(@items) + end + + end + + context "grouping items without rules" do + it "returns the original array when no rules are provided" do + rules = [] + column1 = double(:col1) + column2 = double(:col2) + columns = [column1, column2] + subject = OrderGrouper.new rules, columns + + rules.should_receive(:clone).and_return(rules) + subject.build_tree(@items, rules).should == @items + end + end + + context "grouping items with rules" do + + before(:each) do + @rule1 = double(:rule1) + rule2 = double(:rule2) + @rules = [@rule1, rule2] + @remaining_rules = [rule2] + column1 = double(:col1) + column2 = double(:col2) + @columns = [column1, column2] + end + + it "builds branches by removing a rule from 'rules' and running group_and_sort" do + subject = OrderGrouper.new @rules, @columns + + @rules.should_receive(:clone).and_return(@rules) + @rules.should_receive(:delete_at).with(0) + grouped_tree = double(:grouped_tree) + subject.should_receive(:group_and_sort).and_return(grouped_tree) + + subject.build_tree(@items, @rules).should == grouped_tree + end + + it "separates the first rule from rules before sending to group_and_sort" do + subject = OrderGrouper.new @rules, @columns + + grouped_tree = double(:grouped_tree) + subject.should_receive(:group_and_sort).with(@rule1, @rules[1..-1], @items).and_return(grouped_tree) + + subject.build_tree(@items, @rules).should == grouped_tree + end + + it "should group, then sort, send each group to build_tree, and return a branch" do + summary_columns_object = double(:summary_columns) + @rule1.stub(:[]).with(:summary_columns) { summary_columns_object } + + subject = OrderGrouper.new @rules, @columns + + number_of_categories = 3 + groups = double(:groups) + @items.should_receive(:group_by).and_return(groups) + sorted_groups = {} + 1.upto(number_of_categories) { |i| sorted_groups[i] = double(:group, name: "Group "+ i.to_s ) } + groups.should_receive(:sort_by).and_return(sorted_groups) + group = { group1: 1, group2: 2, group3: 3 } + subject.should_receive(:build_tree).exactly(number_of_categories).times.and_return(group) + + group_tree = {} + 1.upto(number_of_categories) { |i| group_tree[i] = group } + 1.upto(number_of_categories) { |i| group_tree[i][:summary_row] = summary_columns_object } + subject.group_and_sort(@rule1, @remaining_rules, @items).should == group_tree + end + end + + context "building the table Array" do + before(:each) do + rule1 = double(:rule1) + rule2 = double(:rule2) + @rules = [rule1, rule2] + @column1 = double(:col1, :call => "Column1") + @column2 = double(:col2, :call => "Column2") + @columns = [@column1, @column2] + + sumcol1 = double(:sumcol1, :call => "SumColumn1") + sumcol2 = double(:sumcol2, :call => "SumColumn2") + @sumcols = [sumcol1, sumcol2] + + item1 = double(:item1) + item2 = double(:item2) + item3 = double(:item3) + @items1 = [item1, item2] + @items2 = [item2, item3] + @items3 = [item3, item1] + end + it "should return columns when given an Array" do + subject = OrderGrouper.new @rules, @columns + + @column1.should_receive(:call) + @column2.should_receive(:call) + + subject.build_table(@items1).should == [["Column1", "Column2"]] + end + + it "should return a row for each key-value pair when given a Hash" do + groups = { items1: @items1, items2: @items2, items3: @items3 } + + subject = OrderGrouper.new @rules, @columns + + #subject.should_receive(:build_table).exactly(2).times + + expected_return = [] + groups.length.times { expected_return << ["Column1", "Column2"] } + subject.build_table(groups).should == expected_return + end + + it "should return an extra row when a :summary_row key appears in a given Hash" do + groups = { items1: @items1, items2: @items2, items3: @items3, summary_row: { items: { items2: @items2, items3: @items3 }, columns: @sumcols } } + + subject = OrderGrouper.new @rules, @columns + + expected_return = [] + groups.each do |key, group| + if key == :summary_row + expected_return << ["SumColumn1", "SumColumn2"] + else + expected_return << ["Column1", "Column2"] + end + end + subject.build_table(groups).should == expected_return + end + end + end +end \ No newline at end of file