diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 02aba3fe9a..eab6d7d162 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -5,6 +5,7 @@ require 'open_food_network/group_buy_report' require 'open_food_network/order_grouper' require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' +require 'open_food_network/order_cycle_management_report' Spree::Admin::ReportsController.class_eval do @@ -22,11 +23,14 @@ Spree::Admin::ReportsController.class_eval do customers: [ ["Mailing List", :mailing_list], ["Addresses", :addresses] + ], + order_cycle_management: [ + ["Payment Methods Report", :payment_methods_report] ] } # Fetches user's distributors, suppliers and order_cycles - before_filter :load_data, only: [:customers, :products_and_inventory] + before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management] # Render a partial for orders and fulfillment description respond_override :index => { :html => { :success => lambda { @@ -36,6 +40,8 @@ Spree::Admin::ReportsController.class_eval do render_to_string(partial: 'products_and_inventory_description', layout: false, locals: {report_types: REPORT_TYPES[:products_and_inventory]}).html_safe @reports[:customers][:description] = render_to_string(partial: 'customers_description', layout: false, locals: {report_types: REPORT_TYPES[:customers]}).html_safe + @reports[:order_cycle_management][:description] = + render_to_string(partial: 'order_cycle_management_description', layout: false, locals: {report_types: REPORT_TYPES[:order_cycle_management]}).html_safe } } } @@ -54,6 +60,18 @@ Spree::Admin::ReportsController.class_eval do render_report(@report.header, @report.table, params[:csv], "customers.csv") end + def order_cycle_management + @report_types = REPORT_TYPES[:order_cycle_management] + @report_type = params[:report_type] + @report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params + + @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) + + @orders = @search.result + + render_report(@report.header, @report.table, params[:csv], "customers.csv") + end + def orders_and_distributors params[:q] = {} unless params[:q] @@ -621,6 +639,7 @@ Spree::Admin::ReportsController.class_eval do :products_and_inventory => {:name => "Products & Inventory", :description => ''}, :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" } + :order_cycle_management => {:name => "Order Cycle Management", :description => ''} } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 573b790051..22004577eb 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -7,5 +7,14 @@ module Spree [ "#{oc.name}   (#{orders_open_at} - #{orders_close_at})".html_safe, oc.id ] end end + + def report_payment_method_options(orders) + orders.map { |o| o.payments.first.payment_method.andand.name }.uniq + end + + def report_shipping_options(orders) + orders.map { |o| o.shipping_method.andand.name }.uniq + end + end end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 2c10e99b04..23138fcea9 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -128,7 +128,7 @@ class AbilityDecorator end # Reports page - can [:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], :report + can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 00ee7999f5..919c79fe2b 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -66,6 +66,12 @@ Spree::Order.class_eval do where("state != ?", state) } + scope :with_payment_method_name, lambda { |payment_method_name| + joins(:payments => :payment_method). + where('spree_payment_methods.name = ?', payment_method_name). + select('DISTINCT spree_orders.*') + } + # -- Methods def products_available_from_new_distribution diff --git a/app/views/spree/admin/reports/_order_cycle_management_description.html.haml b/app/views/spree/admin/reports/_order_cycle_management_description.html.haml new file mode 100644 index 0000000000..d4d20ae66e --- /dev/null +++ b/app/views/spree/admin/reports/_order_cycle_management_description.html.haml @@ -0,0 +1,4 @@ +%ul{style: "margin-left: 12pt"} + - report_types.each do |report_type| + %li + = link_to report_type[0], "#{order_cycle_management_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml new file mode 100644 index 0000000000..4581d32bc6 --- /dev/null +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -0,0 +1,44 @@ += form_tag spree.order_cycle_management_admin_reports_url do |f| + %br + = label_tag nil, "Order Cycle: " + = select_tag(:order_cycle_id, + options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), + include_blank: true) + %br + %br + = label_tag nil, "Payment Methods (hold Ctrl to select multiple payment methods)" + %br + + = select_tag(:payment_method_name, + options_for_select(report_payment_method_options(@orders), params[:payment_method_name]), + multiple: true, include_blank: true) + %br + %br + = label_tag nil, "Shipping Method: " + = select_tag(:shipping_method_name, + options_for_select(report_shipping_options(@orders), params[:shipping_method_name]), + include_blank: true) + %br + %br + = check_box_tag :csv + = label_tag :csv, "Download as csv" + %br + %br + = button t(:search) + +%br +%br +%table#listing_order_payment_methods.index + %thead + %tr{'data-hook' => "orders_header"} + - @report.header.each do |heading| + %th=heading + %tbody + - @report.table.each do |row| + %tr + - row.each do |column| + %td= column + - if @report.table.empty? + %tr + %td{:colspan => "2"}= t(:none) + diff --git a/config/routes.rb b/config/routes.rb index 23df433a0a..15025cf88c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -116,6 +116,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/order_cycle_management' => 'admin/reports#order_cycle_management', :as => "order_cycle_management_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] diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb new file mode 100644 index 0000000000..f1529482ed --- /dev/null +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -0,0 +1,65 @@ +module OpenFoodNetwork + class OrderCycleManagementReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount ", "Amount Paid"] + + end + + def table + orders.map do |order| + ba = order.billing_address + da = order.distributor.andand.address + [ba.firstname, + ba.lastname, + order.email, + ba.phone, + order.distributor.andand.name, + order.shipping_method.andand.name, + order.payments.first.andand.payment_method.andand.name, + order.payments.first.amount + ] + end + end + + def orders + filter Spree::Order.managed_by(@user).distributed_by_user(@user).complete.where("spree_orders.state != ?", :canceled) + end + + def filter(orders) + filter_to_order_cycle filter_to_payment_method filter_to_shipping_method orders + end + + def filter_to_payment_method (orders) + if params[:payment_method_name].present? + orders.with_payment_method_name(params[:payment_method_name]) + else + orders + end + end + + def filter_to_shipping_method (orders) + if params[:shipping_method_name].present? + orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:shipping_method_name]) + else + orders + end + end + + def filter_to_order_cycle(orders) + if params[:order_cycle_id].present? + orders.where(order_cycle_id: params[:order_cycle_id]) + else + orders + end + end + + + end +end + diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 817f0ad9c4..62fdb9c565 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -59,6 +59,22 @@ feature %q{ end end + describe "Order cycle management report" do + before do + login_to_admin_section + click_link "Reports" + end + + scenario "order payment method report" do + click_link "Order Cycle Management" + rows = find("table#listing_order_payment_methods").all("thead tr") + table = rows.map { |r| r.all("th").map { |c| c.text.strip } } + table.sort.should == [ + ["First Name", "Last Name", "Email", "Phone", "Hub", "Payment Method", "Amount", "Amount Paid"] + ].sort + end + end + scenario "orders and distributors report" do login_to_admin_section click_link 'Reports' diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb new file mode 100644 index 0000000000..c010d6f253 --- /dev/null +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +include AuthenticationWorkflow + +module OpenFoodNetwork + describe OrderCycleManagementReport do + context "as a site admin" do + let(:user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by_name!("admin") + user + end + subject { OrderCycleManagementReport.new user } + + describe "fetching orders" do + it "fetches completed orders" do + o1 = create(:order) + o2 = create(:order, completed_at: 1.day.ago) + subject.orders.should == [o2] + end + + it "does not show cancelled orders" do + o1 = create(:order, state: "canceled", completed_at: 1.day.ago) + o2 = create(:order, completed_at: 1.day.ago) + subject.orders.should == [o2] + end + end + end + + context "as an enterprise user" do + let!(:user) { create_enterprise_user } + + subject { OrderCycleManagementReport.new user } + + describe "fetching orders" do + let(:supplier) { create(:supplier_enterprise) } + let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { create(:order, completed_at: 1.day.ago) } + + it "only shows orders managed by the current user" do + d1 = create(:distributor_enterprise) + d1.enterprise_roles.build(user: user).save + d2 = create(:distributor_enterprise) + d2.enterprise_roles.build(user: create(:user)).save + + o1 = create(:order, distributor: d1, completed_at: 1.day.ago) + o2 = create(:order, distributor: d2, completed_at: 1.day.ago) + + subject.should_receive(:filter).with([o1]).and_return([o1]) + subject.orders.should == [o1] + end + + + it "does not show orders through a hub that the current user does not manage" do + # Given a supplier enterprise with an order for one of its products + supplier.enterprise_roles.build(user: user).save + order.line_items << create(:line_item, product: product) + + # When I fetch orders, I should see no orders + subject.should_receive(:filter).with([]).and_return([]) + subject.orders.should == [] + end + end + + describe "filtering orders" do + let!(:orders) { Spree::Order.scoped } + let!(:supplier) { create(:supplier_enterprise) } + + let!(:oc1) { create(:simple_order_cycle) } + let!(:pm1) { create(:payment_method, name: "PM1") } + let!(:sm1) { create(:shipping_method, name: "ship1") } + let!(:order1) { create(:order, shipping_method: sm1, order_cycle: oc1) } + let!(:payment1) { create(:payment, order: order1, payment_method: pm1) } + + it "returns all orders sans-params" do + subject.filter(orders).should == orders + end + + it "filters to a specific order cycle" do + + oc2 = create(:simple_order_cycle) + order2 = create(:order, order_cycle: oc2) + + subject.stub(:params).and_return(order_cycle_id: oc1.id) + subject.filter(orders).should == [order1] + end + + it "filters to a payment method" do + + pm2 = create(:payment_method, name: "PM2") + order2 = create(:order) + payment2 = create(:payment, order: order2, payment_method: pm2) + + subject.stub(:params).and_return(payment_method_name: pm1.name) + subject.filter(orders).should == [order1] + end + + it "filters to a shipping method" do + + sm2 = create(:shipping_method, name: "ship2") + order2 = create(:order, shipping_method: sm2) + + subject.stub(:params).and_return(shipping_method_name: sm1.name) + subject.filter(orders).should == [order1] + end + + it "should do all the filters at once" do + + subject.stub(:params).and_return( + order_cycle_id: oc1.id, + shipping_method_name: sm1.name, + payment_method_name: pm1.name) + subject.filter(orders).should == [order1] + + end + + + end + end + end +end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 72c00d9ec2..28c4997032 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -77,7 +77,7 @@ describe Spree::Order do subject.update_distribution_charge! end - it "ensures the correct adjustment(s) are created for order cycles" do + it "ensures the correct adjustment(s) are created for order cycles" do EnterpriseFee.stub(:clear_all_adjustments_on_order) line_item = double(:line_item) subject.stub(:line_items) { [line_item] } @@ -328,15 +328,37 @@ describe Spree::Order do end end - describe "scopes" do + describe "scopes" do describe "not_state" do it "finds only orders not in specified state" do o = FactoryGirl.create(:completed_order_with_totals) o.cancel! - Spree::Order.not_state(:canceled).should_not include o end end + + describe "with payment method name" do + let!(:o1) { create(:order) } + let!(:o2) { create(:order) } + let!(:pm1) { create(:payment_method, name: 'foo') } + let!(:pm2) { create(:payment_method, name: 'bar') } + let!(:p1) { create(:payment, order: o1, payment_method: pm1) } + let!(:p2) { create(:payment, order: o2, payment_method: pm2) } + + it "returns the order with payment method name" do + Spree::Order.with_payment_method_name('foo').should == [o1] + end + + it "doesn't return rows with a different payment method name" do + Spree::Order.with_payment_method_name('foobar').should_not include o1 + Spree::Order.with_payment_method_name('foobar').should_not include o2 + end + + it "doesn't return duplicate rows" do + p2 = FactoryGirl.create(:payment, :order => o1, :payment_method => pm1) + Spree::Order.with_payment_method_name('foo').length.should == 1 + end + end end describe "shipping address prepopulation" do @@ -385,5 +407,4 @@ describe Spree::Order do create(:order).deliver_order_confirmation_email end end - end