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,69 @@
1
+ module Shoppe
2
+ class Order < ActiveRecord::Base
3
+
4
+ # An array of all the available statuses for an order
5
+ STATUSES = ['building', 'confirming', 'received', 'accepted', 'rejected', 'shipped']
6
+
7
+ # The Shoppe::User who accepted the order
8
+ #
9
+ # @return [Shoppe::User]
10
+ belongs_to :accepter, :class_name => 'Shoppe::User', :foreign_key => 'accepted_by'
11
+
12
+ # The Shoppe::User who rejected the order
13
+ #
14
+ # @return [Shoppe::User]
15
+ belongs_to :rejecter, :class_name => 'Shoppe::User', :foreign_key => 'rejected_by'
16
+
17
+ # Validations
18
+ validates :status, :inclusion => {:in => STATUSES}
19
+
20
+ # Set the status to building if we don't have a status
21
+ after_initialize { self.status = STATUSES.first if self.status.blank? }
22
+
23
+ # All orders which have been received
24
+ scope :received, -> {where("received_at is not null")}
25
+
26
+ # All orders which are currently pending acceptance/rejection
27
+ scope :pending, -> { where(:status => 'received') }
28
+
29
+ # All ordered ordered by their ID desending
30
+ scope :ordered, -> { order(:id => :desc)}
31
+
32
+ # Is this order still being built by the user?
33
+ #
34
+ # @return [Boolean]
35
+ def building?
36
+ self.status == 'building'
37
+ end
38
+
39
+ # Is this order in the user confirmation step?
40
+ #
41
+ # @return [Boolean]
42
+ def confirming?
43
+ self.status == 'confirming'
44
+ end
45
+
46
+ # Has this order been rejected?
47
+ #
48
+ # @return [Boolean]
49
+ def rejected?
50
+ !!self.rejected_at
51
+ end
52
+
53
+ # Has this order been accepted?
54
+ #
55
+ # @return [Boolean]
56
+ def accepted?
57
+ !!self.accepted_at
58
+ end
59
+
60
+ # Has the order been received?
61
+ #
62
+ # @return [Boolean]
63
+ def received?
64
+ !!self.received_at?
65
+ end
66
+
67
+
68
+ end
69
+ end
@@ -0,0 +1,239 @@
1
+ module Shoppe
2
+ class OrderItem < ActiveRecord::Base
3
+
4
+ self.table_name = 'shoppe_order_items'
5
+
6
+ # The associated order
7
+ #
8
+ # @return [Shoppe::Order]
9
+ belongs_to :order, :class_name => 'Shoppe::Order', :touch => true, :inverse_of => :order_items
10
+
11
+ # The item which has been ordered
12
+ belongs_to :ordered_item, :polymorphic => true
13
+
14
+ # Any stock level adjustments which have been made for this order item
15
+ has_many :stock_level_adjustments, :as => :parent, :dependent => :nullify, :class_name => 'Shoppe::StockLevelAdjustment'
16
+
17
+ # Validations
18
+ validates :quantity, :numericality => true
19
+ validates :ordered_item, :presence => true
20
+
21
+ validate do
22
+ unless in_stock?
23
+ errors.add :quantity, :too_high_quantity
24
+ end
25
+ end
26
+
27
+ # Before saving an order item which belongs to a received order, cache the pricing again if appropriate.
28
+ before_save do
29
+ if order.received? && (unit_price_changed? || unit_cost_price_changed? || tax_rate_changed? || tax_amount_changed?)
30
+ cache_pricing
31
+ end
32
+ end
33
+
34
+ # After saving, if the order has been shipped, reallocate stock appropriate
35
+ after_save do
36
+ if order.shipped?
37
+ allocate_unallocated_stock!
38
+ end
39
+ end
40
+
41
+ # This allows you to add a product to the scoped order. For example Order.first.order_items.add_product(...).
42
+ # This will either increase the quantity of the value in the order or create a new item if one does not
43
+ # exist already.
44
+ #
45
+ # @param ordered_item [Object] an object which implements the Shoppe::OrderableItem protocol
46
+ # @param quantity [Fixnum] the number of items to order
47
+ # @return [Shoppe::OrderItem]
48
+ def self.add_item(ordered_item, quantity = 1)
49
+ raise Errors::UnorderableItem, :ordered_item => ordered_item unless ordered_item.orderable?
50
+ transaction do
51
+ if existing = self.where(:ordered_item_id => ordered_item.id, :ordered_item_type => ordered_item.class.to_s).first
52
+ existing.increase!(quantity)
53
+ existing
54
+ else
55
+ new_item = self.create(:ordered_item => ordered_item, :quantity => 0)
56
+ new_item.increase!(quantity)
57
+ new_item
58
+ end
59
+ end
60
+ end
61
+
62
+ # Remove a product from an order. It will also ensure that the order's custom delivery
63
+ # service is updated if appropriate.
64
+ #
65
+ # @return [Shoppe::OrderItem]
66
+ def remove
67
+ transaction do
68
+ self.destroy!
69
+ self.order.remove_delivery_service_if_invalid
70
+ self
71
+ end
72
+ end
73
+
74
+ # Increases the quantity of items in the order by the number provided. Will raise an error if we don't have
75
+ # the stock to do this.
76
+ #
77
+ # @param quantity [Fixnum]
78
+ def increase!(amount = 1)
79
+ transaction do
80
+ self.quantity += amount
81
+ unless self.in_stock?
82
+ raise Shoppe::Errors::NotEnoughStock, :ordered_item => self.ordered_item, :requested_stock => self.quantity
83
+ end
84
+ self.save!
85
+ self.order.remove_delivery_service_if_invalid
86
+ end
87
+ end
88
+
89
+ # Decreases the quantity of items in the order by the number provided.
90
+ #
91
+ # @param amount [Fixnum]
92
+ def decrease!(amount = 1)
93
+ transaction do
94
+ self.quantity -= amount
95
+ self.quantity == 0 ? self.destroy : self.save!
96
+ self.order.remove_delivery_service_if_invalid
97
+ end
98
+ end
99
+
100
+ # The total weight of the item
101
+ #
102
+ # @return [BigDecimal]
103
+ def weight
104
+ read_attribute(:weight) || ordered_item.try(:weight) || BigDecimal(0)
105
+ end
106
+
107
+ # Return the total weight of the item
108
+ #
109
+ # @return [BigDecimal]
110
+ def total_weight
111
+ quantity * weight
112
+ end
113
+
114
+ # The unit price for the item
115
+ #
116
+ # @return [BigDecimal]
117
+ def unit_price
118
+ read_attribute(:unit_price) || ordered_item.try(:price) || BigDecimal(0)
119
+ end
120
+
121
+ # The cost price for the item
122
+ #
123
+ # @return [BigDecimal]
124
+ def unit_cost_price
125
+ read_attribute(:unit_cost_price) || ordered_item.try(:cost_price) || BigDecimal(0)
126
+ end
127
+
128
+ # The tax rate for the item
129
+ #
130
+ # @return [BigDecimal]
131
+ def tax_rate
132
+ read_attribute(:tax_rate) || ordered_item.try(:tax_rate).try(:rate_for, self.order) || BigDecimal(0)
133
+ end
134
+
135
+ # The total tax for the item
136
+ #
137
+ # @return [BigDecimal]
138
+ def tax_amount
139
+ read_attribute(:tax_amount) || (self.sub_total / BigDecimal(100)) * self.tax_rate
140
+ end
141
+
142
+ # The total cost for the product
143
+ #
144
+ # @return [BigDecimal]
145
+ def total_cost
146
+ quantity * unit_cost_price
147
+ end
148
+
149
+ # The sub total for the product
150
+ #
151
+ # @return [BigDecimal]
152
+ def sub_total
153
+ quantity * unit_price
154
+ end
155
+
156
+ # The total price including tax for the order line
157
+ #
158
+ # @return [BigDecimal]
159
+ def total
160
+ tax_amount + sub_total
161
+ end
162
+
163
+ # Cache the pricing for this order item
164
+ def cache_pricing
165
+ write_attribute :weight, self.weight
166
+ write_attribute :unit_price, self.unit_price
167
+ write_attribute :unit_cost_price, self.unit_cost_price
168
+ write_attribute :tax_rate, self.tax_rate
169
+ end
170
+
171
+ # Cache the pricing for this order item and save
172
+ def cache_pricing!
173
+ cache_pricing
174
+ save!
175
+ end
176
+
177
+ # Trigger when the associated order is confirmed. It handles caching the values
178
+ # of the monetary items and allocating stock as appropriate.
179
+ def confirm!
180
+ cache_pricing!
181
+ allocate_unallocated_stock!
182
+ end
183
+
184
+ # Trigger when the associated order is accepted
185
+ def accept!
186
+ end
187
+
188
+ # Trigged when the associated order is rejected..
189
+ def reject!
190
+ self.stock_level_adjustments.destroy_all
191
+ end
192
+
193
+ # Do we have the stock needed to fulfil this order?
194
+ #
195
+ # @return [Boolean]
196
+ def in_stock?
197
+ if self.ordered_item && self.ordered_item.stock_control?
198
+ self.ordered_item.stock >= unallocated_stock
199
+ else
200
+ true
201
+ end
202
+ end
203
+
204
+ # How much stock remains to be allocated for this order?
205
+ #
206
+ # @return [Fixnum]
207
+ def unallocated_stock
208
+ self.quantity - allocated_stock
209
+ end
210
+
211
+ # How much stock has been allocated to this item?
212
+ #
213
+ # @return [Fixnum]
214
+ def allocated_stock
215
+ 0 - self.stock_level_adjustments.sum(:adjustment)
216
+ end
217
+
218
+ # Validate the stock level against the product and update as appropriate. This method will be executed
219
+ # before an order is completed. If we have run out of this product, we will update the quantity to an
220
+ # appropriate level (or remove the order item) and return the object.
221
+ def validate_stock_levels
222
+ if in_stock?
223
+ false
224
+ else
225
+ self.quantity = self.ordered_item.stock
226
+ self.quantity == 0 ? self.destroy : self.save!
227
+ self
228
+ end
229
+ end
230
+
231
+ # Allocate any unallocated stock for this order item. There is no return value.
232
+ def allocate_unallocated_stock!
233
+ if self.ordered_item.stock_control? && self.unallocated_stock != 0
234
+ self.ordered_item.stock_level_adjustments.create!(:parent => self, :adjustment => 0 - self.unallocated_stock, :description => "Order ##{self.order.number}")
235
+ end
236
+ end
237
+
238
+ end
239
+ end
@@ -0,0 +1,86 @@
1
+ module Shoppe
2
+ class Payment < ActiveRecord::Base
3
+
4
+ # Additional callbacks
5
+ extend ActiveModel::Callbacks
6
+ define_model_callbacks :refund
7
+
8
+ # The associated order
9
+ #
10
+ # @return [Shoppe::Order]
11
+ belongs_to :order, :class_name => 'Shoppe::Order'
12
+
13
+ # An associated payment (only applies to refunds)
14
+ #
15
+ # @return [Shoppe::Payment]
16
+ belongs_to :parent, :class_name => "Shoppe::Payment", :foreign_key => "parent_payment_id"
17
+
18
+ # Validations
19
+ validates :amount, :numericality => true
20
+ validates :reference, :presence => true
21
+ validates :method, :presence => true
22
+
23
+ # Payments can have associated properties
24
+ key_value_store :properties
25
+
26
+ # Callbacks
27
+ after_create :cache_amount_paid
28
+ after_destroy :cache_amount_paid
29
+ before_destroy { self.parent.update_attribute(:amount_refunded, self.parent.amount_refunded + amount) if self.parent }
30
+
31
+ # Is this payment a refund?
32
+ #
33
+ # @return [Boolean]
34
+ def refund?
35
+ self.amount < BigDecimal(0)
36
+ end
37
+
38
+ # Has this payment had any refunds taken from it?
39
+ #
40
+ # @return [Boolean]
41
+ def refunded?
42
+ self.amount_refunded > BigDecimal(0)
43
+ end
44
+
45
+ # How much of the payment can be refunded
46
+ #
47
+ # @return [BigDecimal]
48
+ def refundable_amount
49
+ refundable? ? (self.amount - self.amount_refunded) : BigDecimal(0)
50
+ end
51
+
52
+ # Process a refund from this payment.
53
+ #
54
+ # @param amount [String] the amount which should be refunded
55
+ # @return [Boolean]
56
+ def refund!(amount)
57
+ run_callbacks :refund do
58
+ amount = BigDecimal(amount)
59
+ if refundable_amount >= amount
60
+ transaction do
61
+ self.class.create(:parent => self, :order_id => self.order_id, :amount => 0-amount, :method => self.method, :reference => reference)
62
+ self.update_attribute(:amount_refunded, self.amount_refunded + amount)
63
+ true
64
+ end
65
+ else
66
+ raise Shoppe::Errors::RefundFailed, :message => I18n.t('.refund_failed', refundable_amount: refundable_amount)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Return a transaction URL for viewing further information about this
72
+ # payment.
73
+ #
74
+ # @return [String]
75
+ def transaction_url
76
+ nil
77
+ end
78
+
79
+ private
80
+
81
+ def cache_amount_paid
82
+ self.order.update_attribute(:amount_paid, self.order.payments.sum(:amount))
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,189 @@
1
+ require "roo"
2
+ require "globalize"
3
+
4
+ module Shoppe
5
+ class Product < ActiveRecord::Base
6
+
7
+ self.table_name = 'shoppe_products'
8
+
9
+ # Add dependencies for products
10
+ require_dependency 'shoppe/product/product_attributes'
11
+ require_dependency 'shoppe/product/variants'
12
+
13
+ # Products have a default_image and a data_sheet
14
+ attachment :default_image
15
+ attachment :data_sheet
16
+
17
+ # The product's categorizations
18
+ #
19
+ # @return [Shoppe::ProductCategorization]
20
+ has_many :product_categorizations, dependent: :destroy, class_name: 'Shoppe::ProductCategorization', inverse_of: :product
21
+ # The product's categories
22
+ #
23
+ # @return [Shoppe::ProductCategory]
24
+ has_many :product_categories, class_name: 'Shoppe::ProductCategory', through: :product_categorizations
25
+
26
+ # The product's tax rate
27
+ #
28
+ # @return [Shoppe::TaxRate]
29
+ belongs_to :tax_rate, :class_name => "Shoppe::TaxRate"
30
+
31
+ # Ordered items which are associated with this product
32
+ has_many :order_items, :dependent => :restrict_with_exception, :class_name => 'Shoppe::OrderItem', :as => :ordered_item
33
+
34
+ # Orders which have ordered this product
35
+ has_many :orders, :through => :order_items, :class_name => 'Shoppe::Order'
36
+
37
+ # Stock level adjustments for this product
38
+ has_many :stock_level_adjustments, :dependent => :destroy, :class_name => 'Shoppe::StockLevelAdjustment', :as => :item
39
+
40
+ # Validations
41
+ with_options :if => Proc.new { |p| p.parent.nil? } do |product|
42
+ product.validate :has_at_least_one_product_category
43
+ product.validates :description, :presence => true
44
+ product.validates :short_description, :presence => true
45
+ end
46
+ validates :name, :presence => true
47
+ validates :permalink, :presence => true, :uniqueness => true, :permalink => true
48
+ validates :sku, :presence => true
49
+ validates :weight, :numericality => true
50
+ validates :price, :numericality => true
51
+ validates :cost_price, :numericality => true, :allow_blank => true
52
+
53
+ # Before validation, set the permalink if we don't already have one
54
+ before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
55
+
56
+ # All active products
57
+ scope :active, -> { where(:active => true) }
58
+
59
+ # All featured products
60
+ scope :featured, -> {where(:featured => true)}
61
+
62
+ # Localisations
63
+ translates :name, :permalink, :description, :short_description
64
+ scope :ordered, -> { includes(:translations).order(:name) }
65
+
66
+ # Return the name of the product
67
+ #
68
+ # @return [String]
69
+ def full_name
70
+ self.parent ? "#{self.parent.name} (#{name})" : name
71
+ end
72
+
73
+ # Is this product orderable?
74
+ #
75
+ # @return [Boolean]
76
+ def orderable?
77
+ return false unless self.active?
78
+ return false if self.has_variants?
79
+ true
80
+ end
81
+
82
+ # The price for the product
83
+ #
84
+ # @return [BigDecimal]
85
+ def price
86
+ # self.default_variant ? self.default_variant.price : read_attribute(:price)
87
+ self.default_variant ? self.default_variant.price : read_attribute(:price)
88
+ end
89
+
90
+ # Is this product currently in stock?
91
+ #
92
+ # @return [Boolean]
93
+ def in_stock?
94
+ self.default_variant ? self.default_variant.in_stock? : (stock_control? ? stock > 0 : true)
95
+ end
96
+
97
+ # Return the total number of items currently in stock
98
+ #
99
+ # @return [Fixnum]
100
+ def stock
101
+ self.stock_level_adjustments.sum(:adjustment)
102
+ end
103
+
104
+ # Return the first product category
105
+ #
106
+ # @return [Shoppe::ProductCategory]
107
+ def product_category
108
+ self.product_categories.first rescue nil
109
+ end
110
+
111
+ # Search for products which include the given attributes and return an active record
112
+ # scope of these products. Chainable with other scopes and with_attributes methods.
113
+ # For example:
114
+ #
115
+ # Shoppe::Product.active.with_attribute('Manufacturer', 'Apple').with_attribute('Model', ['Macbook', 'iPhone'])
116
+ #
117
+ # @return [Enumerable]
118
+ def self.with_attributes(key, values)
119
+ product_ids = Shoppe::ProductAttribute.searchable.where(:key => key, :value => values).pluck(:product_id).uniq
120
+ where(:id => product_ids)
121
+ end
122
+
123
+ # Imports products from a spreadsheet file
124
+ # Example:
125
+ #
126
+ # Shoppe:Product.import("path/to/file.csv")
127
+ def self.import(file)
128
+ spreadsheet = open_spreadsheet(file)
129
+ spreadsheet.default_sheet = spreadsheet.sheets.first
130
+ header = spreadsheet.row(1)
131
+ (2..spreadsheet.last_row).each do |i|
132
+ row = Hash[[header, spreadsheet.row(i)].transpose]
133
+
134
+ # Don't import products where the name is blank
135
+ unless row["name"].nil?
136
+ if product = find_by(name: row["name"])
137
+ # Dont import products with the same name but update quantities if they're not the same
138
+ qty = row["qty"].to_i
139
+ if qty > 0 && qty != product.stock
140
+ product.stock_level_adjustments.create!(description: I18n.t('shoppe.import'), adjustment: qty)
141
+ end
142
+ else
143
+ product = new
144
+ product.name = row["name"]
145
+ product.sku = row["sku"]
146
+ product.description = row["description"]
147
+ product.short_description = row["short_description"]
148
+ product.weight = row["weight"]
149
+ product.price = row["price"].nil? ? 0 : row["price"]
150
+ product.permalink = row["permalink"]
151
+
152
+ product.product_categories << begin
153
+ if Shoppe::ProductCategory.find_by(name: row["category_name"]).present?
154
+ Shoppe::ProductCategory.find_by(name: row["category_name"])
155
+ else
156
+ Shoppe::ProductCategory.create(name: row["category_name"])
157
+ end
158
+ end
159
+
160
+ product.save!
161
+
162
+ qty = row["qty"].to_i
163
+ if qty > 0
164
+ product.stock_level_adjustments.create!(description: I18n.t('shoppe.import'), adjustment: qty)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def self.open_spreadsheet(file)
172
+ case File.extname(file.original_filename)
173
+ when ".csv" then Roo::CSV.new(file.path)
174
+ when ".xls" then Roo::Excel.new(file.path)
175
+ when ".xlsx" then Roo::Excelx.new(file.path)
176
+ else raise I18n.t('shoppe.imports.errors.unknown_format', filename: File.original_filename)
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ # Validates
183
+
184
+ def has_at_least_one_product_category
185
+ errors.add(:base, 'must add at least one product category') if self.product_categories.blank?
186
+ end
187
+
188
+ end
189
+ end