├── 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 | SKU
4 | Product
5 | Quantity
6 | Price
7 | Subtotal
8 |
9 | <% if dso.line_items.empty? %>
10 |
11 | This order has no line items.
12 |
13 | <% else %>
14 | <% dso.line_items.each do |li| %>
15 |
16 | <%= li.sku %>
17 | <%= li.name %>
18 | <%= li.quantity %>
19 | <%= li.price %>
20 | <%= li.subtotal %>
21 |
22 | <% end %>
23 | <% end %>
24 |
25 |
26 | Total:
27 | <%= number_to_currency dso.total %>
28 |
29 |
30 |
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 | <%= t("order_number") %>
8 | <%= t("total") %>
9 | <%= t("sent_at") %>
10 | <%= t("confirmed_at") %>
11 | <%= t("shipped_at") %>
12 | <%= t("status") %>
13 |
14 |
15 |
16 | <% @user.supplier.orders.each do |order| %>
17 |
18 | <%= link_to order.id, drop_ship_order_url(order) %>
19 | <%= number_to_currency order.total %>
20 | <%= order.sent_at.to_date unless order.sent_at.nil? %>
21 | <%= order.confirmed_at.to_date unless order.confirmed_at.nil? %>
22 | <%= order.shipped_at.to_date unless order.shipped_at.nil? %>
23 | <%= order.state %>
24 |
25 | <% end %>
26 |
27 |
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 |
10 | Name: <%= @supplier.name %>
11 | Email: <%= mail_to @supplier.email %>
12 | Phone: <%= @supplier.phone %>
13 |
14 | Address:
15 | <%= @supplier.address.address1 %>
16 | <% unless @supplier.address.address2.blank? %><%= @supplier.address.address2 %> <% end %>
17 | <%= @supplier.address.city %>, <%= @supplier.address.state.name %> <%= @supplier.address.country.name %>
18 | <%= @supplier.address.zipcode %>
19 |
20 | Contact: <%= @supplier.contact %>
21 | Contact Email: <%= @supplier.contact_email %>
22 | Contact Phone: <%= @supplier.contact_phone %>
23 |
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 | <%= sort_link @search, :id, t('order.id') %>
10 | <%= sort_link @search, :supplier_name, t('order.supplier.name') %>
11 | <%= sort_link @search, :order_number, t('order.number') %>
12 | <%= sort_link @search, :total, t('order.total') %>
13 | <%= sort_link @search, :sent_at, t('order.sent_at') %>
14 | <%= sort_link @search, :confirmed_at, t('order.confirmed_at') %>
15 | <%= sort_link @search, :shipped_at, t('order.shipped_at') %>
16 | <%= sort_link @search, :status, t('order.status') %>
17 |
18 |
19 |
20 | <%- @collection.each do |dso|%>
21 |
22 | <%= link_to "##{dso.id}", admin_drop_ship_order_url(dso) %>
23 | <%= link_to dso.supplier.name, admin_supplier_url(dso.supplier) %>
24 | <%= link_to dso.order.number, admin_order_url(dso.order) %>
25 | <%= number_to_currency dso.total %>
26 | <%= dso.created_at.to_s(:long) %>
27 | <%= dso.confirmed_at.to_s(:long) if dso.confirmed_at.present? %>
28 | <%= dso.shipped_at.to_s(:long) if dso.shipped_at.present? %>
29 | <%= dso.state %>
30 |
31 | <% end %>
32 |
33 |
34 |
35 | <%= will_paginate @collection, :previous_label => "« #{t('previous')}", :next_label => "#{t('next')} »" %>
36 |
--------------------------------------------------------------------------------
/app/views/admin/suppliers/index.html.erb:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | Listing Suppliers
12 |
13 |
14 |
15 |
16 |
17 | <%= sort_link @search, :name, t('supplier.name') %>
18 | <%= sort_link @search, :phone, t('supplier.phone') %>
19 | <%= sort_link @search, :email, t('supplier.email') %>
20 | <%= t('action') %>
21 |
22 |
23 |
24 | <%- @collection.each do |supplier|%>
25 |
26 | <%= link_to supplier.name, object_url(supplier) %>
27 | <%= supplier.phone %>
28 | <%= mail_to supplier.email %>
29 |
30 | <%= link_to_edit supplier %>
31 | <%= link_to_delete supplier %>
32 |
33 |
34 | <% end %>
35 |
36 |
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 | <%= t 'supplier.name' %>
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 |
--------------------------------------------------------------------------------