diff --git a/.ruby-version b/.ruby-version index ae6d5b9cbe..cd57a8b95d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -1.9.3-p392 +2.1.5 diff --git a/.travis.yml b/.travis.yml index 7a7209d417..5c752d6ee4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false cache: bundler bundler_args: --without development rvm: - - "1.9.3" + - "2.1.5" # The test cases are roughly split according to their test times. # It would be better to use https://github.com/ArturT/knapsack. diff --git a/Gemfile b/Gemfile index 8be45b8651..a49c7ef06a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,12 @@ source 'https://rubygems.org' -ruby "1.9.3" +ruby "2.1.5" gem 'rails', '3.2.21' gem 'rails-i18n', '~> 3.0.0' gem 'i18n', '~> 0.6.11' +gem 'nokogiri' + gem 'pg' gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' gem 'spree_i18n', :github => 'spree/spree_i18n', :branch => '1-3-stable' @@ -114,7 +116,7 @@ group :test do end group :development do - gem 'pry-debugger' + gem 'pry-byebug' gem 'debugger-linecache' gem 'guard' gem 'guard-livereload' diff --git a/Gemfile.lock b/Gemfile.lock index 3dfea17147..28fd15732c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,6 +171,9 @@ GEM httparty (>= 0.6, < 1.0) multi_json (~> 1.0) builder (3.0.4) + byebug (2.7.0) + columnize (~> 0.3) + debugger-linecache (~> 1.2) cancan (1.6.8) capybara (2.2.1) mime-types (>= 1.16) @@ -196,7 +199,7 @@ GEM execjs coffee-script-source (1.3.3) colorize (0.7.7) - columnize (0.3.6) + columnize (0.9.0) comfortable_mexican_sofa (1.6.24) active_link_to (~> 1.0.0) paperclip (>= 2.3.0) @@ -230,12 +233,7 @@ GEM activerecord (~> 3.0) fog (~> 1.0) rails (~> 3.0) - debugger (1.6.1) - columnize (>= 0.3.1) - debugger-linecache (~> 1.2.0) - debugger-ruby_core_source (~> 1.2.3) debugger-linecache (1.2.0) - debugger-ruby_core_source (1.2.3) delayed_job (4.0.4) activesupport (>= 3.0, < 4.2) delayed_job_active_record (4.0.2) @@ -253,7 +251,7 @@ GEM eventmachine (>= 0.12.9) http_parser.rb (~> 0.5.3) erubis (2.7.0) - eventmachine (1.0.3) + eventmachine (1.0.8) excon (0.25.3) execjs (2.5.2) factory_girl (3.3.0) @@ -336,7 +334,7 @@ GEM addressable (~> 2.3) letter_opener (1.0.0) launchy (>= 2.0.4) - libv8 (3.16.14.3) + libv8 (3.16.14.11) listen (2.2.0) celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) @@ -391,9 +389,9 @@ GEM coderay (~> 1.0.5) method_source (~> 0.8) slop (~> 3.4) - pry-debugger (0.2.2) - debugger (~> 1.3) - pry (~> 0.9.10) + pry-byebug (1.3.2) + byebug (~> 2.7) + pry (~> 0.9.12) rabl (0.7.2) activesupport (>= 2.3.14) multi_json (~> 1.0) @@ -594,13 +592,14 @@ DEPENDENCIES letter_opener momentjs-rails newrelic_rpm + nokogiri oj paper_trail (~> 3.0.8) paperclip parallel_tests pg poltergeist - pry-debugger + pry-byebug rabl rack-livereload rack-ssl diff --git a/README.markdown b/README.markdown index 15c6920be1..122e49c946 100644 --- a/README.markdown +++ b/README.markdown @@ -20,7 +20,7 @@ Below are instructions for setting up a development environment for Open Food Ne ## Dependencies * Rails 3.2.x -* Ruby 1.9.3 +* Ruby 2.1.5 * PostgreSQL database * PhantomJS (for testing) * See Gemfile for a list of gems required @@ -44,7 +44,7 @@ You can download the source with the command: For those new to Rails, the following tutorial will help get you up to speed with configuring a Rails environment: http://guides.rubyonrails.org/getting_started.html . -First, check your dependencies: Ensure that you have Ruby >= 1.9.3 installed: +First, check your dependencies: Ensure that you have Ruby 2.1.5 installed: ruby --version diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 57e77123eb..9e1155d6b7 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -53,7 +53,6 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! - flash[:notice] = 'Your order cycle has been updated.' format.html { redirect_to admin_order_cycles_path } format.json { render :json => {:success => true} } @@ -79,6 +78,13 @@ module Admin redirect_to main_app.admin_order_cycles_path, :notice => "Your order cycle #{@order_cycle.name} has been cloned." end + # Send notifications to all producers who are part of the order cycle + def notify_producers + Delayed::Job.enqueue OrderCycleNotificationJob.new(params[:id].to_i) + + redirect_to main_app.admin_order_cycles_path, :notice => 'Emails to be sent to producers have been queued for sending.' + end + protected def collection(show_more=false) diff --git a/app/controllers/spree/admin/adjustments_controller_decorator.rb b/app/controllers/spree/admin/adjustments_controller_decorator.rb new file mode 100644 index 0000000000..12c9ae2c60 --- /dev/null +++ b/app/controllers/spree/admin/adjustments_controller_decorator.rb @@ -0,0 +1,44 @@ +module Spree + module Admin + AdjustmentsController.class_eval do + before_filter :set_included_tax, only: [:create, :update] + before_filter :set_default_tax_rate, only: :edit + + + private + + # Choose a default tax rate to show on the edit form. The adjustment stores its included + # tax in dollars, but doesn't store the source of the tax (ie. TaxRate that generated it). + # We guess which tax rate here, choosing: + # 1. A tax rate that will compute to the same amount as the existing tax + # 2. If that's not present, the first tax rate that's valid for the current order + # When we have to go with 2, we show an error message to ask the admin to check that the + # correct tax is being applied. + def set_default_tax_rate + if @adjustment.included_tax > 0 + trs = TaxRate.match(@order) + tr_yielding_matching_tax = trs.select { |tr| tr.compute_tax(@adjustment.amount) == @adjustment.included_tax }.first.andand.id + tr_valid_for_order = TaxRate.match(@order).first.andand.id + + @tax_rate_id = tr_yielding_matching_tax || tr_valid_for_order + + if tr_yielding_matching_tax.nil? + @adjustment.errors.add :tax_rate_id, "^Please check that the tax rate for this adjustment is correct." + end + end + end + + + def set_included_tax + if params[:tax_rate_id].present? + tax_rate = TaxRate.find params[:tax_rate_id] + amount = params[:adjustment][:amount].to_f + params[:adjustment][:included_tax] = tax_rate.compute_tax amount + + else + params[:adjustment][:included_tax] = 0 + end + end + end + end +end diff --git a/app/jobs/order_cycle_notification_job.rb b/app/jobs/order_cycle_notification_job.rb new file mode 100644 index 0000000000..b18348813e --- /dev/null +++ b/app/jobs/order_cycle_notification_job.rb @@ -0,0 +1,6 @@ +OrderCycleNotificationJob = Struct.new(:order_cycle_id) do + def perform + order_cycle = OrderCycle.find order_cycle_id + order_cycle.suppliers.each { |supplier| ProducerMailer.order_cycle_report(supplier, order_cycle).deliver } + end +end diff --git a/app/mailers/producer_mailer.rb b/app/mailers/producer_mailer.rb new file mode 100644 index 0000000000..5d8ce57eec --- /dev/null +++ b/app/mailers/producer_mailer.rb @@ -0,0 +1,53 @@ +class ProducerMailer < Spree::BaseMailer + + def order_cycle_report(producer, order_cycle) + @producer = producer + @coordinator = order_cycle.coordinator + @order_cycle = order_cycle + @line_items = aggregated_line_items_from(@order_cycle, @producer) + @receival_time = @order_cycle.receival_time_for @producer + @receival_instructions = @order_cycle.receival_instructions_for @producer + + subject = "[#{Spree::Config.site_name}] Order cycle report for #{producer.name}" + + if has_orders? order_cycle, producer + mail(to: @producer.email, + from: from_address, + subject: subject, + reply_to: @coordinator.email, + cc: @coordinator.email) + end + end + + + private + + def has_orders?(order_cycle, producer) + line_items_from(order_cycle, producer).any? + end + + def aggregated_line_items_from(order_cycle, producer) + aggregate_line_items line_items_from(order_cycle, producer) + end + + def line_items_from(order_cycle, producer) + Spree::LineItem. + joins(:order => :order_cycle, :variant => :product). + where('order_cycles.id = ?', order_cycle). + merge(Spree::Product.in_supplier(producer)). + merge(Spree::Order.complete) + end + + def aggregate_line_items(line_items) + # Arrange the items in a hash to group quantities + line_items.inject({}) do |lis, li| + if lis.key? li.variant + lis[li.variant].quantity += li.quantity + else + lis[li.variant] = li + end + + lis + end + end +end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index bc06594b96..44b482c1c4 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -201,6 +201,18 @@ class OrderCycle < ActiveRecord::Base exchanges.outgoing.to_enterprises([distributor]).first end + def exchange_for_supplier(supplier) + exchanges.incoming.from_enterprises([supplier]).first + end + + def receival_time_for(supplier) + exchange_for_supplier(supplier).andand.receival_time + end + + def receival_instructions_for(supplier) + exchange_for_supplier(supplier).andand.receival_instructions + end + def pickup_time_for(distributor) exchange_for_distributor(distributor).andand.pickup_time || distributor.next_collection_at end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 241d1de114..91182568a0 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -132,7 +132,7 @@ class AbilityDecorator can [:admin, :index, :read, :edit, :update], OrderCycle do |order_cycle| OrderCycle.accessible_by(user).include? order_cycle end - can [:bulk_update, :clone, :destroy], OrderCycle do |order_cycle| + can [:bulk_update, :clone, :destroy, :notify_producers], OrderCycle do |order_cycle| user.enterprises.include? order_cycle.coordinator end can [:for_order_cycle], Enterprise diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 5f3847e218..b179fe3036 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -24,6 +24,10 @@ module Spree update_attributes! included_tax: tax.round(2) end + def display_included_tax + Spree::Money.new(included_tax, { :currency => currency }) + end + def has_tax? included_tax > 0 end diff --git a/app/models/spree/tax_rate_decorator.rb b/app/models/spree/tax_rate_decorator.rb index 42eae86b8c..121d379584 100644 --- a/app/models/spree/tax_rate_decorator.rb +++ b/app/models/spree/tax_rate_decorator.rb @@ -1,20 +1,61 @@ -Spree::TaxRate.class_eval do - class << self - def match_with_sales_tax_registration(order) - return [] if order.distributor && !order.distributor.charges_sales_tax - match_without_sales_tax_registration(order) +module Spree + TaxRate.class_eval do + class << self + def match_with_sales_tax_registration(order) + return [] if order.distributor && !order.distributor.charges_sales_tax + match_without_sales_tax_registration(order) + end + alias_method_chain :match, :sales_tax_registration end - alias_method_chain :match, :sales_tax_registration - end - def adjust_with_included_tax(order) - adjust_without_included_tax(order) + def adjust_with_included_tax(order) + adjust_without_included_tax(order) - order.reload - (order.adjustments.tax + order.price_adjustments).each do |a| - a.set_absolute_included_tax! a.amount + order.reload + (order.adjustments.tax + order.price_adjustments).each do |a| + a.set_absolute_included_tax! a.amount + end + end + alias_method_chain :adjust, :included_tax + + + # Manually apply a TaxRate to a particular amount. TaxRates normally compute against + # LineItems or Orders, so we mock out a line item here to fit the interface + # that our calculator (usually DefaultTax) expects. + def compute_tax(amount) + product = OpenStruct.new tax_category: tax_category + line_item = LineItem.new quantity: 1 + line_item.define_singleton_method(:product) { product } + line_item.define_singleton_method(:price) { amount } + + # Tax on adjustments (represented by the included_tax field) is always inclusive of + # tax. However, there's nothing to stop an admin from setting one up with a tax rate + # that's marked as not inclusive of tax, and that would result in the DefaultTax + # calculator generating a slightly incorrect value. Therefore, we treat the tax + # rate as inclusive of tax for the calculations below, regardless of its original + # setting. + with_tax_included_in_price do + calculator.compute line_item + end + end + + + private + + def with_tax_included_in_price + old_included_in_price = self.included_in_price + + self.included_in_price = true + calculator.calculable.included_in_price = true + + result = yield + + ensure + self.included_in_price = old_included_in_price + calculator.calculable.included_in_price = old_included_in_price + + result end end - alias_method_chain :adjust, :included_tax end diff --git a/app/overrides/spree/admin/adjustments/_adjustments_table/add_tax_to_body.html.haml.deface b/app/overrides/spree/admin/adjustments/_adjustments_table/add_tax_to_body.html.haml.deface new file mode 100644 index 0000000000..398d99eaf6 --- /dev/null +++ b/app/overrides/spree/admin/adjustments/_adjustments_table/add_tax_to_body.html.haml.deface @@ -0,0 +1,9 @@ +/ replace_contents "[data-hook='adjustment_row']" + +%td.align-center.created_at= pretty_time(adjustment.created_at) +%td.align-center.label= adjustment.label +%td.align-center.amount= adjustment.display_amount.to_html +%td.align-center.included-tax= adjustment.display_included_tax.to_html +%td.actions + = link_to_edit adjustment, no_text: true + = link_to_delete adjustment, no_text: true \ No newline at end of file diff --git a/app/overrides/spree/admin/adjustments/_adjustments_table/add_tax_to_head.html.haml.deface b/app/overrides/spree/admin/adjustments/_adjustments_table/add_tax_to_head.html.haml.deface new file mode 100644 index 0000000000..6da264865a --- /dev/null +++ b/app/overrides/spree/admin/adjustments/_adjustments_table/add_tax_to_head.html.haml.deface @@ -0,0 +1,8 @@ +/ replace_contents "[data-hook='adjustmment_head']" + +%tr + %th= "#{t('spree.date')}/#{t('spree.time')}" + %th= t(:description) + %th= t(:amount) + %th= t(:included_tax) + %th.actions diff --git a/app/overrides/spree/admin/adjustments/_form/add_tax_rate.html.haml.deface b/app/overrides/spree/admin/adjustments/_form/add_tax_rate.html.haml.deface new file mode 100644 index 0000000000..a37acc1030 --- /dev/null +++ b/app/overrides/spree/admin/adjustments/_form/add_tax_rate.html.haml.deface @@ -0,0 +1,6 @@ +/ replace_contents "[data-hook='admin_adjustment_form_fields']" + +- if @adjustment.new_record? + = render 'new_form', f: f +- else + = render 'edit_form', f: f diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index 7f0fb983f9..ad9ace7001 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -7,6 +7,11 @@ - else {{ (incomingExchangeVariantsFor(exchange.enterprise_id)).length }} selected +- if type == 'supplier' + %td.receival-details + = text_field_tag 'order_cycle_incoming_exchange_{{ $index }}_receival_time', '', 'id' => 'order_cycle_incoming_exchange_{{ $index }}_receival_time', 'placeholder' => 'Receive at (ie. Date / Time)', 'ng-model' => 'exchange.receival_time' + %br/ + = text_field_tag 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', '', 'id' => 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', 'placeholder' => 'Receival instructions', 'ng-model' => 'exchange.receival_instructions' - if type == 'distributor' %td.collection-details = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'exchange.pickup_time', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index a5f7ec1518..fe3746bee6 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -9,6 +9,7 @@ %tr %th Supplier %th Products + %th Receival details %th Fees %th.actions %tbody{'ng-repeat' => 'exchange in order_cycle.incoming_exchanges'} diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index 2f17ecf85b..ab9c9c1b29 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -1,7 +1,13 @@ +- if can? :notify_producers, @order_cycle + = content_for :page_actions do + %li + = button_to "Notify producers", main_app.notify_producers_admin_order_cycle_path, :id => 'admin_notify_producers', :confirm => 'Are you sure?' + + %h1 Edit Order Cycle -- ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' +- ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' = form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit($event)'} do |f| - if order_cycles_simple_form = render 'simple_form', f: f diff --git a/app/views/json/_flash.rabl b/app/views/json/_flash.rabl index dce07f849a..2900b2b948 100644 --- a/app/views/json/_flash.rabl +++ b/app/views/json/_flash.rabl @@ -1,2 +1,2 @@ -object OpenStruct.new(flash) +object OpenStruct.new(flash.to_hash) attributes :info, :success, :error diff --git a/app/views/producer_mailer/order_cycle_report.text.haml b/app/views/producer_mailer/order_cycle_report.text.haml new file mode 100644 index 0000000000..1bd9099426 --- /dev/null +++ b/app/views/producer_mailer/order_cycle_report.text.haml @@ -0,0 +1,33 @@ +Dear #{@producer.name}, +\ +We now have all the consumer orders for next food drop. Please drop off your delivery at #{@receival_time}. + +- if @receival_instructions + Extra instructions: #{@receival_instructions} + +Please deliver to #{@coordinator.address.address1}, #{@coordinator.address.city}, #{@coordinator.address.zipcode} during the regular delivery time. If this is not convenient then please call #{@coordinator.phone}. + +Note: If you have to arrange a different delivery day and time, it is requested that you do not come on site during drop off/pick up times. + +\ +Orders summary +================ +\ +Here is a summary of the orders for your products: +\ +- @line_items.each_pair do |variant, line_item| + #{variant.sku} - #{raw(variant.product.supplier.name)} - #{raw(variant.product_and_variant_name)} (QTY: #{line_item.quantity}) @ #{line_item.single_money} = #{line_item.display_amount} + +\ +Details +========= +\ +For a detailed orders breakdown, please log into your account. + +Please confirm that you have received this email. + +Please send me an invoice for this amount so we can send you payment. + +If you need to phone on the day please call #{@coordinator.phone}. +\ +Thanks and best wishes - #{@coordinator.name} diff --git a/app/views/spree/admin/adjustments/_edit_form.html.haml b/app/views/spree/admin/adjustments/_edit_form.html.haml new file mode 100644 index 0000000000..5ca71ff9f7 --- /dev/null +++ b/app/views/spree/admin/adjustments/_edit_form.html.haml @@ -0,0 +1,25 @@ +.row + .alpha.four.columns + = f.field_container :amount do + = f.label :amount, raw(t(:amount) + content_tag(:span, " *", :class => "required")) + = text_field :adjustment, :amount, :class => 'fullwidth' + = f.error_message_on :amount + + .four.columns + = f.field_container :included_tax do + = f.label :included_tax, t(:included_tax) + = text_field :adjustment, :included_tax, disabled: true, class: 'fullwidth' + = f.error_message_on :included_tax + + .omega.four.columns + = f.field_container :tax_rate_id do + = f.label :tax_rate_id, t(:tax_rate) + = select_tag :tax_rate_id, options_from_collection_for_select(Spree::TaxRate.all, :id, :name, @tax_rate_id), prompt: t(:remove_tax), class: 'select2 fullwidth' + = f.error_message_on :tax_rate_id + +.row + .alpha.omega.twelve.columns + = f.field_container :label do + = f.label :label, raw(t(:description) + content_tag(:span, " *", :class => "required")) + = text_field :adjustment, :label, :class => 'fullwidth' + = f.error_message_on :label diff --git a/app/views/spree/admin/adjustments/_new_form.html.haml b/app/views/spree/admin/adjustments/_new_form.html.haml new file mode 100644 index 0000000000..a3e831aa8d --- /dev/null +++ b/app/views/spree/admin/adjustments/_new_form.html.haml @@ -0,0 +1,19 @@ +.row + .alpha.three.columns + = f.field_container :amount do + = f.label :amount, raw(t(:amount) + content_tag(:span, " *", :class => "required")) + = text_field :adjustment, :amount, :class => 'fullwidth' + = f.error_message_on :amount + + .omega.three.columns + = f.field_container :tax_rate_id do + = f.label :tax_rate_id, t(:tax_rate) + = select_tag :tax_rate_id, options_from_collection_for_select(Spree::TaxRate.all, :id, :name), prompt: t(:none), class: 'select2 fullwidth' + = f.error_message_on :tax_rate_id + +.row + .alpha.omega.twelve.columns + = f.field_container :label do + = f.label :label, raw(t(:description) + content_tag(:span, " *", :class => "required")) + = text_field :adjustment, :label, :class => 'fullwidth' + = f.error_message_on :label diff --git a/config/application.rb b/config/application.rb index 5a31bc5774..8a492f5121 100644 --- a/config/application.rb +++ b/config/application.rb @@ -47,7 +47,10 @@ module Openfoodnetwork # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %W(#{config.root}/app/presenters) + config.autoload_paths += %W( + #{config.root}/app/presenters + #{config.root}/app/jobs + ) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. diff --git a/config/locales/en.yml b/config/locales/en.yml index a2568c42c7..b7babd27bc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -631,3 +631,5 @@ Please follow the instructions there to make your enterprise visible on the Open transport_fee: "Transport fee" fundraising_fee: "Fundraising fee" price_graph: "Price graph" + included_tax: "Included tax" + remove_tax: "Remove tax" diff --git a/config/routes.rb b/config/routes.rb index 32a8c90e37..2fd0787af0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,7 +62,11 @@ Openfoodnetwork::Application.routes.draw do namespace :admin do resources :order_cycles do post :bulk_update, on: :collection, as: :bulk_update - get :clone, on: :member + + member do + get :clone + post :notify_producers + end end resources :enterprises do diff --git a/db/migrate/20141229094516_add_receival_time_to_exchange.rb b/db/migrate/20141229094516_add_receival_time_to_exchange.rb new file mode 100644 index 0000000000..06933ed051 --- /dev/null +++ b/db/migrate/20141229094516_add_receival_time_to_exchange.rb @@ -0,0 +1,6 @@ +class AddReceivalTimeToExchange < ActiveRecord::Migration + def change + add_column :exchanges, :receival_time, :string + add_column :exchanges, :receival_instructions, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 436d84303c..37ad6f1a44 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -386,6 +386,8 @@ ActiveRecord::Schema.define(:version => 20151002020537) do t.datetime "created_at", :null => false t.datetime "updated_at", :null => false t.boolean "incoming", :default => false, :null => false + t.string "receival_time" + t.string "receival_instructions" end add_index "exchanges", ["order_cycle_id"], :name => "index_exchanges_on_order_cycle_id" diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb index 070cb4a91c..4962bc148e 100644 --- a/lib/open_food_network/enterprise_fee_applicator.rb +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -35,43 +35,8 @@ module OpenFoodNetwork tax_rates = enterprise_fee.tax_category ? enterprise_fee.tax_category.tax_rates.match(order) : [] tax_rates.sum do |rate| - compute_tax rate, adjustment.amount + rate.compute_tax adjustment.amount end end - - # Apply a TaxRate to a particular amount. TaxRates normally compute against - # LineItems or Orders, so we mock out a line item here to fit the interface - # that our calculator (usually DefaultTax) expects. - def compute_tax(tax_rate, amount) - product = OpenStruct.new tax_category: tax_rate.tax_category - line_item = Spree::LineItem.new quantity: 1 - line_item.define_singleton_method(:product) { product } - line_item.define_singleton_method(:price) { amount } - - # The enterprise fee adjustments for which we're calculating tax are always inclusive of - # tax. However, there's nothing to stop an admin from setting one up with a tax rate - # that's marked as not inclusive of tax, and that would result in the DefaultTax - # calculator generating a slightly incorrect value. Therefore, we treat the tax - # rate as inclusive of tax for the calculations below, regardless of its original - # setting. - with_tax_included_in_price(tax_rate) do - tax_rate.calculator.compute line_item - end - end - - def with_tax_included_in_price(tax_rate) - old_included_in_price = tax_rate.included_in_price - - tax_rate.included_in_price = true - tax_rate.calculator.calculable.included_in_price = true - - result = yield - - tax_rate.included_in_price = old_included_in_price - tax_rate.calculator.calculable.included_in_price = old_included_in_price - - result - end end end - diff --git a/lib/open_food_network/order_cycle_form_applicator.rb b/lib/open_food_network/order_cycle_form_applicator.rb index 69ba284cd8..540a18da13 100644 --- a/lib/open_food_network/order_cycle_form_applicator.rb +++ b/lib/open_food_network/order_cycle_form_applicator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/order_cycle_permissions' + module OpenFoodNetwork # There are two translator classes on the boundary between Angular and Rails: On the Angular side, @@ -21,10 +23,14 @@ module OpenFoodNetwork if exchange_exists?(exchange[:enterprise_id], @order_cycle.coordinator_id, true) update_exchange(exchange[:enterprise_id], @order_cycle.coordinator_id, true, - {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids}) + {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids, + receival_time: exchange[:receival_time], + receival_instructions: exchange[:receival_instructions]}) else add_exchange(exchange[:enterprise_id], @order_cycle.coordinator_id, true, - {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids}) + {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids, + receival_time: exchange[:receival_time], + receival_instructions: exchange[:receival_instructions],}) end end @@ -35,12 +41,16 @@ module OpenFoodNetwork if exchange_exists?(@order_cycle.coordinator_id, exchange[:enterprise_id], false) update_exchange(@order_cycle.coordinator_id, exchange[:enterprise_id], false, - {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids, - pickup_time: exchange[:pickup_time], pickup_instructions: exchange[:pickup_instructions]}) + {variant_ids: variant_ids, + enterprise_fee_ids: enterprise_fee_ids, + pickup_time: exchange[:pickup_time], + pickup_instructions: exchange[:pickup_instructions]}) else add_exchange(@order_cycle.coordinator_id, exchange[:enterprise_id], false, - {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids, - pickup_time: exchange[:pickup_time], pickup_instructions: exchange[:pickup_instructions]}) + {variant_ids: variant_ids, + enterprise_fee_ids: enterprise_fee_ids, + pickup_time: exchange[:pickup_time], + pickup_instructions: exchange[:pickup_instructions]}) end end diff --git a/script/ci/includes.sh b/script/ci/includes.sh index f8ccd7b4b6..323a4f4589 100644 --- a/script/ci/includes.sh +++ b/script/ci/includes.sh @@ -1,5 +1,5 @@ function load_environment { - source /var/lib/jenkins/.rvm/environments/ruby-1.9.3-p392 + source /var/lib/jenkins/.rvm/environments/ruby-2.1.5 if [ ! -f config/application.yml ]; then ln -s application.yml.example config/application.yml fi diff --git a/spec/controllers/admin/order_cycles_controller_spec.rb b/spec/controllers/admin/order_cycles_controller_spec.rb index 8200b0d037..0483c43714 100644 --- a/spec/controllers/admin/order_cycles_controller_spec.rb +++ b/spec/controllers/admin/order_cycles_controller_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' module Admin describe OrderCyclesController do include AuthenticationWorkflow + let!(:distributor_owner) { create_enterprise_user enterprise_limit: 2 } before do @@ -101,6 +102,34 @@ module Admin end end + + describe "notifying producers" do + let(:user) { create_enterprise_user } + let(:admin_user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by_name!('admin') + user + end + let(:order_cycle) { create(:simple_order_cycle) } + + before do + controller.stub spree_current_user: admin_user + end + + it "enqueues a job" do + expect do + spree_post :notify_producers, {id: order_cycle.id} + end.to enqueue_job OrderCycleNotificationJob + end + + it "redirects back to the order cycles path with a success message" do + spree_post :notify_producers, {id: order_cycle.id} + expect(response).to redirect_to admin_order_cycles_path + flash[:notice].should == 'Emails to be sent to producers have been queued for sending.' + end + end + + describe "destroy" do let!(:distributor) { create(:distributor_enterprise, owner: distributor_owner) } diff --git a/spec/controllers/spree/admin/adjustments_controller_spec.rb b/spec/controllers/spree/admin/adjustments_controller_spec.rb new file mode 100644 index 0000000000..bb20fb792b --- /dev/null +++ b/spec/controllers/spree/admin/adjustments_controller_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +module Spree + describe Admin::AdjustmentsController do + include AuthenticationWorkflow + + before { login_as_admin } + + describe "setting included tax" do + let(:order) { create(:order) } + let(:tax_rate) { create(:tax_rate, amount: 0.1, calculator: Spree::Calculator::DefaultTax.new) } + + describe "creating an adjustment" do + it "sets included tax to zero when no tax rate is specified" do + spree_post :create, {order_id: order.number, adjustment: {label: 'Testing included tax', amount: '110'}, tax_rate_id: ''} + response.should redirect_to spree.admin_order_adjustments_path(order) + + a = Adjustment.last + a.label.should == 'Testing included tax' + a.amount.should == 110 + a.included_tax.should == 0 + end + + it "calculates included tax when a tax rate is provided" do + spree_post :create, {order_id: order.number, adjustment: {label: 'Testing included tax', amount: '110'}, tax_rate_id: tax_rate.id.to_s} + response.should redirect_to spree.admin_order_adjustments_path(order) + + a = Adjustment.last + a.label.should == 'Testing included tax' + a.amount.should == 110 + a.included_tax.should == 10 + end + end + + describe "updating an adjustment" do + let(:adjustment) { create(:adjustment, adjustable: order, amount: 1100, included_tax: 100) } + + it "sets included tax to zero when no tax rate is specified" do + spree_put :update, {order_id: order.number, id: adjustment.id, adjustment: {label: 'Testing included tax', amount: '110'}, tax_rate_id: ''} + response.should redirect_to spree.admin_order_adjustments_path(order) + + a = Adjustment.last + a.label.should == 'Testing included tax' + a.amount.should == 110 + a.included_tax.should == 0 + end + + it "calculates included tax when a tax rate is provided" do + spree_put :update, {order_id: order.number, id: adjustment.id, adjustment: {label: 'Testing included tax', amount: '110'}, tax_rate_id: tax_rate.id.to_s} + response.should redirect_to spree.admin_order_adjustments_path(order) + + a = Adjustment.last + a.label.should == 'Testing included tax' + a.amount.should == 110 + a.included_tax.should == 10 + end + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index e0c8831b52..0db34ab78c 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -15,9 +15,11 @@ FactoryGirl.define do # Incoming Exchanges ex1 = create(:exchange, :order_cycle => oc, :incoming => true, - :sender => supplier1, :receiver => oc.coordinator) + :sender => supplier1, :receiver => oc.coordinator, + :receival_time => 'time 0', :receival_instructions => 'instructions 0') ex2 = create(:exchange, :order_cycle => oc, :incoming => true, - :sender => supplier2, :receiver => oc.coordinator) + :sender => supplier2, :receiver => oc.coordinator, + :receival_time => 'time 1', :receival_instructions => 'instructions 1') ExchangeFee.create!(exchange: ex1, enterprise_fee: create(:enterprise_fee, enterprise: ex1.sender)) ExchangeFee.create!(exchange: ex2, @@ -71,7 +73,7 @@ FactoryGirl.define do after(:create) do |oc, proxy| proxy.suppliers.each do |supplier| - ex = create(:exchange, :order_cycle => oc, :sender => supplier, :receiver => oc.coordinator, :incoming => true, :pickup_time => 'time', :pickup_instructions => 'instructions') + ex = create(:exchange, :order_cycle => oc, :sender => supplier, :receiver => oc.coordinator, :incoming => true, :receival_time => 'time', :receival_instructions => 'instructions') proxy.variants.each { |v| ex.variants << v } end diff --git a/spec/features/admin/adjustments_spec.rb b/spec/features/admin/adjustments_spec.rb new file mode 100644 index 0000000000..b9a6ee3465 --- /dev/null +++ b/spec/features/admin/adjustments_spec.rb @@ -0,0 +1,89 @@ +require "spec_helper" + +feature %q{ + As an administrator + I want to manage adjustments on orders +} do + include AuthenticationWorkflow + include WebHelper + + let!(:user) { create(:user) } + let!(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) } + let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } + + let!(:order) { create(:order_with_totals_and_distribution, user: user, distributor: distributor, order_cycle: order_cycle, state: 'complete', payment_state: 'balance_due') } + let!(:tax_rate) { create(:tax_rate, name: 'GST', calculator: build(:calculator, preferred_amount: 10), zone: create(:zone_with_member)) } + + before do + order.finalize! + create(:check_payment, order: order, amount: order.total) + end + + scenario "adding taxed adjustments to an order" do + # When I go to the adjustments page for the order + login_to_admin_section + visit spree.admin_orders_path + page.find('td.actions a.icon-edit').click + click_link 'Adjustments' + + # And I create a new adjustment with tax + click_link 'New Adjustment' + fill_in 'adjustment_amount', with: 110 + fill_in 'adjustment_label', with: 'Late fee' + select 'GST', from: 'tax_rate_id' + click_button 'Continue' + + # Then I should see the adjustment, with the correct tax + page.should have_selector 'td.label', text: 'Late fee' + page.should have_selector 'td.amount', text: '110' + page.should have_selector 'td.included-tax', text: '10' + end + + scenario "modifying taxed adjustments on an order" do + # Given a taxed adjustment + adjustment = create(:adjustment, adjustable: order, amount: 110, included_tax: 10) + + # When I go to the adjustments page for the order + login_to_admin_section + visit spree.admin_orders_path + page.find('td.actions a.icon-edit').click + click_link 'Adjustments' + page.find('td.actions a.icon-edit').click + + # Then I should see the uneditable included tax and our tax rate as the default + page.should have_field :adjustment_included_tax, with: '10.00', disabled: true + page.should have_select :tax_rate_id, selected: 'GST' + + # When I edit the adjustment, removing the tax + select 'Remove tax', from: :tax_rate_id + click_button 'Continue' + + # Then the adjustment tax should be cleared + page.should have_selector 'td.amount', text: '110' + page.should have_selector 'td.included-tax', text: '0' + end + + scenario "modifying an untaxed adjustment on an order" do + # Given an untaxed adjustment + adjustment = create(:adjustment, adjustable: order, amount: 110, included_tax: 0) + + # When I go to the adjustments page for the order + login_to_admin_section + visit spree.admin_orders_path + page.find('td.actions a.icon-edit').click + click_link 'Adjustments' + page.find('td.actions a.icon-edit').click + + # Then I should see the uneditable included tax and 'Remove tax' as the default tax rate + page.should have_field :adjustment_included_tax, with: '0.00', disabled: true + page.should have_select :tax_rate_id, selected: [] + + # When I edit the adjustment, setting a tax rate + select 'GST', from: :tax_rate_id + click_button 'Continue' + + # Then the adjustment tax should be recalculated + page.should have_selector 'td.amount', text: '110' + page.should have_selector 'td.included-tax', text: '10' + end +end diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 5bcca783bd..271d3bd2f2 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -294,7 +294,6 @@ feature %q{ expect(page).to have_selector "tr#li_#{li1.id}" expect(page).to have_selector "tr#li_#{li2.id}" select2_select oc1.name, from: "order_cycle_filter" - expect(page).to have_selector "#loading img.spinner" expect(page).to_not have_selector "#loading img.spinner" expect(page).to have_selector "tr#li_#{li1.id}" expect(page).to_not have_selector "tr#li_#{li2.id}" diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 4a30c3a60c..575d37d7e7 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -1,8 +1,8 @@ require "spec_helper" feature %q{ - As a payment administrator - I want to capture multiple payments quickly from the one page + As an administrator + I want to manage orders } do include AuthenticationWorkflow include WebHelper @@ -10,7 +10,7 @@ feature %q{ background do @user = create(:user) @product = create(:simple_product) - @distributor = create(:distributor_enterprise) + @distributor = create(:distributor_enterprise, charges_sales_tax: true) @order_cycle = create(:simple_order_cycle, distributors: [@distributor], variants: [@product.variants.first]) @order = create(:order_with_totals_and_distribution, user: @user, distributor: @distributor, order_cycle: @order_cycle, state: 'complete', payment_state: 'balance_due') @@ -47,7 +47,7 @@ feature %q{ o.order_cycle.should == order_cycle end - scenario "can add a product to an existing order", js: true do + scenario "can add a product to an existing order", js: true, retry: 3 do login_to_admin_section visit '/admin/orders' @@ -106,6 +106,7 @@ feature %q{ current_path.should == spree.admin_orders_path end + context "as an enterprise manager" do let(:coordinator1) { create(:distributor_enterprise) } let(:coordinator2) { create(:distributor_enterprise) } diff --git a/spec/jobs/order_cycle_notification_job_spec.rb b/spec/jobs/order_cycle_notification_job_spec.rb new file mode 100644 index 0000000000..674bdbdd24 --- /dev/null +++ b/spec/jobs/order_cycle_notification_job_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe OrderCycleNotificationJob do + let(:order_cycle) { create(:order_cycle) } + + it "sends a mail to each supplier" do + mail = double(:mail) + allow(mail).to receive(:deliver) + expect(ProducerMailer).to receive(:order_cycle_report).twice.and_return(mail) + run_job OrderCycleNotificationJob.new(order_cycle.id) + end +end diff --git a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb index 6703f844ab..9ad1d222b3 100644 --- a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb @@ -65,33 +65,4 @@ module OpenFoodNetwork efa.send(:order_adjustment_label).should == "Whole order - packing fee by distributor Ballantyne" end end - - describe "ensuring that tax rate is marked as tax included_in_price" do - let(:efa) { EnterpriseFeeApplicator.new nil, nil, nil } - let(:tax_rate) { create(:tax_rate, included_in_price: false, calculator: Spree::Calculator::DefaultTax.new) } - - it "sets included_in_price to true" do - efa.send(:with_tax_included_in_price, tax_rate) do - tax_rate.included_in_price.should be_true - end - end - - it "sets the included_in_price value accessible to the calculator to true" do - efa.send(:with_tax_included_in_price, tax_rate) do - tax_rate.calculator.calculable.included_in_price.should be_true - end - end - - it "passes through the return value of the block" do - efa.send(:with_tax_included_in_price, tax_rate) do - 'asdf' - end.should == 'asdf' - end - - it "restores both values to their original afterwards" do - efa.send(:with_tax_included_in_price, tax_rate) {} - tax_rate.included_in_price.should be_false - tax_rate.calculator.calculable.included_in_price.should be_false - end - end end diff --git a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb index 5314c4376c..91882510b4 100644 --- a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb +++ b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb @@ -11,7 +11,7 @@ module OpenFoodNetwork coordinator_id = 123 supplier_id = 456 - incoming_exchange = {:enterprise_id => supplier_id, :incoming => true, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2]} + incoming_exchange = {:enterprise_id => supplier_id, :incoming => true, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2], :receival_time => 'receival time', :receival_instructions => 'receival instructions'} oc = double(:order_cycle, :coordinator_id => coordinator_id, :exchanges => [], :incoming_exchanges => [incoming_exchange], :outgoing_exchanges => []) @@ -19,7 +19,7 @@ module OpenFoodNetwork applicator.should_receive(:incoming_exchange_variant_ids).with(incoming_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(supplier_id, coordinator_id, true).and_return(false) - applicator.should_receive(:add_exchange).with(supplier_id, coordinator_id, true, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2]}) + applicator.should_receive(:add_exchange).with(supplier_id, coordinator_id, true, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :receival_time => 'receival time', :receival_instructions => 'receival instructions'}) applicator.should_receive(:destroy_untouched_exchanges) applicator.go! @@ -47,7 +47,7 @@ module OpenFoodNetwork coordinator_id = 123 supplier_id = 456 - incoming_exchange = {:enterprise_id => supplier_id, :incoming => true, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2]} + incoming_exchange = {:enterprise_id => supplier_id, :incoming => true, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2], :receival_time => 'receival time', :receival_instructions => 'receival instructions'} oc = double(:order_cycle, :coordinator_id => coordinator_id, @@ -59,7 +59,7 @@ module OpenFoodNetwork applicator.should_receive(:incoming_exchange_variant_ids).with(incoming_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(supplier_id, coordinator_id, true).and_return(true) - applicator.should_receive(:update_exchange).with(supplier_id, coordinator_id, true, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2]}) + applicator.should_receive(:update_exchange).with(supplier_id, coordinator_id, true, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :receival_time => 'receival time', :receival_instructions => 'receival instructions'}) applicator.should_receive(:destroy_untouched_exchanges) applicator.go! diff --git a/spec/mailers/producer_mailer_spec.rb b/spec/mailers/producer_mailer_spec.rb new file mode 100644 index 0000000000..2bc1d77bef --- /dev/null +++ b/spec/mailers/producer_mailer_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' +require 'yaml' + +describe ProducerMailer do + let(:s1) { create(:supplier_enterprise) } + let(:s2) { create(:supplier_enterprise) } + let(:s3) { create(:supplier_enterprise) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let(:p1) { create(:product, price: 12.34, supplier: s1) } + let(:p2) { create(:product, price: 23.45, supplier: s2) } + let(:p3) { create(:product, price: 34.56, supplier: s1) } + let(:order_cycle) { create(:simple_order_cycle) } + let!(:incoming_exchange) { order_cycle.exchanges.create! sender: s1, receiver: d1, incoming: true, receival_time: '10am Saturday', receival_instructions: 'Outside shed.' } + + let!(:order) do + order = create(:order, distributor: d1, order_cycle: order_cycle, state: 'complete') + order.line_items << create(:line_item, variant: p1.master) + order.line_items << create(:line_item, variant: p1.master) + order.line_items << create(:line_item, variant: p2.master) + order.finalize! + order.save + order + end + let!(:order_incomplete) do + order = create(:order, distributor: d1, order_cycle: order_cycle, state: 'payment') + order.line_items << create(:line_item, variant: p3.master) + order.save + order + end + let(:mail) { ActionMailer::Base.deliveries.last } + + before do + ActionMailer::Base.deliveries.clear + ProducerMailer.order_cycle_report(s1, order_cycle).deliver + end + + it "should send an email when an order cycle is closed" do + ActionMailer::Base.deliveries.count.should == 1 + end + + it "sets a reply-to of the enterprise email" do + mail.reply_to.should == [s1.email] + end + + it "includes receival time" do + mail.body.should include '10am Saturday' + end + + it "includes receival instructions" do + mail.body.should include 'Outside shed.' + end + + it "cc's the enterprise" do + mail.cc.should == [s1.email] + end + + it "contains an aggregated list of produce" do + body_lines_including(mail, p1.name).each do |line| + line.should include 'QTY: 2' + line.should include '@ $10.00 = $20.00' + end + end + + it "does not include incomplete orders" do + mail.body.should_not include p3.name + end + + it "sends no mail when the producer has no orders" do + expect do + ProducerMailer.order_cycle_report(s3, order_cycle).deliver + end.to change(ActionMailer::Base.deliveries, :count).by(0) + end + + + private + + def body_lines_including(mail, s) + mail.body.to_s.lines.select { |line| line.include? s } + end +end diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb index 0df7aeafab..2ba0e26b24 100644 --- a/spec/models/exchange_spec.rb +++ b/spec/models/exchange_spec.rb @@ -277,6 +277,7 @@ describe Exchange do 'payment_enterprise_id' => exchange.payment_enterprise_id, 'variant_ids' => exchange.variant_ids.sort, 'enterprise_fee_ids' => exchange.enterprise_fee_ids.sort, 'pickup_time' => exchange.pickup_time, 'pickup_instructions' => exchange.pickup_instructions, + 'receival_time' => exchange.receival_time, 'receival_instructions' => exchange.receival_instructions, 'created_at' => exchange.created_at, 'updated_at' => exchange.updated_at} end @@ -286,7 +287,8 @@ describe Exchange do 'incoming' => exchange.incoming, 'payment_enterprise_id' => exchange.payment_enterprise_id, 'variant_ids' => exchange.variant_ids.sort, 'enterprise_fee_ids' => exchange.enterprise_fee_ids.sort, - 'pickup_time' => exchange.pickup_time, 'pickup_instructions' => exchange.pickup_instructions} + 'pickup_time' => exchange.pickup_time, 'pickup_instructions' => exchange.pickup_instructions, + 'receival_time' => exchange.receival_time, 'receival_instructions' => exchange.receival_instructions} end end diff --git a/spec/models/spree/tax_rate_spec.rb b/spec/models/spree/tax_rate_spec.rb index 25aad986a0..03b2cb39aa 100644 --- a/spec/models/spree/tax_rate_spec.rb +++ b/spec/models/spree/tax_rate_spec.rb @@ -29,5 +29,42 @@ module Spree end end end + + describe "ensuring that tax rate is marked as tax included_in_price" do + let(:tax_rate) { create(:tax_rate, included_in_price: false, calculator: Spree::Calculator::DefaultTax.new) } + + it "sets included_in_price to true" do + tax_rate.send(:with_tax_included_in_price) do + tax_rate.included_in_price.should be_true + end + end + + it "sets the included_in_price value accessible to the calculator to true" do + tax_rate.send(:with_tax_included_in_price) do + tax_rate.calculator.calculable.included_in_price.should be_true + end + end + + it "passes through the return value of the block" do + tax_rate.send(:with_tax_included_in_price) do + 'asdf' + end.should == 'asdf' + end + + it "restores both values to their original afterwards" do + tax_rate.send(:with_tax_included_in_price) {} + tax_rate.included_in_price.should be_false + tax_rate.calculator.calculable.included_in_price.should be_false + end + + it "restores both values when an exception is raised" do + expect do + tax_rate.send(:with_tax_included_in_price) { raise Exception.new 'oops' } + end.to raise_error 'oops' + + tax_rate.included_in_price.should be_false + tax_rate.calculator.calculable.included_in_price.should be_false + end + end end end diff --git a/spec/support/html_helper.rb b/spec/support/html_helper.rb index 54c6ffe1f1..ffa261fc39 100644 --- a/spec/support/html_helper.rb +++ b/spec/support/html_helper.rb @@ -1,6 +1,6 @@ module OpenFoodNetwork module HtmlHelper - def save_and_open(html) + def html_save_and_open(html) require "launchy" file = Tempfile.new('html') file.write html