diff --git a/Gemfile.lock b/Gemfile.lock index da143e47ed..2e5e808e92 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,7 +23,7 @@ GIT GIT remote: git://github.com/eaterprises/spree_paypal_express.git - revision: ec7fd298f117fe6c598070dc0e7a38887b9bcbc4 + revision: 7dd46e6e549d82c4d603f685a7cb7fbb4af0d973 branch: 1-1-stable specs: spree_paypal_express (1.1.0) @@ -320,13 +320,13 @@ GEM libv8 (~> 3.3.10) thor (0.16.0) tilt (1.3.3) - treetop (1.4.11) + treetop (1.4.12) polyglot polyglot (>= 0.3.1) truncate_html (0.5.5) turn (0.8.3) ansi - tzinfo (0.3.33) + tzinfo (0.3.34) uglifier (1.2.4) execjs (>= 0.3.0) multi_json (>= 1.0.2) diff --git a/app/assets/javascripts/store/products.js b/app/assets/javascripts/store/products.js index 3e972161a6..0dabff5f4d 100644 --- a/app/assets/javascripts/store/products.js +++ b/app/assets/javascripts/store/products.js @@ -10,6 +10,15 @@ $(document).ready(function() { // Product page with master price only $(".add-to-cart input.title:not(#quantity):not(.max_quantity)").change(products_update_price_without_variant).change(); + + // Product page other + $("#distributor_id").change(function() { + var distributor_html = distributors[$(this).val()]; + if(!distributor_html) { + distributor_html = 'When you select a distributor for your order, their address and pickup times will be displayed here.'; + } + $("#product-distributor-details .distributor-details").html(distributor_html); + }); }); diff --git a/app/assets/stylesheets/store/openfoodweb.css.scss b/app/assets/stylesheets/store/openfoodweb.css.scss index d183ffd10b..24c0b1fd5a 100644 --- a/app/assets/stylesheets/store/openfoodweb.css.scss +++ b/app/assets/stylesheets/store/openfoodweb.css.scss @@ -144,6 +144,19 @@ ul.product-listing { } +/* Distributor details on product details page */ +fieldset#product-distributor-details { + float: right; + margin-top: 0; + width: 250px; + @extend #shipping; +} + +#product-variants { + float: none; +} + + /* Add to cart form on product details page */ #cart-form { .error-distributor { diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 3528746df4..11b3a34beb 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -7,6 +7,16 @@ class EnterprisesController < BaseController @suppliers = Enterprise.is_supplier end + def distributors + @distributors = Enterprise.is_distributor + + respond_to do |format| + format.js do + @distributor_details = Hash[@distributors.map { |d| [d.id, render_to_string(:partial => 'enterprises/distributor_details', :locals => {:distributor => d})] }] + end + end + end + def show options = {:enterprise_id => params[:id]} options.merge(params.reject { |k,v| k == :id }) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 88424d31c7..b65b68fad1 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -1,11 +1,15 @@ 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"}}) + Spree::Admin::ReportsController::AVAILABLE_REPORTS.merge!({:order_cycles => {:name => "Order Cycle Reports", :description => "Reports for Order Cycles"}}) def orders_and_distributors params[:q] = {} unless params[:q] @@ -67,4 +71,424 @@ 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 = Enterprise.is_distributor + @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 || 0 }, + 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 || 0 } }, + proc { |lis| lis.sum { |li| (li.max_quantity || 0) * li.variant.weight || 0 } } ] }, + { 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 || 0 }, + 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 || 0 }, + 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 = Enterprise.is_distributor + @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 order_cycles + 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 + #payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments + + @distributors = Enterprise.is_distributor + @report_type = params[:report_type] + + case params[:report_type] + when "order_cycle_supplier_totals" + table_items = line_items + @include_blank = 'All' + + header = ["Supplier", "Product", "Variant", "Amount", "Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + + columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.options_text }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.variant.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "status" }, + proc { |line_items| "incoming transport" } ] + + rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.options_text } } ] + + when "order_cycle_supplier_totals_by_distributor" + table_items = line_items + @include_blank = 'All' + + header = ["Supplier", "Product", "Variant", "To Distributor", "Amount", "Cost per Unit", "Total Cost", "Shipping Method"] + + columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.options_text }, + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.variant.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "shipping method" } ] + + rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.options_text }, + summary_columns: [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.options_text }, + proc { |line_items| "TOTAL" }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.variant.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + + when "order_cycle_distributor_totals_by_supplier" + table_items = line_items + @include_blank = 'All' + + header = ["Distributor", "Supplier", "Product", "Variant", "Amount", "Cost per Unit", "Total Cost", "Total Shipping Cost", "Shipping Method"] + + columns = [ proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.options_text }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.variant.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "" }, + proc { |line_items| "shipping method" } ] + + rules = [ { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name }, + summary_columns: [ proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "total shipping cost" }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.options_text } } ] + + when "order_cycle_customer_totals" + table_items = line_items + @include_blank = false + + header = ["Customer", "Email", "Phone", "Product", "Variant", "Amount", "Total Cost", "Paid?", "Packed?", "Shipped?"] + + columns = [ proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.order.email }, + proc { |line_items| line_items.first.order.bill_address.phone }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.options_text }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" } ] + + rules = [ { group_by: proc { |line_item| line_item.order.user }, + sort_by: proc { |user| user.to_s }, + summary_columns: [ proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.order.email }, + proc { |line_items| line_items.first.order.bill_address.phone }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "work out whether paid or not" }, + proc { |line_items| "" }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.options_text } } ] + + else + table_items = line_items + + header = ["Supplier", "Product", "Variant", "Amount", "Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + + columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.options_text }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.variant.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.variant.price } }, + proc { |line_items| "status" }, + proc { |line_items| "incoming transport" } ] + + rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.options_text } } ] + + end + + order_grouper = OpenFoodWeb::OrderGrouper.new rules, columns + + @header = header + @table = order_grouper.table(table_items) + csv_file_name = "order_cycles.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/overrides/add_distributor_details_to_product.rb b/app/overrides/add_distributor_details_to_product.rb new file mode 100644 index 0000000000..d841c82ee3 --- /dev/null +++ b/app/overrides/add_distributor_details_to_product.rb @@ -0,0 +1,9 @@ +Deface::Override.new(:virtual_path => "spree/products/show", + :insert_before => "[data-hook='cart_form']", + :partial => "spree/products/distributor_details", + :name => "product_distributor_details") + +Deface::Override.new(:virtual_path => "spree/products/show", + :insert_after => "[data-hook='product_show']", + :text => "<%= javascript_include_tag main_app.distributors_enterprises_path(:format => :js) %>", + :name => "product_distributor_details_js") diff --git a/app/views/enterprises/_distributor_details.html.haml b/app/views/enterprises/_distributor_details.html.haml new file mode 100644 index 0000000000..950ba1e7f2 --- /dev/null +++ b/app/views/enterprises/_distributor_details.html.haml @@ -0,0 +1,23 @@ +%h2= distributor.name +%p + %strong Address: + %br/ + = render 'spree/shared/address', :address => distributor.address +%p + %strong Next collection time: + %br/ + = distributor.next_collection_at +%p + %strong Regular collection times: + %br/ + = distributor.pickup_times +%p + %strong Contact: + %br/ + = distributor.contact + %br/ + = "Phone: #{distributor.phone}" + %br/ + = "Email: #{distributor.email}" +%p= distributor.description +%p= link_to distributor.website, distributor.website if distributor.website diff --git a/app/views/enterprises/distributors.js.erb b/app/views/enterprises/distributors.js.erb new file mode 100644 index 0000000000..aed01164da --- /dev/null +++ b/app/views/enterprises/distributors.js.erb @@ -0,0 +1 @@ +distributors = <%= @distributor_details.to_json.html_safe %>; \ No newline at end of file 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/order_cycles.html.haml b/app/views/spree/admin/reports/order_cycles.html.haml new file mode 100644 index 0000000000..33bd97cb06 --- /dev/null +++ b/app/views/spree/admin/reports/order_cycles.html.haml @@ -0,0 +1,39 @@ += form_for @search, :url => spree.order_cycles_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 => @include_blank) + %br + = label_tag nil, "Report Type: " + = select_tag(:report_type, options_for_select([['Order Cycle Supplier Totals',:order_cycle_supplier_totals], ['Order Cycle Supplier Totals by Distributor',:order_cycle_supplier_totals_by_distributor], ['Order Cycle Distributor Totals by Supplier',:order_cycle_distributor_totals_by_supplier], ['Order Cycle Customer Totals',:order_cycle_customer_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/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/app/views/spree/checkout/_distributor.html.haml b/app/views/spree/checkout/_distributor.html.haml index eaaf31217b..24f656709b 100644 --- a/app/views/spree/checkout/_distributor.html.haml +++ b/app/views/spree/checkout/_distributor.html.haml @@ -1,26 +1,4 @@ .columns.omega.six %fieldset#shipping %legend Distributor - %h2= @order.distributor.name - %p - %strong Address: - %br/ - = render 'spree/shared/address', :address => @order.distributor.address - %p - %strong Next collection time: - %br/ - = @order.distributor.next_collection_at - %p - %strong Regular collection times: - %br/ - = @order.distributor.pickup_times - %p - %strong Contact: - %br/ - = @order.distributor.contact - %br/ - = "Phone: #{@order.distributor.phone}" - %br/ - = "Email: #{@order.distributor.email}" - %p= @order.distributor.description - %p= link_to @order.distributor.website, @order.distributor.website if @order.distributor.website + = render 'enterprises/distributor_details', :distributor => @order.distributor diff --git a/app/views/spree/products/_distributor_details.html.haml b/app/views/spree/products/_distributor_details.html.haml new file mode 100644 index 0000000000..a8fe4af9a3 --- /dev/null +++ b/app/views/spree/products/_distributor_details.html.haml @@ -0,0 +1,8 @@ +%fieldset#product-distributor-details.columns.five.omega + %legend Distributor + .distributor-details + - order = current_order(false) + - if order.andand.distributor.present? + = render 'enterprises/distributor_details', :distributor => order.distributor + - else + When you select a distributor for your order, their address and pickup times will be displayed here. diff --git a/config/routes.rb b/config/routes.rb index 1a018ba3ae..60454a3ae1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ Openfoodweb::Application.routes.draw do resources :enterprises do get :suppliers, :on => :collection + get :distributors, :on => :collection get :select_distributor, :on => :member get :deselect_distributor, :on => :collection end @@ -21,4 +22,7 @@ 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] + match '/admin/reports/order_cycles' => 'admin/reports#order_cycles', :as => "order_cycles_admin_reports", :via => [:get, :post] end diff --git a/db/migrate/20121031203807_change_group_buy_unit_size_from_string_to_float.rb b/db/migrate/20121031203807_change_group_buy_unit_size_from_string_to_float.rb new file mode 100644 index 0000000000..44caf3beaa --- /dev/null +++ b/db/migrate/20121031203807_change_group_buy_unit_size_from_string_to_float.rb @@ -0,0 +1,21 @@ +class ChangeGroupBuyUnitSizeFromStringToFloat < ActiveRecord::Migration + class Spree::Product < ActiveRecord::Base; end + + def up + add_column :spree_products, :group_buy_unit_size_f, :float + Spree::Product.reset_column_information + + Spree::Product.all.each do |product| + product.group_buy_unit_size_f = product.group_buy_unit_size.to_f + product.save! + end + + remove_column :spree_products, :group_buy_unit_size + rename_column :spree_products, :group_buy_unit_size_f, :group_buy_unit_size + end + + def down + change_column :spree_products, :group_buy_unit_size, :string + end + +end diff --git a/db/schema.rb b/db/schema.rb index 197f21417b..4bbccb5c05 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -490,7 +490,7 @@ ActiveRecord::Schema.define(:version => 20121031222403) do t.integer "count_on_hand", :default => 0, :null => false t.integer "supplier_id" t.boolean "group_buy" - t.string "group_buy_unit_size" + t.float "group_buy_unit_size" end add_index "spree_products", ["available_on"], :name => "index_products_on_available_on" diff --git a/lib/open_food_web/group_buy_report.rb b/lib/open_food_web/group_buy_report.rb index f8582f88a1..3234d1078c 100644 --- a/lib/open_food_web/group_buy_report.rb +++ b/lib/open_food_web/group_buy_report.rb @@ -38,8 +38,8 @@ module OpenFoodWeb end # Sum quantities for each product (Total line) - sum_quantities = line_items_by_product.sum { |li| li.variant.weight * li.quantity } - sum_max_quantities = line_items_by_product.sum { |li| li.variant.weight * (li.max_quantity || 0) } + sum_quantities = line_items_by_product.sum { |li| (li.variant.weight || 0) * li.quantity } + sum_max_quantities = line_items_by_product.sum { |li| (li.variant.weight || 0) * (li.max_quantity || 0) } variants_and_quantities << GroupBuyProductRow.new(product, sum_quantities, sum_max_quantities) end 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/group_buy_report_spec.rb b/spec/lib/open_food_web/group_buy_report_spec.rb index 0dae0d3d47..ec23cab070 100644 --- a/spec/lib/open_food_web/group_buy_report_spec.rb +++ b/spec/lib/open_food_web/group_buy_report_spec.rb @@ -36,7 +36,7 @@ module OpenFoodWeb @orders << order2 @supplier2 = create(:supplier_enterprise) - @variant3 = create(:variant) + @variant3 = create(:variant, :weight => nil) @variant3.product.supplier = @supplier2 @variant3.product.save! product_distribution = create(:product_distribution, :product => @variant3.product, :distributor => distributor, :shipping_method => create(:shipping_method)) @@ -82,12 +82,12 @@ module OpenFoodWeb table_row_objects = subject.variants_and_quantities - variant_rows = table_row_objects.select{ |r| r.class == OpenFoodWeb::GroupBuyVariantRow } - product_rows = table_row_objects.select{ |r| r.class == OpenFoodWeb::GroupBuyProductRow } + variant_rows = table_row_objects.select { |r| r.class == OpenFoodWeb::GroupBuyVariantRow } + product_rows = table_row_objects.select { |r| r.class == OpenFoodWeb::GroupBuyProductRow } supplier_groups = variant_rows.group_by { |r| r.variant.product.supplier } - variant_groups = variant_rows.group_by{ |r| r.variant } - product_groups = product_rows.group_by{ |r| r.product } + variant_groups = variant_rows.group_by { |r| r.variant } + product_groups = product_rows.group_by { |r| r.product } supplier_groups.length.should == 2 variant_groups.length.should == 3 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 diff --git a/spec/requests/admin/product_spec.rb b/spec/requests/admin/product_spec.rb index fff5417726..9bcacd13d3 100644 --- a/spec/requests/admin/product_spec.rb +++ b/spec/requests/admin/product_spec.rb @@ -49,14 +49,14 @@ feature %q{ fill_in 'product_price', :with => '19.99' select 'New supplier', :from => 'product_supplier_id' choose 'product_group_buy_1' - fill_in 'Group buy unit size', :with => '10 kg' + fill_in 'Group buy unit size', :with => '10' click_button 'Create' flash_message.should == 'Product "A new product !!!" has been successfully created!' product = Spree::Product.find_by_name('A new product !!!') product.group_buy.should be_true - product.group_buy_unit_size.should == '10 kg' + product.group_buy_unit_size.should == 10.0 end end diff --git a/spec/requests/admin/reports_spec.rb b/spec/requests/admin/reports_spec.rb index e3e2d8fcd2..2b6c62ee11 100644 --- a/spec/requests/admin/reports_spec.rb +++ b/spec/requests/admin/reports_spec.rb @@ -24,4 +24,28 @@ feature %q{ page.should have_content 'Supplier' end + scenario "bulk co-op report" do + login_to_admin_section + click_link 'Reports' + click_link 'Bulk Co-Op' + + page.should have_content 'Supplier' + end + + scenario "payments reports" do + login_to_admin_section + click_link 'Reports' + click_link 'Payment Reports' + + page.should have_content 'Payment State' + end + + scenario "order cycle reports" do + login_to_admin_section + click_link 'Reports' + click_link 'Order Cycle Reports' + + page.should have_content 'Supplier' + end + end diff --git a/spec/requests/consumer/product_spec.rb b/spec/requests/consumer/product_spec.rb index f86b3bb409..e8741548e2 100644 --- a/spec/requests/consumer/product_spec.rb +++ b/spec/requests/consumer/product_spec.rb @@ -22,4 +22,62 @@ feature %q{ page.should have_selector 'td', :text => d.name end + describe "viewing distributor details" do + context "without Javascript" do + it "displays a holding message when no distributor is selected" do + p = create(:product) + + visit spree.product_path p + + page.should have_selector '#product-distributor-details', :text => 'When you select a distributor for your order, their address and pickup times will be displayed here.' + end + + it "displays distributor details when one is selected" do + d = create(:distributor_enterprise) + p = create(:product, :distributors => [d]) + + visit spree.root_path + click_link d.name + visit spree.product_path p + + within '#product-distributor-details' do + [d.name, + d.address.address1, + d.address.city, + d.address.zipcode, + d.address.state_text, + d.address.country.name, + d.pickup_times, + d.next_collection_at, + d.contact, + d.phone, + d.email, + d.description, + d.website].each do |value| + + page.should have_content value + end + end + end + end + + context "with Javascript", js: true do + it "changes distributor details when the distributor is changed" do + d1 = create(:distributor_enterprise) + d2 = create(:distributor_enterprise) + d3 = create(:distributor_enterprise) + p = create(:product, :distributors => [d1, d2, d3]) + + visit spree.product_path p + + [d1, d2, d3].each do |d| + select d.name, :from => 'distributor_id' + + within '#product-distributor-details' do + page.should have_selector 'h2', :text => d.name + end + end + end + end + end end