diff --git a/Gemfile.lock b/Gemfile.lock index 76fddfd060..f919568a7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,7 +23,7 @@ GIT GIT remote: git://github.com/openfoodfoundation/spree.git - revision: 4e0075b07acb56864aca89eee3d9670136176c23 + revision: afcc23e489eb604a3e2651598a7c8364e2acc7b3 branch: 1-3-stable specs: spree (1.3.6.beta) @@ -123,7 +123,7 @@ GEM active_link_to (1.0.0) active_model_serializers (0.8.1) activemodel (>= 3.0) - activemerchant (1.46.0) + activemerchant (1.48.0) activesupport (>= 3.2.14, < 5.0.0) builder (>= 2.1.2, < 4.0.0) i18n (>= 0.6.9) @@ -181,7 +181,7 @@ GEM climate_control (0.0.3) activesupport (>= 3.0) cliver (0.3.2) - cocaine (0.5.5) + cocaine (0.5.7) climate_control (>= 0.0.3, < 1.0) coderay (1.0.9) coffee-rails (3.2.2) @@ -191,7 +191,7 @@ GEM coffee-script-source execjs coffee-script-source (1.3.3) - colorize (0.7.5) + colorize (0.7.7) columnize (0.3.6) comfortable_mexican_sofa (1.6.24) active_link_to (~> 1.0.0) @@ -496,7 +496,7 @@ GEM sprockets (>= 2.0.0) turn (0.8.3) ansi - tzinfo (0.3.43) + tzinfo (0.3.44) uglifier (1.2.4) execjs (>= 0.3.0) multi_json (>= 1.0.2) diff --git a/app/assets/stylesheets/admin/icons.css.scss b/app/assets/stylesheets/admin/icons.css.scss new file mode 100644 index 0000000000..e7737ce8e9 --- /dev/null +++ b/app/assets/stylesheets/admin/icons.css.scss @@ -0,0 +1,3 @@ +@import 'plugins/font-awesome'; + +.icon-refund:before { @extend .icon-ok:before } diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index 3e3c255ab3..42e7068afc 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -9,6 +9,13 @@ Spree::Admin::OrdersController.class_eval do # in an auth failure as the @order object is nil for collection actions before_filter :check_authorization, :except => :bulk_management + # After updating an order, the fees should be updated as well + # Currently, adding or deleting line items does not trigger updating the + # fees! This is a quick fix for that. + # TODO: update fees when adding/removing line items + # instead of the update_distribution_charge method. + after_filter :update_distribution_charge, :only => :update + respond_override :index => { :html => { :success => lambda { # Filter orders to only show those distributed by current user (or all for admin user) @@ -17,4 +24,17 @@ Spree::Admin::OrdersController.class_eval do page(params[:page]). per(params[:per_page] || Spree::Config[:orders_per_page]) } } } + + # Overwrite to use confirm_email_for_customer instead of confirm_email. + # This uses a new template. See mailers/spree/order_mailer_decorator.rb. + def resend + Spree::OrderMailer.confirm_email_for_customer(@order.id, true).deliver + flash[:success] = t(:order_email_resent) + + respond_with(@order) { |format| format.html { redirect_to :back } } + end + + def update_distribution_charge + @order.update_distribution_charge! + end end diff --git a/app/controllers/spree/admin/variants_controller_decorator.rb b/app/controllers/spree/admin/variants_controller_decorator.rb index acb55327ae..539ce83183 100644 --- a/app/controllers/spree/admin/variants_controller_decorator.rb +++ b/app/controllers/spree/admin/variants_controller_decorator.rb @@ -6,15 +6,18 @@ Spree::Admin::VariantsController.class_eval do @variants = Spree::Variant.ransack(search_params.merge(:m => 'or')).result - if params[:distributor_id].present? - distributor = Enterprise.find params[:distributor_id] - @variants = @variants.in_distributor(distributor) - end - if params[:order_cycle_id].present? order_cycle = OrderCycle.find params[:order_cycle_id] @variants = @variants.in_order_cycle(order_cycle) end + + if params[:distributor_id].present? + distributor = Enterprise.find params[:distributor_id] + @variants = @variants.in_distributor(distributor) + # Perform scoping after all filtering is done. + # Filtering could be a problem on scoped variants. + @variants.each { |v| v.scope_to_hub(distributor) } + end end def destroy diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index d5e18c3d9b..0dc6977965 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -198,6 +198,18 @@ Spree::Order.class_eval do end end + # Does this order have shipments that can be shipped? + def ready_to_ship? + self.shipments.any?{|s| s.can_ship?} + end + + # Ship all pending orders + def ship + self.shipments.each do |s| + s.ship if s.can_ship? + end + end + def available_shipping_methods(display_on = nil) Spree::ShippingMethod.all_available(self, display_on) end diff --git a/app/models/spree/payment_decorator.rb b/app/models/spree/payment_decorator.rb new file mode 100644 index 0000000000..1a80f0269f --- /dev/null +++ b/app/models/spree/payment_decorator.rb @@ -0,0 +1,52 @@ +module Spree + Payment.class_eval do + # Pin payments lacks void and credit methods, but it does have refund + # Here we swap credit out for refund and remove void as a possible action + def actions_with_pin_payment_adaptations + actions = actions_without_pin_payment_adaptations + if payment_method.is_a? Gateway::Pin + actions << 'refund' if actions.include? 'credit' + actions.reject! { |a| ['credit', 'void'].include? a } + end + actions + end + alias_method_chain :actions, :pin_payment_adaptations + + + def refund!(refund_amount=nil) + protect_from_connection_error do + check_environment + + refund_amount = calculate_refund_amount(refund_amount) + + if payment_method.payment_profiles_supported? + response = payment_method.refund((refund_amount * 100).round, source, response_code, gateway_options) + else + response = payment_method.refund((refund_amount * 100).round, response_code, gateway_options) + end + + record_response(response) + + if response.success? + self.class.create({ :order => order, + :source => self, + :payment_method => payment_method, + :amount => refund_amount.abs * -1, + :response_code => response.authorization, + :state => 'completed' }, :without_protection => true) + else + gateway_error(response) + end + end + end + + + private + + def calculate_refund_amount(refund_amount=nil) + refund_amount ||= credit_allowed >= order.outstanding_balance.abs ? order.outstanding_balance.abs : credit_allowed.abs + refund_amount.to_f + end + + end +end diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 86a0b5fd69..03543793b2 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -74,6 +74,12 @@ Spree::Variant.class_eval do self.option_values.destroy ovs end + # Used like "product.name - full_name". If called like this, a product with + # name "Bread" would be displayed as one of these: + # Bread - 1kg # if display_name blank + # Bread - Spelt Sourdough, 1kg # if display_name is "Spelt Sourdough, 1kg" + # Bread - 1kg Spelt Sourdough # if unit_to_display is "1kg Spelt Sourdough" + # Bread - Spelt Sourdough (1kg) # if display_name is "Spelt Sourdough" and unit_to_display is "1kg" def full_name return unit_to_display if display_name.blank? return display_name if display_name.downcase.include? unit_to_display.downcase diff --git a/app/overrides/spree/admin/orders/edit/add_ship_button.html.haml.deface b/app/overrides/spree/admin/orders/edit/add_ship_button.html.haml.deface new file mode 100644 index 0000000000..47fbdea70a --- /dev/null +++ b/app/overrides/spree/admin/orders/edit/add_ship_button.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_before "code[erb-loud]:contains('button_link_to t(:resend)')" +- if @order.ready_to_ship? + %li= button_link_to t(:ship), fire_admin_order_url(@order, :e => 'ship'), :method => :put, :data => { :confirm => t(:are_you_sure) } diff --git a/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface b/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface new file mode 100644 index 0000000000..cf0b2b0abb --- /dev/null +++ b/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface @@ -0,0 +1,6 @@ +/ insert_bottom "[data-hook='admin_orders_index_row_actions']" +-# See also: app/overrides/add_capture_order_shortcut.rb + +- if order.ready_to_ship? + - # copied from backend/app/views/spree/admin/payments/_list.html.erb + = link_to_with_icon "icon-road", t(:ship), fire_admin_order_url(order, :e => 'ship'), :method => :put, :no_text => true, :data => {:action => 'ship', :confirm => t(:are_you_sure)} diff --git a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface new file mode 100644 index 0000000000..75a2bc4689 --- /dev/null +++ b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface @@ -0,0 +1,6 @@ +/ insert_bottom "[data-hook='admin_orders_index_rows'] td:nth-child(3)" + +- if order.special_instructions.present? + %br + %span{class: "icon-warning-sign with-tip", title: order.special_instructions} + notes diff --git a/app/overrides/spree/admin/shared/_order_tabs/add_customer_name.html.haml.deface b/app/overrides/spree/admin/shared/_order_tabs/add_customer_name.html.haml.deface new file mode 100644 index 0000000000..2e3780460e --- /dev/null +++ b/app/overrides/spree/admin/shared/_order_tabs/add_customer_name.html.haml.deface @@ -0,0 +1,6 @@ +/ insert_after "code[erb-silent]:contains('content_for :page_title')" + +- if @order.bill_address.present? + = @order.bill_address.firstname + = @order.bill_address.lastname + \- diff --git a/app/views/spree/admin/variants/_autocomplete.js.erb b/app/views/spree/admin/variants/_autocomplete.js.erb new file mode 100644 index 0000000000..7b52c0a716 --- /dev/null +++ b/app/views/spree/admin/variants/_autocomplete.js.erb @@ -0,0 +1,33 @@ + diff --git a/app/views/spree/admin/variants/search.rabl b/app/views/spree/admin/variants/search.rabl new file mode 100644 index 0000000000..afd3f39ce6 --- /dev/null +++ b/app/views/spree/admin/variants/search.rabl @@ -0,0 +1,34 @@ +# +# overriding spree/core/app/views/spree/admin/variants/search.rabl +# +collection @variants +attributes :sku, :options_text, :count_on_hand, :id, :cost_price + +node(:name) do |v| + # TODO: when products must have a unit, full_name will always be present + variant_specific = v.full_name + if variant_specific.present? + "#{v.name} - #{v.full_name}" + else + v.name + end +end + +node(:full_name) do |v| + v.full_name +end + +node(:producer_name) do |v| + v.product.supplier.name +end + +child(:images => :images) do + attributes :mini_url +end + +child(:option_values => :option_values) do + child(:option_type => :option_type) do + attributes :name, :presentation + end + attributes :name, :presentation +end diff --git a/spec/controllers/spree/admin/orders_controller_spec.rb b/spec/controllers/spree/admin/orders_controller_spec.rb new file mode 100644 index 0000000000..5724732c50 --- /dev/null +++ b/spec/controllers/spree/admin/orders_controller_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Spree::Admin::OrdersController do + let!(:order) { create(:order) } + + context "updating an order with line items" do + let(:line_item) { create(:line_item) } + before { login_as_admin } + + it "updates distribution charges" do + order.line_items << line_item + order.save + Spree::Order.any_instance.should_receive(:update_distribution_charge!) + spree_put :update, { + id: order, + order: { + number: order.number, + distributor_id: order.distributor_id, + order_cycle_id: order.order_cycle_id, + line_items_attributes: [ + { + id: line_item.id, + quantity: line_item.quantity + } + ] + } + } + end + end +end diff --git a/spec/controllers/spree/admin/variants_controller_spec.rb b/spec/controllers/spree/admin/variants_controller_spec.rb index 9c6d77194d..22972a7301 100644 --- a/spec/controllers/spree/admin/variants_controller_spec.rb +++ b/spec/controllers/spree/admin/variants_controller_spec.rb @@ -8,6 +8,7 @@ module Spree describe "search action" do let!(:p1) { create(:simple_product, name: 'Product 1') } let!(:p2) { create(:simple_product, name: 'Product 2') } + let!(:vo) { create(:variant_override, variant: p1.master, hub: d, count_on_hand: 44) } let!(:d) { create(:distributor_enterprise) } let!(:oc) { create(:simple_order_cycle, distributors: [d], variants: [p1.master]) } @@ -16,6 +17,12 @@ module Spree assigns(:variants).should == [p1.master] end + it "applies variant overrides" do + spree_get :search, q: 'Prod', distributor_id: d.id.to_s + assigns(:variants).should == [p1.master] + assigns(:variants).first.count_on_hand.should == 44 + end + it "filters by order cycle" do spree_get :search, q: 'Prod', order_cycle_id: oc.id.to_s assigns(:variants).should == [p1.master] diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 3c70c2243c..ecc19f5493 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -186,6 +186,59 @@ describe Spree::Order do end end + describe "an order without shipping method" do + let(:order) { create(:order) } + + it "cannot be shipped" do + order.ready_to_ship?.should == false + end + end + + describe "an unpaid order with a shipment" do + let(:order) { create(:order, shipping_method: shipping_method) } + let(:shipping_method) { create(:shipping_method) } + + before do + order.create_shipment! + order.reload + order.state = 'complete' + order.shipment.update!(order) + end + + it "cannot be shipped" do + order.ready_to_ship?.should == false + end + end + + describe "a paid order without a shipment" do + let(:order) { create(:order) } + + before do + order.payment_state = 'paid' + order.state = 'complete' + end + + it "cannot be shipped" do + order.ready_to_ship?.should == false + end + end + + describe "a paid order with a shipment" do + let(:order) { create(:order, shipping_method: shipping_method) } + let(:shipping_method) { create(:shipping_method) } + + before do + order.create_shipment! + order.payment_state = 'paid' + order.state = 'complete' + order.shipment.update!(order) + end + + it "can be shipped" do + order.ready_to_ship?.should == true + end + end + describe "getting the shipping tax" do let(:order) { create(:order, shipping_method: shipping_method) } let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 50.0)) } diff --git a/spec/models/spree/payment_spec.rb b/spec/models/spree/payment_spec.rb new file mode 100644 index 0000000000..8a781e7700 --- /dev/null +++ b/spec/models/spree/payment_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +module Spree + describe Payment do + describe "available actions" do + context "for most gateways" do + let(:payment) { create(:payment, source: create(:credit_card)) } + + it "can capture and void" do + payment.actions.sort.should == %w(capture void).sort + end + + describe "when a payment has been taken" do + before do + payment.stub(:state) { 'completed' } + payment.stub(:order) { double(:order, payment_state: 'credit_owed') } + end + + it "can void and credit" do + payment.actions.sort.should == %w(void credit).sort + end + end + end + + context "for Pin Payments" do + let(:d) { create(:distributor_enterprise) } + let(:pin) { Gateway::Pin.create! name: 'pin', distributor_ids: [d.id]} + let(:payment) { create(:payment, source: create(:credit_card), payment_method: pin) } + + it "does not void" do + payment.actions.should_not include 'void' + end + + describe "when a payment has been taken" do + before do + payment.stub(:state) { 'completed' } + payment.stub(:order) { double(:order, payment_state: 'credit_owed') } + end + + it "can refund instead of crediting" do + payment.actions.should_not include 'credit' + payment.actions.should include 'refund' + end + end + end + end + + describe "refunding" do + let(:payment) { create(:payment) } + let(:success) { double(:success? => true, authorization: 'abc123') } + let(:failure) { double(:success? => false) } + + it "always checks the environment" do + payment.payment_method.stub(:refund) { success } + payment.should_receive(:check_environment) + payment.refund! + end + + describe "calculating refund amount" do + it "returns the parameter amount when given" do + payment.send(:calculate_refund_amount, 123).should === 123.0 + end + + it "refunds up to the value of the payment when the outstanding balance is larger" do + payment.stub(:credit_allowed) { 123 } + payment.stub(:order) { double(:order, outstanding_balance: 1000) } + payment.send(:calculate_refund_amount).should == 123 + end + + it "refunds up to the outstanding balance of the order when the payment is larger" do + payment.stub(:credit_allowed) { 1000 } + payment.stub(:order) { double(:order, outstanding_balance: 123) } + payment.send(:calculate_refund_amount).should == 123 + end + end + + describe "performing refunds" do + before do + payment.stub(:calculate_refund_amount) { 123 } + payment.payment_method.should_receive(:refund).and_return(success) + end + + it "performs the refund without payment profiles" do + payment.payment_method.stub(:payment_profiles_supported?) { false } + payment.refund! + end + + it "performs the refund with payment profiles" do + payment.payment_method.stub(:payment_profiles_supported?) { true } + payment.refund! + end + end + + it "records the response" do + payment.stub(:calculate_refund_amount) { 123 } + payment.payment_method.stub(:refund).and_return(success) + payment.should_receive(:record_response).with(success) + payment.refund! + end + + it "records a payment on success" do + payment.stub(:calculate_refund_amount) { 123 } + payment.payment_method.stub(:refund).and_return(success) + payment.stub(:record_response) + + expect do + payment.refund! + end.to change(Payment, :count).by(1) + + p = Payment.last + p.order.should == payment.order + p.source.should == payment + p.payment_method.should == payment.payment_method + p.amount.should == -123 + p.response_code.should == success.authorization + p.state.should == 'completed' + end + + it "logs the error on failure" do + payment.stub(:calculate_refund_amount) { 123 } + payment.payment_method.stub(:refund).and_return(failure) + payment.stub(:record_response) + payment.should_receive(:gateway_error).with(failure) + payment.refund! + end + end + end +end