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.
Files changed (239) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +55 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/images/shoppe/chosen-sprite.png +0 -0
  6. data/app/assets/images/shoppe/chosen-sprite@2x.png +0 -0
  7. data/app/assets/images/shoppe/document.svg +1 -0
  8. data/app/assets/images/shoppe/icons/bag.svg +1 -0
  9. data/app/assets/images/shoppe/icons/balance.svg +1 -0
  10. data/app/assets/images/shoppe/icons/box.svg +1 -0
  11. data/app/assets/images/shoppe/icons/building.svg +1 -0
  12. data/app/assets/images/shoppe/icons/chart.svg +1 -0
  13. data/app/assets/images/shoppe/icons/chat.svg +1 -0
  14. data/app/assets/images/shoppe/icons/checkbox.svg +1 -0
  15. data/app/assets/images/shoppe/icons/checkbox2.svg +1 -0
  16. data/app/assets/images/shoppe/icons/cloud.svg +1 -0
  17. data/app/assets/images/shoppe/icons/cone.svg +1 -0
  18. data/app/assets/images/shoppe/icons/credit_card.svg +1 -0
  19. data/app/assets/images/shoppe/icons/currency.svg +1 -0
  20. data/app/assets/images/shoppe/icons/edit.svg +14 -0
  21. data/app/assets/images/shoppe/icons/flowchart.svg +1 -0
  22. data/app/assets/images/shoppe/icons/gift.svg +1 -0
  23. data/app/assets/images/shoppe/icons/globe.svg +1 -0
  24. data/app/assets/images/shoppe/icons/id.svg +1 -0
  25. data/app/assets/images/shoppe/icons/id2.svg +1 -0
  26. data/app/assets/images/shoppe/icons/locked.svg +1 -0
  27. data/app/assets/images/shoppe/icons/report.svg +1 -0
  28. data/app/assets/images/shoppe/icons/search.svg +1 -0
  29. data/app/assets/images/shoppe/icons/support.svg +1 -0
  30. data/app/assets/images/shoppe/icons/tags.svg +1 -0
  31. data/app/assets/images/shoppe/icons/toolbox.svg +1 -0
  32. data/app/assets/images/shoppe/icons/unlocked.svg +1 -0
  33. data/app/assets/images/shoppe/icons/wallet.svg +1 -0
  34. data/app/assets/images/shoppe/logo.svg +47 -0
  35. data/app/assets/images/shoppe/move.svg +1 -0
  36. data/app/assets/images/shoppe/shoppe.svg +25 -0
  37. data/app/assets/images/shoppe/square.svg +9 -0
  38. data/app/assets/images/shoppe/statuses/accepted.svg +14 -0
  39. data/app/assets/images/shoppe/statuses/paid.svg +16 -0
  40. data/app/assets/images/shoppe/statuses/received.svg +15 -0
  41. data/app/assets/images/shoppe/statuses/rejected.svg +14 -0
  42. data/app/assets/images/shoppe/statuses/shipped.svg +14 -0
  43. data/app/assets/images/shoppe/table-tear-off.png +0 -0
  44. data/app/assets/javascripts/shoppe/application.coffee +123 -0
  45. data/app/assets/javascripts/shoppe/chosen.jquery.js +1166 -0
  46. data/app/assets/javascripts/shoppe/jquery_ui.js +6 -0
  47. data/app/assets/javascripts/shoppe/mousetrap.js +9 -0
  48. data/app/assets/javascripts/shoppe/order_form.coffee +47 -0
  49. data/app/assets/stylesheets/shoppe/application.scss +601 -0
  50. data/app/assets/stylesheets/shoppe/chosen.scss +424 -0
  51. data/app/assets/stylesheets/shoppe/dialog.scss +25 -0
  52. data/app/assets/stylesheets/shoppe/elements.scss +79 -0
  53. data/app/assets/stylesheets/shoppe/printable.scss +67 -0
  54. data/app/assets/stylesheets/shoppe/reset.scss +93 -0
  55. data/app/assets/stylesheets/shoppe/sub.scss +106 -0
  56. data/app/assets/stylesheets/shoppe/variables.scss +1 -0
  57. data/app/controllers/shoppe/addresses_controller.rb +47 -0
  58. data/app/controllers/shoppe/application_controller.rb +46 -0
  59. data/app/controllers/shoppe/attachments_controller.rb +14 -0
  60. data/app/controllers/shoppe/countries_controller.rb +47 -0
  61. data/app/controllers/shoppe/customers_controller.rb +55 -0
  62. data/app/controllers/shoppe/dashboard_controller.rb +9 -0
  63. data/app/controllers/shoppe/delivery_service_prices_controller.rb +45 -0
  64. data/app/controllers/shoppe/delivery_services_controller.rb +47 -0
  65. data/app/controllers/shoppe/orders_controller.rb +119 -0
  66. data/app/controllers/shoppe/payments_controller.rb +33 -0
  67. data/app/controllers/shoppe/product_categories_controller.rb +47 -0
  68. data/app/controllers/shoppe/product_category_localisations_controller.rb +58 -0
  69. data/app/controllers/shoppe/product_localisations_controller.rb +58 -0
  70. data/app/controllers/shoppe/products_controller.rb +58 -0
  71. data/app/controllers/shoppe/sessions_controller.rb +34 -0
  72. data/app/controllers/shoppe/settings_controller.rb +16 -0
  73. data/app/controllers/shoppe/stock_level_adjustments_controller.rb +40 -0
  74. data/app/controllers/shoppe/tax_rates_controller.rb +49 -0
  75. data/app/controllers/shoppe/users_controller.rb +53 -0
  76. data/app/controllers/shoppe/variants_controller.rb +50 -0
  77. data/app/helpers/shoppe/application_helper.rb +60 -0
  78. data/app/helpers/shoppe/product_category_helper.rb +36 -0
  79. data/app/mailers/shoppe/order_mailer.rb +25 -0
  80. data/app/mailers/shoppe/user_mailer.rb +10 -0
  81. data/app/models/shoppe/address.rb +44 -0
  82. data/app/models/shoppe/country.rb +27 -0
  83. data/app/models/shoppe/customer.rb +41 -0
  84. data/app/models/shoppe/delivery_service.rb +33 -0
  85. data/app/models/shoppe/delivery_service_price.rb +31 -0
  86. data/app/models/shoppe/order.rb +100 -0
  87. data/app/models/shoppe/order/actions.rb +94 -0
  88. data/app/models/shoppe/order/billing.rb +105 -0
  89. data/app/models/shoppe/order/delivery.rb +229 -0
  90. data/app/models/shoppe/order/states.rb +69 -0
  91. data/app/models/shoppe/order_item.rb +239 -0
  92. data/app/models/shoppe/payment.rb +86 -0
  93. data/app/models/shoppe/product.rb +189 -0
  94. data/app/models/shoppe/product/product_attributes.rb +20 -0
  95. data/app/models/shoppe/product/variants.rb +51 -0
  96. data/app/models/shoppe/product_attribute.rb +66 -0
  97. data/app/models/shoppe/product_categorization.rb +14 -0
  98. data/app/models/shoppe/product_category.rb +75 -0
  99. data/app/models/shoppe/setting.rb +68 -0
  100. data/app/models/shoppe/stock_level_adjustment.rb +19 -0
  101. data/app/models/shoppe/tax_rate.rb +46 -0
  102. data/app/models/shoppe/user.rb +49 -0
  103. data/app/validators/permalink_validator.rb +7 -0
  104. data/app/views/layouts/shoppe/application.html.haml +34 -0
  105. data/app/views/layouts/shoppe/printable.html.haml +11 -0
  106. data/app/views/layouts/shoppe/sub.html.haml +10 -0
  107. data/app/views/shoppe/addresses/_form.html.haml +33 -0
  108. data/app/views/shoppe/addresses/edit.html.haml +5 -0
  109. data/app/views/shoppe/addresses/new.html.haml +5 -0
  110. data/app/views/shoppe/countries/_form.html.haml +35 -0
  111. data/app/views/shoppe/countries/edit.html.haml +6 -0
  112. data/app/views/shoppe/countries/index.html.haml +25 -0
  113. data/app/views/shoppe/countries/new.html.haml +7 -0
  114. data/app/views/shoppe/customers/_addresses.html.haml +20 -0
  115. data/app/views/shoppe/customers/_form.html.haml +30 -0
  116. data/app/views/shoppe/customers/_search_form.html.haml +13 -0
  117. data/app/views/shoppe/customers/edit.html.haml +5 -0
  118. data/app/views/shoppe/customers/index.html.haml +32 -0
  119. data/app/views/shoppe/customers/new.html.haml +5 -0
  120. data/app/views/shoppe/customers/show.html.haml +53 -0
  121. data/app/views/shoppe/delivery_service_prices/_form.html.haml +44 -0
  122. data/app/views/shoppe/delivery_service_prices/edit.html.haml +6 -0
  123. data/app/views/shoppe/delivery_service_prices/index.html.haml +23 -0
  124. data/app/views/shoppe/delivery_service_prices/new.html.haml +6 -0
  125. data/app/views/shoppe/delivery_services/_form.html.haml +38 -0
  126. data/app/views/shoppe/delivery_services/edit.html.haml +9 -0
  127. data/app/views/shoppe/delivery_services/index.html.haml +27 -0
  128. data/app/views/shoppe/delivery_services/new.html.haml +6 -0
  129. data/app/views/shoppe/order_mailer/accepted.text.erb +12 -0
  130. data/app/views/shoppe/order_mailer/received.text.erb +9 -0
  131. data/app/views/shoppe/order_mailer/rejected.text.erb +10 -0
  132. data/app/views/shoppe/order_mailer/shipped.text.erb +16 -0
  133. data/app/views/shoppe/orders/_form.html.haml +58 -0
  134. data/app/views/shoppe/orders/_order_details.html.haml +57 -0
  135. data/app/views/shoppe/orders/_order_items.html.haml +38 -0
  136. data/app/views/shoppe/orders/_order_items_form.html.haml +61 -0
  137. data/app/views/shoppe/orders/_payments_form.html.haml +15 -0
  138. data/app/views/shoppe/orders/_payments_table.html.haml +37 -0
  139. data/app/views/shoppe/orders/_search_form.html.haml +24 -0
  140. data/app/views/shoppe/orders/_status_bar.html.haml +35 -0
  141. data/app/views/shoppe/orders/despatch_note.html.haml +45 -0
  142. data/app/views/shoppe/orders/edit.html.haml +21 -0
  143. data/app/views/shoppe/orders/index.html.haml +39 -0
  144. data/app/views/shoppe/orders/new.html.haml +14 -0
  145. data/app/views/shoppe/orders/show.html.haml +25 -0
  146. data/app/views/shoppe/payments/refund.html.haml +13 -0
  147. data/app/views/shoppe/product_categories/_form.html.haml +50 -0
  148. data/app/views/shoppe/product_categories/edit.html.haml +8 -0
  149. data/app/views/shoppe/product_categories/index.html.haml +27 -0
  150. data/app/views/shoppe/product_categories/new.html.haml +6 -0
  151. data/app/views/shoppe/product_category_localisations/form.html.haml +29 -0
  152. data/app/views/shoppe/product_category_localisations/index.html.haml +26 -0
  153. data/app/views/shoppe/product_localisations/form.html.haml +32 -0
  154. data/app/views/shoppe/product_localisations/index.html.haml +26 -0
  155. data/app/views/shoppe/products/_form.html.haml +118 -0
  156. data/app/views/shoppe/products/_table.html.haml +42 -0
  157. data/app/views/shoppe/products/edit.html.haml +9 -0
  158. data/app/views/shoppe/products/import.html.haml +67 -0
  159. data/app/views/shoppe/products/index.html.haml +9 -0
  160. data/app/views/shoppe/products/new.html.haml +7 -0
  161. data/app/views/shoppe/sessions/new.html.haml +12 -0
  162. data/app/views/shoppe/sessions/reset.html.haml +12 -0
  163. data/app/views/shoppe/settings/edit.html.haml +19 -0
  164. data/app/views/shoppe/shared/error.html.haml +6 -0
  165. data/app/views/shoppe/stock_level_adjustments/index.html.haml +40 -0
  166. data/app/views/shoppe/tax_rates/form.html.haml +28 -0
  167. data/app/views/shoppe/tax_rates/index.html.haml +17 -0
  168. data/app/views/shoppe/user_mailer/new_password.text.erb +9 -0
  169. data/app/views/shoppe/users/_form.html.haml +27 -0
  170. data/app/views/shoppe/users/edit.html.haml +5 -0
  171. data/app/views/shoppe/users/index.html.haml +17 -0
  172. data/app/views/shoppe/users/new.html.haml +7 -0
  173. data/app/views/shoppe/variants/form.html.haml +68 -0
  174. data/app/views/shoppe/variants/index.html.haml +33 -0
  175. data/config/locales/de.yml +653 -0
  176. data/config/locales/en.yml +744 -0
  177. data/config/locales/es.yml +653 -0
  178. data/config/locales/pl.yml +662 -0
  179. data/config/locales/pt-BR.yml +655 -0
  180. data/config/routes.rb +53 -0
  181. data/db/countries.txt +252 -0
  182. data/db/migrate/20130926094549_create_shoppe_initial_schema.rb +175 -0
  183. data/db/migrate/20131024201501_add_address_type_to_shoppe_tax_rates.rb +5 -0
  184. data/db/migrate/20131024204815_create_shoppe_payments.rb +32 -0
  185. data/db/migrate/20131102143930_remove_default_on_order_item_weight.rb +9 -0
  186. data/db/migrate/20141013192427_create_shoppe_customers.rb +14 -0
  187. data/db/migrate/20141026175622_add_indexes_to_shoppe_order_items.rb +6 -0
  188. data/db/migrate/20141026175943_add_indexes_to_shoppe_orders.rb +7 -0
  189. data/db/migrate/20141026180333_add_indexes_to_shoppe_payments.rb +6 -0
  190. data/db/migrate/20141026180835_add_indexes_to_shoppe_product_attributes.rb +7 -0
  191. data/db/migrate/20141026180952_add_indexes_to_shoppe_product_categories.rb +5 -0
  192. data/db/migrate/20141026181040_add_indexes_to_shoppe_products.rb +8 -0
  193. data/db/migrate/20141026181312_add_indexes_to_shoppe_settings.rb +5 -0
  194. data/db/migrate/20141026181354_add_indexes_to_shoppe_stock_level_adjustments.rb +6 -0
  195. data/db/migrate/20141026181559_add_indexes_to_shoppe_users.rb +5 -0
  196. data/db/migrate/20141026181716_add_indexes_to_shoppe_delivery_services.rb +9 -0
  197. data/db/migrate/20141026181717_allow_multiple_shoppe_products_per_shoppe_product_category.rb +46 -0
  198. data/db/migrate/20141026181718_add_nested_to_product_categories.rb +30 -0
  199. data/db/migrate/20141027215005_create_shoppe_addresses.rb +17 -0
  200. data/db/migrate/20150315215633_add_customer_to_shoppe_orders.rb +5 -0
  201. data/db/migrate/20150513171350_create_shoppe_product_category_translation_table.rb +17 -0
  202. data/db/migrate/20150519173350_create_shoppe_product_translation_table.rb +18 -0
  203. data/db/schema.rb +308 -0
  204. data/db/seeds.rb +137 -0
  205. data/db/seeds_data/poe400.jpg +0 -0
  206. data/db/seeds_data/snom-870-blk.jpg +0 -0
  207. data/db/seeds_data/snom-870-grey.jpg +0 -0
  208. data/db/seeds_data/snom-mm2.jpg +0 -0
  209. data/db/seeds_data/spa303.jpg +0 -0
  210. data/db/seeds_data/t18p.jpg +0 -0
  211. data/db/seeds_data/t20p.jpg +0 -0
  212. data/db/seeds_data/t22p.jpg +0 -0
  213. data/db/seeds_data/t26p.jpg +0 -0
  214. data/db/seeds_data/t41pn.jpg +0 -0
  215. data/db/seeds_data/t46gn.jpg +0 -0
  216. data/db/seeds_data/w52p.jpg +0 -0
  217. data/db/seeds_data/yhs32.jpg +0 -0
  218. data/lib/shoppe.rb +60 -0
  219. data/lib/shoppe/associated_countries.rb +20 -0
  220. data/lib/shoppe/country_importer.rb +15 -0
  221. data/lib/shoppe/default_navigation.rb +21 -0
  222. data/lib/shoppe/engine.rb +55 -0
  223. data/lib/shoppe/error.rb +21 -0
  224. data/lib/shoppe/errors/inappropriate_delivery_service.rb +6 -0
  225. data/lib/shoppe/errors/insufficient_stock_to_fulfil.rb +15 -0
  226. data/lib/shoppe/errors/invalid_configuration.rb +6 -0
  227. data/lib/shoppe/errors/not_enough_stock.rb +15 -0
  228. data/lib/shoppe/errors/payment_declined.rb +6 -0
  229. data/lib/shoppe/errors/refund_failed.rb +6 -0
  230. data/lib/shoppe/errors/unorderable_item.rb +6 -0
  231. data/lib/shoppe/navigation_manager.rb +81 -0
  232. data/lib/shoppe/orderable_item.rb +39 -0
  233. data/lib/shoppe/settings.rb +26 -0
  234. data/lib/shoppe/settings_loader.rb +16 -0
  235. data/lib/shoppe/setup_generator.rb +10 -0
  236. data/lib/shoppe/version.rb +3 -0
  237. data/lib/shoppe/view_helpers.rb +16 -0
  238. data/lib/tasks/shoppe.rake +29 -0
  239. 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