kylekthompson-shoppe 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +55 -0
- data/Rakefile +36 -0
- data/app/assets/images/shoppe/chosen-sprite.png +0 -0
- data/app/assets/images/shoppe/chosen-sprite@2x.png +0 -0
- data/app/assets/images/shoppe/document.svg +1 -0
- data/app/assets/images/shoppe/icons/bag.svg +1 -0
- data/app/assets/images/shoppe/icons/balance.svg +1 -0
- data/app/assets/images/shoppe/icons/box.svg +1 -0
- data/app/assets/images/shoppe/icons/building.svg +1 -0
- data/app/assets/images/shoppe/icons/chart.svg +1 -0
- data/app/assets/images/shoppe/icons/chat.svg +1 -0
- data/app/assets/images/shoppe/icons/checkbox.svg +1 -0
- data/app/assets/images/shoppe/icons/checkbox2.svg +1 -0
- data/app/assets/images/shoppe/icons/cloud.svg +1 -0
- data/app/assets/images/shoppe/icons/cone.svg +1 -0
- data/app/assets/images/shoppe/icons/credit_card.svg +1 -0
- data/app/assets/images/shoppe/icons/currency.svg +1 -0
- data/app/assets/images/shoppe/icons/edit.svg +14 -0
- data/app/assets/images/shoppe/icons/flowchart.svg +1 -0
- data/app/assets/images/shoppe/icons/gift.svg +1 -0
- data/app/assets/images/shoppe/icons/globe.svg +1 -0
- data/app/assets/images/shoppe/icons/id.svg +1 -0
- data/app/assets/images/shoppe/icons/id2.svg +1 -0
- data/app/assets/images/shoppe/icons/locked.svg +1 -0
- data/app/assets/images/shoppe/icons/report.svg +1 -0
- data/app/assets/images/shoppe/icons/search.svg +1 -0
- data/app/assets/images/shoppe/icons/support.svg +1 -0
- data/app/assets/images/shoppe/icons/tags.svg +1 -0
- data/app/assets/images/shoppe/icons/toolbox.svg +1 -0
- data/app/assets/images/shoppe/icons/unlocked.svg +1 -0
- data/app/assets/images/shoppe/icons/wallet.svg +1 -0
- data/app/assets/images/shoppe/logo.svg +47 -0
- data/app/assets/images/shoppe/move.svg +1 -0
- data/app/assets/images/shoppe/shoppe.svg +25 -0
- data/app/assets/images/shoppe/square.svg +9 -0
- data/app/assets/images/shoppe/statuses/accepted.svg +14 -0
- data/app/assets/images/shoppe/statuses/paid.svg +16 -0
- data/app/assets/images/shoppe/statuses/received.svg +15 -0
- data/app/assets/images/shoppe/statuses/rejected.svg +14 -0
- data/app/assets/images/shoppe/statuses/shipped.svg +14 -0
- data/app/assets/images/shoppe/table-tear-off.png +0 -0
- data/app/assets/javascripts/shoppe/application.coffee +123 -0
- data/app/assets/javascripts/shoppe/chosen.jquery.js +1166 -0
- data/app/assets/javascripts/shoppe/jquery_ui.js +6 -0
- data/app/assets/javascripts/shoppe/mousetrap.js +9 -0
- data/app/assets/javascripts/shoppe/order_form.coffee +47 -0
- data/app/assets/stylesheets/shoppe/application.scss +601 -0
- data/app/assets/stylesheets/shoppe/chosen.scss +424 -0
- data/app/assets/stylesheets/shoppe/dialog.scss +25 -0
- data/app/assets/stylesheets/shoppe/elements.scss +79 -0
- data/app/assets/stylesheets/shoppe/printable.scss +67 -0
- data/app/assets/stylesheets/shoppe/reset.scss +93 -0
- data/app/assets/stylesheets/shoppe/sub.scss +106 -0
- data/app/assets/stylesheets/shoppe/variables.scss +1 -0
- data/app/controllers/shoppe/addresses_controller.rb +47 -0
- data/app/controllers/shoppe/application_controller.rb +46 -0
- data/app/controllers/shoppe/attachments_controller.rb +14 -0
- data/app/controllers/shoppe/countries_controller.rb +47 -0
- data/app/controllers/shoppe/customers_controller.rb +55 -0
- data/app/controllers/shoppe/dashboard_controller.rb +9 -0
- data/app/controllers/shoppe/delivery_service_prices_controller.rb +45 -0
- data/app/controllers/shoppe/delivery_services_controller.rb +47 -0
- data/app/controllers/shoppe/orders_controller.rb +119 -0
- data/app/controllers/shoppe/payments_controller.rb +33 -0
- data/app/controllers/shoppe/product_categories_controller.rb +47 -0
- data/app/controllers/shoppe/product_category_localisations_controller.rb +58 -0
- data/app/controllers/shoppe/product_localisations_controller.rb +58 -0
- data/app/controllers/shoppe/products_controller.rb +58 -0
- data/app/controllers/shoppe/sessions_controller.rb +34 -0
- data/app/controllers/shoppe/settings_controller.rb +16 -0
- data/app/controllers/shoppe/stock_level_adjustments_controller.rb +40 -0
- data/app/controllers/shoppe/tax_rates_controller.rb +49 -0
- data/app/controllers/shoppe/users_controller.rb +53 -0
- data/app/controllers/shoppe/variants_controller.rb +50 -0
- data/app/helpers/shoppe/application_helper.rb +60 -0
- data/app/helpers/shoppe/product_category_helper.rb +36 -0
- data/app/mailers/shoppe/order_mailer.rb +25 -0
- data/app/mailers/shoppe/user_mailer.rb +10 -0
- data/app/models/shoppe/address.rb +44 -0
- data/app/models/shoppe/country.rb +27 -0
- data/app/models/shoppe/customer.rb +41 -0
- data/app/models/shoppe/delivery_service.rb +33 -0
- data/app/models/shoppe/delivery_service_price.rb +31 -0
- data/app/models/shoppe/order.rb +100 -0
- data/app/models/shoppe/order/actions.rb +94 -0
- data/app/models/shoppe/order/billing.rb +105 -0
- data/app/models/shoppe/order/delivery.rb +229 -0
- data/app/models/shoppe/order/states.rb +69 -0
- data/app/models/shoppe/order_item.rb +239 -0
- data/app/models/shoppe/payment.rb +86 -0
- data/app/models/shoppe/product.rb +189 -0
- data/app/models/shoppe/product/product_attributes.rb +20 -0
- data/app/models/shoppe/product/variants.rb +51 -0
- data/app/models/shoppe/product_attribute.rb +66 -0
- data/app/models/shoppe/product_categorization.rb +14 -0
- data/app/models/shoppe/product_category.rb +75 -0
- data/app/models/shoppe/setting.rb +68 -0
- data/app/models/shoppe/stock_level_adjustment.rb +19 -0
- data/app/models/shoppe/tax_rate.rb +46 -0
- data/app/models/shoppe/user.rb +49 -0
- data/app/validators/permalink_validator.rb +7 -0
- data/app/views/layouts/shoppe/application.html.haml +34 -0
- data/app/views/layouts/shoppe/printable.html.haml +11 -0
- data/app/views/layouts/shoppe/sub.html.haml +10 -0
- data/app/views/shoppe/addresses/_form.html.haml +33 -0
- data/app/views/shoppe/addresses/edit.html.haml +5 -0
- data/app/views/shoppe/addresses/new.html.haml +5 -0
- data/app/views/shoppe/countries/_form.html.haml +35 -0
- data/app/views/shoppe/countries/edit.html.haml +6 -0
- data/app/views/shoppe/countries/index.html.haml +25 -0
- data/app/views/shoppe/countries/new.html.haml +7 -0
- data/app/views/shoppe/customers/_addresses.html.haml +20 -0
- data/app/views/shoppe/customers/_form.html.haml +30 -0
- data/app/views/shoppe/customers/_search_form.html.haml +13 -0
- data/app/views/shoppe/customers/edit.html.haml +5 -0
- data/app/views/shoppe/customers/index.html.haml +32 -0
- data/app/views/shoppe/customers/new.html.haml +5 -0
- data/app/views/shoppe/customers/show.html.haml +53 -0
- data/app/views/shoppe/delivery_service_prices/_form.html.haml +44 -0
- data/app/views/shoppe/delivery_service_prices/edit.html.haml +6 -0
- data/app/views/shoppe/delivery_service_prices/index.html.haml +23 -0
- data/app/views/shoppe/delivery_service_prices/new.html.haml +6 -0
- data/app/views/shoppe/delivery_services/_form.html.haml +38 -0
- data/app/views/shoppe/delivery_services/edit.html.haml +9 -0
- data/app/views/shoppe/delivery_services/index.html.haml +27 -0
- data/app/views/shoppe/delivery_services/new.html.haml +6 -0
- data/app/views/shoppe/order_mailer/accepted.text.erb +12 -0
- data/app/views/shoppe/order_mailer/received.text.erb +9 -0
- data/app/views/shoppe/order_mailer/rejected.text.erb +10 -0
- data/app/views/shoppe/order_mailer/shipped.text.erb +16 -0
- data/app/views/shoppe/orders/_form.html.haml +58 -0
- data/app/views/shoppe/orders/_order_details.html.haml +57 -0
- data/app/views/shoppe/orders/_order_items.html.haml +38 -0
- data/app/views/shoppe/orders/_order_items_form.html.haml +61 -0
- data/app/views/shoppe/orders/_payments_form.html.haml +15 -0
- data/app/views/shoppe/orders/_payments_table.html.haml +37 -0
- data/app/views/shoppe/orders/_search_form.html.haml +24 -0
- data/app/views/shoppe/orders/_status_bar.html.haml +35 -0
- data/app/views/shoppe/orders/despatch_note.html.haml +45 -0
- data/app/views/shoppe/orders/edit.html.haml +21 -0
- data/app/views/shoppe/orders/index.html.haml +39 -0
- data/app/views/shoppe/orders/new.html.haml +14 -0
- data/app/views/shoppe/orders/show.html.haml +25 -0
- data/app/views/shoppe/payments/refund.html.haml +13 -0
- data/app/views/shoppe/product_categories/_form.html.haml +50 -0
- data/app/views/shoppe/product_categories/edit.html.haml +8 -0
- data/app/views/shoppe/product_categories/index.html.haml +27 -0
- data/app/views/shoppe/product_categories/new.html.haml +6 -0
- data/app/views/shoppe/product_category_localisations/form.html.haml +29 -0
- data/app/views/shoppe/product_category_localisations/index.html.haml +26 -0
- data/app/views/shoppe/product_localisations/form.html.haml +32 -0
- data/app/views/shoppe/product_localisations/index.html.haml +26 -0
- data/app/views/shoppe/products/_form.html.haml +118 -0
- data/app/views/shoppe/products/_table.html.haml +42 -0
- data/app/views/shoppe/products/edit.html.haml +9 -0
- data/app/views/shoppe/products/import.html.haml +67 -0
- data/app/views/shoppe/products/index.html.haml +9 -0
- data/app/views/shoppe/products/new.html.haml +7 -0
- data/app/views/shoppe/sessions/new.html.haml +12 -0
- data/app/views/shoppe/sessions/reset.html.haml +12 -0
- data/app/views/shoppe/settings/edit.html.haml +19 -0
- data/app/views/shoppe/shared/error.html.haml +6 -0
- data/app/views/shoppe/stock_level_adjustments/index.html.haml +40 -0
- data/app/views/shoppe/tax_rates/form.html.haml +28 -0
- data/app/views/shoppe/tax_rates/index.html.haml +17 -0
- data/app/views/shoppe/user_mailer/new_password.text.erb +9 -0
- data/app/views/shoppe/users/_form.html.haml +27 -0
- data/app/views/shoppe/users/edit.html.haml +5 -0
- data/app/views/shoppe/users/index.html.haml +17 -0
- data/app/views/shoppe/users/new.html.haml +7 -0
- data/app/views/shoppe/variants/form.html.haml +68 -0
- data/app/views/shoppe/variants/index.html.haml +33 -0
- data/config/locales/de.yml +653 -0
- data/config/locales/en.yml +744 -0
- data/config/locales/es.yml +653 -0
- data/config/locales/pl.yml +662 -0
- data/config/locales/pt-BR.yml +655 -0
- data/config/routes.rb +53 -0
- data/db/countries.txt +252 -0
- data/db/migrate/20130926094549_create_shoppe_initial_schema.rb +175 -0
- data/db/migrate/20131024201501_add_address_type_to_shoppe_tax_rates.rb +5 -0
- data/db/migrate/20131024204815_create_shoppe_payments.rb +32 -0
- data/db/migrate/20131102143930_remove_default_on_order_item_weight.rb +9 -0
- data/db/migrate/20141013192427_create_shoppe_customers.rb +14 -0
- data/db/migrate/20141026175622_add_indexes_to_shoppe_order_items.rb +6 -0
- data/db/migrate/20141026175943_add_indexes_to_shoppe_orders.rb +7 -0
- data/db/migrate/20141026180333_add_indexes_to_shoppe_payments.rb +6 -0
- data/db/migrate/20141026180835_add_indexes_to_shoppe_product_attributes.rb +7 -0
- data/db/migrate/20141026180952_add_indexes_to_shoppe_product_categories.rb +5 -0
- data/db/migrate/20141026181040_add_indexes_to_shoppe_products.rb +8 -0
- data/db/migrate/20141026181312_add_indexes_to_shoppe_settings.rb +5 -0
- data/db/migrate/20141026181354_add_indexes_to_shoppe_stock_level_adjustments.rb +6 -0
- data/db/migrate/20141026181559_add_indexes_to_shoppe_users.rb +5 -0
- data/db/migrate/20141026181716_add_indexes_to_shoppe_delivery_services.rb +9 -0
- data/db/migrate/20141026181717_allow_multiple_shoppe_products_per_shoppe_product_category.rb +46 -0
- data/db/migrate/20141026181718_add_nested_to_product_categories.rb +30 -0
- data/db/migrate/20141027215005_create_shoppe_addresses.rb +17 -0
- data/db/migrate/20150315215633_add_customer_to_shoppe_orders.rb +5 -0
- data/db/migrate/20150513171350_create_shoppe_product_category_translation_table.rb +17 -0
- data/db/migrate/20150519173350_create_shoppe_product_translation_table.rb +18 -0
- data/db/schema.rb +308 -0
- data/db/seeds.rb +137 -0
- data/db/seeds_data/poe400.jpg +0 -0
- data/db/seeds_data/snom-870-blk.jpg +0 -0
- data/db/seeds_data/snom-870-grey.jpg +0 -0
- data/db/seeds_data/snom-mm2.jpg +0 -0
- data/db/seeds_data/spa303.jpg +0 -0
- data/db/seeds_data/t18p.jpg +0 -0
- data/db/seeds_data/t20p.jpg +0 -0
- data/db/seeds_data/t22p.jpg +0 -0
- data/db/seeds_data/t26p.jpg +0 -0
- data/db/seeds_data/t41pn.jpg +0 -0
- data/db/seeds_data/t46gn.jpg +0 -0
- data/db/seeds_data/w52p.jpg +0 -0
- data/db/seeds_data/yhs32.jpg +0 -0
- data/lib/shoppe.rb +60 -0
- data/lib/shoppe/associated_countries.rb +20 -0
- data/lib/shoppe/country_importer.rb +15 -0
- data/lib/shoppe/default_navigation.rb +21 -0
- data/lib/shoppe/engine.rb +55 -0
- data/lib/shoppe/error.rb +21 -0
- data/lib/shoppe/errors/inappropriate_delivery_service.rb +6 -0
- data/lib/shoppe/errors/insufficient_stock_to_fulfil.rb +15 -0
- data/lib/shoppe/errors/invalid_configuration.rb +6 -0
- data/lib/shoppe/errors/not_enough_stock.rb +15 -0
- data/lib/shoppe/errors/payment_declined.rb +6 -0
- data/lib/shoppe/errors/refund_failed.rb +6 -0
- data/lib/shoppe/errors/unorderable_item.rb +6 -0
- data/lib/shoppe/navigation_manager.rb +81 -0
- data/lib/shoppe/orderable_item.rb +39 -0
- data/lib/shoppe/settings.rb +26 -0
- data/lib/shoppe/settings_loader.rb +16 -0
- data/lib/shoppe/setup_generator.rb +10 -0
- data/lib/shoppe/version.rb +3 -0
- data/lib/shoppe/view_helpers.rb +16 -0
- data/lib/tasks/shoppe.rake +29 -0
- metadata +662 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class Customer < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = "shoppe_customers"
|
|
5
|
+
|
|
6
|
+
has_many :addresses, :dependent => :restrict_with_exception, :class_name => "Shoppe::Address"
|
|
7
|
+
|
|
8
|
+
has_many :orders, :dependent => :restrict_with_exception, :class_name => "Shoppe::Order"
|
|
9
|
+
|
|
10
|
+
# Validations
|
|
11
|
+
validates :email, :presence => true, :uniqueness => true, :format => {:with => /\A\b[A-Z0-9\.\_\%\-\+]+@(?:[A-Z0-9\-]+\.)+[A-Z]{2,6}\b\z/i}
|
|
12
|
+
validates :phone, :presence => true, :format => {:with => /\A[\d\ \-x\(\)]{7,}\z/}
|
|
13
|
+
|
|
14
|
+
# All customers ordered by their ID desending
|
|
15
|
+
scope :ordered, -> { order(:id => :desc)}
|
|
16
|
+
|
|
17
|
+
# The name of the customer in the format of "Company (First Last)" or if they don't have
|
|
18
|
+
# company specified, just "First Last".
|
|
19
|
+
#
|
|
20
|
+
# @return [String]
|
|
21
|
+
def name
|
|
22
|
+
company.blank? ? full_name : "#{company} (#{full_name})"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The full name of the customer created by concatinting the first & last name
|
|
26
|
+
#
|
|
27
|
+
# @return [String]
|
|
28
|
+
def full_name
|
|
29
|
+
"#{first_name} #{last_name}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.ransackable_attributes(auth_object = nil)
|
|
33
|
+
["id", "first_name", "last_name", "company", "email", "phone", "mobile"] + _ransackers.keys
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.ransackable_associations(auth_object = nil)
|
|
37
|
+
[]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class DeliveryService < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'shoppe_delivery_services'
|
|
5
|
+
|
|
6
|
+
# Validations
|
|
7
|
+
validates :name, :presence => true
|
|
8
|
+
validates :courier, :presence => true
|
|
9
|
+
|
|
10
|
+
# Orders which are assigned to this delivery service
|
|
11
|
+
has_many :orders, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Order'
|
|
12
|
+
|
|
13
|
+
# Prices for the different levels of service within this delivery service
|
|
14
|
+
has_many :delivery_service_prices, :dependent => :destroy, :class_name => 'Shoppe::DeliveryServicePrice'
|
|
15
|
+
|
|
16
|
+
# All active delivery services
|
|
17
|
+
scope :active, -> { where(:active => true)}
|
|
18
|
+
|
|
19
|
+
# Returns a tracking URL for the passed order
|
|
20
|
+
#
|
|
21
|
+
# @param order [Shoppe::Order]
|
|
22
|
+
# @return [String] the full URL for the order.
|
|
23
|
+
def tracking_url_for(order)
|
|
24
|
+
return nil if self.tracking_url.blank?
|
|
25
|
+
tracking_url = self.tracking_url.dup
|
|
26
|
+
tracking_url.gsub!("{{consignment_number}}", CGI.escape(order.consignment_number.to_s))
|
|
27
|
+
tracking_url.gsub!("{{delivery_postcode}}", CGI.escape(order.delivery_postcode.to_s))
|
|
28
|
+
tracking_url.gsub!("{{billing_postcode}}", CGI.escape(order.billing_postcode.to_s))
|
|
29
|
+
tracking_url
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class DeliveryServicePrice < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# Set the table name
|
|
5
|
+
self.table_name = 'shoppe_delivery_service_prices'
|
|
6
|
+
|
|
7
|
+
include Shoppe::AssociatedCountries
|
|
8
|
+
|
|
9
|
+
# The delivery service which this price belongs to
|
|
10
|
+
belongs_to :delivery_service, :class_name => 'Shoppe::DeliveryService'
|
|
11
|
+
|
|
12
|
+
# The tax rate which should be applied
|
|
13
|
+
belongs_to :tax_rate, :class_name => "Shoppe::TaxRate"
|
|
14
|
+
|
|
15
|
+
# Validations
|
|
16
|
+
validates :code, :presence => true
|
|
17
|
+
validates :price, :numericality => true
|
|
18
|
+
validates :cost_price, :numericality => true, :allow_blank => true
|
|
19
|
+
validates :min_weight, :numericality => true
|
|
20
|
+
validates :max_weight, :numericality => true
|
|
21
|
+
|
|
22
|
+
# All prices ordered by their price ascending
|
|
23
|
+
scope :ordered, -> { order(:price => :asc) }
|
|
24
|
+
|
|
25
|
+
# All prices which are suitable for the weight passed.
|
|
26
|
+
#
|
|
27
|
+
# @param weight [BigDecimal] the weight of the order
|
|
28
|
+
scope :for_weight, -> weight { where("min_weight <= ? AND max_weight >= ?", weight, weight) }
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'shoppe_orders'
|
|
5
|
+
|
|
6
|
+
# Orders can have properties
|
|
7
|
+
key_value_store :properties
|
|
8
|
+
|
|
9
|
+
# Require dependencies
|
|
10
|
+
require_dependency 'shoppe/order/states'
|
|
11
|
+
require_dependency 'shoppe/order/actions'
|
|
12
|
+
require_dependency 'shoppe/order/billing'
|
|
13
|
+
require_dependency 'shoppe/order/delivery'
|
|
14
|
+
|
|
15
|
+
# All items which make up this order
|
|
16
|
+
has_many :order_items, :dependent => :destroy, :class_name => 'Shoppe::OrderItem', :inverse_of => :order
|
|
17
|
+
accepts_nested_attributes_for :order_items, :allow_destroy => true, :reject_if => Proc.new { |a| a['ordered_item_id'].blank? }
|
|
18
|
+
|
|
19
|
+
# All products which are part of this order (accessed through the items)
|
|
20
|
+
has_many :products, :through => :order_items, :class_name => 'Shoppe::Product', :source => :ordered_item, :source_type => 'Shoppe::Product'
|
|
21
|
+
|
|
22
|
+
# The order can belong to a customer
|
|
23
|
+
belongs_to :customer, :class_name => 'Shoppe::Customer'
|
|
24
|
+
has_many :addresses, :through => :customers, :class_name => "Shoppe::Address"
|
|
25
|
+
|
|
26
|
+
# Validations
|
|
27
|
+
validates :token, :presence => true
|
|
28
|
+
with_options :if => Proc.new { |o| !o.building? } do |order|
|
|
29
|
+
order.validates :email_address, :format => {:with => /\A\b[A-Z0-9\.\_\%\-\+]+@(?:[A-Z0-9\-]+\.)+[A-Z]{2,6}\b\z/i}
|
|
30
|
+
order.validates :phone_number, :format => {:with => /\A[\d\ \-x\(\)]{7,}\z/}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Set some defaults
|
|
34
|
+
before_validation { self.token = SecureRandom.uuid if self.token.blank? }
|
|
35
|
+
|
|
36
|
+
# Some methods for setting the billing & delivery addresses
|
|
37
|
+
attr_accessor :save_addresses, :billing_address_id, :delivery_address_id
|
|
38
|
+
|
|
39
|
+
# The order number
|
|
40
|
+
#
|
|
41
|
+
# @return [String] - the order number padded with at least 5 zeros
|
|
42
|
+
def number
|
|
43
|
+
id ? id.to_s.rjust(6, '0') : nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The length of time the customer spent building the order before submitting it to us.
|
|
47
|
+
# The time from first item in basket to received.
|
|
48
|
+
#
|
|
49
|
+
# @return [Float] - the length of time
|
|
50
|
+
def build_time
|
|
51
|
+
return nil if self.received_at.blank?
|
|
52
|
+
self.created_at - self.received_at
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The name of the customer in the format of "Company (First Last)" or if they don't have
|
|
56
|
+
# company specified, just "First Last".
|
|
57
|
+
#
|
|
58
|
+
# @return [String]
|
|
59
|
+
def customer_name
|
|
60
|
+
company.blank? ? full_name : "#{company} (#{full_name})"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The full name of the customer created by concatinting the first & last name
|
|
64
|
+
#
|
|
65
|
+
# @return [String]
|
|
66
|
+
def full_name
|
|
67
|
+
"#{first_name} #{last_name}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Is this order empty? (i.e. doesn't have any items associated with it)
|
|
71
|
+
#
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def empty?
|
|
74
|
+
order_items.empty?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Does this order have items?
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def has_items?
|
|
81
|
+
total_items > 0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Return the number of items in the order?
|
|
85
|
+
#
|
|
86
|
+
# @return [Integer]
|
|
87
|
+
def total_items
|
|
88
|
+
order_items.inject(0) { |t,i| t + i.quantity }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.ransackable_attributes(auth_object = nil)
|
|
92
|
+
["id", "billing_postcode", "billing_address1", "billing_address2", "billing_address3", "billing_address4", "first_name", "last_name", "company", "email_address", "phone_number", "consignment_number", "status", "received_at"] + _ransackers.keys
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.ransackable_associations(auth_object = nil)
|
|
96
|
+
[]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
extend ActiveModel::Callbacks
|
|
5
|
+
|
|
6
|
+
# These additional callbacks allow for applications to hook into other
|
|
7
|
+
# parts of the order lifecycle.
|
|
8
|
+
define_model_callbacks :confirmation, :acceptance, :rejection
|
|
9
|
+
|
|
10
|
+
# This method should be called by the base application when the user has completed their
|
|
11
|
+
# first round of entering details. This will mark the order as "confirming" which means
|
|
12
|
+
# the customer now just must confirm.
|
|
13
|
+
#
|
|
14
|
+
# @param params [Hash] a hash of order attributes
|
|
15
|
+
# @return [Boolean]
|
|
16
|
+
def proceed_to_confirm(params = {})
|
|
17
|
+
self.status = 'confirming'
|
|
18
|
+
if self.update(params)
|
|
19
|
+
true
|
|
20
|
+
else
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# This method should be executed by the application when the order should be completed
|
|
26
|
+
# by the customer. It will raise exceptions if anything goes wrong or return true if
|
|
27
|
+
# the order has been confirmed successfully
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def confirm!
|
|
31
|
+
no_stock_of = self.order_items.select(&:validate_stock_levels)
|
|
32
|
+
unless no_stock_of.empty?
|
|
33
|
+
raise Shoppe::Errors::InsufficientStockToFulfil, :order => self, :out_of_stock_items => no_stock_of
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
run_callbacks :confirmation do
|
|
37
|
+
# If we have successfully charged the card (i.e. no exception) we can go ahead and mark this
|
|
38
|
+
# order as 'received' which means it can be accepted by staff.
|
|
39
|
+
self.status = 'received'
|
|
40
|
+
self.received_at = Time.now
|
|
41
|
+
self.save!
|
|
42
|
+
|
|
43
|
+
self.order_items.each(&:confirm!)
|
|
44
|
+
|
|
45
|
+
# Send an email to the customer
|
|
46
|
+
deliver_received_order_email
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# We're all good.
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Mark order as accepted
|
|
54
|
+
#
|
|
55
|
+
# @param user [Shoppe::User] the user who carried out this action
|
|
56
|
+
def accept!(user = nil)
|
|
57
|
+
run_callbacks :acceptance do
|
|
58
|
+
self.accepted_at = Time.now
|
|
59
|
+
self.accepter = user if user
|
|
60
|
+
self.status = 'accepted'
|
|
61
|
+
self.save!
|
|
62
|
+
self.order_items.each(&:accept!)
|
|
63
|
+
#deliver_accepted_order_email
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Mark order as rejected
|
|
68
|
+
#
|
|
69
|
+
# @param user [Shoppe::User] the user who carried out the action
|
|
70
|
+
def reject!(user = nil)
|
|
71
|
+
run_callbacks :rejection do
|
|
72
|
+
self.rejected_at = Time.now
|
|
73
|
+
self.rejecter = user if user
|
|
74
|
+
self.status = 'rejected'
|
|
75
|
+
self.save!
|
|
76
|
+
self.order_items.each(&:reject!)
|
|
77
|
+
#deliver_rejected_order_email
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def deliver_accepted_order_email
|
|
82
|
+
Shoppe::OrderMailer.accepted(self).deliver_now
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def deliver_rejected_order_email
|
|
86
|
+
Shoppe::OrderMailer.rejected(self).deliver_now
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def deliver_received_order_email
|
|
90
|
+
Shoppe::OrderMailer.received(self).deliver_now
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# The country which this order should be billed to
|
|
5
|
+
#
|
|
6
|
+
# @return [Shoppe::Country]
|
|
7
|
+
belongs_to :billing_country, :class_name => 'Shoppe::Country', :foreign_key => 'billing_country_id'
|
|
8
|
+
|
|
9
|
+
# Payments which have been stored for the order
|
|
10
|
+
has_many :payments, :dependent => :destroy, :class_name => 'Shoppe::Payment'
|
|
11
|
+
|
|
12
|
+
# Validations
|
|
13
|
+
with_options :if => Proc.new { |o| !o.building? } do |order|
|
|
14
|
+
order.validates :first_name, :presence => true
|
|
15
|
+
order.validates :last_name, :presence => true
|
|
16
|
+
order.validates :billing_address1, :presence => true
|
|
17
|
+
order.validates :billing_address3, :presence => true
|
|
18
|
+
order.validates :billing_address4, :presence => true
|
|
19
|
+
order.validates :billing_postcode, :presence => true
|
|
20
|
+
order.validates :billing_country, :presence => true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The name for billing purposes
|
|
24
|
+
#
|
|
25
|
+
# @return [String]
|
|
26
|
+
def billing_name
|
|
27
|
+
company.blank? ? full_name : "#{full_name} (#{company})"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The total cost of the order
|
|
31
|
+
#
|
|
32
|
+
# @return [BigDecimal]
|
|
33
|
+
def total_cost
|
|
34
|
+
self.delivery_cost_price +
|
|
35
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.total_cost }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Return the price for the order
|
|
39
|
+
#
|
|
40
|
+
# @return [BigDecimal]
|
|
41
|
+
def profit
|
|
42
|
+
total_before_tax - total_cost
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The total price of all items in the basket excluding delivery
|
|
46
|
+
#
|
|
47
|
+
# @return [BigDecimal]
|
|
48
|
+
def items_sub_total
|
|
49
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.sub_total }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The total price of the order before tax
|
|
53
|
+
#
|
|
54
|
+
# @return [BigDecimal]
|
|
55
|
+
def total_before_tax
|
|
56
|
+
self.delivery_price + self.items_sub_total
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The total amount of tax due on this order
|
|
60
|
+
#
|
|
61
|
+
# @return [BigDecimal]
|
|
62
|
+
def tax
|
|
63
|
+
self.delivery_tax_amount +
|
|
64
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.tax_amount }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The total of the order including tax
|
|
68
|
+
#
|
|
69
|
+
# @return [BigDecimal]
|
|
70
|
+
def total
|
|
71
|
+
self.delivery_price +
|
|
72
|
+
self.delivery_tax_amount +
|
|
73
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.total }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The total amount due on the order
|
|
77
|
+
#
|
|
78
|
+
# @return [BigDecimal]
|
|
79
|
+
def balance
|
|
80
|
+
@balance ||= total - amount_paid
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Is there a payment still outstanding on this order?
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def payment_outstanding?
|
|
87
|
+
balance > BigDecimal(0)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Has this order been paid in full?
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def paid_in_full?
|
|
94
|
+
!payment_outstanding?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Is the order invoiced?
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
def invoiced?
|
|
101
|
+
!invoice_number.blank?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# The associated delivery service
|
|
5
|
+
#
|
|
6
|
+
# @return [Shoppe::DeliveryService]
|
|
7
|
+
belongs_to :delivery_service, :class_name => 'Shoppe::DeliveryService'
|
|
8
|
+
|
|
9
|
+
# The country where this order is being delivered to (if one has been provided)
|
|
10
|
+
#
|
|
11
|
+
# @return [Shoppe::Country]
|
|
12
|
+
belongs_to :delivery_country, :class_name => 'Shoppe::Country', :foreign_key => 'delivery_country_id'
|
|
13
|
+
|
|
14
|
+
# The user who marked the order has shipped
|
|
15
|
+
#
|
|
16
|
+
# @return [Shoppe::User]
|
|
17
|
+
belongs_to :shipper, :class_name => 'Shoppe::User', :foreign_key => 'shipped_by'
|
|
18
|
+
|
|
19
|
+
# Set up a callback for use when an order is shipped
|
|
20
|
+
define_model_callbacks :ship
|
|
21
|
+
|
|
22
|
+
# Validations
|
|
23
|
+
with_options :if => :separate_delivery_address? do |order|
|
|
24
|
+
order.validates :delivery_name, :presence => true
|
|
25
|
+
order.validates :delivery_address1, :presence => true
|
|
26
|
+
order.validates :delivery_address3, :presence => true
|
|
27
|
+
order.validates :delivery_address4, :presence => true
|
|
28
|
+
order.validates :delivery_postcode, :presence => true
|
|
29
|
+
order.validates :delivery_country, :presence => true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
validate do
|
|
33
|
+
if self.delivery_required?
|
|
34
|
+
if self.delivery_service.nil?
|
|
35
|
+
errors.add :delivery_service_id, :must_be_specified
|
|
36
|
+
elsif !self.valid_delivery_service?
|
|
37
|
+
errors.add :delivery_service_id, :not_suitable
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
before_confirmation do
|
|
43
|
+
# Ensure that before we confirm the order that the delivery service which has been selected
|
|
44
|
+
# is appropritae for the contents of the order.
|
|
45
|
+
if self.delivery_required? && !self.valid_delivery_service?
|
|
46
|
+
raise Shoppe::Errors::InappropriateDeliveryService, :order => self
|
|
47
|
+
end
|
|
48
|
+
cache_delivery_pricing
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# If an order has been received and something changes the delivery service or the delivery price
|
|
52
|
+
# is cleared, we will re-cache all the delivery pricing so that we have the latest.
|
|
53
|
+
before_save do
|
|
54
|
+
if received? && (delivery_service_id_changed? || (self.delivery_price_changed? && read_attribute(:delivery_price).blank?))
|
|
55
|
+
self.delivery_price = nil
|
|
56
|
+
self.delivery_cost_price = nil
|
|
57
|
+
self.delivery_tax_rate = nil
|
|
58
|
+
self.delivery_tax_amount = nil
|
|
59
|
+
cache_delivery_pricing
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# If there isn't a seperate address needed, clear all the fields back to nil
|
|
64
|
+
before_validation do
|
|
65
|
+
unless separate_delivery_address?
|
|
66
|
+
self.delivery_name = nil
|
|
67
|
+
self.delivery_address1 = nil
|
|
68
|
+
self.delivery_address2 = nil
|
|
69
|
+
self.delivery_address3 = nil
|
|
70
|
+
self.delivery_address4 = nil
|
|
71
|
+
self.delivery_postcode = nil
|
|
72
|
+
self.delivery_country = nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Create some delivery_ methods which will mimic the billing methods if the order does
|
|
77
|
+
# not need a seperate address.
|
|
78
|
+
[:delivery_name, :delivery_address1, :delivery_address2, :delivery_address3, :delivery_address4, :delivery_postcode, :delivery_country].each do |f|
|
|
79
|
+
define_method(f) do
|
|
80
|
+
separate_delivery_address? ? super() : send(f.to_s.gsub('delivery_', 'billing_'))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Cache delivery prices for the order
|
|
85
|
+
def cache_delivery_pricing
|
|
86
|
+
if self.delivery_service
|
|
87
|
+
write_attribute :delivery_service_id, self.delivery_service.id
|
|
88
|
+
write_attribute :delivery_price, self.delivery_price
|
|
89
|
+
write_attribute :delivery_cost_price, self.delivery_cost_price
|
|
90
|
+
write_attribute :delivery_tax_rate, self.delivery_tax_rate
|
|
91
|
+
else
|
|
92
|
+
write_attribute :delivery_service_id, nil
|
|
93
|
+
write_attribute :delivery_price, nil
|
|
94
|
+
write_attribute :delivery_cost_price, nil
|
|
95
|
+
write_attribute :delivery_tax_rate, nil
|
|
96
|
+
write_attribute :delivery_tax_amount, nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Cache prices and save the order
|
|
101
|
+
def cache_delivery_pricing!
|
|
102
|
+
cache_delivery_pricing
|
|
103
|
+
save!
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Has this order been shipped?
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean]
|
|
109
|
+
def shipped?
|
|
110
|
+
!!self.shipped_at?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The total weight of the order
|
|
114
|
+
#
|
|
115
|
+
# @return [BigDecimal]
|
|
116
|
+
def total_weight
|
|
117
|
+
order_items.inject(BigDecimal(0)) { |t,i| t + i.total_weight}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Is delivery required for this order?
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean]
|
|
123
|
+
def delivery_required?
|
|
124
|
+
total_weight > BigDecimal(0)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# An array of all the delivery services which are suitable for this order in it's
|
|
128
|
+
# current state (based on its current weight)
|
|
129
|
+
#
|
|
130
|
+
# @return [Array] an array of Shoppe::DeliveryService objects
|
|
131
|
+
def available_delivery_services
|
|
132
|
+
delivery_service_prices.map(&:delivery_service).uniq
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# An array of all the delivery service prices which can be applied to this order.
|
|
136
|
+
#
|
|
137
|
+
# @return [Array] an array of Shoppe:DeliveryServicePrice objects
|
|
138
|
+
def delivery_service_prices
|
|
139
|
+
if delivery_required?
|
|
140
|
+
prices = Shoppe::DeliveryServicePrice.joins(:delivery_service).where(:shoppe_delivery_services => {:active => true}).order(:price).for_weight(total_weight)
|
|
141
|
+
prices = prices.select { |p| p.countries.empty? || p.country?(self.delivery_country) }
|
|
142
|
+
prices.sort{ |x,y| (y.delivery_service.default? ? 1 : 0) <=> (x.delivery_service.default? ? 1 : 0) } # Order by truthiness
|
|
143
|
+
else
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The recommended delivery service for this order
|
|
149
|
+
#
|
|
150
|
+
# @return [Shoppe::DeliveryService]
|
|
151
|
+
def delivery_service
|
|
152
|
+
super || available_delivery_services.first
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Return the delivery price for this order in its current state
|
|
156
|
+
#
|
|
157
|
+
# @return [BigDecimal]
|
|
158
|
+
def delivery_service_price
|
|
159
|
+
self.delivery_service && self.delivery_service.delivery_service_prices.for_weight(self.total_weight).first
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The price for delivering this order in its current state
|
|
163
|
+
#
|
|
164
|
+
# @return [BigDecimal]
|
|
165
|
+
def delivery_price
|
|
166
|
+
read_attribute(:delivery_price) || delivery_service_price.try(:price) || BigDecimal(0)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The cost of delivering this order in its current state
|
|
170
|
+
#
|
|
171
|
+
# @return [BigDecimal]
|
|
172
|
+
def delivery_cost_price
|
|
173
|
+
read_attribute(:delivery_cost_price) || delivery_service_price.try(:cost_price) || BigDecimal(0)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The tax amount due for the delivery of this order in its current state
|
|
177
|
+
#
|
|
178
|
+
# @return [BigDecimal]
|
|
179
|
+
def delivery_tax_amount
|
|
180
|
+
read_attribute(:delivery_tax_amount) ||
|
|
181
|
+
delivery_price / BigDecimal(100) * delivery_tax_rate
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The tax rate for the delivery of this order in its current state
|
|
185
|
+
#
|
|
186
|
+
# @return [BigDecimal]
|
|
187
|
+
def delivery_tax_rate
|
|
188
|
+
read_attribute(:delivery_tax_rate) ||
|
|
189
|
+
delivery_service_price.try(:tax_rate).try(:rate_for, self) ||
|
|
190
|
+
BigDecimal(0)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Is the currently assigned delivery service appropriate for this order?
|
|
194
|
+
#
|
|
195
|
+
# @return [Boolean]
|
|
196
|
+
def valid_delivery_service?
|
|
197
|
+
self.delivery_service ? self.available_delivery_services.include?(self.delivery_service) : !self.delivery_required?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Remove the associated delivery service if it's invalid
|
|
201
|
+
def remove_delivery_service_if_invalid
|
|
202
|
+
unless self.valid_delivery_service?
|
|
203
|
+
self.delivery_service = nil
|
|
204
|
+
self.save
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The URL which can be used to track the delivery of this order
|
|
209
|
+
#
|
|
210
|
+
# @return [String]
|
|
211
|
+
def courier_tracking_url
|
|
212
|
+
return nil if self.shipped_at.blank? || self.consignment_number.blank?
|
|
213
|
+
@courier_tracking_url ||= self.delivery_service.tracking_url_for(self)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Mark this order as shipped
|
|
217
|
+
def ship!(consignment_number, user = nil)
|
|
218
|
+
run_callbacks :ship do
|
|
219
|
+
self.shipped_at = Time.now
|
|
220
|
+
self.shipper = user if user
|
|
221
|
+
self.status = 'shipped'
|
|
222
|
+
self.consignment_number = consignment_number
|
|
223
|
+
self.save!
|
|
224
|
+
#Shoppe::OrderMailer.shipped(self).deliver_now
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
end
|
|
229
|
+
end
|