├── app ├── views │ ├── admin │ │ ├── drop_ship_orders │ │ │ ├── _form.html.erb │ │ │ ├── new.html.erb │ │ │ ├── index.html.erb │ │ │ └── show.html.erb │ │ ├── orders │ │ │ ├── _dso_order_header.html.erb │ │ │ ├── _dso_order_row.html.erb │ │ │ ├── _dso_sidebar_tab.html.erb │ │ │ ├── _confirm_drop_ship_info.html.erb │ │ │ └── _drop_ship_info.html.erb │ │ ├── shared │ │ │ ├── _product_form_right.html.erb │ │ │ ├── _suppliers_tab.html.erb │ │ │ └── _supplier_tabs.html.erb │ │ └── suppliers │ │ │ ├── new.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── show.html.erb │ │ │ ├── index.html.erb │ │ │ └── _form.html.erb │ ├── products │ │ ├── _supplier_info.html.erb │ │ └── _properties.html.erb │ ├── drop_ship_order_mailer │ │ ├── shipment.html.erb │ │ ├── confirmation.html.erb │ │ ├── shipment_notification.html.erb │ │ └── supplier_order.html.erb │ ├── drop_ship_orders │ │ ├── _affiliate_info.html.erb │ │ ├── _shipment_info.html.erb │ │ ├── _shipping_info.html.erb │ │ ├── edit.html.erb │ │ ├── _details.html.erb │ │ ├── show.html.erb │ │ ├── _table.html.erb │ │ └── _form.html.erb │ ├── users │ │ └── _drop_ship_orders.html.erb │ └── supplier_mailer │ │ └── welcome.html.erb ├── models │ ├── user_decorator.rb │ ├── drop_ship_line_item.rb │ ├── product_decorator.rb │ ├── ability_decorator.rb │ ├── supplier_product.rb │ ├── line_item_decorator.rb │ ├── order_decorator.rb │ ├── supplier.rb │ └── drop_ship_order.rb ├── mailers │ ├── supplier_mailer.rb │ └── drop_ship_order_mailer.rb ├── controllers │ ├── admin │ │ ├── orders_controller_decorator.rb │ │ ├── products_controller_decorator.rb │ │ ├── suppliers_controller.rb │ │ └── drop_ship_orders_controller.rb │ └── drop_ship_orders_controller.rb ├── helpers │ └── admin │ │ └── base_helper_decorator.rb └── validators │ ├── url_validator.rb │ └── email_validator.rb ├── Gemfile ├── test ├── fixtures │ ├── roles.yml │ ├── supplier_products.yml │ ├── suppliers.yml │ ├── variants.yml │ ├── calculators.yml │ ├── line_items.yml │ ├── addresses.yml │ ├── orders.yml │ ├── products.yml │ ├── states.yml │ └── countries.yml ├── dummy_hooks │ ├── after_app_generator.rb │ ├── after_migrate.rb.sample │ ├── before_migrate.rb │ └── templates │ │ └── initializers │ │ └── action_mailer.rb ├── unit │ ├── product_test.rb │ ├── user_test.rb │ ├── line_item_test.rb │ ├── supplier_product_test.rb │ ├── drop_ship_line_item_test.rb │ ├── supplier_test.rb │ ├── order_test.rb │ └── drop_ship_order_test.rb ├── support │ ├── helper_methods.rb │ └── factories.rb ├── functional │ └── admin │ │ └── order_controller_test.rb └── test_helper.rb ├── lib ├── spree_drop_shipping │ ├── version.rb │ └── custom_hooks.rb ├── generators │ ├── templates │ │ └── db │ │ │ └── migrate │ │ │ ├── add_supplier_to_line_items.rb │ │ │ ├── create_supplier_products.rb │ │ │ ├── create_drop_ship_line_items.rb │ │ │ ├── create_suppliers.rb │ │ │ └── create_drop_ship_orders.rb │ └── spree_drop_shipping │ │ └── install_generator.rb ├── spree_drop_shipping.rb └── tasks │ ├── sample_drop_ship_orders.rake │ └── sample_suppliers.rake ├── features ├── step_definitions │ ├── products.rb │ ├── suppliers.rb │ ├── admin │ │ └── products.rb │ ├── web_steps.rb │ └── supplier_orders.rb ├── support │ ├── paths.rb │ ├── selectors.rb │ └── env.rb ├── admin │ ├── products.feature │ └── suppliers.feature └── supplier_order.feature ├── public └── stylesheets │ └── admin │ ├── suppliers.css │ └── drop_ship_orders.css ├── .gitignore ├── config ├── cucumber.yml ├── routes.rb └── locales │ └── en.yml ├── Rakefile ├── LICENSE ├── spree_drop_shipping.gemspec └── README.md /app/views/admin/drop_ship_orders/_form.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /app/views/admin/orders/_dso_order_header.html.erb: -------------------------------------------------------------------------------- 1 | <%= t(".dso_ids") %> 2 | -------------------------------------------------------------------------------- /test/fixtures/roles.yml: -------------------------------------------------------------------------------- 1 | user: 2 | name: user 3 | 4 | admin: 5 | name: admin -------------------------------------------------------------------------------- /lib/spree_drop_shipping/version.rb: -------------------------------------------------------------------------------- 1 | module SpreeDropShipping 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /app/views/products/_supplier_info.html.erb: -------------------------------------------------------------------------------- 1 |
Supplier Info
2 |
<%= supplier.name %>
3 | -------------------------------------------------------------------------------- /features/step_definitions/products.rb: -------------------------------------------------------------------------------- 1 | Given /^I have some products$/ do 2 | assert 0 < Product.count 3 | end 4 | -------------------------------------------------------------------------------- /public/stylesheets/admin/suppliers.css: -------------------------------------------------------------------------------- 1 | fieldset.floating { 2 | width: 40%; 3 | float: left; 4 | margin-right: 10px; 5 | } -------------------------------------------------------------------------------- /test/dummy_hooks/after_app_generator.rb: -------------------------------------------------------------------------------- 1 | template "initializers/action_mailer.rb", "config/initializers/action_mailer.rb" 2 | -------------------------------------------------------------------------------- /test/dummy_hooks/after_migrate.rb.sample: -------------------------------------------------------------------------------- 1 | rake "db:migrate db:seed db:sample db:sample:suppliers db:sample:drop_ship_orders", :env => "development" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .rvmrc 4 | .DS_Store 5 | Gemfile.lock 6 | pkg/* 7 | test/dummy 8 | test/dummy_hooks/after_migrate.rb 9 | rerun.txt 10 | -------------------------------------------------------------------------------- /app/models/user_decorator.rb: -------------------------------------------------------------------------------- 1 | User.class_eval do 2 | 3 | has_one :supplier 4 | 5 | def has_supplier? 6 | !supplier.nil? 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/views/drop_ship_order_mailer/shipment.html.erb: -------------------------------------------------------------------------------- 1 |

Thank You for completing Order #<%= @dso.id %>

2 | 3 |

Thanks,

4 | 5 |

- <%= Spree::Config[:site_name] %>

6 | -------------------------------------------------------------------------------- /test/dummy_hooks/before_migrate.rb: -------------------------------------------------------------------------------- 1 | # install spree and migrate db 2 | rake "spree_core:install spree_auth:install spree_sample:install" 3 | run "rails g spree_drop_shipping:install" 4 | -------------------------------------------------------------------------------- /test/fixtures/supplier_products.yml: -------------------------------------------------------------------------------- 1 | ror_bag_supplier_1: 2 | supplier: supplier_1 3 | product: ds_ror_bag 4 | 5 | ror_mug_supplier_2: 6 | supplier: supplier_2 7 | product: ds_ror_mug 8 | -------------------------------------------------------------------------------- /app/views/admin/orders/_dso_order_row.html.erb: -------------------------------------------------------------------------------- 1 | <%= order.has_drop_ship_orders? ? order.drop_ship_orders.map{|i| link_to i.id, admin_drop_ship_order_path(i) }.join(", ").html_safe : nil %> 2 | -------------------------------------------------------------------------------- /test/unit/product_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class ProductTest < ActiveSupport::TestCase 4 | 5 | should have_one(:supplier_product) 6 | should have_one(:supplier) 7 | 8 | end -------------------------------------------------------------------------------- /app/views/admin/orders/_dso_sidebar_tab.html.erb: -------------------------------------------------------------------------------- 1 | > 2 | <%= link_to t("drop_ship_orders"), admin_order_drop_ship_orders_url(@order) %> 3 | 4 | -------------------------------------------------------------------------------- /app/views/admin/shared/_product_form_right.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= label_tag :supplier_id, t('.supplier') %>
3 | <%= select_tag :supplier_id, options_for_select(@supplier_options.unshift([t('.none'), ""]), @product.has_supplier? ? @product.supplier.id : nil) %> 4 |

5 | -------------------------------------------------------------------------------- /test/unit/user_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | 5 | should have_one(:supplier) 6 | 7 | should "respond to has_supplier?" do 8 | assert subject.respond_to?(:has_supplier?) 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /features/step_definitions/suppliers.rb: -------------------------------------------------------------------------------- 1 | Given /^I have an existing supplier named "([^"]*)"$/ do |name| 2 | Factory.create(:supplier, :name => name) 3 | end 4 | 5 | Given /^I'm logged in as "([^"]*)"$/ do |name| 6 | supplier = Supplier.find_by_name(name) 7 | login_as supplier.user 8 | end 9 | -------------------------------------------------------------------------------- /test/support/helper_methods.rb: -------------------------------------------------------------------------------- 1 | module HelperMethods 2 | 3 | def random_email 4 | random_token.split("").insert(random_token.length / 2, "@").push(".com").join("") 5 | end 6 | 7 | def random_token 8 | Digest::SHA1.hexdigest((Time.now.to_i * rand).to_s) 9 | end 10 | 11 | end -------------------------------------------------------------------------------- /app/models/drop_ship_line_item.rb: -------------------------------------------------------------------------------- 1 | class DropShipLineItem < ActiveRecord::Base 2 | 3 | belongs_to :drop_ship_order 4 | 5 | validates :drop_ship_order_id, :variant_id, :sku, :quantity, :price, :presence => true 6 | 7 | def subtotal 8 | self.quantity.to_i * self.price.to_f 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/templates/db/migrate/add_supplier_to_line_items.rb: -------------------------------------------------------------------------------- 1 | class AddSupplierToLineItems < ActiveRecord::Migration 2 | 3 | def self.up 4 | add_column :line_items, :supplier_id, :integer 5 | end 6 | 7 | def self.down 8 | remove_column :line_items, :supplier_id 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy_hooks/templates/initializers/action_mailer.rb: -------------------------------------------------------------------------------- 1 | # config/initializers/action_mailer.rb 2 | 3 | ActionMailer::Base.default_url_options[:host] = "localhost:3000" 4 | ActionMailer::Base.delivery_method = :sendmail 5 | ActionMailer::Base.sendmail_settings = { 6 | :arguments => '-r no-reply@example.com' 7 | } 8 | -------------------------------------------------------------------------------- /app/views/products/_properties.html.erb: -------------------------------------------------------------------------------- 1 | <% unless @product_properties.empty? %> 2 |
3 | <% for product_property in @product_properties %> 4 |
<%= product_property.property.presentation %>
5 |
<%= product_property.value %>
6 | <% end %> 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /test/fixtures/suppliers.yml: -------------------------------------------------------------------------------- 1 | <% for i in 1..5 do%> 2 | supplier_<%= i %>: 3 | name: <%= "Big Store #{i}" %> 4 | email: <%= "big.store.#{i}@example.com" %> 5 | phone: "800-555-5555" 6 | url: <%= "http://#{i}.example.com" %> 7 | contact: "Steve" 8 | contact_phone: "555-555-5555" 9 | address: billing_address 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/models/product_decorator.rb: -------------------------------------------------------------------------------- 1 | Product.class_eval do 2 | 3 | has_one :supplier_product, :dependent => :destroy 4 | has_one :supplier, :through => :supplier_product 5 | 6 | # Returns true if the product has a drop shipping supplier 7 | def has_supplier? 8 | supplier_product.present? && supplier.present? 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /public/stylesheets/admin/drop_ship_orders.css: -------------------------------------------------------------------------------- 1 | table.index tr.totals td { 2 | padding: 10px; 3 | background-color: #f9f9f9; 4 | font-size: 15px; 5 | font-weight: bold; 6 | text-align: right; 7 | line-height: 17px; 8 | } 9 | 10 | table.index tr.totals td span.total { 11 | margin-left: 10px; 12 | font-size: 17px; 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/_affiliate_info.html.erb: -------------------------------------------------------------------------------- 1 |

Affiliate Information

2 | 3 |
4 |
Name
5 |
<%= Spree::Config[:site_name] %>
6 |
Domain
7 |
<%= Spree::Config[:domain] || request.host %>
8 |
Email
9 |
<%= mail_to Spree::Config[:email] || "info@#{request.host}" %>
10 |
11 | -------------------------------------------------------------------------------- /features/step_definitions/admin/products.rb: -------------------------------------------------------------------------------- 1 | Given /^supplier named "([^"]*)" is linked to the first product$/ do |name| 2 | supplier = Supplier.find_by_name(name) 3 | product = Product.first 4 | # remove any linked suppliers 5 | SupplierProduct.where(:product_id => product.id).destroy_all 6 | SupplierProduct.create(:supplier => supplier, :product => product) 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/templates/db/migrate/create_supplier_products.rb: -------------------------------------------------------------------------------- 1 | class CreateSupplierProducts < ActiveRecord::Migration 2 | 3 | def self.up 4 | create_table :supplier_products do |t| 5 | t.references :supplier 6 | t.references :product 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :supplier_products 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /app/views/admin/suppliers/new.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t '.new_supplier' %>

2 | 3 | <%= render 'shared/error_messages', :target => @supplier %> 4 | 5 | <%= form_for([:admin, @supplier]) do |f| %> 6 | <%= render "form", :form => f %> 7 |

8 | <%= button t('create') %> <%= t('or') %> <%= link_to t('cancel'), collection_url %> 9 |

10 | <% end %> 11 | 12 | -------------------------------------------------------------------------------- /app/models/ability_decorator.rb: -------------------------------------------------------------------------------- 1 | class AbilityDecorator 2 | 3 | include CanCan::Ability 4 | 5 | def initialize(user) 6 | can :read, DropShipOrder do |order| 7 | order.user && order.user == user 8 | end 9 | can :update, DropShipOrder do |order| 10 | order.user && order.user == user 11 | end 12 | end 13 | end 14 | 15 | Ability.register_ability(AbilityDecorator) 16 | -------------------------------------------------------------------------------- /app/views/admin/drop_ship_orders/new.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t '.new_supplier' %>

2 | 3 | <%= render 'shared/error_messages', :target => @supplier %> 4 | 5 | <%= form_for([:admin, @supplier]) do |f| %> 6 | <%= render "form", :form => f %> 7 |

8 | <%= button t('create') %> <%= t('or') %> <%= link_to t('cancel'), collection_url %> 9 |

10 | <% end %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/drop_ship_order_mailer/confirmation.html.erb: -------------------------------------------------------------------------------- 1 |

Thank You for confirming drop ship order Drop Shop Order #<%= @dso.id %>

2 | 3 |

After shipping this order, please update its status on the site by clicking on the link below.

4 | 5 | <%= link_to "Update Order #{@dso.id}", edit_drop_ship_order_url(@dso) %> 6 | 7 |

Thanks,

8 | 9 |

- <%= Spree::Config[:site_name] %>

10 | -------------------------------------------------------------------------------- /app/mailers/supplier_mailer.rb: -------------------------------------------------------------------------------- 1 | class SupplierMailer < ActionMailer::Base 2 | 3 | default_url_options[:host] = "localhost:3000" 4 | 5 | default :from => 'no-reply@example.com' 6 | 7 | def welcome(supplier) 8 | @supplier = supplier 9 | @user = supplier.user 10 | mail :to => @supplier.email_with_name, :subject => "#{Spree::Config[:site_name]} - Welcome!" 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/views/admin/suppliers/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "admin/shared/supplier_tabs", :current => "Edit Supplier" %> 2 | 3 | <%= render 'shared/error_messages', :target => @supplier %> 4 | 5 | <%= form_for([:admin, @supplier]) do |f| %> 6 | <%= render "form", :form => f %> 7 |

8 | <%= button t("update") %> <%= t('or') %> <%= link_to t('cancel'), collection_url %> 9 |

10 | <% end %> -------------------------------------------------------------------------------- /app/views/admin/orders/_confirm_drop_ship_info.html.erb: -------------------------------------------------------------------------------- 1 | <% if @order.has_drop_ship_orders? %> 2 | 3 | <% label = @order.drop_ship_orders.reject(&:sent?).empty? ? ".resend_drop_ship_orders" : ".approve_drop_ship_orders" %> 4 | <%= button_link_to t(label), drop_ship_approve_admin_order_path(@order), :icon => 'accept', :confirm => t(".drop_ship_order_approval_confrmation") if @order.has_drop_ship_orders? %> 5 | 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/admin/shared/_suppliers_tab.html.erb: -------------------------------------------------------------------------------- 1 | <%#= tab :suppliers # wtf.. why isn't this working?? %> 2 | 3 |
  • "> 4 | <%= link_to "Suppliers", admin_suppliers_path %> 5 |
  • 6 |
  • "> 7 | <%= link_to "Drop Ship Orders", admin_drop_ship_orders_path %> 8 |
  • 9 | -------------------------------------------------------------------------------- /app/controllers/admin/orders_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Admin::OrdersController.class_eval do 2 | 3 | def drop_ship_approve 4 | @order = load_order 5 | if @order.approve_drop_ship_orders 6 | flash[:notice] = I18n.t('admin.drop_ship_orders.orders_sent') 7 | else 8 | flash[:error] = I18n.t('admin.drop_ship_orders.orders_not_sent') 9 | end 10 | redirect_to admin_order_path(@order) 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /test/unit/line_item_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class LineItemTest < ActiveSupport::TestCase 4 | 5 | should "respond to has_supplier?" do 6 | assert subject.respond_to?(:has_supplier?) 7 | end 8 | 9 | should "provide drop ship attributes" do 10 | hash = { :variant_id => nil, :sku => nil, :name => nil, :quantity => nil, :price => nil } 11 | assert_equal hash, subject.drop_ship_attributes 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /test/unit/supplier_product_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class SupplierProductTest < ActiveSupport::TestCase 4 | 5 | def setup 6 | 7 | end 8 | 9 | should validate_presence_of(:supplier_id) 10 | should validate_presence_of(:product_id) 11 | 12 | should belong_to(:supplier) 13 | should belong_to(:product) 14 | 15 | should "have a supplier model" do 16 | assert defined?(SupplierProduct) 17 | end 18 | 19 | end -------------------------------------------------------------------------------- /app/models/supplier_product.rb: -------------------------------------------------------------------------------- 1 | class SupplierProduct < ActiveRecord::Base 2 | 3 | #========================================== 4 | # Associations 5 | 6 | belongs_to :supplier 7 | belongs_to :product 8 | 9 | #========================================== 10 | # Validations 11 | 12 | validates_associated :supplier 13 | validates_associated :product 14 | validates :supplier_id, :presence => true 15 | validates :product_id, :presence => true 16 | 17 | end -------------------------------------------------------------------------------- /app/views/admin/orders/_drop_ship_info.html.erb: -------------------------------------------------------------------------------- 1 | <% if @order.has_drop_ship_orders? %> 2 |
    3 |

    Drop Ship Information

    4 | <% @order.drop_ship_orders.each do |dso| %> 5 |

    <%= link_to "##{dso.id}", admin_drop_ship_order_path(dso) %> - <%= link_to dso.supplier.name, admin_supplier_path(dso.supplier) %> - <%= dso.state %>

    6 | <%= render "drop_ship_orders/table", :dso => dso %> 7 | <% end %> 8 |
    9 | <% end %> 10 | -------------------------------------------------------------------------------- /test/fixtures/variants.yml: -------------------------------------------------------------------------------- 1 | ror_tote_v: 2 | product: ror_tote 3 | sku: ROR-00011 4 | price: 15.99 5 | cost_price: 13.00 6 | is_master: true 7 | count_on_hand: 10 8 | 9 | ds_ror_bag_v: 10 | product: ds_ror_bag 11 | sku: ROR-00012 12 | price: 22.99 13 | cost_price: 21.00 14 | is_master: true 15 | count_on_hand: 10 16 | 17 | ds_ror_mug_v: 18 | product: ds_ror_mug 19 | sku: ROR-00014 20 | price: 13.99 21 | cost_price: 11.00 22 | is_master: true 23 | count_on_hand: 10 24 | -------------------------------------------------------------------------------- /test/fixtures/calculators.yml: -------------------------------------------------------------------------------- 1 | ups_ground: 2 | calculable: ups_ground 3 | calculable_type: ShippingMethod 4 | type: Calculator::FlatRate 5 | ups_two_day: 6 | calculable: ups_two_day 7 | calculable_type: ShippingMethod 8 | type: Calculator::FlatRate 9 | ups_one_day: 10 | calculable: ups_one_day 11 | calculable_type: ShippingMethod 12 | type: Calculator::FlatRate 13 | flat_rate_coupon_calculator: 14 | calculable: spree_coupon 15 | calculable_type: Promotion 16 | type: Calculator::FlatRate 17 | -------------------------------------------------------------------------------- /test/fixtures/line_items.yml: -------------------------------------------------------------------------------- 1 | <% for i in 1..5 do%> 2 | li_<%= i %>: 3 | order: order_<%= i %> 4 | variant: ror_tote_v 5 | quantity: <%= rand(4) + 1 %> 6 | price: 15.99 7 | <% end %> 8 | 9 | ds_li_1: 10 | supplier: supplier_1 11 | order: order_1 12 | variant: ds_ror_bag_v 13 | quantity: <%= rand(4) + 1 %> 14 | price: 15.99 15 | 16 | ds_li_2: 17 | supplier: supplier_2 18 | order: order_2 19 | variant: ds_ror_mug_v 20 | quantity: <%= rand(4) + 1 %> 21 | price: 15.99 22 | -------------------------------------------------------------------------------- /test/fixtures/addresses.yml: -------------------------------------------------------------------------------- 1 | billing_address: 2 | firstname: Bill 3 | lastname: Billerman 4 | address1: 1027 State St 5 | address2: 6 | city: Santa Barbara 7 | state_id: 889445952 8 | zipcode: 16804 9 | country_id: 214 10 | phone: 555-555-1027 11 | 12 | 13 | shipping_address: 14 | firstname: Boxy 15 | lastname: Shipperman 16 | address1: 1000 State St 17 | address2: 18 | city: Santa Barbara 19 | state_id: 889445952 20 | zipcode: 16804 21 | country_id: 214 22 | phone: 555-555-5555 23 | 24 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/_shipment_info.html.erb: -------------------------------------------------------------------------------- 1 |

    Shipment Information

    2 | 3 |
    4 |
    Shipping Method
    5 |
    <%= dso.shipping_method %>
    6 | <% unless dso.confirmation_number.blank? %> 7 |
    Confirmation Number
    8 |
    <%= dso.confirmation_number %>
    9 | <% end %> 10 |
    Tracking Number
    11 |
    <%= dso.tracking_number %>
    12 | <% unless dso.notes.blank? %> 13 |
    Notes
    14 |
    <%= simple_format dso.notes %>
    15 | <% end %> 16 |
    17 | -------------------------------------------------------------------------------- /app/helpers/admin/base_helper_decorator.rb: -------------------------------------------------------------------------------- 1 | Admin::BaseHelper.class_eval do 2 | 3 | def error_message_on(object, method, options = {}) 4 | object = convert_to_model(object).to_s.gsub(/[^a-z0-9]/, '_') 5 | obj = object.respond_to?(:errors) ? object : instance_variable_get("@#{object}") 6 | 7 | if obj && obj.errors[method].present? 8 | errors = obj.errors[method].map{|err| h(err)}.join('
    ').html_safe 9 | content_tag(:span, errors, :class => 'formError') 10 | else 11 | '' 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /config/cucumber.yml: -------------------------------------------------------------------------------- 1 | <% 2 | rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" 3 | rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" 4 | std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" 5 | %> 6 | default: <%= std_opts %> features 7 | wip: --tags @wip:3 --wip features 8 | rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip 9 | autocuke: -r features --drb --format pretty --strict --tags @wip 10 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | namespace :admin do 4 | 5 | resources :orders do 6 | resources :drop_ship_orders 7 | member do 8 | get :drop_ship_approve 9 | end 10 | end 11 | 12 | resources :suppliers do 13 | resources :drop_ship_orders, :as => 'orders', :path => 'orders' 14 | end 15 | 16 | resources :drop_ship_orders do 17 | member do 18 | get :deliver 19 | end 20 | end 21 | end 22 | 23 | resources :drop_ship_orders 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/_shipping_info.html.erb: -------------------------------------------------------------------------------- 1 |

    Shipping Information

    2 | 3 |
    4 |
    Address
    5 |
    <%= address.firstname %> <%= address.lastname %>
    6 |
    <%= address.address1 %>
    7 | <% unless address.address2.blank? %> 8 |
    <%= address.address2 %>
    9 | <% end %> 10 |
    <%= address.state.name %>, <%= address.country.name %> <%= address.zipcode %>
    11 |
    Phone
    12 |
    <%= address.phone %>
    13 |
    Email
    14 |
    <%= mail_to @dso.order.email %>
    15 |
    16 | -------------------------------------------------------------------------------- /lib/generators/templates/db/migrate/create_drop_ship_line_items.rb: -------------------------------------------------------------------------------- 1 | class CreateDropShipLineItems < ActiveRecord::Migration 2 | 3 | def self.up 4 | create_table :drop_ship_line_items do |t| 5 | t.references :drop_ship_order 6 | t.integer :variant_id 7 | t.string :sku 8 | t.string :name 9 | t.integer :quantity, :default => 1 10 | t.decimal :price, :precision => 8, :scale => 2, :null => false 11 | t.timestamps 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table :drop_ship_line_items 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rake' 10 | require 'rake/testtask' 11 | 12 | Bundler::GemHelper.install_tasks 13 | 14 | Rake::TestTask.new(:test) do |t| 15 | t.libs << 'lib' 16 | t.libs << 'test' 17 | t.pattern = 'test/**/*_test.rb' 18 | t.verbose = false 19 | end 20 | 21 | require 'cucumber/rake/task' 22 | Cucumber::Rake::Task.new do |t| 23 | t.cucumber_opts = %w{--format pretty} 24 | end 25 | 26 | task :default => [ :test, :cucumber ] 27 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Order #<%= @dso.id %>

    2 | 3 |
    4 | <%= render "details", :dso => @dso %> 5 |
    6 | 7 |
    8 | <%= render "shared/error_messages", :target => @dso %> 9 | <%= render "form", :dso => @dso %> 10 |
    11 | 12 |
    13 | 14 |
    15 | <%= render "affiliate_info" %> 16 |
    17 | 18 |
    19 | <%= render "shipping_info", :address => @address %> 20 |
    21 | 22 |
    23 | 24 |

    Product Information

    25 | 26 | <%= render "drop_ship_orders/table", :dso => @dso %> 27 | -------------------------------------------------------------------------------- /app/validators/url_validator.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | class UrlValidator < ActiveModel::EachValidator 4 | 5 | def validate_each(record, attribute, value) 6 | 7 | begin 8 | uri = URI.parse(value) 9 | allowed_schemes = options[:schemes] || %w(http https) 10 | raise(URI::InvalidURIError) unless allowed_schemes.include?(uri.scheme) 11 | raise(URI::InvalidURIError) if [ :scheme, :host ].any?{ |i| uri.send(i).blank? } 12 | rescue URI::InvalidURIError => e 13 | record.errors.add(attribute, :invalid, :default => options[:message], :value => value) 14 | end 15 | 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/_details.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    Order Reference Number
    3 | <% if current_user && current_user.has_role?("admin") %> 4 |
    <%= link_to @dso.order.number, admin_order_path(@dso.order) %>
    5 | <% else %> 6 |
    <%= @dso.order.number %>
    7 | <% end %> 8 |
    Status
    9 |
    <%= @dso.state %>
    10 | <% %w(sent_at confirmed_at shipped_at).each do |attribute| %> 11 | <% time = @dso.send(attribute) %> 12 | <% unless time.nil? %> 13 |
    <%= attribute.titleize %>
    14 |
    <%= time.to_s(:long) %>
    15 | <% end %> 16 | <% end %> 17 |
    18 | -------------------------------------------------------------------------------- /lib/generators/templates/db/migrate/create_suppliers.rb: -------------------------------------------------------------------------------- 1 | class CreateSuppliers < ActiveRecord::Migration 2 | 3 | def self.up 4 | create_table :suppliers do |t| 5 | t.references :user 6 | t.references :address 7 | t.string :name 8 | t.string :email 9 | t.string :phone 10 | t.string :url 11 | t.string :contact 12 | t.string :contact_email 13 | t.string :contact_phone 14 | t.timestamps 15 | end 16 | add_index :suppliers, :user_id 17 | add_index :suppliers, :address_id 18 | end 19 | 20 | def self.down 21 | drop_table :suppliers 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/show.html.erb: -------------------------------------------------------------------------------- 1 |

    Order #<%= @dso.id %>

    2 | 3 |
    4 | <%= render "drop_ship_orders/details", :dso => @dso %> 5 |
    6 | 7 |
    8 | <%= render "drop_ship_orders/shipment_info", :dso => @dso if @dso.complete? %> 9 |
    10 | 11 |
    12 | 13 |
    14 | <%= render "drop_ship_orders/affiliate_info" %> 15 |
    16 | 17 |
    18 | <%= render "drop_ship_orders/shipping_info", :address => @address %> 19 |
    20 | 21 |
    22 | 23 |

    Product Information

    24 | 25 | <%= render "drop_ship_orders/table", :dso => @dso %> 26 | -------------------------------------------------------------------------------- /app/views/drop_ship_order_mailer/shipment_notification.html.erb: -------------------------------------------------------------------------------- 1 |

    Dear Customer,

    2 | 3 |

    <%= @dso.order.number %> has been shipped.

    4 | 5 |

    Shipment Information

    6 |
    7 |
    Shipping Method
    8 |
    <%= @dso.shipping_method %>
    9 |
    Confirmation Number
    10 |
    <%= @dso.confirmation_number %>
    11 |
    Tracking Number
    12 |
    <%= @dso.tracking_number %>
    13 | <% if @dso.notes.present? %> 14 |
    Notes
    15 |
    <%= simple_format @dso.notes %>
    16 | <% end %> 17 |
    18 | 19 |

    Thanks,

    20 | 21 |

    - <%= Spree::Config[:site_name] %>

    22 | -------------------------------------------------------------------------------- /lib/generators/templates/db/migrate/create_drop_ship_orders.rb: -------------------------------------------------------------------------------- 1 | class CreateDropShipOrders < ActiveRecord::Migration 2 | 3 | def self.up 4 | create_table :drop_ship_orders do |t| 5 | t.references :order 6 | t.references :supplier 7 | t.float :total 8 | t.string :shipping_method 9 | t.string :confirmation_number 10 | t.string :tracking_number 11 | t.text :notes 12 | t.datetime :sent_at 13 | t.datetime :confirmed_at 14 | t.datetime :shipped_at 15 | t.string :state, :default => "active" 16 | t.timestamps 17 | end 18 | end 19 | 20 | def self.down 21 | drop_table :drop_ship_orders 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /app/models/line_item_decorator.rb: -------------------------------------------------------------------------------- 1 | LineItem.class_eval do 2 | 3 | scope :will_drop_ship, where("supplier_id IS NOT NULL") 4 | 5 | belongs_to :supplier 6 | 7 | before_validation :set_supplier_id 8 | 9 | def set_supplier_id 10 | id = variant.product.supplier_product.supplier_id rescue nil 11 | self.supplier_id = id if id && variant.product.has_supplier? 12 | end 13 | 14 | def has_supplier? 15 | supplier_id.present? 16 | end 17 | 18 | def drop_ship_attributes 19 | { 20 | :variant_id => variant_id, 21 | :sku => variant ? variant.sku : nil, 22 | :name => variant && variant.product ? variant.product.name : nil, 23 | :quantity => quantity, 24 | :price => price 25 | } 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/_table.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <% if dso.line_items.empty? %> 10 | 11 | 12 | 13 | <% else %> 14 | <% dso.line_items.each do |li| %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <% end %> 23 | <% end %> 24 | 25 | 29 | 30 |
    SKUProductQuantityPriceSubtotal
    This order has no line items.
    <%= li.sku %><%= li.name %><%= li.quantity %><%= li.price %><%= li.subtotal %>
    26 | Total: 27 | <%= number_to_currency dso.total %> 28 |
    31 | -------------------------------------------------------------------------------- /app/views/drop_ship_order_mailer/supplier_order.html.erb: -------------------------------------------------------------------------------- 1 |

    Order #<%= @dso.id %>

    2 |

    Reference Number <%= @dso.order.number %>

    3 | 4 |
    5 | 6 |

    Hello <%= @supplier.name %>,

    7 | 8 |

    You've got a new order! Here's the details:

    9 | 10 |

    Product Information

    11 | 12 | <%= render "drop_ship_orders/table", :dso => @dso %> 13 | 14 |

    Shipping Information

    15 | 16 | <%= render "drop_ship_orders/shipping_info", :address => @address %> 17 | 18 |

    Confirmation

    19 | 20 | <%= link_to "You must click here to confirm this order", edit_drop_ship_order_url(@dso) %> 21 | 22 |

    Thanks,

    23 | 24 |

    - <%= Spree::Config[:site_name] %>

    25 | -------------------------------------------------------------------------------- /app/controllers/admin/products_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Admin::ProductsController.class_eval do 2 | 3 | before_filter :get_suppliers, :only => [ :edit, :update ] 4 | update.before :attach_supplier 5 | 6 | def attach_supplier 7 | if params[:supplier_id].present? 8 | @supplier = Supplier.find(params[:supplier_id]) rescue nil 9 | return unless @supplier 10 | unless @product.has_supplier? && @product.supplier == @supplier 11 | @product.supplier_product.destroy if @product.has_supplier? 12 | @supplier.supplier_products.create(:product_id => @product.id) 13 | end 14 | elsif @product.has_supplier? 15 | @product.supplier_product.destroy 16 | end 17 | end 18 | 19 | def get_suppliers 20 | @supplier_options = Supplier.order(:name).all.map{|s| [ s.name, s.id ] } 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /lib/spree_drop_shipping/custom_hooks.rb: -------------------------------------------------------------------------------- 1 | module SpreeDropShipping 2 | class CustomHooks < Spree::ThemeSupport::HookListener 3 | 4 | insert_after :account_summary, 'users/drop_ship_orders' 5 | 6 | insert_after :admin_tabs, 'admin/shared/suppliers_tab' 7 | insert_after :admin_product_form_right, 'admin/shared/product_form_right' 8 | 9 | insert_after :admin_orders_index_headers, 'admin/orders/dso_order_header' 10 | insert_after :admin_orders_index_rows, 'admin/orders/dso_order_row' 11 | 12 | insert_after :admin_order_tabs, 'admin/orders/dso_sidebar_tab' 13 | 14 | insert_after :admin_order_show_details, 'admin/orders/drop_ship_info' 15 | insert_before :admin_order_show_buttons, 'admin/orders/confirm_drop_ship_info' 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/admin/shared/_supplier_tabs.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= t('.editing_supplier') + " “#{@supplier.name}”".html_safe %>

    2 | 3 | <% content_for :sidebar do %> 4 | 5 | 18 |
    19 | 20 | <% end %> 21 | -------------------------------------------------------------------------------- /test/unit/drop_ship_line_item_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class DropShipLineItemTest < ActiveSupport::TestCase 4 | 5 | should belong_to(:drop_ship_order) 6 | 7 | should validate_presence_of(:drop_ship_order_id) 8 | should validate_presence_of(:variant_id) 9 | should validate_presence_of(:sku) 10 | should validate_presence_of(:quantity) 11 | should validate_presence_of(:price) 12 | 13 | should "calculate subtotal when line item is blank" do 14 | assert_equal 0, subject.subtotal 15 | end 16 | 17 | context "An unsaved, valid line item" do 18 | 19 | subject { 20 | DropShipLineItem.new(:drop_ship_order_id => 1, :variant_id => 1, :sku => "ABC123", :quantity => 2, :price => 2.75) 21 | } 22 | 23 | should "calculate subtotal" do 24 | assert_equal 5.50, subject.subtotal 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/views/drop_ship_orders/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for dso do |f| %> 2 | <% if dso.confirmed? %> 3 |

    4 | <%= f.label :shipping_method %>
    5 | <%= f.text_field :shipping_method, :class => 'text' %> 6 |

    7 |

    8 | <%= f.label :confirmation_number %>
    9 | <%= f.text_field :confirmation_number, :class => 'text' %> 10 |

    11 |

    12 | <%= f.label :tracking_number %>
    13 | <%= f.text_field :tracking_number, :class => 'text' %> 14 |

    15 |

    16 | <%= f.label :notes %>
    17 | <%= f.text_area :notes, :class => 'text' %> 18 |

    19 |

    <%= f.submit "Process and finalize order" %>

    20 | <% elsif dso.sent? %> 21 |

    Please confirm this order!

    22 |

    <%= f.submit "Confirm Order" %>

    23 | <% end %> 24 | <% end %> 25 | -------------------------------------------------------------------------------- /app/validators/email_validator.rb: -------------------------------------------------------------------------------- 1 | class EmailValidator < ActiveModel::EachValidator 2 | def validate_each(record,attribute,value) 3 | begin 4 | m = Mail::Address.new(value) 5 | # We must check that value contains a domain and that value is an email address 6 | r = m.domain && m.address == value 7 | t = m.__send__(:tree) 8 | # We need to dig into treetop 9 | # A valid domain must have dot_atom_text elements size > 1 10 | # user@localhost is excluded 11 | # treetop must respond to domain 12 | # We exclude valid email values like 13 | # Hence we use m.__send__(tree).domain 14 | r &&= (t.domain.dot_atom_text.elements.size > 1) 15 | rescue Exception => e 16 | r = false 17 | end 18 | record.errors.add(attribute, :invalid, :default => options[:message], :value => value) unless r 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/order_decorator.rb: -------------------------------------------------------------------------------- 1 | Order.class_eval do 2 | 3 | has_many :drop_ship_orders 4 | 5 | # HACK! - injects `finalize_for_dropship!` into the after complete transition 6 | # 7 | # state = self.state_machine.callbacks[:after].select{|i| i.known_states[0] == "complete" }[0] 8 | # state.instance_variable_set("@methods", [ :finalize!, :finalize_for_dropship! ]) 9 | 10 | def has_drop_ship_orders? 11 | !drop_ship_orders.empty? 12 | end 13 | 14 | def finalize_for_dropship! 15 | self.line_items.will_drop_ship.all.group_by{|li| li.supplier_id }.each do |supplier_id, supplier_items| 16 | supplier = Supplier.find(supplier_id) 17 | supplier.orders.create(:order => self).add(supplier_items) #.deliver! 18 | end 19 | end 20 | 21 | def approve_drop_ship_orders 22 | drop_ship_orders.select{|dso| dso.deliver }.length == drop_ship_orders.length 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lib/spree_drop_shipping.rb: -------------------------------------------------------------------------------- 1 | require 'spree_core' 2 | require 'spree_auth' 3 | require 'spree_sample' unless Rails.env.production? 4 | 5 | require 'spree_drop_shipping/custom_hooks' 6 | 7 | module SpreeDropShipping 8 | 9 | class Engine < Rails::Engine 10 | 11 | config.autoload_paths += %W(#{config.root}/lib) 12 | 13 | initializer "static assets" do |app| 14 | app.middleware.insert_before ::Rack::Lock, ::ActionDispatch::Static, "#{config.root}/public" 15 | end 16 | 17 | def self.activate 18 | 19 | Order.class_eval do 20 | state_machine do 21 | after_transition :to => 'complete', :do => :finalize_for_dropship! 22 | end 23 | end 24 | 25 | Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator.rb")) do |c| 26 | Rails.env.production? ? require(c) : load(c) 27 | end 28 | 29 | end 30 | 31 | config.to_prepare &method(:activate).to_proc 32 | 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /features/support/paths.rb: -------------------------------------------------------------------------------- 1 | module NavigationHelpers 2 | # Maps a name to a path. Used by the 3 | # 4 | # When /^I go to (.+)$/ do |page_name| 5 | # 6 | # step definition in web_steps.rb 7 | # 8 | def path_to(page_name) 9 | case page_name 10 | 11 | when /the home\s?page/ 12 | '/' 13 | when /the new post page/ 14 | new_post_path 15 | 16 | 17 | # Add more mappings here. 18 | # Here is an example that pulls values out of the Regexp: 19 | # 20 | # when /^(.*)'s profile page$/i 21 | # user_profile_path(User.find_by_login($1)) 22 | 23 | else 24 | begin 25 | page_name =~ /the (.*) page/ 26 | path_components = $1.split(/\s+/) 27 | self.send(path_components.push('path').join('_').to_sym) 28 | rescue Object => e 29 | raise "Can't find mapping from \"#{page_name}\" to a path.\n" + 30 | "Now, go and add a mapping in #{__FILE__}" 31 | end 32 | end 33 | end 34 | end 35 | 36 | World(NavigationHelpers) 37 | -------------------------------------------------------------------------------- /app/controllers/admin/suppliers_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::SuppliersController < Admin::ResourceController 2 | 3 | before_filter :load_data, :only => [ :new, :create, :edit, :update ] 4 | 5 | create.before :attach_address 6 | 7 | def index 8 | # render :template => request.xhr? ? 'admin/uploads/picker' : 'admin/uploads/index', :layout => !request.xhr? 9 | end 10 | 11 | def new 12 | @supplier = Supplier.new 13 | @supplier.address = Address.new 14 | end 15 | 16 | def attach_address 17 | @supplier.address = Address.new(params[:address]) 18 | end 19 | 20 | private 21 | 22 | def load_data 23 | @state_options = State.order(:name).all.map{|s| [s.name, s.id] } 24 | @country_options = Country.order(:name).all.map{|c| [c.name, c.id] } 25 | end 26 | 27 | def collection 28 | params[:search] ||= {} 29 | params[:search][:meta_sort] ||= "name.asc" 30 | @search = Supplier.search(params[:search]) 31 | @collection = @search.paginate(:per_page => Spree::Config[:orders_per_page], :page => params[:page]) 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /features/support/selectors.rb: -------------------------------------------------------------------------------- 1 | module HtmlSelectorsHelpers 2 | # Maps a name to a selector. Used primarily by the 3 | # 4 | # When /^(.+) within (.+)$/ do |step, scope| 5 | # 6 | # step definitions in web_steps.rb 7 | # 8 | def selector_for(locator) 9 | case locator 10 | 11 | when /the page/ 12 | "html > body" 13 | 14 | # Add more mappings here. 15 | # Here is an example that pulls values out of the Regexp: 16 | # 17 | # when /the (notice|error|info) flash/ 18 | # ".flash.#{$1}" 19 | 20 | # You can also return an array to use a different selector 21 | # type, like: 22 | # 23 | # when /the header/ 24 | # [:xpath, "//header"] 25 | 26 | # This allows you to provide a quoted selector as the scope 27 | # for "within" steps as was previously the default for the 28 | # web steps: 29 | when /"(.+)"/ 30 | $1 31 | 32 | else 33 | raise "Can't find mapping from \"#{locator}\" to a selector.\n" + 34 | "Now, go and add a mapping in #{__FILE__}" 35 | end 36 | end 37 | end 38 | 39 | World(HtmlSelectorsHelpers) 40 | -------------------------------------------------------------------------------- /lib/tasks/sample_drop_ship_orders.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | namespace :sample do 3 | desc "Create sample drop ship orders" 4 | task :drop_ship_orders => :environment do 5 | 6 | if Order.count == 0 7 | puts "Please run `rake db:sample` first to create products and orders" 8 | exit 9 | end 10 | 11 | if Supplier.count == 0 12 | puts "Please run `rake db:sample:suppliers` first to create suppliers" 13 | exit 14 | end 15 | 16 | count = 0 17 | @orders = Order.complete.includes(:line_items).all 18 | @suppliers = Supplier.all 19 | 20 | puts "Linking existing line items to suppliers" 21 | LineItem.where("supplier_id IS NULL").all.each do |li| 22 | print "*" if li.update_attributes(:supplier_id => @suppliers.shuffle.first.id) 23 | end 24 | puts 25 | 26 | puts "Creating drop ship orders for existing orders" 27 | Order.all.each do |order| 28 | print "*" if order.finalize_for_dropship! 29 | end 30 | puts 31 | 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/generators/spree_drop_shipping/install_generator.rb: -------------------------------------------------------------------------------- 1 | module SpreeDropShipping 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | 5 | include Rails::Generators::Migration 6 | 7 | def self.count! 8 | @count ||= 0 9 | (@count += 1) * 3 10 | end 11 | 12 | def self.next_migration_number(path) 13 | @time ||= Time.new.utc 14 | if ActiveRecord::Base.timestamped_migrations 15 | (@time + self.count!).strftime("%Y%m%d%H%M%S") 16 | else 17 | "%.3d" % (current_migration_number(dirname) + 1) 18 | end 19 | end 20 | 21 | desc "Installs required migrations for spree_essentials" 22 | source_root File.expand_path("../../templates", __FILE__) 23 | 24 | def copy_migrations 25 | 26 | %w(create_suppliers create_supplier_products create_drop_ship_orders create_drop_ship_line_items add_supplier_to_line_items).each do |m| 27 | migration_template "db/migrate/#{m}.rb", "db/migrate/#{m}.rb" 28 | end 29 | 30 | end 31 | 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /app/views/admin/suppliers/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "admin/shared/supplier_tabs", :current => "Supplier Details" %> 2 | 3 |

    <%= @supplier.name %>

    4 |

    <%= render "admin/shared/address", :address => @supplier.address %>

    5 |
    6 |
    Email
    7 |
    <%= @supplier.email %>
    8 |
    Phone
    9 |
    <%= @supplier.phone %>
    10 |
    URL
    11 |
    <%= @supplier.url %>
    12 |
    Contact
    13 |
    <%= @supplier.contact %>
    14 | <% unless @supplier.contact_email.blank? %> 15 |
    Contact Email
    16 |
    <%= mail_to @supplier.contact_email %>
    17 | <% end %> 18 | <% unless @supplier.contact_phone.blank? %> 19 |
    Contact Phone
    20 |
    <%= @supplier.contact_phone %>
    21 | <% end %> 22 |
    Order Count
    23 |
    <%= link_to @supplier.orders.count, admin_supplier_orders_path(@supplier) %>
    24 |
    User ID
    25 |
    <%= link_to @supplier.user.id, admin_user_path(@supplier.user) %>
    26 | 27 |
    28 | 29 |
    30 | 31 |

    32 | <%= link_to_edit @supplier %>   33 | <%= link_to t('back'), collection_url %> 34 |

    35 | -------------------------------------------------------------------------------- /app/mailers/drop_ship_order_mailer.rb: -------------------------------------------------------------------------------- 1 | class DropShipOrderMailer < ActionMailer::Base 2 | 3 | default_url_options[:host] = "localhost:3000" 4 | 5 | default :from => 'no-reply@example.com' 6 | 7 | def supplier_order(dso) 8 | get_defaults(dso) 9 | send_mail "#{Spree::Config[:site_name]} - Order ##{dso.id}" 10 | end 11 | 12 | def confirmation(dso) 13 | get_defaults(dso) 14 | send_mail "Confirmation - #{Spree::Config[:site_name]} - Order ##{dso.id}" 15 | end 16 | 17 | def shipment(dso) 18 | get_defaults(dso) 19 | send_mail "Shipped - #{Spree::Config[:site_name]} - Order ##{dso.id}" 20 | end 21 | 22 | def shipment_notification(dso) 23 | get_defaults(dso) 24 | mail :to => @order.email, :cc => "spencer@citrusme.com", :subject => "Shipped - #{Spree::Config[:site_name]} - Order ##{dso.id}" 25 | end 26 | 27 | private 28 | 29 | def get_defaults(dso) 30 | @dso = dso 31 | @order = dso.order 32 | @supplier = dso.supplier 33 | @address = @order.ship_address 34 | end 35 | 36 | def send_mail(subject) 37 | mail :to => @supplier.email_with_name, :subject => subject 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/admin/drop_ship_orders_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::DropShipOrdersController < Admin::ResourceController 2 | 3 | def show 4 | @dso = load_resource 5 | @supplier = @dso.supplier 6 | @address = @dso.order.ship_address 7 | end 8 | 9 | def deliver 10 | @dso = load_resource 11 | if @dso.deliver 12 | flash[:notice] = "Drop ship order ##{@dso.id} was successfully sent!" 13 | else 14 | flash[:error] = "Drop ship order ##{@dso.id} could not be sent." 15 | end 16 | redirect_to admin_drop_ship_order_path(@dso) 17 | end 18 | 19 | private 20 | 21 | def collection 22 | params[:search] ||= {} 23 | params[:search][:meta_sort] ||= "id.desc" 24 | scope = if params[:supplier_id] && @supplier = Supplier.find(params[:supplier_id]) 25 | @supplier.orders 26 | elsif params[:order_id] && @order = Order.find_by_number(params[:order_id]) 27 | @order.drop_ship_orders 28 | else 29 | DropShipOrder.scoped 30 | end 31 | @search = scope.includes(:supplier).search(params[:search]) 32 | @collection = @search.paginate(:per_page => Spree::Config[:orders_per_page], :page => params[:page]) 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /app/views/users/_drop_ship_orders.html.erb: -------------------------------------------------------------------------------- 1 | <% if @user.has_supplier? %> 2 |

    Drop Ship Orders

    3 | <% if @user.supplier.orders.present? %> 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% @user.supplier.orders.each do |order| %> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% end %> 26 | 27 |
    <%= t("order_number") %><%= t("total") %><%= t("sent_at") %><%= t("confirmed_at") %><%= t("shipped_at") %><%= t("status") %>
    <%= link_to order.id, drop_ship_order_url(order) %><%= number_to_currency order.total %><%= order.sent_at.to_date unless order.sent_at.nil? %><%= order.confirmed_at.to_date unless order.confirmed_at.nil? %><%= order.shipped_at.to_date unless order.shipped_at.nil? %><%= order.state %>
    28 | <% else %> 29 |

    <%= t(:you_have_no_orders_yet) %>

    30 | <% end %> 31 |
    32 | <% end %> 33 | -------------------------------------------------------------------------------- /features/admin/products.feature: -------------------------------------------------------------------------------- 1 | @no-txn 2 | Feature: Admin products and suppliers 3 | 4 | In order to tell suppliers which products were ordered 5 | As an admin 6 | I want to link products to suppliers 7 | 8 | Scenario: Linking a product to a supplier 9 | Given I have an existing supplier named "Some Big Store" 10 | And I'm on the admin products page 11 | Then I follow "Edit" 12 | And I should see "Drop Shipping Supplier" 13 | Then I select "Some Big Store" from "Drop Shipping Supplier" 14 | And I press "Update" 15 | Then I should see "successfully updated." 16 | And "Drop Shipping Supplier" should have "Some Big Store" selected 17 | 18 | Scenario: Un-Linking a product from a supplier 19 | Given I have an existing supplier named "Some Big Store" 20 | And supplier named "Some Big Store" is linked to the first product 21 | And I'm on the edit admin product page for the first product 22 | Then I should see "Drop Shipping Supplier" 23 | And "Drop Shipping Supplier" should have "Some Big Store" selected 24 | Then I select "--- none ---" from "Drop Shipping Supplier" 25 | And I press "Update" 26 | Then I should see "successfully updated." 27 | And "Drop Shipping Supplier" should have "" selected 28 | -------------------------------------------------------------------------------- /app/views/supplier_mailer/welcome.html.erb: -------------------------------------------------------------------------------- 1 |

    Hello!

    2 | 3 |

    You've been signed up as a drop ship supplier for <%= Spree::Config[:site_name] %> 4 | 5 |

    Please verify that the information below is correct.

    6 | 7 |
    8 | 9 | 24 | 25 |
    26 | 27 |

    Next steps? Login and change your password

    28 | 29 | <%= link_to "Login and change my password", edit_user_password_url(:reset_password_token => @user.reset_password_token) %> 30 | 31 |

    Thanks,

    32 | 33 |

    - <%= Spree::Config[:site_name] %>

    34 | -------------------------------------------------------------------------------- /test/functional/admin/order_controller_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | class Admin::OrdersControllerTest < ActionController::TestCase 4 | 5 | setup do 6 | @user = Factory.create(:admin_user) 7 | sign_in @user 8 | ActionMailer::Base.deliveries = [] 9 | end 10 | 11 | context "An existing order with drop ship items" do 12 | 13 | setup do 14 | @supplier = suppliers(:supplier_1) 15 | @supplier2 = suppliers(:supplier_2) 16 | @order = orders(:pending) 17 | @order.ship_address = Address.last 18 | @order.line_items = [ line_items(:ds_li_1), line_items(:ds_li_2) ] 19 | @order.update! && @order.next! 20 | end 21 | 22 | should "approve drop ship orders" do 23 | get :drop_ship_approve, :id => @order.to_param 24 | assert_response :redirect 25 | assert assigns(:order) 26 | end 27 | 28 | should "send emails to suppliers" do 29 | ActionMailer::Base.deliveries = [] 30 | assert_equal 0, ActionMailer::Base.deliveries.length 31 | get :drop_ship_approve, :id => @order.to_param 32 | tos = ActionMailer::Base.deliveries.map(&:to).flatten.uniq 33 | @order.drop_ship_orders.each do |dso| 34 | tos.include?(dso.supplier.email) 35 | end 36 | end 37 | 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Envinronment 2 | ENV["RAILS_ENV"] = "test" 3 | require 'spork' 4 | 5 | Spork.prefork do 6 | 7 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 8 | require "rails/test_help" 9 | require "shoulda" 10 | require "factory_girl" 11 | require "sqlite3" 12 | require "turn" 13 | 14 | ActionMailer::Base.delivery_method = :test 15 | ActionMailer::Base.perform_deliveries = true 16 | ActionMailer::Base.default_url_options[:host] = "example.com" 17 | 18 | Rails.backtrace_cleaner.remove_silencers! 19 | 20 | # Run any available migration if needed 21 | ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__) 22 | 23 | # Include devise helpers for controller tests 24 | class ActionController::TestCase 25 | include Devise::TestHelpers 26 | self.fixture_path = File.expand_path('../fixtures', __FILE__) 27 | end 28 | 29 | class ActiveSupport::TestCase 30 | self.fixture_path = File.expand_path('../fixtures', __FILE__) 31 | fixtures :all 32 | end 33 | 34 | end 35 | 36 | Spork.each_run do 37 | 38 | # Load support files 39 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| load f } 40 | Dir["#{File.expand_path("../../", __FILE__)}/app/**/*.rb"].each { |f| load f } 41 | 42 | include HelperMethods 43 | 44 | end 45 | -------------------------------------------------------------------------------- /test/support/factories.rb: -------------------------------------------------------------------------------- 1 | begin 2 | 3 | FactoryGirl.define do 4 | 5 | factory :address do 6 | firstname "SUPPLIER" 7 | lastname "SUPPLIER" 8 | address1 "100 State St" 9 | city "Santa Barbara" 10 | phone "555-555-5555" 11 | zipcode "93101" 12 | state { State.find_by_name("California") } 13 | country { Country.find_by_name("United States") } 14 | end 15 | 16 | factory :supplier do 17 | name "Big Store" 18 | email { random_email } 19 | phone "800-555-5555" 20 | url "http://example.com" 21 | contact "Steve" 22 | contact_phone "555-555-5555" 23 | address { Factory.create(:address) } 24 | end 25 | 26 | factory :user do 27 | email { random_email } 28 | password "spree123" 29 | password_confirmation "spree123" 30 | roles { [Role.find_or_create_by_name("user")] } 31 | end 32 | 33 | factory :drop_ship_order do 34 | supplier { Factory.create(:supplier) } 35 | order { Order.complete.last } 36 | total 0 37 | end 38 | 39 | factory :line_item do 40 | variant_id { Variant.first.id } 41 | supplier_id { Variant.first.product.supplier.id } 42 | quantity 1 43 | price 15.99 44 | end 45 | 46 | factory :admin_user, :parent => :user do 47 | roles { [Role.find_or_create_by_name("admin")] } 48 | end 49 | 50 | end 51 | 52 | rescue FactoryGirl::DuplicateDefinitionError 53 | 54 | puts "factories are already defined..." 55 | 56 | end -------------------------------------------------------------------------------- /app/controllers/drop_ship_orders_controller.rb: -------------------------------------------------------------------------------- 1 | class DropShipOrdersController < Spree::BaseController 2 | 3 | before_filter :check_authorization 4 | 5 | #before_filter :get_dso 6 | 7 | def show 8 | redirect_to edit_drop_ship_order_path(@dso) unless @dso.complete? 9 | end 10 | 11 | def edit 12 | if @dso.sent? 13 | flash[:notice] = I18n.t('supplier_orders.flash.sent') 14 | elsif @dso.confirmed? 15 | if @dso.errors.empty? 16 | flash[:notice] = I18n.t('supplier_orders.flash.confirmed') 17 | end 18 | end 19 | redirect_to @dso if @dso.complete? 20 | end 21 | 22 | def update 23 | 24 | if @dso.sent? 25 | success = @dso.confirm 26 | url = edit_drop_ship_order_path(@dso) 27 | elsif @dso.confirmed? 28 | success = @dso.update_attributes(params[:drop_ship_order]) && @dso.ship 29 | url = drop_ship_order_path(@dso) 30 | flash[:notice] = I18n.t('supplier_orders.flash.shipped') if success 31 | end 32 | 33 | if success 34 | redirect_to url 35 | else 36 | flash[:error] = I18n.t("supplier_orders.flash.#{@dso.confirmed? ? 'confirmation_failure' : 'finalize_failure'}") 37 | render :edit 38 | end 39 | 40 | end 41 | 42 | private 43 | 44 | def get_dso 45 | @dso = DropShipOrder.includes(:line_items, :order => [ :ship_address ]).find(params[:id]) 46 | @address = @dso.order.ship_address 47 | end 48 | 49 | def check_authorization 50 | get_dso 51 | authorize!(:show, @dso) 52 | end 53 | 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/tasks/sample_suppliers.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | namespace :sample do 3 | desc "Create sample suppliers and randomly link to products" 4 | task :suppliers => :environment do 5 | 6 | @usa = Country.find_by_iso("US") 7 | @ca = @usa.states.find_by_abbr("CA") 8 | 9 | count = Supplier.count 10 | puts "Creating Suppliers..." 11 | 5.times{|i| 12 | name = "Supplier #{count + i + 1}" 13 | supplier = Supplier.new(:name => name, :email => "#{name.parameterize}@example.com", :phone => "800-555-5555", :url => "http://example.com", :contact => "Somebody", :contact_phone => "555-555-5555") 14 | supplier.build_address(:firstname => name, :lastname => name, :address1 => "100 State St", :city => "Santa Barbara", :phone => "555-555-5555", :zipcode => "93101", :state_id => @ca.id, :country_id => @usa.id) 15 | print "*" if supplier.save 16 | } 17 | puts 18 | puts "#{Supplier.count - count} suppliers created" 19 | 20 | SupplierProduct.destroy_all 21 | 22 | puts "Randomly linking Products & Suppliers..." 23 | 24 | @supplier_ids = Supplier.select('id').all.map(&:id).shuffle 25 | @products = Product.all 26 | count = 0 27 | 28 | @products.each do |product| 29 | SupplierProduct.create(:product_id => product.id, :supplier_id => @supplier_ids[rand(@supplier_ids.length)]) 30 | count += 1 31 | print "*" 32 | end 33 | 34 | puts 35 | puts "#{count} products linked." 36 | 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'spork' 2 | 3 | ENV["RAILS_ENV"] = "cucumber" 4 | ENV["RAILS_ROOT"] = File.expand_path("../../../test/dummy", __FILE__) 5 | 6 | Spork.prefork do 7 | require 'cucumber/rails' 8 | require 'factory_girl' 9 | require 'faker' 10 | 11 | %w(calculator_factory zone_factory shipping_method_factory payment_method_factory ).map{|f| require "spree_core/testing_support/factories/#{f}" } 12 | 13 | I18n.reload! 14 | 15 | Capybara.default_driver = :selenium 16 | Capybara.default_selector = :css 17 | 18 | include Warden::Test::Helpers 19 | 20 | ActionMailer::Base.delivery_method = :test 21 | ActionMailer::Base.perform_deliveries = true 22 | ActionMailer::Base.default_url_options[:host] = "example.com" 23 | 24 | ActionController::Base.allow_rescue = false 25 | 26 | Cucumber::Rails::World.use_transactional_fixtures = false 27 | DatabaseCleaner.strategy = :truncation 28 | 29 | end 30 | 31 | Spork.each_run do 32 | 33 | Dir["#{File.expand_path("../../../", __FILE__)}/test/support/**/*.rb"].each { |f| require f } 34 | 35 | Before do |s| 36 | 37 | Fixtures.reset_cache 38 | fixtures_folder = File.expand_path("../../../test/fixtures", __FILE__) 39 | fixtures = Dir[File.join(fixtures_folder, '*.yml')].map {|f| File.basename(f, '.yml') } 40 | Fixtures.create_fixtures(fixtures_folder, fixtures) 41 | 42 | ActionMailer::Base.deliveries = [] 43 | 44 | if s.feature.name.match(/^Admin\s/) 45 | @user = Factory.create(:admin_user) 46 | login_as @user 47 | end 48 | 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Spencer Steffen and Citrus Media Group. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Citrus Media Group nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /app/views/admin/drop_ship_orders/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "admin/shared/supplier_tabs", :current => "Supplier Orders" if @supplier %> 2 | <%= render "admin/shared/order_tabs", :current => "Drop Ship Orders" if @order %> 3 | 4 |

    Listing Orders

    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <%- @collection.each do |dso|%> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | <% end %> 32 | 33 |
    <%= sort_link @search, :id, t('order.id') %><%= sort_link @search, :supplier_name, t('order.supplier.name') %><%= sort_link @search, :order_number, t('order.number') %><%= sort_link @search, :total, t('order.total') %><%= sort_link @search, :sent_at, t('order.sent_at') %><%= sort_link @search, :confirmed_at, t('order.confirmed_at') %><%= sort_link @search, :shipped_at, t('order.shipped_at') %><%= sort_link @search, :status, t('order.status') %>
    <%= link_to "##{dso.id}", admin_drop_ship_order_url(dso) %><%= link_to dso.supplier.name, admin_supplier_url(dso.supplier) %><%= link_to dso.order.number, admin_order_url(dso.order) %><%= number_to_currency dso.total %><%= dso.created_at.to_s(:long) %><%= dso.confirmed_at.to_s(:long) if dso.confirmed_at.present? %><%= dso.shipped_at.to_s(:long) if dso.shipped_at.present? %><%= dso.state %>
    34 | 35 | <%= will_paginate @collection, :previous_label => "« #{t('previous')}", :next_label => "#{t('next')} »" %> 36 | -------------------------------------------------------------------------------- /app/views/admin/suppliers/index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
      3 |
    • 4 |

      <%= button_link_to "New Supplier", new_object_url, :icon => 'add' %>

      5 |
    • 6 |
    7 |
    8 |
    9 | 10 | 11 |

    Listing Suppliers

    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <%- @collection.each do |supplier|%> 25 | 26 | 27 | 28 | 29 | 33 | 34 | <% end %> 35 | 36 |
    <%= sort_link @search, :name, t('supplier.name') %><%= sort_link @search, :phone, t('supplier.phone') %><%= sort_link @search, :email, t('supplier.email') %><%= t('action') %>
    <%= link_to supplier.name, object_url(supplier) %><%= supplier.phone %><%= mail_to supplier.email %> 30 | <%= link_to_edit supplier %>   31 | <%= link_to_delete supplier %> 32 |
    37 | 38 | <%= will_paginate(:prev => "« #{t('previous')}", :next => "#{t('next')} »") %> 39 | 40 | <% content_for :sidebar do %> 41 |
    42 |

    <%= t('search') %>

    43 | 44 | <% @supplier = Supplier.metasearch %> 45 | <%= form_for [:admin, @supplier] do |f| %> 46 | <%- locals = {:f => f} %> 47 | <%= hook :admin_suppliers_index_search, locals do %> 48 |

    49 |
    50 | <%= f.text_field :name_like, :size => 25 %> 51 |

    52 | <% end %> 53 | <%= hook :admin_suppliers_index_search_buttons, locals do %> 54 |

    <%= button t('search') %>

    55 | <% end %> 56 | <% end %> 57 |
    58 | <% end %> -------------------------------------------------------------------------------- /app/views/admin/drop_ship_orders/show.html.erb: -------------------------------------------------------------------------------- 1 |

    Order #<%= @dso.id %>

    2 | 3 |
    4 | <%= render "drop_ship_orders/details", :dso => @dso %> 5 |
    6 | 7 |
    8 | <% if @dso.complete? %> 9 | <%= render "drop_ship_orders/shipment_info", :dso => @dso %> 10 | <% elsif !@dso.confirmed? %> 11 | <%= link_to "#{@dso.sent? ? "Resend" : "Send"} Order to Supplier".html_safe, deliver_admin_drop_ship_order_path(@dso), :class => 'button' %> 12 | <% end %> 13 |
    14 | 15 |
    16 | 17 |
    18 |

    Supplier Information

    19 |
    20 |
    Name
    21 |
    <%= @supplier.name %>
    22 |
    Address
    23 |
    <%= render "admin/shared/address", :address => @supplier.address %>
    24 |
    Email
    25 |
    <%= @supplier.email %>
    26 |
    Phone
    27 |
    <%= @supplier.phone %>
    28 |
    URL
    29 |
    <%= @supplier.url %>
    30 |
    Contact
    31 |
    <%= @supplier.contact %>
    32 | <% unless @supplier.contact_email.blank? %> 33 |
    Contact Email
    34 |
    <%= mail_to @supplier.contact_email %>
    35 | <% end %> 36 | <% unless @supplier.contact_phone.blank? %> 37 |
    Contact Phone
    38 |
    <%= @supplier.contact_phone %>
    39 | <% end %> 40 |
    41 |
    42 | 43 |
    44 | <%= render "drop_ship_orders/shipping_info", :address => @address %> 45 |
    46 | 47 |
    48 | 49 |

    Product Information

    50 | 51 | <%= render "drop_ship_orders/table", :dso => @dso %> 52 | 53 | <% content_for :head do %> 54 | 60 | <% end %> 61 | -------------------------------------------------------------------------------- /app/models/supplier.rb: -------------------------------------------------------------------------------- 1 | class Supplier < ActiveRecord::Base 2 | 3 | #========================================== 4 | # Associations 5 | 6 | belongs_to :user 7 | belongs_to :address 8 | has_many :supplier_products, :dependent => :destroy 9 | has_many :products, :through => :supplier_products 10 | has_many :orders, :class_name => "DropShipOrder", :dependent => :nullify 11 | 12 | #========================================== 13 | # Validations 14 | 15 | validates_associated :address 16 | validates :address_id, :name, :phone, :presence => true 17 | validates :email, :presence => true, :email => true 18 | validates :url, :url => true 19 | 20 | #========================================== 21 | # Callbacks 22 | 23 | before_validation :save_address, :on => :create 24 | after_create :create_user_and_send_welcome 25 | 26 | #========================================== 27 | # Instance Methods 28 | 29 | # Returns the supplier's email address and name in mail format 30 | def email_with_name 31 | "#{name} <#{email}>" 32 | end 33 | 34 | def url=(value) 35 | value = "http://#{value}" unless value =~ /https?:\/\// 36 | write_attribute :url, value 37 | end 38 | 39 | 40 | #========================================== 41 | # Protected Methods 42 | 43 | protected 44 | 45 | def create_user_and_send_welcome 46 | password = Digest::SHA1.hexdigest(email.to_s)[0..16] 47 | user = self.create_user(:email => email, :password => password, :password_confirmation => password) 48 | user.send(:generate_reset_password_token!) 49 | SupplierMailer.welcome(self).deliver! 50 | self.save 51 | end 52 | 53 | def save_address # :nodoc: 54 | unless address.nil? 55 | address.phone = phone 56 | address.save 57 | write_attribute :address_id, address.id 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spree_drop_shipping.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "spree_drop_shipping/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "spree_drop_shipping" 7 | s.version = SpreeDropShipping::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Spencer Steffen"] 10 | s.email = ["spencer@citrusme.com"] 11 | s.homepage = "https://github.com/citrus/spree_drop_shipping" 12 | s.summary = %q{Links products to suppliers and forwards orders to appropriate suppliers.} 13 | s.description = %q{Links products to suppliers and forwards orders to appropriate suppliers. Please see README for more details.} 14 | 15 | s.rubyforge_project = "spree_drop_shipping" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | 21 | s.require_paths = ["lib"] 22 | 23 | # Spree 24 | s.add_dependency('spree_core', '~> 0.60.4') 25 | s.add_dependency('spree_auth', '~> 0.60.4') 26 | 27 | # Development 28 | s.add_development_dependency('minitest', '>= 2.1.0') 29 | s.add_development_dependency('spree_sample', '~> 0.60.4') 30 | s.add_development_dependency('dummier', '>= 0.2.4') 31 | s.add_development_dependency('shoulda', '>= 3.0.0.beta2') 32 | s.add_development_dependency('spork', '>= 0.9.0.rc9') 33 | s.add_development_dependency('spork-testunit', '>= 0.0.5') 34 | s.add_development_dependency('factory_girl', '>= 2.0.4') 35 | s.add_development_dependency('cucumber-rails', '>= 1.0.2') 36 | s.add_development_dependency('database_cleaner', '>= 0.6.7') 37 | s.add_development_dependency('sqlite3', '>= 1.3.4') 38 | s.add_development_dependency('turn', '>= 0.8.2') 39 | 40 | end 41 | -------------------------------------------------------------------------------- /features/supplier_order.feature: -------------------------------------------------------------------------------- 1 | Feature: Supplier Drop ship orders 2 | 3 | In order to fulfill the vendor's orders 4 | As a supplier 5 | I want to receive orders for products that drop ship 6 | 7 | Background: 8 | Given I have some products 9 | And I have a shipping method 10 | And I have a bogus payment method 11 | 12 | Scenario: An order is placed for a drop shippable product 13 | Given I have an existing supplier named "Some Big Store" 14 | And supplier named "Some Big Store" is linked to the first product 15 | And I'm placing an order for the first product 16 | And I'm on the order confirmation step 17 | Then I press "Place Order" 18 | And I should see "Your order has been processed successfully" in the flash notice 19 | Then supplier named "Some Big Store" should have 1 order for the first product 20 | # And "Some Big Store" should receive an order email 21 | 22 | Scenario: Supplier receives drop ship order 23 | Given I have an existing supplier named "Some Big Store" 24 | And supplier named "Some Big Store" has been sent a drop ship order for the first product 25 | And I'm logged in as "Some Big Store" 26 | When I follow "You must click here to confirm this order" from within the email body 27 | Then I should be editing the last drop ship order 28 | And I should see "Please review and click 'Confirm Order' to continue." in the flash notice 29 | When I press "Confirm Order" 30 | Then I should be editing the last drop ship order 31 | And I should see the confirmation flash message 32 | 33 | Scenario: Supplier ships confirmed drop ship order 34 | Given I have an existing supplier named "Some Big Store" 35 | And supplier named "Some Big Store" has been sent a drop ship order for the first product 36 | And I'm logged in as "Some Big Store" 37 | And the last drop ship order has been confirmed 38 | And I'm on the edit drop ship order page for the last drop ship order 39 | When I fill in the following: 40 | | Shipping method | UPS Ground | 41 | | Confirmation number | 668435154858 | 42 | | Tracking number | 1Z209342093f2039 | 43 | When I press "Process and finalize order" 44 | Then I should be viewing the last drop ship order 45 | And I should see "Thank you for your shipment!" in the flash notice 46 | -------------------------------------------------------------------------------- /test/unit/supplier_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class SupplierTest < ActiveSupport::TestCase 4 | 5 | should validate_presence_of(:address_id) 6 | should validate_presence_of(:name) 7 | should validate_presence_of(:phone) 8 | 9 | should belong_to(:address) 10 | should have_many(:supplier_products) 11 | should have_many(:products) 12 | should have_many(:orders) 13 | 14 | context "A new supplier" do 15 | 16 | setup do 17 | @supplier = Supplier.new 18 | end 19 | 20 | should "require email" do 21 | assert !@supplier.valid? 22 | end 23 | 24 | should "validate email" do 25 | %w(test something.com some.thing.com something@ another@something that@one. @one.com @one@one.com one@one@one.com another@.com").each do |email| 26 | @supplier.email = email 27 | assert !@supplier.valid? 28 | end 29 | end 30 | 31 | should "allow valid email" do 32 | @supplier.email = "test@example.com" 33 | @supplier.valid? 34 | assert !@supplier.errors.keys.include?(:email) 35 | end 36 | 37 | should "not require http:// for url, but add it automatically" do 38 | @supplier.url = "example.com" 39 | @supplier.valid? 40 | assert !@supplier.errors.keys.include?(:url) 41 | assert_equal "http://example.com", @supplier.url 42 | end 43 | 44 | should "allow valid url" do 45 | @supplier.url = "http://example.com" 46 | @supplier.valid? 47 | assert !@supplier.errors.keys.include?(:url) 48 | end 49 | 50 | end 51 | 52 | 53 | context "An unsaved, valid supplier" do 54 | 55 | setup do 56 | @supplier = Factory.build(:supplier) 57 | end 58 | 59 | should "create user upon create" do 60 | @supplier.save 61 | assert_not_nil @supplier.user 62 | end 63 | 64 | should "send welcome email" do 65 | @supplier.save 66 | assert_equal "#{Spree::Config[:site_name]} - Welcome!", ActionMailer::Base.deliveries.last.subject 67 | end 68 | 69 | end 70 | 71 | 72 | context "An existing supplier" do 73 | 74 | setup do 75 | @supplier = Factory.create(:supplier) 76 | end 77 | 78 | should "return email address with name" do 79 | assert_equal "#{@supplier.name} <#{@supplier.email}>", @supplier.email_with_name 80 | end 81 | 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /test/fixtures/orders.yml: -------------------------------------------------------------------------------- 1 | <% 2 | order_date = Time.now 3 | 1.upto(10) do |i| 4 | order_date -= rand(12).hours 5 | item_total = "#{1 + rand(400)}.#{rand(100)}".to_f 6 | charges_total = "#{1 + rand(30)}.#{rand(100)}".to_f 7 | %> 8 | order_<%= i %>: 9 | number: <%= "R#{Array.new(9){rand(9)}.join}" %> 10 | user: user_<%= i %> 11 | state: complete 12 | email: <%= "order#{i}@example.com" %> 13 | item_total: <%= item_total %> 14 | created_at: <%=order_date.to_s(:db)%> 15 | completed_at: <%=order_date.to_s(:db)%> 16 | total: <%= item_total + charges_total %> 17 | adjustment_total: <%= charges_total %> 18 | ship_address: ship_address_<%= i %> 19 | bill_address: bill_address_<%= i %> 20 | <% end %> 21 | 22 | 23 | incomplete: 24 | id: 1069267039 25 | user_id: 1057526560 26 | number: R107687410 27 | item_total: 29.01 28 | total: 29.01 29 | created_at: 2011-01-12 03:07:39 30 | updated_at: 2011-01-12 03:07:39 31 | state: cart 32 | adjustment_total: 0.00 33 | credit_total: 0.00 34 | completed_at: NULL 35 | bill_address_id: NULL 36 | ship_address_id: NULL 37 | payment_total: 0.00 38 | shipping_method_id: NULL 39 | shipment_state: NULL 40 | payment_state: 'balance_due' 41 | email: 'spree@example.com' 42 | special_instructions: NULL 43 | 44 | pending: 45 | id: 1069267040 46 | user_id: 1057526560 47 | number: R107687411 48 | item_total: 29.01 49 | total: 29.01 50 | created_at: 2011-01-12 03:07:39 51 | updated_at: 2011-01-12 03:07:39 52 | state: confirm 53 | adjustment_total: 0.00 54 | credit_total: 0.00 55 | completed_at: NULL 56 | bill_address_id: 234234234 57 | ship_address_id: 324234234 58 | payment_total: 0.00 59 | shipping_method_id: 2425245 60 | shipment_state: NULL 61 | payment_state: 'pending' 62 | email: 'spree@example.com' 63 | special_instructions: NULL 64 | 65 | complete: 66 | id: 1069267041 67 | user_id: 1057526560 68 | number: R107687412 69 | item_total: 29.01 70 | total: 29.01 71 | created_at: 2011-01-12 03:07:39 72 | updated_at: 2011-01-12 03:07:39 73 | state: complete 74 | adjustment_total: 0.00 75 | credit_total: 0.00 76 | completed_at: <%= Time.now - 5.hours %> 77 | bill_address_id: 2342342 78 | ship_address_id: 2342342 79 | payment_total: 0.00 80 | shipping_method_id: 25425245 81 | shipment_state: NULL 82 | payment_state: 'completed' 83 | email: 'spree@example.com' 84 | special_instructions: NULL 85 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | 3 | successfully_created: "%{resource} was successfully created." 4 | successfully_updated: "%{resource} was successfully updated." 5 | successfully_removed: "%{resource} was successfully removed." 6 | 7 | 8 | activerecord: 9 | errors: 10 | models: 11 | supplier: 12 | attributes: 13 | email: 14 | invalid: is invalid 15 | url: 16 | invalid: is invalid. Please be sure to include include 'http://' 17 | not_responding: is invalid, not responding or redirecting to another location 18 | 19 | supplier: 20 | name: Name 21 | email: Email 22 | phone: Phone 23 | url: URL 24 | contact: Contact 25 | contact_phone: Contact Phone 26 | contact_email: Contact Email 27 | 28 | address1: Address 29 | address2: Address (continued) 30 | city: City 31 | state: State 32 | zipcode: Zipcode 33 | country: Country 34 | 35 | 36 | supplier_orders: 37 | flash: 38 | sent: "Please review and click 'Confirm Order' to continue." 39 | confirmed: "We've been notified that you've confirmed this order. To complete the order please, upon shipping, enter the shipping information and click 'Process and finalize order'. Thank You." 40 | shipped: "Thank you for your shipment!" 41 | confirmation_failure: "We're sorry, this order could not be confirmed. Please try again." 42 | finalize_failure: "We're sorry, this order could not be finalized at this time. Please try again." 43 | 44 | admin: 45 | orders: 46 | dso_order_header: 47 | dso_ids: DSO IDs 48 | confirm_drop_ship_info: 49 | approve_drop_ship_orders: Approve Drop Ship Orders 50 | resend_drop_ship_orders: Resend Drop Ship Orders 51 | drop_ship_order_approval_confrmation: This will send each drop ship order to it's relative supplier. Continue? 52 | shared: 53 | product_form_right: 54 | supplier: Drop Shipping Supplier 55 | none: --- none --- 56 | suppliers: 57 | new: 58 | new_supplier: New Supplier 59 | form: 60 | supplier_details: Supplier Details 61 | supplier_address: Supplier Address 62 | 63 | drop_ship_orders: 64 | orders_sent: All drop ship orders have successfully been sent 65 | orders_not_sent: Some or all of the drop ship orders could not be sent. Please contact the suppliers directly. 66 | -------------------------------------------------------------------------------- /test/unit/order_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class OrderTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | DropShipOrder.destroy_all 7 | end 8 | 9 | should have_many(:drop_ship_orders) 10 | 11 | should "have respond to finalize_for_dropship!" do 12 | assert subject.respond_to?(:finalize_for_dropship!) 13 | end 14 | 15 | should "check for drop ship order" do 16 | assert !subject.has_drop_ship_orders? 17 | subject.drop_ship_orders = [ subject.drop_ship_orders.build ] 18 | assert subject.has_drop_ship_orders? 19 | end 20 | 21 | context "An existing order" do 22 | 23 | setup do 24 | @product = products(:ds_ror_bag) 25 | @order = orders(:incomplete) 26 | end 27 | 28 | should "add variant and set supplier id" do 29 | @order.add_variant(@product.master) 30 | assert_not_nil @order.line_items.last.supplier_id 31 | end 32 | 33 | end 34 | 35 | 36 | context "A ready to finalize order" do 37 | 38 | setup do 39 | @supplier = suppliers(:supplier_1) 40 | @supplier2 = suppliers(:supplier_2) 41 | @order = orders(:pending) 42 | @order.ship_address = Address.last 43 | @order.line_items = [ line_items(:ds_li_1), line_items(:ds_li_2) ] 44 | @order.update! 45 | end 46 | 47 | should "create a drop ship order for supplier during finalize" do 48 | assert_equal 0, @supplier.orders.count 49 | @order.next! 50 | assert_equal 1, @supplier.orders.count 51 | end 52 | 53 | should "become complete by setting completed at" do 54 | @order.next! 55 | assert_not_nil @order.completed_at 56 | end 57 | 58 | context "that's been finalized" do 59 | 60 | setup do 61 | @order.next! 62 | end 63 | 64 | should "have orders for each supplier" do 65 | assert_equal 2, @order.drop_ship_orders.count 66 | end 67 | 68 | should "approve attached drop ship orders" do 69 | assert @order.approve_drop_ship_orders 70 | end 71 | 72 | context "and it's drop ship orders have been approved" do 73 | 74 | setup do 75 | @order.approve_drop_ship_orders 76 | end 77 | 78 | should "have it's orders sent" do 79 | @order.drop_ship_orders.each do |dso| 80 | assert dso.state?(:sent) 81 | end 82 | end 83 | 84 | end 85 | 86 | end 87 | end 88 | 89 | 90 | end 91 | -------------------------------------------------------------------------------- /features/admin/suppliers.feature: -------------------------------------------------------------------------------- 1 | @no-txn 2 | Feature: Admin suppliers interface 3 | 4 | In order to send orders to suppliers 5 | As an admin 6 | I want to manage product suppliers 7 | 8 | Scenario: Visiting admin suppliers index 9 | Given I'm on the admin products page 10 | Then I should see "Suppliers" in the main menu 11 | When I follow "Suppliers" 12 | Then I should see "Listing Suppliers" 13 | 14 | Scenario: Creating a new supplier 15 | Given I'm on the admin suppliers page 16 | When I follow "New Supplier" 17 | Then I should see "New Supplier" 18 | And I should see "Supplier Details" 19 | And I should see "Supplier Address" 20 | When I fill in the following: 21 | | Name | Some Big Store | 22 | | Email | somebigstore@example.com | 23 | | Phone | 800-555-5555 | 24 | | URL | http://somebigstore.example.com | 25 | | Contact | Steve | 26 | | Contact Email | steve@example.com | 27 | | Contact Phone | 555-555-5555 | 28 | | Address | 100 State St | 29 | | City | Santa Barbara | 30 | | Zipcode | 93101 | 31 | And I select "California" from "State" 32 | When I press "Create" 33 | Then I should see "Listing Suppliers" 34 | And I should see "Some Big Store" 35 | And I should see "successfully created" in the flash notice 36 | 37 | Scenario: Updating an existing supplier 38 | Given I have an existing supplier named "Some Big Store" 39 | And I'm on the admin suppliers page 40 | When I follow "Edit" 41 | Then I should see "Editing Supplier" 42 | And I should see "Supplier Details" 43 | And I should see "Supplier Address" 44 | When I fill in the following: 45 | | Name | Another Big Store | 46 | | Address | 101 State St | 47 | When I press "Update" 48 | Then I should see "Listing Suppliers" 49 | And I should see "Another Big Store" 50 | And I should see "successfully updated" in the flash notice 51 | 52 | Scenario: Deleting an existing supplier 53 | Given I have an existing supplier named "Some Big Store" 54 | And I'm on the admin suppliers page 55 | When I follow "Delete" 56 | And I wait for 1 second 57 | Then I should see "Are you sure?" in the popup message 58 | When I confirm the popup message 59 | Then I should but don't see "successfully removed" 60 | 61 | # so you're wondering wtf, right? ya.. I can't figure out how to 62 | # make the confirmation popup ajax request to work.. 63 | 64 | -------------------------------------------------------------------------------- /test/fixtures/products.yml: -------------------------------------------------------------------------------- 1 | ror_tote: 2 | name: Ruby on Rails Tote 3 | description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. 4 | available_on: <%= Time.zone.now.to_s(:db) %> 5 | permalink: ruby-on-rails-tote 6 | count_on_hand: 45 7 | 8 | ds_ror_bag: 9 | name: Ruby on Rails Bag 10 | description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. 11 | available_on: <%= Time.zone.now.to_s(:db) %> 12 | permalink: ruby-on-rails-bag 13 | count_on_hand: 45 14 | 15 | ds_ror_mug: 16 | name: Ruby on Rails Mug 17 | description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. 18 | available_on: <%= Time.zone.now.to_s(:db) %> 19 | permalink: ruby-on-rails-mug 20 | count_on_hand: 75 21 | -------------------------------------------------------------------------------- /app/models/drop_ship_order.rb: -------------------------------------------------------------------------------- 1 | class DropShipOrder < ActiveRecord::Base 2 | 3 | #========================================== 4 | # Associations 5 | 6 | belongs_to :order 7 | belongs_to :supplier 8 | has_many :line_items, :class_name => "DropShipLineItem" 9 | has_one :user, :through => :supplier 10 | 11 | #========================================== 12 | # Validations 13 | 14 | validates :supplier_id, :order_id, :presence => true 15 | 16 | #========================================== 17 | # Callbacks 18 | 19 | before_save :update_total 20 | 21 | #========================================== 22 | # State Machine 23 | 24 | state_machine :initial => 'active' do 25 | 26 | after_transition :on => :deliver, :do => :perform_delivery 27 | after_transition :on => :confirm, :do => :perform_confirmation 28 | after_transition :on => :ship, :do => :perform_shipment 29 | 30 | event :deliver do 31 | transition [ :active, :sent ] => :sent 32 | end 33 | 34 | event :confirm do 35 | transition :sent => :confirmed 36 | end 37 | 38 | event :ship do 39 | transition :confirmed => :complete 40 | end 41 | 42 | state :complete do 43 | validates :shipping_method, :tracking_number, :presence => true 44 | end 45 | 46 | end 47 | 48 | 49 | #========================================== 50 | # Instance Methods 51 | 52 | # Adds line items to the drop ship order. This method will group similar line items 53 | # and update quantities as necessary. You can add a single line item or an array of 54 | # line items. 55 | def add(new_items) 56 | new_items = [ new_items ].flatten.reject{|li| li.supplier_id.nil? || li.supplier_id != self.supplier_id } 57 | attributes = [] 58 | new_items.group_by(&:variant_id).each do |variant_id, items| 59 | quantity = items.map(&:quantity).inject(:+) 60 | if item = self.line_items.find_by_variant_id(variant_id) 61 | item.update_attributes(:quantity => item.quantity + quantity) 62 | else 63 | attributes << items.first.drop_ship_attributes.update(:quantity => quantity) 64 | end 65 | end 66 | self.line_items.create(attributes) unless attributes.empty? 67 | self.save ? self : nil 68 | end 69 | 70 | # Updates the drop ship order's total by getting the sum of its line items' subtotals 71 | def update_total 72 | self.total = self.line_items.reload.map(&:subtotal).inject(:+).to_f 73 | end 74 | 75 | # Don't allow drop ship orders to be destroyed 76 | def destroy 77 | false 78 | end 79 | 80 | 81 | #========================================== 82 | # Private Methods 83 | 84 | private 85 | 86 | def perform_delivery # :nodoc: 87 | self.update_attribute(:sent_at, Time.now) 88 | DropShipOrderMailer.supplier_order(self).deliver! 89 | end 90 | 91 | def perform_confirmation # :nodoc: 92 | self.update_attribute(:confirmed_at, Time.now) 93 | DropShipOrderMailer.confirmation(self).deliver! 94 | end 95 | 96 | def perform_shipment # :nodoc: 97 | self.update_attribute(:shipped_at, Time.now) 98 | DropShipOrderMailer.shipment(self).deliver! 99 | DropShipOrderMailer.shipment_notification(self).deliver! 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /features/step_definitions/web_steps.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'cgi' 3 | require File.expand_path("../../support/paths.rb", __FILE__) 4 | require File.expand_path("../../support/selectors.rb", __FILE__) 5 | require File.expand_path("../../../test/support/helper_methods.rb", __FILE__) 6 | 7 | include HelperMethods 8 | 9 | def get_parent(parent) 10 | case parent.sub(/^the\s/, '') 11 | when "main menu"; "#admin-menu" 12 | when "flash notice"; ".flash" 13 | when "popup message"; "#popup_message" 14 | when "index table"; "table.index" 15 | end 16 | end 17 | 18 | 19 | 20 | # hack hack hack! 21 | # some weird bug for cuke/capy/selenium and ajax 22 | Then /^I should but don't see "([^"]*)"$/ do |text| 23 | puts "\nConfirmation popup? Thought so.. there's a weird bug here..." 24 | puts "Let's just pretend you saw #{text.inspect} where you wanted to...\n\n" 25 | end 26 | 27 | 28 | 29 | #======================================================================== 30 | # Givens 31 | 32 | Given /^I'm on the ((?!page).*) page$/ do |path| 33 | path = "#{path.downcase.gsub(/\s/, '_')}_path".to_sym 34 | begin 35 | visit send(path) 36 | rescue 37 | puts "#{path} could not be found!" 38 | end 39 | end 40 | 41 | Given /^I'm on the ((?!page).*) page for (.*)$/ do |path, id| 42 | case id 43 | when "the first product" 44 | id = Product.first 45 | when "the last drop ship order" 46 | id = DropShipOrder.last 47 | end 48 | path = "#{path.downcase.gsub(/\s/, '_')}_path".to_sym 49 | begin 50 | visit send(path, id) 51 | rescue 52 | puts "#{path} could not be found!" 53 | end 54 | end 55 | 56 | 57 | #======================================================================== 58 | # Actions 59 | 60 | When /^(?:|I )press "([^"]*)"$/ do |button| 61 | # wtf button text spree! 62 | button = "#popup_ok" if button == "OK" 63 | click_button(button) 64 | end 65 | 66 | When /^I press "([^"]*)" in (.*)$/ do |button, parent| 67 | # wtf button text spree! 68 | button = "#popup_ok" if button == "OK" 69 | within get_parent(parent) do 70 | click_button(button) 71 | end 72 | end 73 | 74 | When /^(?:|I )follow "([^"]*)"$/ do |link| 75 | click_link(link) 76 | end 77 | 78 | When /^I wait for (\d+) seconds?$/ do |seconds| 79 | sleep seconds.to_f 80 | end 81 | 82 | When /^I confirm the popup message$/ do 83 | find_by_id("popup_ok").click 84 | end 85 | 86 | 87 | 88 | 89 | 90 | 91 | #======================================================================== 92 | # Assertions 93 | 94 | Then /^I should see "([^"]*)"$/ do |text| 95 | assert page.has_content?(text) 96 | end 97 | 98 | Then /^I should see "([^"]*)" in (.*)$/ do |text, parent| 99 | within get_parent(parent) do 100 | assert page.has_content?(text) 101 | end 102 | end 103 | 104 | Then /^I should not see "([^"]*)"$/ do |text| 105 | assert_not page.has_content?(text) 106 | end 107 | 108 | Then /^I should not see "([^"]*)" in (.*)$/ do |text, parent| 109 | within get_parent(parent) do 110 | assert_not page.has_content?(text) 111 | end 112 | end 113 | 114 | Then /^"([^"]*)" should equal "([^"]*)"$/ do |field, value| 115 | assert_equal value, find_field(field).value 116 | end 117 | 118 | Then /^"([^"]*)" should have "([^"]*)" selected$/ do |field, value| 119 | field = find_field(field) 120 | has_match = field.text =~ /#{value}/ 121 | has_match = field.value =~ /#{value}/ unless has_match 122 | assert has_match 123 | end 124 | 125 | 126 | #======================================================================== 127 | # Forms 128 | 129 | When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value| 130 | fill_in(field, :with => value) 131 | end 132 | 133 | When /^(?:|I )fill in the following:$/ do |fields| 134 | fields.rows_hash.each do |name, value| 135 | step %{I fill in "#{name}" with "#{value}"} 136 | end 137 | end 138 | 139 | When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field| 140 | select(value, :from => field) 141 | end 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /app/views/admin/suppliers/_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 | <%= t('.supplier_details') %> 4 | 5 | <%= form.field_container :name do %> 6 | <%= form.label :name, t('supplier.name') %>
    7 | <%= form.text_field :name, :class => 'title' %> 8 | <%= error_message_on :supplier, :name %> 9 | <% end %> 10 | 11 | <%= form.field_container :email do %> 12 | <%= form.label :email, t('supplier.email') %>
    13 | <%= form.email_field :email, :class => 'text' %> 14 | <%= error_message_on :supplier, :email %> 15 | <% end %> 16 | 17 | <%= form.field_container :phone do %> 18 | <%= form.label :phone, t('supplier.phone') %>
    19 | <%= form.text_field :phone, :class => 'text' %> 20 | <%= error_message_on :supplier, :phone %> 21 | <% end %> 22 | 23 | <%= form.field_container :url do %> 24 | <%= form.label :url, t('supplier.url') %>
    25 | <%= form.text_field :url, :class => 'text' %> 26 | <%= error_message_on :supplier, :url %> 27 | <% end %> 28 | 29 | <%= form.field_container :contact do %> 30 | <%= form.label :contact, t('supplier.contact') %>
    31 | <%= form.text_field :contact, :class => 'text' %> 32 | <%= error_message_on :supplier, :contact %> 33 | <% end %> 34 | 35 | <%= form.field_container :contact_email do %> 36 | <%= form.label :contact_email, t('supplier.contact_email') %>
    37 | <%= form.email_field :contact_email, :class => 'text' %> 38 | <%= error_message_on :supplier, :contact_email %> 39 | <% end %> 40 | 41 | <%= form.field_container :contact_phone do %> 42 | <%= form.label :contact_phone, t('supplier.contact_phone') %>
    43 | <%= form.text_field :contact_phone, :class => 'text' %> 44 | <%= error_message_on :supplier, :contact_phone %> 45 | <% end %> 46 | 47 |
    48 | 49 | 50 | 51 |
    52 | 53 | <%= t('.supplier_address') %> 54 | 55 | <%= fields_for @supplier.address do |f| %> 56 | 57 | <%= f.hidden_field :firstname, :value => 'SUPPLIER' %> 58 | <%= f.hidden_field :lastname, :value => 'SUPPLIER' %> 59 | 60 | <%= f.field_container :address1 do %> 61 | <%= f.label :address1, t('supplier.address1') %>
    62 | <%= f.text_field :address1, :class => 'text' %> 63 | <%= error_message_on :address, :address1 %> 64 | <% end %> 65 | 66 | <%= f.field_container :address2 do %> 67 | <%= f.label :address2, t('supplier.address2') %>
    68 | <%= f.text_field :address2, :class => 'text' %> 69 | <%= error_message_on :address, :address2 %> 70 | <% end %> 71 | 72 | <%= f.field_container :city do %> 73 | <%= f.label :city, t('supplier.city') %>
    74 | <%= f.text_field :city, :class => 'text' %> 75 | <%= error_message_on :address, :city %> 76 | <% end %> 77 | 78 | <%= f.field_container :state_id do %> 79 | <%= f.label :state_id, t('supplier.state') %>
    80 | <%= f.select :state_id, @state_options, :class => 'text' %> 81 | <%= error_message_on :address, :state_id %> 82 | <% end %> 83 | 84 | <%= f.field_container :zipcode do %> 85 | <%= f.label :zipcode, t('supplier.zipcode') %>
    86 | <%= f.text_field :zipcode, :class => 'text' %> 87 | <%= error_message_on :address, :zipcode %> 88 | <% end %> 89 | 90 | <%= f.field_container :country_id do %> 91 | <%= f.label :country_id, t('supplier.country') %>
    92 | <%= f.select :country_id, @country_options, :selected => @supplier.new_record? ? 214 : @supplier.address.country_id, :class => 'text' %> 93 | <%= error_message_on :address, :country_id %> 94 | <% end %> 95 | 96 | <% end %> 97 | 98 |
    99 | 100 |
    101 | 102 | 119 | 120 | <% content_for :head do %> 121 | <%= stylesheet_link_tag 'admin/suppliers' %> 122 | <% end %> 123 | -------------------------------------------------------------------------------- /features/step_definitions/supplier_orders.rb: -------------------------------------------------------------------------------- 1 | def visit_first_product 2 | @product = Product.first 3 | visit(product_path(@product)) 4 | click_button("Add To Cart") 5 | assert has_content?(@product.name) 6 | end 7 | 8 | def click_checkout 9 | # click the checkout button 10 | btn = find(:xpath, '//a[@class="button checkout primary"][last()]') 11 | assert_equal "checkout", btn.text 12 | btn.click 13 | end 14 | 15 | def register_as_quest 16 | # fill in the registration step if necessary 17 | if has_selector? "#guest_checkout" 18 | within "#guest_checkout" do 19 | fill_in "Email", :with => Faker::Internet.email 20 | end 21 | click_button "Continue" 22 | end 23 | end 24 | 25 | def complete_billing_form 26 | # fill in the billing info 27 | within "#billing" do 28 | fill_in "First Name", :with => "Joe" 29 | fill_in "Last Name", :with => "Schmo" 30 | fill_in "Street Address", :with => "111 State St" 31 | fill_in "City", :with => "Santa Barbara" 32 | select("California", :from => "order_bill_address_attributes_state_id") 33 | fill_in "Zip", :with => "93101" 34 | select("United States", :from => "Country") 35 | fill_in "Phone", :with => Faker::PhoneNumber.phone_number 36 | end 37 | end 38 | 39 | def use_billing_for_shipping 40 | check "order_use_billing" 41 | end 42 | 43 | def select_shipping_method(method="UPS Ground") 44 | # select shipping method 45 | assert has_content?("Shipping Method") 46 | choose method 47 | end 48 | 49 | def complete_credit_card_form 50 | # create a payment 51 | within "#payment-methods" do 52 | fill_in "card_number", :with => "4111111111111111" 53 | fill_in "card_code", :with => "123" 54 | end 55 | end 56 | 57 | def save_and_continue 58 | click_button "Save and Continue" 59 | end 60 | 61 | 62 | 63 | Given /^I'm placing an order for the first product$/ do 64 | visit_first_product 65 | click_checkout 66 | register_as_quest 67 | complete_billing_form 68 | use_billing_for_shipping 69 | save_and_continue 70 | select_shipping_method 71 | save_and_continue 72 | complete_credit_card_form 73 | save_and_continue 74 | end 75 | 76 | Given /^I'm on the order confirmation step$/ do 77 | # confirm payment 78 | assert has_content?("Confirm") 79 | assert has_content?(@product.name) 80 | assert_equal "/checkout/confirm", current_path 81 | end 82 | 83 | Given /^I have a shipping method$/ do 84 | Factory.create(:shipping_method) 85 | end 86 | 87 | Given /^I have a bogus payment method$/ do 88 | Factory.create(:bogus_payment_method) 89 | end 90 | 91 | Given /^supplier named "([^"]*)" has been sent a drop ship order for the first product$/ do |name| 92 | supplier = Supplier.find_by_name(name) 93 | order = Order.find_by_state('complete') 94 | order.ship_address = Address.find_by_firstname("Boxy") 95 | order.save 96 | dso = Factory.create(:drop_ship_order, :supplier => supplier, :order => order) 97 | dso.add(order.line_items).deliver! 98 | end 99 | 100 | Given /^the last drop ship order has been confirmed$/ do 101 | DropShipOrder.last.confirm! 102 | end 103 | 104 | Then /^I should see the confirmation flash message$/ do 105 | step "I should see \"#{I18n.t('supplier_orders.flash.confirmed')}\" in the flash notice" 106 | end 107 | 108 | Then /^supplier named "([^"]*)" should have (\d+) orders? for the first product$/ do |name, order_count| 109 | supplier = Supplier.find_by_name(name) 110 | product = Product.first 111 | assert_equal order_count.to_i, supplier.orders.count 112 | assert_equal product.sku, supplier.orders.first.line_items.first.sku 113 | end 114 | 115 | #Then /^"([^"]*)" should receive an order email$/ do |name| 116 | # supplier = Supplier.find_by_name(name) 117 | # flunk if ActionMailer::Base.deliveries.empty? 118 | # assert_equal supplier.email, ActionMailer::Base.deliveries.last.to.first 119 | # assert_equal "#{Spree::Config[:site_name]} - Order ##{supplier.orders.last.id}", ActionMailer::Base.deliveries.last.subject 120 | #end 121 | 122 | Then /^I follow "([^"]*)" from within the email body$/ do |link| 123 | email = ActionMailer::Base.deliveries.last 124 | doc = Nokogiri::HTML(email.body.to_s) 125 | a = doc.xpath("//a[contains(.,'#{link}')]") 126 | if a 127 | visit a.attribute("href").to_s.sub("http://example.com", "") 128 | else 129 | raise Capybara::ElementNotFound, "Could not find link with text #{link.inspect}" 130 | end 131 | end 132 | 133 | Then /^I should be editing the last drop ship order$/ do 134 | assert_equal edit_drop_ship_order_path(DropShipOrder.last), current_path 135 | end 136 | 137 | Then /^I should be viewing the last drop ship order$/ do 138 | assert_equal drop_ship_order_path(DropShipOrder.last), current_path 139 | end 140 | 141 | 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spree Drop Shipping 2 | =================== 3 | 4 | ... is nearing completion! 5 | 6 | --- 7 | 8 | ### What is drop shipping? 9 | 10 | "Drop shipping is a supply chain management technique in which the retailer does not keep goods in stock, but instead transfers customer orders and shipment details to either the manufacturer or a wholesaler, who then ships the goods directly to the customer." [[wikipedia](http://en.wikipedia.org/wiki/Drop_shipping)] 11 | 12 | --- 13 | 14 | So the main goal with spree_drop_shipping is to link products to suppliers and forward orders to appropriate suppliers. 15 | 16 | In more detail, once an order is placed for a product that drop ships a drop ship order is created for the product's supplier. This drop ship order is sent to the supplier via Email. The supplier then follows a link to the order within the email where they are prompted to confirm the order. 17 | 18 | After the supplier has confirmed an order and is ready to ship, they can log into the site and update the drop ship order with a shipping method, confirmation number and tracking number. Once they 'process & finalize' the order, the customer is notified with the shipment details. 19 | 20 | 21 | Todo 22 | ---- 23 | 24 | - Finish/Style Email templates 25 | - Drop ship order styles 26 | - Finish I18n implementation 27 | - Spree 0.70+ compatibility 28 | - Better documentation 29 | 30 | 31 | Installation 32 | ------------ 33 | 34 | Here's how to install spree_drop_shipping into your existing spree site: 35 | 36 | 37 | Add the following to your Gemfile: 38 | 39 | gem 'spree_drop_shipping', :git => 'git://github.com/citrus/spree_drop_shipping.git' 40 | 41 | Make your bundle happy: 42 | 43 | bundle install 44 | 45 | Now run the generator: 46 | 47 | rails g spree_drop_shipping:install 48 | 49 | Then migrate your database: 50 | 51 | rake db:migrate 52 | 53 | And reboot your server: 54 | 55 | rails s 56 | 57 | You should be up and running now! 58 | 59 | 60 | ### Sample Data 61 | 62 | If you'd like to generate sample data, use the included rake tasks 63 | 64 | rake db:sample # Loads sample data into the store 65 | rake db:sample:suppliers # Create sample suppliers and randomly link to products 66 | rake db:sample:drop_ship_orders # Create sample drop ship orders 67 | 68 | 69 | Testing 70 | ------- 71 | 72 | Clone this repo to where you develop, bundle up, then run `dummier' to get the show started: 73 | 74 | git clone git://github.com/citrus/spree_drop_shipping.git 75 | cd spree_drop_shipping 76 | bundle install 77 | bundle exec dummier 78 | 79 | This will generate a fresh rails app in test/dummy, install spree & spree_drop_shipping, then migrate the test database. Sweet. 80 | 81 | 82 | ### Spork + Cucumber 83 | 84 | To run the cucumber features, boot spork like this: 85 | 86 | bundle exec spork 87 | 88 | Then, in another window, run: 89 | 90 | cucumber --drb 91 | 92 | 93 | ### Spork + Test::Unit 94 | 95 | If you want to run shoulda tests, start spork with: 96 | 97 | bundle exec spork TestUnit 98 | #or 99 | bundle exec spork t 100 | 101 | In another window, run all tests: 102 | 103 | testdrb test/**/*_test.rb 104 | 105 | Or just a specific test: 106 | 107 | testdrb test/unit/supplier_test.rb 108 | 109 | 110 | ### No Spork 111 | 112 | If you don't want to spork, just use rake: 113 | 114 | # cucumber/capybara 115 | rake cucumber 116 | 117 | # test/unit 118 | rake test 119 | 120 | # both 121 | rake 122 | 123 | POW! 124 | 125 | 126 | Demo 127 | ---- 128 | 129 | You can easily use the test/dummy app as a demo of spree_drop_shipping. Just `cd` to where you develop and run: 130 | 131 | git clone git://github.com/citrus/spree_drop_shipping.git 132 | cd spree_drop_shipping 133 | bundle install 134 | bundle exec dummier 135 | cd test/dummy 136 | rake db:migrate db:seed db:sample 137 | rails s 138 | 139 | 140 | You can also enable the `after_migrate` [dummier](https://github.com/citrus/dummier) hook by renaming `after_migrate.rb.sample` to `after_migrate.rb` in `lib/dummy_hooks` then re-run `bundle exec dummier`. In other words, just do this: 141 | 142 | cd spree_drop_shipping 143 | mv lib/dummy_hooks/after_migrate.rb.sample lib/dummy_hooks/after_migrate.rb 144 | bundle exec dummier 145 | cd test/dummy 146 | rails s 147 | 148 | 149 | Contributors 150 | ------------ 151 | 152 | So far it's just me; Spencer Steffen. 153 | 154 | If you'd like to help out feel free to fork and send me pull requests! 155 | 156 | 157 | License 158 | ------- 159 | 160 | Copyright (c) 2011 Spencer Steffen and Citrus, released under the New BSD License. All rights reserved. 161 | -------------------------------------------------------------------------------- /test/fixtures/states.yml: -------------------------------------------------------------------------------- 1 | --- 2 | states_043: 3 | name: Michigan 4 | country_id: "214" 5 | id: "931624400" 6 | abbr: MI 7 | states_032: 8 | name: South Dakota 9 | country_id: "214" 10 | id: "615306087" 11 | abbr: SD 12 | states_021: 13 | name: Washington 14 | country_id: "214" 15 | id: "414569975" 16 | abbr: WA 17 | states_010: 18 | name: Wisconsin 19 | country_id: "214" 20 | id: "103680699" 21 | abbr: WI 22 | states_044: 23 | name: Arizona 24 | country_id: "214" 25 | id: "948208802" 26 | abbr: AZ 27 | states_033: 28 | name: Illinois 29 | country_id: "214" 30 | id: "625629523" 31 | abbr: IL 32 | states_022: 33 | name: New Hampshire 34 | country_id: "214" 35 | id: "426832442" 36 | abbr: NH 37 | states_011: 38 | name: North Carolina 39 | country_id: "214" 40 | id: "177087202" 41 | abbr: NC 42 | states_045: 43 | name: Kansas 44 | country_id: "214" 45 | id: "969722173" 46 | abbr: KS 47 | states_034: 48 | name: Missouri 49 | country_id: "214" 50 | id: "653576146" 51 | abbr: MO 52 | states_023: 53 | name: Arkansas 54 | country_id: "214" 55 | id: "471470972" 56 | abbr: AR 57 | states_012: 58 | name: Nevada 59 | country_id: "214" 60 | id: "179539703" 61 | abbr: NV 62 | states_001: 63 | name: District of Columbia 64 | country_id: "214" 65 | id: "6764998" 66 | abbr: DC 67 | states_046: 68 | name: Idaho 69 | country_id: "214" 70 | id: "982433740" 71 | abbr: ID 72 | states_035: 73 | name: Nebraska 74 | country_id: "214" 75 | id: "673350891" 76 | abbr: NE 77 | states_024: 78 | name: Pennsylvania 79 | country_id: "214" 80 | id: "471711976" 81 | abbr: PA 82 | states_013: 83 | name: Hawaii 84 | country_id: "214" 85 | id: "199950338" 86 | abbr: HI 87 | states_002: 88 | name: Utah 89 | country_id: "214" 90 | id: "17199670" 91 | abbr: UT 92 | states_047: 93 | name: Vermont 94 | country_id: "214" 95 | id: "989115415" 96 | abbr: VT 97 | states_036: 98 | name: Delaware 99 | country_id: "214" 100 | id: "721598219" 101 | abbr: DE 102 | states_025: 103 | name: Rhode Island 104 | country_id: "214" 105 | id: "474001862" 106 | abbr: RI 107 | states_014: 108 | name: Oklahoma 109 | country_id: "214" 110 | id: "248548169" 111 | abbr: OK 112 | states_003: 113 | name: Louisiana 114 | country_id: "214" 115 | id: "37199952" 116 | abbr: LA 117 | states_048: 118 | name: Montana 119 | country_id: "214" 120 | id: "999156632" 121 | abbr: MT 122 | states_037: 123 | name: Tennessee 124 | country_id: "214" 125 | id: "726305632" 126 | abbr: TN 127 | states_026: 128 | name: Maryland 129 | country_id: "214" 130 | id: "480368357" 131 | abbr: MD 132 | states_015: 133 | name: Florida 134 | country_id: "214" 135 | id: "267271847" 136 | abbr: FL 137 | states_004: 138 | name: Virginia 139 | country_id: "214" 140 | id: "41111624" 141 | abbr: VA 142 | states_049: 143 | name: Minnesota 144 | country_id: "214" 145 | id: "1032288924" 146 | abbr: MN 147 | states_038: 148 | name: New Jersey 149 | country_id: "214" 150 | id: "750950030" 151 | abbr: NJ 152 | states_027: 153 | name: Ohio 154 | country_id: "214" 155 | id: "485193526" 156 | abbr: OH 157 | states_016: 158 | name: California 159 | country_id: "214" 160 | id: "276110813" 161 | abbr: CA 162 | states_005: 163 | name: North Dakota 164 | country_id: "214" 165 | id: "51943165" 166 | abbr: ND 167 | states_050: 168 | name: Maine 169 | country_id: "214" 170 | id: "1055056709" 171 | abbr: ME 172 | states_039: 173 | name: Indiana 174 | country_id: "214" 175 | id: "769938586" 176 | abbr: IN 177 | states_028: 178 | name: Texas 179 | country_id: "214" 180 | id: "525212995" 181 | abbr: TX 182 | states_017: 183 | name: Oregon 184 | country_id: "214" 185 | id: "298914262" 186 | abbr: OR 187 | states_006: 188 | name: Wyoming 189 | country_id: "214" 190 | id: "66390489" 191 | abbr: WY 192 | states_051: 193 | name: Alabama 194 | country_id: "214" 195 | id: "1061493585" 196 | abbr: AL 197 | states_040: 198 | name: Iowa 199 | country_id: "214" 200 | id: "825306985" 201 | abbr: IA 202 | states_029: 203 | name: Mississippi 204 | country_id: "214" 205 | id: "532363768" 206 | abbr: MS 207 | states_018: 208 | name: Kentucky 209 | country_id: "214" 210 | id: "308473843" 211 | abbr: KY 212 | states_007: 213 | name: New Mexico 214 | country_id: "214" 215 | id: "69729944" 216 | abbr: NM 217 | states_041: 218 | name: Georgia 219 | country_id: "214" 220 | id: "876916760" 221 | abbr: GA 222 | states_030: 223 | name: Colorado 224 | country_id: "214" 225 | id: "536031023" 226 | abbr: CO 227 | states_019: 228 | name: Massachusetts 229 | country_id: "214" 230 | id: "385551075" 231 | abbr: MA 232 | states_008: 233 | name: Connecticut 234 | country_id: "214" 235 | id: "69870734" 236 | abbr: CT 237 | states_042: 238 | name: New York 239 | country_id: "214" 240 | id: "889445952" 241 | abbr: NY 242 | states_031: 243 | name: South Carolina 244 | country_id: "214" 245 | id: "597434151" 246 | abbr: SC 247 | states_020: 248 | name: Alaska 249 | country_id: "214" 250 | id: "403740659" 251 | abbr: AK 252 | states_009: 253 | name: West Virginia 254 | country_id: "214" 255 | id: "91367981" 256 | abbr: WV 257 | -------------------------------------------------------------------------------- /test/unit/drop_ship_order_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class DropShipOrderTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | ActionMailer::Base.deliveries = [] 7 | end 8 | 9 | should belong_to(:order) 10 | should belong_to(:supplier) 11 | should have_many(:line_items) 12 | should have_one(:user).through(:supplier) 13 | 14 | should validate_presence_of(:supplier_id) 15 | should validate_presence_of(:order_id) 16 | 17 | context "A new drop ship order" do 18 | 19 | setup do 20 | @dso = DropShipOrder.new 21 | end 22 | 23 | should "respond to add" do 24 | @dso.respond_to?(:add) 25 | end 26 | 27 | should "calcluate total when empty" do 28 | assert_equal 0.0, @dso.update_total 29 | end 30 | 31 | end 32 | 33 | 34 | context "A suppliers active drop ship order" do 35 | 36 | setup do 37 | @supplier = suppliers(:supplier_1) 38 | @dso = @supplier.orders.create(:order_id => 1) 39 | end 40 | 41 | should "add line relevant line items" do 42 | @line_items = [ line_items(:li_1), line_items(:ds_li_1), line_items(:ds_li_2) ] 43 | @dso.add(@line_items) 44 | assert_equal 1, @dso.line_items.count 45 | end 46 | 47 | should "group line items and increment quantity" do 48 | @line_items = [ line_items(:ds_li_1), line_items(:ds_li_1), line_items(:ds_li_1) ] 49 | quantity = @line_items.map(&:quantity).inject(:+) 50 | @dso.add(@line_items) 51 | assert_equal quantity, @dso.line_items.last.quantity 52 | end 53 | 54 | should "increment quantity of items already in order" do 55 | @line_item = Factory.build(:line_item) 56 | @dso.add(@line_item) 57 | @dso.add(@line_item) 58 | count = @dso.line_items.count 59 | quantity = @dso.line_items.first.quantity 60 | assert_equal 1, count 61 | assert_equal 2, quantity 62 | end 63 | 64 | should "increment quantity of grouped items already in order" do 65 | @line_items = [ Factory.build(:line_item), Factory.build(:line_item) ] 66 | @dso.add(@line_items) 67 | @dso.add(@line_items) 68 | count = @dso.line_items.count 69 | quantity = @dso.line_items.first.quantity 70 | assert_equal 1, count 71 | assert_equal 4, quantity 72 | end 73 | 74 | should "add items and update total" do 75 | @line_items = [ line_items(:ds_li_1), line_items(:ds_li_1), line_items(:ds_li_1) ] 76 | price = @line_items.map{|li| li.quantity * li.price }.inject(:+) 77 | @dso.add(@line_items) 78 | assert_equal price, @dso.total 79 | end 80 | 81 | end 82 | 83 | 84 | context "A drop ship order's state machine" do 85 | 86 | setup do 87 | ActionMailer::Base.deliveries = [] 88 | @dso = Factory.create(:drop_ship_order) 89 | @dso.order.ship_address = Address.find_by_firstname("Boxy") 90 | @dso.save 91 | end 92 | 93 | should "start in the 'active' state" do 94 | assert_equal "active", @dso.state 95 | end 96 | 97 | context "when delivered" do 98 | 99 | setup do 100 | @dso.deliver! 101 | end 102 | 103 | should "move to the 'sent' state" do 104 | assert_equal "sent", @dso.state 105 | end 106 | 107 | should "set sent at" do 108 | assert_not_nil @dso.sent_at 109 | end 110 | 111 | should "send order to supplier" do 112 | assert_equal @dso.supplier.email, ActionMailer::Base.deliveries.last.to.first 113 | assert_equal "#{Spree::Config[:site_name]} - Order ##{@dso.id}", ActionMailer::Base.deliveries.last.subject 114 | end 115 | 116 | context "and confirmed" do 117 | 118 | setup do 119 | @dso.confirm! 120 | end 121 | 122 | should "move to the 'confirmed' state" do 123 | assert_equal "confirmed", @dso.state 124 | end 125 | 126 | should "set confirmed at" do 127 | assert_not_nil @dso.confirmed_at 128 | end 129 | 130 | should "send confirmation to supplier" do 131 | assert_equal @dso.supplier.email, ActionMailer::Base.deliveries.last.to.first 132 | assert_equal "Confirmation - #{Spree::Config[:site_name]} - Order ##{@dso.id}", ActionMailer::Base.deliveries.last.subject 133 | end 134 | 135 | context "and shipped" do 136 | 137 | setup do 138 | @dso.update_attributes( 139 | :shipping_method => "UPS Ground", 140 | :confirmation_number => "935468423", 141 | :tracking_number => "1Z03294230492345234" 142 | ) 143 | @dso.ship! 144 | end 145 | 146 | should "move to the 'complete' state" do 147 | assert_equal "complete", @dso.state 148 | end 149 | 150 | should "set shipped at" do 151 | assert_not_nil @dso.shipped_at 152 | end 153 | 154 | should "send shipment email to supplier" do 155 | # the ship state sends two emails.. so we'll get the second to last here 156 | index = ActionMailer::Base.deliveries.length - 2 157 | assert_equal @dso.supplier.email, ActionMailer::Base.deliveries[index].to.first 158 | assert_equal "Shipped - #{Spree::Config[:site_name]} - Order ##{@dso.id}", ActionMailer::Base.deliveries[index].subject 159 | end 160 | 161 | should "send shipment email to customer" do 162 | assert_equal @dso.order.email, ActionMailer::Base.deliveries.last.to.first 163 | assert_equal "Shipped - #{Spree::Config[:site_name]} - Order ##{@dso.id}", ActionMailer::Base.deliveries.last.subject 164 | end 165 | 166 | end 167 | 168 | end 169 | 170 | end 171 | 172 | end 173 | 174 | end 175 | -------------------------------------------------------------------------------- /test/fixtures/countries.yml: -------------------------------------------------------------------------------- 1 | --- 2 | countries_039: 3 | name: Chad 4 | iso3: TCD 5 | iso: TD 6 | iso_name: CHAD 7 | id: "39" 8 | numcode: "148" 9 | countries_065: 10 | name: Faroe Islands 11 | iso3: FRO 12 | iso: FO 13 | iso_name: FAROE ISLANDS 14 | id: "65" 15 | numcode: "234" 16 | countries_092: 17 | name: India 18 | iso3: IND 19 | iso: IN 20 | iso_name: INDIA 21 | id: "92" 22 | numcode: "356" 23 | countries_146: 24 | name: Nicaragua 25 | iso3: NIC 26 | iso: NI 27 | iso_name: NICARAGUA 28 | id: "146" 29 | numcode: "558" 30 | countries_172: 31 | name: Saint Lucia 32 | iso3: LCA 33 | iso: LC 34 | iso_name: SAINT LUCIA 35 | id: "172" 36 | numcode: "662" 37 | countries_066: 38 | name: Fiji 39 | iso3: FJI 40 | iso: FJ 41 | iso_name: FIJI 42 | id: "66" 43 | numcode: "242" 44 | countries_093: 45 | name: Indonesia 46 | iso3: IDN 47 | iso: ID 48 | iso_name: INDONESIA 49 | id: "93" 50 | numcode: "360" 51 | countries_147: 52 | name: Niger 53 | iso3: NER 54 | iso: NE 55 | iso_name: NIGER 56 | id: "147" 57 | numcode: "562" 58 | countries_173: 59 | name: Saint Pierre and Miquelon 60 | iso3: SPM 61 | iso: PM 62 | iso_name: SAINT PIERRE AND MIQUELON 63 | id: "173" 64 | numcode: "666" 65 | countries_067: 66 | name: Finland 67 | iso3: FIN 68 | iso: FI 69 | iso_name: FINLAND 70 | id: "67" 71 | numcode: "246" 72 | countries_148: 73 | name: Nigeria 74 | iso3: NGA 75 | iso: NG 76 | iso_name: NIGERIA 77 | id: "148" 78 | numcode: "566" 79 | countries_174: 80 | name: Saint Vincent and the Grenadines 81 | iso3: VCT 82 | iso: VC 83 | iso_name: SAINT VINCENT AND THE GRENADINES 84 | id: "174" 85 | numcode: "670" 86 | countries_068: 87 | name: France 88 | iso3: FRA 89 | iso: FR 90 | iso_name: FRANCE 91 | id: "68" 92 | numcode: "250" 93 | countries_094: 94 | name: Iran, Islamic Republic of 95 | iso3: IRN 96 | iso: IR 97 | iso_name: IRAN, ISLAMIC REPUBLIC OF 98 | id: "94" 99 | numcode: "364" 100 | countries_149: 101 | name: Niue 102 | iso3: NIU 103 | iso: NU 104 | iso_name: NIUE 105 | id: "149" 106 | numcode: "570" 107 | countries_175: 108 | name: Samoa 109 | iso3: WSM 110 | iso: WS 111 | iso_name: SAMOA 112 | id: "175" 113 | numcode: "882" 114 | countries_069: 115 | name: French Guiana 116 | iso3: GUF 117 | iso: GF 118 | iso_name: FRENCH GUIANA 119 | id: "69" 120 | numcode: "254" 121 | countries_095: 122 | name: Iraq 123 | iso3: IRQ 124 | iso: IQ 125 | iso_name: IRAQ 126 | id: "95" 127 | numcode: "368" 128 | countries_176: 129 | name: San Marino 130 | iso3: SMR 131 | iso: SM 132 | iso_name: SAN MARINO 133 | id: "176" 134 | numcode: "674" 135 | countries_096: 136 | name: Ireland 137 | iso3: IRL 138 | iso: IE 139 | iso_name: IRELAND 140 | id: "96" 141 | numcode: "372" 142 | countries_177: 143 | name: Sao Tome and Principe 144 | iso3: STP 145 | iso: ST 146 | iso_name: SAO TOME AND PRINCIPE 147 | id: "177" 148 | numcode: "678" 149 | countries_097: 150 | name: Israel 151 | iso3: ISR 152 | iso: IL 153 | iso_name: ISRAEL 154 | id: "97" 155 | numcode: "376" 156 | countries_178: 157 | name: Saudi Arabia 158 | iso3: SAU 159 | iso: SA 160 | iso_name: SAUDI ARABIA 161 | id: "178" 162 | numcode: "682" 163 | countries_098: 164 | name: Italy 165 | iso3: ITA 166 | iso: IT 167 | iso_name: ITALY 168 | id: "98" 169 | numcode: "380" 170 | countries_179: 171 | name: Senegal 172 | iso3: SEN 173 | iso: SN 174 | iso_name: SENEGAL 175 | id: "179" 176 | numcode: "686" 177 | countries_099: 178 | name: Jamaica 179 | iso3: JAM 180 | iso: JM 181 | iso_name: JAMAICA 182 | id: "99" 183 | numcode: "388" 184 | countries_100: 185 | name: Japan 186 | iso3: JPN 187 | iso: JP 188 | iso_name: JAPAN 189 | id: "100" 190 | numcode: "392" 191 | countries_101: 192 | name: Jordan 193 | iso3: JOR 194 | iso: JO 195 | iso_name: JORDAN 196 | id: "101" 197 | numcode: "400" 198 | countries_020: 199 | name: Belgium 200 | iso3: BEL 201 | iso: BE 202 | iso_name: BELGIUM 203 | id: "20" 204 | numcode: "56" 205 | countries_021: 206 | name: Belize 207 | iso3: BLZ 208 | iso: BZ 209 | iso_name: BELIZE 210 | id: "21" 211 | numcode: "84" 212 | countries_102: 213 | name: Kazakhstan 214 | iso3: KAZ 215 | iso: KZ 216 | iso_name: KAZAKHSTAN 217 | id: "102" 218 | numcode: "398" 219 | countries_210: 220 | name: Uganda 221 | iso3: UGA 222 | iso: UG 223 | iso_name: UGANDA 224 | id: "210" 225 | numcode: "800" 226 | countries_022: 227 | name: Benin 228 | iso3: BEN 229 | iso: BJ 230 | iso_name: BENIN 231 | id: "22" 232 | numcode: "204" 233 | countries_103: 234 | name: Kenya 235 | iso3: KEN 236 | iso: KE 237 | iso_name: KENYA 238 | id: "103" 239 | numcode: "404" 240 | countries_211: 241 | name: Ukraine 242 | iso3: UKR 243 | iso: UA 244 | iso_name: UKRAINE 245 | id: "211" 246 | numcode: "804" 247 | countries_023: 248 | name: Bermuda 249 | iso3: BMU 250 | iso: BM 251 | iso_name: BERMUDA 252 | id: "23" 253 | numcode: "60" 254 | countries_104: 255 | name: Kiribati 256 | iso3: KIR 257 | iso: KI 258 | iso_name: KIRIBATI 259 | id: "104" 260 | numcode: "296" 261 | countries_130: 262 | name: Mexico 263 | iso3: MEX 264 | iso: MX 265 | iso_name: MEXICO 266 | id: "130" 267 | numcode: "484" 268 | countries_212: 269 | name: United Arab Emirates 270 | iso3: ARE 271 | iso: AE 272 | iso_name: UNITED ARAB EMIRATES 273 | id: "212" 274 | numcode: "784" 275 | countries_024: 276 | name: Bhutan 277 | iso3: BTN 278 | iso: BT 279 | iso_name: BHUTAN 280 | id: "24" 281 | numcode: "64" 282 | countries_050: 283 | name: Cuba 284 | iso3: CUB 285 | iso: CU 286 | iso_name: CUBA 287 | id: "50" 288 | numcode: "192" 289 | countries_105: 290 | name: North Korea 291 | iso3: PRK 292 | iso: KP 293 | iso_name: KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF 294 | id: "105" 295 | numcode: "408" 296 | countries_131: 297 | name: Micronesia, Federated States of 298 | iso3: FSM 299 | iso: FM 300 | iso_name: MICRONESIA, FEDERATED STATES OF 301 | id: "131" 302 | numcode: "583" 303 | countries_213: 304 | name: United Kingdom 305 | iso3: GBR 306 | iso: GB 307 | iso_name: UNITED KINGDOM 308 | id: "213" 309 | numcode: "826" 310 | countries_025: 311 | name: Bolivia 312 | iso3: BOL 313 | iso: BO 314 | iso_name: BOLIVIA 315 | id: "25" 316 | numcode: "68" 317 | countries_051: 318 | name: Cyprus 319 | iso3: CYP 320 | iso: CY 321 | iso_name: CYPRUS 322 | id: "51" 323 | numcode: "196" 324 | countries_106: 325 | name: South Korea 326 | iso3: KOR 327 | iso: KR 328 | iso_name: KOREA, REPUBLIC OF 329 | id: "106" 330 | numcode: "410" 331 | countries_132: 332 | name: Moldova, Republic of 333 | iso3: MDA 334 | iso: MD 335 | iso_name: MOLDOVA, REPUBLIC OF 336 | id: "132" 337 | numcode: "498" 338 | countries_214: 339 | name: United States 340 | iso3: USA 341 | iso: US 342 | iso_name: UNITED STATES 343 | id: "214" 344 | numcode: "840" 345 | countries_026: 346 | name: Bosnia and Herzegovina 347 | iso3: BIH 348 | iso: BA 349 | iso_name: BOSNIA AND HERZEGOVINA 350 | id: "26" 351 | numcode: "70" 352 | countries_052: 353 | name: Czech Republic 354 | iso3: CZE 355 | iso: CZ 356 | iso_name: CZECH REPUBLIC 357 | id: "52" 358 | numcode: "203" 359 | countries_107: 360 | name: Kuwait 361 | iso3: KWT 362 | iso: KW 363 | iso_name: KUWAIT 364 | id: "107" 365 | numcode: "414" 366 | countries_133: 367 | name: Monaco 368 | iso3: MCO 369 | iso: MC 370 | iso_name: MONACO 371 | id: "133" 372 | numcode: "492" 373 | countries_215: 374 | name: Uruguay 375 | iso3: URY 376 | iso: UY 377 | iso_name: URUGUAY 378 | id: "215" 379 | numcode: "858" 380 | countries_027: 381 | name: Botswana 382 | iso3: BWA 383 | iso: BW 384 | iso_name: BOTSWANA 385 | id: "27" 386 | numcode: "72" 387 | countries_053: 388 | name: Denmark 389 | iso3: DNK 390 | iso: DK 391 | iso_name: DENMARK 392 | id: "53" 393 | numcode: "208" 394 | countries_080: 395 | name: Guadeloupe 396 | iso3: GLP 397 | iso: GP 398 | iso_name: GUADELOUPE 399 | id: "80" 400 | numcode: "312" 401 | countries_108: 402 | name: Kyrgyzstan 403 | iso3: KGZ 404 | iso: KG 405 | iso_name: KYRGYZSTAN 406 | id: "108" 407 | numcode: "417" 408 | countries_134: 409 | name: Mongolia 410 | iso3: MNG 411 | iso: MN 412 | iso_name: MONGOLIA 413 | id: "134" 414 | numcode: "496" 415 | countries_160: 416 | name: Philippines 417 | iso3: PHL 418 | iso: PH 419 | iso_name: PHILIPPINES 420 | id: "160" 421 | numcode: "608" 422 | countries_028: 423 | name: Brazil 424 | iso3: BRA 425 | iso: BR 426 | iso_name: BRAZIL 427 | id: "28" 428 | numcode: "76" 429 | countries_054: 430 | name: Djibouti 431 | iso3: DJI 432 | iso: DJ 433 | iso_name: DJIBOUTI 434 | id: "54" 435 | numcode: "262" 436 | countries_081: 437 | name: Guam 438 | iso3: GUM 439 | iso: GU 440 | iso_name: GUAM 441 | id: "81" 442 | numcode: "316" 443 | countries_109: 444 | name: Lao People's Democratic Republic 445 | iso3: LAO 446 | iso: LA 447 | iso_name: LAO PEOPLE'S DEMOCRATIC REPUBLIC 448 | id: "109" 449 | numcode: "418" 450 | countries_135: 451 | name: Montserrat 452 | iso3: MSR 453 | iso: MS 454 | iso_name: MONTSERRAT 455 | id: "135" 456 | numcode: "500" 457 | countries_161: 458 | name: Pitcairn 459 | iso3: PCN 460 | iso: PN 461 | iso_name: PITCAIRN 462 | id: "161" 463 | numcode: "612" 464 | countries_216: 465 | name: Uzbekistan 466 | iso3: UZB 467 | iso: UZ 468 | iso_name: UZBEKISTAN 469 | id: "216" 470 | numcode: "860" 471 | countries_029: 472 | name: Brunei Darussalam 473 | iso3: BRN 474 | iso: BN 475 | iso_name: BRUNEI DARUSSALAM 476 | id: "29" 477 | numcode: "96" 478 | countries_055: 479 | name: Dominica 480 | iso3: DMA 481 | iso: DM 482 | iso_name: DOMINICA 483 | id: "55" 484 | numcode: "212" 485 | countries_082: 486 | name: Guatemala 487 | iso3: GTM 488 | iso: GT 489 | iso_name: GUATEMALA 490 | id: "82" 491 | numcode: "320" 492 | countries_136: 493 | name: Morocco 494 | iso3: MAR 495 | iso: MA 496 | iso_name: MOROCCO 497 | id: "136" 498 | numcode: "504" 499 | countries_162: 500 | name: Poland 501 | iso3: POL 502 | iso: PL 503 | iso_name: POLAND 504 | id: "162" 505 | numcode: "616" 506 | countries_217: 507 | name: Vanuatu 508 | iso3: VUT 509 | iso: VU 510 | iso_name: VANUATU 511 | id: "217" 512 | numcode: "548" 513 | countries_056: 514 | name: Dominican Republic 515 | iso3: DOM 516 | iso: DO 517 | iso_name: DOMINICAN REPUBLIC 518 | id: "56" 519 | numcode: "214" 520 | countries_137: 521 | name: Mozambique 522 | iso3: MOZ 523 | iso: MZ 524 | iso_name: MOZAMBIQUE 525 | id: "137" 526 | numcode: "508" 527 | countries_163: 528 | name: Portugal 529 | iso3: PRT 530 | iso: PT 531 | iso_name: PORTUGAL 532 | id: "163" 533 | numcode: "620" 534 | countries_190: 535 | name: Sudan 536 | iso3: SDN 537 | iso: SD 538 | iso_name: SUDAN 539 | id: "190" 540 | numcode: "736" 541 | countries_218: 542 | name: Venezuela 543 | iso3: VEN 544 | iso: VE 545 | iso_name: VENEZUELA 546 | id: "218" 547 | numcode: "862" 548 | countries_057: 549 | name: Ecuador 550 | iso3: ECU 551 | iso: EC 552 | iso_name: ECUADOR 553 | id: "57" 554 | numcode: "218" 555 | countries_083: 556 | name: Guinea 557 | iso3: GIN 558 | iso: GN 559 | iso_name: GUINEA 560 | id: "83" 561 | numcode: "324" 562 | countries_138: 563 | name: Myanmar 564 | iso3: MMR 565 | iso: MM 566 | iso_name: MYANMAR 567 | id: "138" 568 | numcode: "104" 569 | countries_164: 570 | name: Puerto Rico 571 | iso3: PRI 572 | iso: PR 573 | iso_name: PUERTO RICO 574 | id: "164" 575 | numcode: "630" 576 | countries_191: 577 | name: Suriname 578 | iso3: SUR 579 | iso: SR 580 | iso_name: SURINAME 581 | id: "191" 582 | numcode: "740" 583 | countries_219: 584 | name: Viet Nam 585 | iso3: VNM 586 | iso: VN 587 | iso_name: VIET NAM 588 | id: "219" 589 | numcode: "704" 590 | countries_058: 591 | name: Egypt 592 | iso3: EGY 593 | iso: EG 594 | iso_name: EGYPT 595 | id: "58" 596 | numcode: "818" 597 | countries_084: 598 | name: Guinea-Bissau 599 | iso3: GNB 600 | iso: GW 601 | iso_name: GUINEA-BISSAU 602 | id: "84" 603 | numcode: "624" 604 | countries_139: 605 | name: Namibia 606 | iso3: NAM 607 | iso: NA 608 | iso_name: NAMIBIA 609 | id: "139" 610 | numcode: "516" 611 | countries_165: 612 | name: Qatar 613 | iso3: QAT 614 | iso: QA 615 | iso_name: QATAR 616 | id: "165" 617 | numcode: "634" 618 | countries_192: 619 | name: Svalbard and Jan Mayen 620 | iso3: SJM 621 | iso: SJ 622 | iso_name: SVALBARD AND JAN MAYEN 623 | id: "192" 624 | numcode: "744" 625 | countries_059: 626 | name: El Salvador 627 | iso3: SLV 628 | iso: SV 629 | iso_name: EL SALVADOR 630 | id: "59" 631 | numcode: "222" 632 | countries_085: 633 | name: Guyana 634 | iso3: GUY 635 | iso: GY 636 | iso_name: GUYANA 637 | id: "85" 638 | numcode: "328" 639 | countries_166: 640 | name: Reunion 641 | iso3: REU 642 | iso: RE 643 | iso_name: REUNION 644 | id: "166" 645 | numcode: "638" 646 | countries_086: 647 | name: Haiti 648 | iso3: HTI 649 | iso: HT 650 | iso_name: HAITI 651 | id: "86" 652 | numcode: "332" 653 | countries_167: 654 | name: Romania 655 | iso3: ROM 656 | iso: RO 657 | iso_name: ROMANIA 658 | id: "167" 659 | numcode: "642" 660 | countries_193: 661 | name: Swaziland 662 | iso3: SWZ 663 | iso: SZ 664 | iso_name: SWAZILAND 665 | id: "193" 666 | numcode: "748" 667 | countries_087: 668 | name: Holy See (Vatican City State) 669 | iso3: VAT 670 | iso: VA 671 | iso_name: HOLY SEE (VATICAN CITY STATE) 672 | id: "87" 673 | numcode: "336" 674 | countries_168: 675 | name: Russian Federation 676 | iso3: RUS 677 | iso: RU 678 | iso_name: RUSSIAN FEDERATION 679 | id: "168" 680 | numcode: "643" 681 | countries_194: 682 | name: Sweden 683 | iso3: SWE 684 | iso: SE 685 | iso_name: SWEDEN 686 | id: "194" 687 | numcode: "752" 688 | countries_088: 689 | name: Honduras 690 | iso3: HND 691 | iso: HN 692 | iso_name: HONDURAS 693 | id: "88" 694 | numcode: "340" 695 | countries_169: 696 | name: Rwanda 697 | iso3: RWA 698 | iso: RW 699 | iso_name: RWANDA 700 | id: "169" 701 | numcode: "646" 702 | countries_195: 703 | name: Switzerland 704 | iso3: CHE 705 | iso: CH 706 | iso_name: SWITZERLAND 707 | id: "195" 708 | numcode: "756" 709 | countries_089: 710 | name: Hong Kong 711 | iso3: HKG 712 | iso: HK 713 | iso_name: HONG KONG 714 | id: "89" 715 | numcode: "344" 716 | countries_196: 717 | name: Syrian Arab Republic 718 | iso3: SYR 719 | iso: SY 720 | iso_name: SYRIAN ARAB REPUBLIC 721 | id: "196" 722 | numcode: "760" 723 | countries_197: 724 | name: Taiwan 725 | iso3: TWN 726 | iso: TW 727 | iso_name: TAIWAN, PROVINCE OF CHINA 728 | id: "197" 729 | numcode: "158" 730 | countries_198: 731 | name: Tajikistan 732 | iso3: TJK 733 | iso: TJ 734 | iso_name: TAJIKISTAN 735 | id: "198" 736 | numcode: "762" 737 | countries_199: 738 | name: Tanzania, United Republic of 739 | iso3: TZA 740 | iso: TZ 741 | iso_name: TANZANIA, UNITED REPUBLIC OF 742 | id: "199" 743 | numcode: "834" 744 | countries_010: 745 | name: Armenia 746 | iso3: ARM 747 | iso: AM 748 | iso_name: ARMENIA 749 | id: "10" 750 | numcode: "51" 751 | countries_011: 752 | name: Aruba 753 | iso3: ABW 754 | iso: AW 755 | iso_name: ARUBA 756 | id: "11" 757 | numcode: "533" 758 | countries_012: 759 | name: Australia 760 | iso3: AUS 761 | iso: AU 762 | iso_name: AUSTRALIA 763 | id: "12" 764 | numcode: "36" 765 | countries_200: 766 | name: Thailand 767 | iso3: THA 768 | iso: TH 769 | iso_name: THAILAND 770 | id: "200" 771 | numcode: "764" 772 | countries_013: 773 | name: Austria 774 | iso3: AUT 775 | iso: AT 776 | iso_name: AUSTRIA 777 | id: "13" 778 | numcode: "40" 779 | countries_120: 780 | name: Madagascar 781 | iso3: MDG 782 | iso: MG 783 | iso_name: MADAGASCAR 784 | id: "120" 785 | numcode: "450" 786 | countries_201: 787 | name: Togo 788 | iso3: TGO 789 | iso: TG 790 | iso_name: TOGO 791 | id: "201" 792 | numcode: "768" 793 | countries_014: 794 | name: Azerbaijan 795 | iso3: AZE 796 | iso: AZ 797 | iso_name: AZERBAIJAN 798 | id: "14" 799 | numcode: "31" 800 | countries_040: 801 | name: Chile 802 | iso3: CHL 803 | iso: CL 804 | iso_name: CHILE 805 | id: "40" 806 | numcode: "152" 807 | countries_121: 808 | name: Malawi 809 | iso3: MWI 810 | iso: MW 811 | iso_name: MALAWI 812 | id: "121" 813 | numcode: "454" 814 | countries_202: 815 | name: Tokelau 816 | iso3: TKL 817 | iso: TK 818 | iso_name: TOKELAU 819 | id: "202" 820 | numcode: "772" 821 | countries_015: 822 | name: Bahamas 823 | iso3: BHS 824 | iso: BS 825 | iso_name: BAHAMAS 826 | id: "15" 827 | numcode: "44" 828 | countries_041: 829 | name: China 830 | iso3: CHN 831 | iso: CN 832 | iso_name: CHINA 833 | id: "41" 834 | numcode: "156" 835 | countries_122: 836 | name: Malaysia 837 | iso3: MYS 838 | iso: MY 839 | iso_name: MALAYSIA 840 | id: "122" 841 | numcode: "458" 842 | countries_203: 843 | name: Tonga 844 | iso3: TON 845 | iso: TO 846 | iso_name: TONGA 847 | id: "203" 848 | numcode: "776" 849 | countries_016: 850 | name: Bahrain 851 | iso3: BHR 852 | iso: BH 853 | iso_name: BAHRAIN 854 | id: "16" 855 | numcode: "48" 856 | countries_042: 857 | name: Colombia 858 | iso3: COL 859 | iso: CO 860 | iso_name: COLOMBIA 861 | id: "42" 862 | numcode: "170" 863 | countries_123: 864 | name: Maldives 865 | iso3: MDV 866 | iso: MV 867 | iso_name: MALDIVES 868 | id: "123" 869 | numcode: "462" 870 | countries_204: 871 | name: Trinidad and Tobago 872 | iso3: TTO 873 | iso: TT 874 | iso_name: TRINIDAD AND TOBAGO 875 | id: "204" 876 | numcode: "780" 877 | countries_017: 878 | name: Bangladesh 879 | iso3: BGD 880 | iso: BD 881 | iso_name: BANGLADESH 882 | id: "17" 883 | numcode: "50" 884 | countries_043: 885 | name: Comoros 886 | iso3: COM 887 | iso: KM 888 | iso_name: COMOROS 889 | id: "43" 890 | numcode: "174" 891 | countries_070: 892 | name: French Polynesia 893 | iso3: PYF 894 | iso: PF 895 | iso_name: FRENCH POLYNESIA 896 | id: "70" 897 | numcode: "258" 898 | countries_124: 899 | name: Mali 900 | iso3: MLI 901 | iso: ML 902 | iso_name: MALI 903 | id: "124" 904 | numcode: "466" 905 | countries_150: 906 | name: Norfolk Island 907 | iso3: NFK 908 | iso: NF 909 | iso_name: NORFOLK ISLAND 910 | id: "150" 911 | numcode: "574" 912 | countries_205: 913 | name: Tunisia 914 | iso3: TUN 915 | iso: TN 916 | iso_name: TUNISIA 917 | id: "205" 918 | numcode: "788" 919 | countries_018: 920 | name: Barbados 921 | iso3: BRB 922 | iso: BB 923 | iso_name: BARBADOS 924 | id: "18" 925 | numcode: "52" 926 | countries_044: 927 | name: Congo 928 | iso3: COG 929 | iso: CG 930 | iso_name: CONGO 931 | id: "44" 932 | numcode: "178" 933 | countries_071: 934 | name: Gabon 935 | iso3: GAB 936 | iso: GA 937 | iso_name: GABON 938 | id: "71" 939 | numcode: "266" 940 | countries_125: 941 | name: Malta 942 | iso3: MLT 943 | iso: MT 944 | iso_name: MALTA 945 | id: "125" 946 | numcode: "470" 947 | countries_151: 948 | name: Northern Mariana Islands 949 | iso3: MNP 950 | iso: MP 951 | iso_name: NORTHERN MARIANA ISLANDS 952 | id: "151" 953 | numcode: "580" 954 | countries_206: 955 | name: Turkey 956 | iso3: TUR 957 | iso: TR 958 | iso_name: TURKEY 959 | id: "206" 960 | numcode: "792" 961 | countries_045: 962 | name: Congo, the Democratic Republic of the 963 | iso3: COD 964 | iso: CD 965 | iso_name: CONGO, THE DEMOCRATIC REPUBLIC OF THE 966 | id: "45" 967 | numcode: "180" 968 | countries_126: 969 | name: Marshall Islands 970 | iso3: MHL 971 | iso: MH 972 | iso_name: MARSHALL ISLANDS 973 | id: "126" 974 | numcode: "584" 975 | countries_152: 976 | name: Norway 977 | iso3: NOR 978 | iso: "NO" 979 | iso_name: NORWAY 980 | id: "152" 981 | numcode: "578" 982 | countries_207: 983 | name: Turkmenistan 984 | iso3: TKM 985 | iso: TM 986 | iso_name: TURKMENISTAN 987 | id: "207" 988 | numcode: "795" 989 | countries_019: 990 | name: Belarus 991 | iso3: BLR 992 | iso: BY 993 | iso_name: BELARUS 994 | id: "19" 995 | numcode: "112" 996 | countries_046: 997 | name: Cook Islands 998 | iso3: COK 999 | iso: CK 1000 | iso_name: COOK ISLANDS 1001 | id: "46" 1002 | numcode: "184" 1003 | countries_072: 1004 | name: Gambia 1005 | iso3: GMB 1006 | iso: GM 1007 | iso_name: GAMBIA 1008 | id: "72" 1009 | numcode: "270" 1010 | countries_127: 1011 | name: Martinique 1012 | iso3: MTQ 1013 | iso: MQ 1014 | iso_name: MARTINIQUE 1015 | id: "127" 1016 | numcode: "474" 1017 | countries_153: 1018 | name: Oman 1019 | iso3: OMN 1020 | iso: OM 1021 | iso_name: OMAN 1022 | id: "153" 1023 | numcode: "512" 1024 | countries_180: 1025 | name: Seychelles 1026 | iso3: SYC 1027 | iso: SC 1028 | iso_name: SEYCHELLES 1029 | id: "180" 1030 | numcode: "690" 1031 | countries_208: 1032 | name: Turks and Caicos Islands 1033 | iso3: TCA 1034 | iso: TC 1035 | iso_name: TURKS AND CAICOS ISLANDS 1036 | id: "208" 1037 | numcode: "796" 1038 | countries_073: 1039 | name: Georgia 1040 | iso3: GEO 1041 | iso: GE 1042 | iso_name: GEORGIA 1043 | id: "73" 1044 | numcode: "268" 1045 | countries_128: 1046 | name: Mauritania 1047 | iso3: MRT 1048 | iso: MR 1049 | iso_name: MAURITANIA 1050 | id: "128" 1051 | numcode: "478" 1052 | countries_154: 1053 | name: Pakistan 1054 | iso3: PAK 1055 | iso: PK 1056 | iso_name: PAKISTAN 1057 | id: "154" 1058 | numcode: "586" 1059 | countries_181: 1060 | name: Sierra Leone 1061 | iso3: SLE 1062 | iso: SL 1063 | iso_name: SIERRA LEONE 1064 | id: "181" 1065 | numcode: "694" 1066 | countries_209: 1067 | name: Tuvalu 1068 | iso3: TUV 1069 | iso: TV 1070 | iso_name: TUVALU 1071 | id: "209" 1072 | numcode: "798" 1073 | countries_047: 1074 | name: Costa Rica 1075 | iso3: CRI 1076 | iso: CR 1077 | iso_name: COSTA RICA 1078 | id: "47" 1079 | numcode: "188" 1080 | countries_074: 1081 | name: Germany 1082 | iso3: DEU 1083 | iso: DE 1084 | iso_name: GERMANY 1085 | id: "74" 1086 | numcode: "276" 1087 | countries_129: 1088 | name: Mauritius 1089 | iso3: MUS 1090 | iso: MU 1091 | iso_name: MAURITIUS 1092 | id: "129" 1093 | numcode: "480" 1094 | countries_155: 1095 | name: Palau 1096 | iso3: PLW 1097 | iso: PW 1098 | iso_name: PALAU 1099 | id: "155" 1100 | numcode: "585" 1101 | countries_048: 1102 | name: Cote D'Ivoire 1103 | iso3: CIV 1104 | iso: CI 1105 | iso_name: COTE D'IVOIRE 1106 | id: "48" 1107 | numcode: "384" 1108 | countries_156: 1109 | name: Panama 1110 | iso3: PAN 1111 | iso: PA 1112 | iso_name: PANAMA 1113 | id: "156" 1114 | numcode: "591" 1115 | countries_182: 1116 | name: Singapore 1117 | iso3: SGP 1118 | iso: SG 1119 | iso_name: SINGAPORE 1120 | id: "182" 1121 | numcode: "702" 1122 | countries_049: 1123 | name: Croatia 1124 | iso3: HRV 1125 | iso: HR 1126 | iso_name: CROATIA 1127 | id: "49" 1128 | numcode: "191" 1129 | countries_075: 1130 | name: Ghana 1131 | iso3: GHA 1132 | iso: GH 1133 | iso_name: GHANA 1134 | id: "75" 1135 | numcode: "288" 1136 | countries_157: 1137 | name: Papua New Guinea 1138 | iso3: PNG 1139 | iso: PG 1140 | iso_name: PAPUA NEW GUINEA 1141 | id: "157" 1142 | numcode: "598" 1143 | countries_183: 1144 | name: Slovakia 1145 | iso3: SVK 1146 | iso: SK 1147 | iso_name: SLOVAKIA 1148 | id: "183" 1149 | numcode: "703" 1150 | countries_076: 1151 | name: Gibraltar 1152 | iso3: GIB 1153 | iso: GI 1154 | iso_name: GIBRALTAR 1155 | id: "76" 1156 | numcode: "292" 1157 | countries_158: 1158 | name: Paraguay 1159 | iso3: PRY 1160 | iso: PY 1161 | iso_name: PARAGUAY 1162 | id: "158" 1163 | numcode: "600" 1164 | countries_184: 1165 | name: Slovenia 1166 | iso3: SVN 1167 | iso: SI 1168 | iso_name: SLOVENIA 1169 | id: "184" 1170 | numcode: "705" 1171 | countries_077: 1172 | name: Greece 1173 | iso3: GRC 1174 | iso: GR 1175 | iso_name: GREECE 1176 | id: "77" 1177 | numcode: "300" 1178 | countries_159: 1179 | name: Peru 1180 | iso3: PER 1181 | iso: PE 1182 | iso_name: PERU 1183 | id: "159" 1184 | numcode: "604" 1185 | countries_185: 1186 | name: Solomon Islands 1187 | iso3: SLB 1188 | iso: SB 1189 | iso_name: SOLOMON ISLANDS 1190 | id: "185" 1191 | numcode: "90" 1192 | countries_078: 1193 | name: Greenland 1194 | iso3: GRL 1195 | iso: GL 1196 | iso_name: GREENLAND 1197 | id: "78" 1198 | numcode: "304" 1199 | countries_186: 1200 | name: Somalia 1201 | iso3: SOM 1202 | iso: SO 1203 | iso_name: SOMALIA 1204 | id: "186" 1205 | numcode: "706" 1206 | countries_079: 1207 | name: Grenada 1208 | iso3: GRD 1209 | iso: GD 1210 | iso_name: GRENADA 1211 | id: "79" 1212 | numcode: "308" 1213 | countries_187: 1214 | name: South Africa 1215 | iso3: ZAF 1216 | iso: ZA 1217 | iso_name: SOUTH AFRICA 1218 | id: "187" 1219 | numcode: "710" 1220 | countries_188: 1221 | name: Spain 1222 | iso3: ESP 1223 | iso: ES 1224 | iso_name: SPAIN 1225 | id: "188" 1226 | numcode: "724" 1227 | countries_189: 1228 | name: Sri Lanka 1229 | iso3: LKA 1230 | iso: LK 1231 | iso_name: SRI LANKA 1232 | id: "189" 1233 | numcode: "144" 1234 | countries_001: 1235 | name: Afghanistan 1236 | iso3: AFG 1237 | iso: AF 1238 | iso_name: AFGHANISTAN 1239 | id: "1" 1240 | numcode: "4" 1241 | countries_002: 1242 | name: Albania 1243 | iso3: ALB 1244 | iso: AL 1245 | iso_name: ALBANIA 1246 | id: "2" 1247 | numcode: "8" 1248 | countries_003: 1249 | name: Algeria 1250 | iso3: DZA 1251 | iso: DZ 1252 | iso_name: ALGERIA 1253 | id: "3" 1254 | numcode: "12" 1255 | countries_110: 1256 | name: Latvia 1257 | iso3: LVA 1258 | iso: LV 1259 | iso_name: LATVIA 1260 | id: "110" 1261 | numcode: "428" 1262 | countries_004: 1263 | name: American Samoa 1264 | iso3: ASM 1265 | iso: AS 1266 | iso_name: AMERICAN SAMOA 1267 | id: "4" 1268 | numcode: "16" 1269 | countries_030: 1270 | name: Bulgaria 1271 | iso3: BGR 1272 | iso: BG 1273 | iso_name: BULGARIA 1274 | id: "30" 1275 | numcode: "100" 1276 | countries_111: 1277 | name: Lebanon 1278 | iso3: LBN 1279 | iso: LB 1280 | iso_name: LEBANON 1281 | id: "111" 1282 | numcode: "422" 1283 | countries_005: 1284 | name: Andorra 1285 | iso3: AND 1286 | iso: AD 1287 | iso_name: ANDORRA 1288 | id: "5" 1289 | numcode: "20" 1290 | countries_031: 1291 | name: Burkina Faso 1292 | iso3: BFA 1293 | iso: BF 1294 | iso_name: BURKINA FASO 1295 | id: "31" 1296 | numcode: "854" 1297 | countries_112: 1298 | name: Lesotho 1299 | iso3: LSO 1300 | iso: LS 1301 | iso_name: LESOTHO 1302 | id: "112" 1303 | numcode: "426" 1304 | countries_006: 1305 | name: Angola 1306 | iso3: AGO 1307 | iso: AO 1308 | iso_name: ANGOLA 1309 | id: "6" 1310 | numcode: "24" 1311 | countries_032: 1312 | name: Burundi 1313 | iso3: BDI 1314 | iso: BI 1315 | iso_name: BURUNDI 1316 | id: "32" 1317 | numcode: "108" 1318 | countries_113: 1319 | name: Liberia 1320 | iso3: LBR 1321 | iso: LR 1322 | iso_name: LIBERIA 1323 | id: "113" 1324 | numcode: "430" 1325 | countries_220: 1326 | name: Virgin Islands, British 1327 | iso3: VGB 1328 | iso: VG 1329 | iso_name: VIRGIN ISLANDS, BRITISH 1330 | id: "220" 1331 | numcode: "92" 1332 | countries_007: 1333 | name: Anguilla 1334 | iso3: AIA 1335 | iso: AI 1336 | iso_name: ANGUILLA 1337 | id: "7" 1338 | numcode: "660" 1339 | countries_033: 1340 | name: Cambodia 1341 | iso3: KHM 1342 | iso: KH 1343 | iso_name: CAMBODIA 1344 | id: "33" 1345 | numcode: "116" 1346 | countries_060: 1347 | name: Equatorial Guinea 1348 | iso3: GNQ 1349 | iso: GQ 1350 | iso_name: EQUATORIAL GUINEA 1351 | id: "60" 1352 | numcode: "226" 1353 | countries_114: 1354 | name: Libyan Arab Jamahiriya 1355 | iso3: LBY 1356 | iso: LY 1357 | iso_name: LIBYAN ARAB JAMAHIRIYA 1358 | id: "114" 1359 | numcode: "434" 1360 | countries_140: 1361 | name: Nauru 1362 | iso3: NRU 1363 | iso: NR 1364 | iso_name: NAURU 1365 | id: "140" 1366 | numcode: "520" 1367 | countries_221: 1368 | name: Virgin Islands, U.S. 1369 | iso3: VIR 1370 | iso: VI 1371 | iso_name: VIRGIN ISLANDS, U.S. 1372 | id: "221" 1373 | numcode: "850" 1374 | countries_008: 1375 | name: Antigua and Barbuda 1376 | iso3: ATG 1377 | iso: AG 1378 | iso_name: ANTIGUA AND BARBUDA 1379 | id: "8" 1380 | numcode: "28" 1381 | countries_034: 1382 | name: Cameroon 1383 | iso3: CMR 1384 | iso: CM 1385 | iso_name: CAMEROON 1386 | id: "34" 1387 | numcode: "120" 1388 | countries_115: 1389 | name: Liechtenstein 1390 | iso3: LIE 1391 | iso: LI 1392 | iso_name: LIECHTENSTEIN 1393 | id: "115" 1394 | numcode: "438" 1395 | countries_141: 1396 | name: Nepal 1397 | iso3: NPL 1398 | iso: NP 1399 | iso_name: NEPAL 1400 | id: "141" 1401 | numcode: "524" 1402 | countries_222: 1403 | name: Wallis and Futuna 1404 | iso3: WLF 1405 | iso: WF 1406 | iso_name: WALLIS AND FUTUNA 1407 | id: "222" 1408 | numcode: "876" 1409 | countries_223: 1410 | name: Western Sahara 1411 | iso3: ESH 1412 | iso: EH 1413 | iso_name: WESTERN SAHARA 1414 | id: "223" 1415 | numcode: "732" 1416 | countries_009: 1417 | name: Argentina 1418 | iso3: ARG 1419 | iso: AR 1420 | iso_name: ARGENTINA 1421 | id: "9" 1422 | numcode: "32" 1423 | countries_035: 1424 | name: Canada 1425 | iso3: CAN 1426 | iso: CA 1427 | iso_name: CANADA 1428 | id: "35" 1429 | numcode: "124" 1430 | countries_061: 1431 | name: Eritrea 1432 | iso3: ERI 1433 | iso: ER 1434 | iso_name: ERITREA 1435 | id: "61" 1436 | numcode: "232" 1437 | countries_116: 1438 | name: Lithuania 1439 | iso3: LTU 1440 | iso: LT 1441 | iso_name: LITHUANIA 1442 | id: "116" 1443 | numcode: "440" 1444 | countries_142: 1445 | name: Netherlands 1446 | iso3: NLD 1447 | iso: NL 1448 | iso_name: NETHERLANDS 1449 | id: "142" 1450 | numcode: "528" 1451 | countries_224: 1452 | name: Yemen 1453 | iso3: YEM 1454 | iso: YE 1455 | iso_name: YEMEN 1456 | id: "224" 1457 | numcode: "887" 1458 | countries_036: 1459 | name: Cape Verde 1460 | iso3: CPV 1461 | iso: CV 1462 | iso_name: CAPE VERDE 1463 | id: "36" 1464 | numcode: "132" 1465 | countries_062: 1466 | name: Estonia 1467 | iso3: EST 1468 | iso: EE 1469 | iso_name: ESTONIA 1470 | id: "62" 1471 | numcode: "233" 1472 | countries_117: 1473 | name: Luxembourg 1474 | iso3: LUX 1475 | iso: LU 1476 | iso_name: LUXEMBOURG 1477 | id: "117" 1478 | numcode: "442" 1479 | countries_143: 1480 | name: Netherlands Antilles 1481 | iso3: ANT 1482 | iso: AN 1483 | iso_name: NETHERLANDS ANTILLES 1484 | id: "143" 1485 | numcode: "530" 1486 | countries_170: 1487 | name: Saint Helena 1488 | iso3: SHN 1489 | iso: SH 1490 | iso_name: SAINT HELENA 1491 | id: "170" 1492 | numcode: "654" 1493 | countries_225: 1494 | name: Zambia 1495 | iso3: ZMB 1496 | iso: ZM 1497 | iso_name: ZAMBIA 1498 | id: "225" 1499 | numcode: "894" 1500 | countries_037: 1501 | name: Cayman Islands 1502 | iso3: CYM 1503 | iso: KY 1504 | iso_name: CAYMAN ISLANDS 1505 | id: "37" 1506 | numcode: "136" 1507 | countries_063: 1508 | name: Ethiopia 1509 | iso3: ETH 1510 | iso: ET 1511 | iso_name: ETHIOPIA 1512 | id: "63" 1513 | numcode: "231" 1514 | countries_090: 1515 | name: Hungary 1516 | iso3: HUN 1517 | iso: HU 1518 | iso_name: HUNGARY 1519 | id: "90" 1520 | numcode: "348" 1521 | countries_118: 1522 | name: Macao 1523 | iso3: MAC 1524 | iso: MO 1525 | iso_name: MACAO 1526 | id: "118" 1527 | numcode: "446" 1528 | countries_144: 1529 | name: New Caledonia 1530 | iso3: NCL 1531 | iso: NC 1532 | iso_name: NEW CALEDONIA 1533 | id: "144" 1534 | numcode: "540" 1535 | countries_226: 1536 | name: Zimbabwe 1537 | iso3: ZWE 1538 | iso: ZW 1539 | iso_name: ZIMBABWE 1540 | id: "226" 1541 | numcode: "716" 1542 | countries_038: 1543 | name: Central African Republic 1544 | iso3: CAF 1545 | iso: CF 1546 | iso_name: CENTRAL AFRICAN REPUBLIC 1547 | id: "38" 1548 | numcode: "140" 1549 | countries_064: 1550 | name: Falkland Islands (Malvinas) 1551 | iso3: FLK 1552 | iso: FK 1553 | iso_name: FALKLAND ISLANDS (MALVINAS) 1554 | id: "64" 1555 | numcode: "238" 1556 | countries_091: 1557 | name: Iceland 1558 | iso3: ISL 1559 | iso: IS 1560 | iso_name: ICELAND 1561 | id: "91" 1562 | numcode: "352" 1563 | countries_119: 1564 | name: Macedonia 1565 | iso3: MKD 1566 | iso: MK 1567 | iso_name: MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF 1568 | id: "119" 1569 | numcode: "807" 1570 | countries_145: 1571 | name: New Zealand 1572 | iso3: NZL 1573 | iso: NZ 1574 | iso_name: NEW ZEALAND 1575 | id: "145" 1576 | numcode: "554" 1577 | countries_171: 1578 | name: Saint Kitts and Nevis 1579 | iso3: KNA 1580 | iso: KN 1581 | iso_name: SAINT KITTS AND NEVIS 1582 | id: "171" 1583 | numcode: "659" 1584 | --------------------------------------------------------------------------------