Merge branch 'master' into line_item_naming

Conflicts:
	app/views/admin/order_cycles/edit.html.haml
This commit is contained in:
Rob Harrington
2015-11-13 09:54:26 +11:00
23 changed files with 296 additions and 20 deletions

View File

@@ -5,6 +5,8 @@ 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'

View File

@@ -594,6 +594,7 @@ DEPENDENCIES
letter_opener
momentjs-rails
newrelic_rpm
nokogiri
oj
paper_trail (~> 3.0.8)
paperclip

View File

@@ -61,7 +61,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} }
@@ -87,6 +86,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

View File

@@ -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

View File

@@ -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

View File

@@ -196,6 +196,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

View File

@@ -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

View File

@@ -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'

View File

@@ -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'}

View File

@@ -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.orderCycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit($event)'} do |f|
- if order_cycles_simple_form
= render 'simple_form', f: f

View File

@@ -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}

View File

@@ -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.

View File

@@ -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

View File

@@ -0,0 +1,6 @@
class AddReceivalTimeToExchange < ActiveRecord::Migration
def change
add_column :exchanges, :receival_time, :string
add_column :exchanges, :receival_instructions, :string
end
end

View File

@@ -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"

View File

@@ -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

View File

@@ -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
@@ -135,6 +136,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) }

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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!

View File

@@ -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

View File

@@ -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