tienda 1.0.0

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 (193) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +35 -0
  4. data/app/assets/images/tienda/chosen-sprite.png +0 -0
  5. data/app/assets/images/tienda/chosen-sprite@2x.png +0 -0
  6. data/app/assets/images/tienda/document.svg +1 -0
  7. data/app/assets/images/tienda/icons/bag.svg +1 -0
  8. data/app/assets/images/tienda/icons/balance.svg +1 -0
  9. data/app/assets/images/tienda/icons/box.svg +1 -0
  10. data/app/assets/images/tienda/icons/building.svg +1 -0
  11. data/app/assets/images/tienda/icons/chart.svg +1 -0
  12. data/app/assets/images/tienda/icons/chat.svg +1 -0
  13. data/app/assets/images/tienda/icons/checkbox.svg +1 -0
  14. data/app/assets/images/tienda/icons/checkbox2.svg +1 -0
  15. data/app/assets/images/tienda/icons/cloud.svg +1 -0
  16. data/app/assets/images/tienda/icons/cone.svg +1 -0
  17. data/app/assets/images/tienda/icons/credit_card.svg +1 -0
  18. data/app/assets/images/tienda/icons/currency.svg +1 -0
  19. data/app/assets/images/tienda/icons/edit.svg +14 -0
  20. data/app/assets/images/tienda/icons/flowchart.svg +1 -0
  21. data/app/assets/images/tienda/icons/gift.svg +1 -0
  22. data/app/assets/images/tienda/icons/globe.svg +1 -0
  23. data/app/assets/images/tienda/icons/id.svg +1 -0
  24. data/app/assets/images/tienda/icons/id2.svg +1 -0
  25. data/app/assets/images/tienda/icons/locked.svg +1 -0
  26. data/app/assets/images/tienda/icons/report.svg +1 -0
  27. data/app/assets/images/tienda/icons/search.svg +1 -0
  28. data/app/assets/images/tienda/icons/support.svg +1 -0
  29. data/app/assets/images/tienda/icons/tags.svg +1 -0
  30. data/app/assets/images/tienda/icons/toolbox.svg +1 -0
  31. data/app/assets/images/tienda/icons/unlocked.svg +1 -0
  32. data/app/assets/images/tienda/icons/wallet.svg +1 -0
  33. data/app/assets/images/tienda/logo.svg +47 -0
  34. data/app/assets/images/tienda/move.svg +1 -0
  35. data/app/assets/images/tienda/shoppe.svg +25 -0
  36. data/app/assets/images/tienda/square.svg +9 -0
  37. data/app/assets/images/tienda/statuses/accepted.svg +14 -0
  38. data/app/assets/images/tienda/statuses/paid.svg +16 -0
  39. data/app/assets/images/tienda/statuses/received.svg +15 -0
  40. data/app/assets/images/tienda/statuses/rejected.svg +14 -0
  41. data/app/assets/images/tienda/statuses/shipped.svg +14 -0
  42. data/app/assets/images/tienda/table-tear-off.png +0 -0
  43. data/app/assets/javascripts/tienda/application.coffee +119 -0
  44. data/app/assets/javascripts/tienda/chosen.jquery.js +1166 -0
  45. data/app/assets/javascripts/tienda/jquery_ui.js +6 -0
  46. data/app/assets/javascripts/tienda/mousetrap.js +9 -0
  47. data/app/assets/javascripts/tienda/order_form.coffee +47 -0
  48. data/app/assets/stylesheets/tienda/application.scss +585 -0
  49. data/app/assets/stylesheets/tienda/chosen.scss +424 -0
  50. data/app/assets/stylesheets/tienda/dialog.scss +25 -0
  51. data/app/assets/stylesheets/tienda/elements.scss +79 -0
  52. data/app/assets/stylesheets/tienda/printable.scss +67 -0
  53. data/app/assets/stylesheets/tienda/reset.scss +93 -0
  54. data/app/assets/stylesheets/tienda/sub.scss +106 -0
  55. data/app/assets/stylesheets/tienda/variables.scss +1 -0
  56. data/app/controllers/tienda/application_controller.rb +46 -0
  57. data/app/controllers/tienda/attachments_controller.rb +14 -0
  58. data/app/controllers/tienda/countries_controller.rb +47 -0
  59. data/app/controllers/tienda/dashboard_controller.rb +9 -0
  60. data/app/controllers/tienda/delivery_service_prices_controller.rb +45 -0
  61. data/app/controllers/tienda/delivery_services_controller.rb +47 -0
  62. data/app/controllers/tienda/orders_controller.rb +90 -0
  63. data/app/controllers/tienda/payments_controller.rb +33 -0
  64. data/app/controllers/tienda/product_categories_controller.rb +47 -0
  65. data/app/controllers/tienda/products_controller.rb +58 -0
  66. data/app/controllers/tienda/sessions_controller.rb +34 -0
  67. data/app/controllers/tienda/settings_controller.rb +16 -0
  68. data/app/controllers/tienda/stock_level_adjustments_controller.rb +40 -0
  69. data/app/controllers/tienda/tax_rates_controller.rb +49 -0
  70. data/app/controllers/tienda/users_controller.rb +53 -0
  71. data/app/controllers/tienda/variants_controller.rb +50 -0
  72. data/app/helpers/tienda/application_helper.rb +60 -0
  73. data/app/mailers/tienda/order_mailer.rb +25 -0
  74. data/app/mailers/tienda/user_mailer.rb +10 -0
  75. data/app/models/tienda/country.rb +27 -0
  76. data/app/models/tienda/delivery_service.rb +33 -0
  77. data/app/models/tienda/delivery_service_price.rb +31 -0
  78. data/app/models/tienda/order/actions.rb +98 -0
  79. data/app/models/tienda/order/billing.rb +105 -0
  80. data/app/models/tienda/order/delivery.rb +229 -0
  81. data/app/models/tienda/order/states.rb +69 -0
  82. data/app/models/tienda/order.rb +93 -0
  83. data/app/models/tienda/order_item.rb +239 -0
  84. data/app/models/tienda/payment.rb +80 -0
  85. data/app/models/tienda/product/product_attributes.rb +20 -0
  86. data/app/models/tienda/product/variants.rb +51 -0
  87. data/app/models/tienda/product.rb +169 -0
  88. data/app/models/tienda/product_attribute.rb +66 -0
  89. data/app/models/tienda/product_category.rb +23 -0
  90. data/app/models/tienda/setting.rb +68 -0
  91. data/app/models/tienda/stock_level_adjustment.rb +19 -0
  92. data/app/models/tienda/tax_rate.rb +46 -0
  93. data/app/models/tienda/user.rb +49 -0
  94. data/app/validators/permalink_validator.rb +7 -0
  95. data/app/views/layouts/tienda/application.html.haml +34 -0
  96. data/app/views/layouts/tienda/printable.html.haml +11 -0
  97. data/app/views/layouts/tienda/sub.html.haml +10 -0
  98. data/app/views/tienda/countries/_form.html.haml +35 -0
  99. data/app/views/tienda/countries/edit.html.haml +6 -0
  100. data/app/views/tienda/countries/index.html.haml +25 -0
  101. data/app/views/tienda/countries/new.html.haml +7 -0
  102. data/app/views/tienda/delivery_service_prices/_form.html.haml +44 -0
  103. data/app/views/tienda/delivery_service_prices/edit.html.haml +6 -0
  104. data/app/views/tienda/delivery_service_prices/index.html.haml +23 -0
  105. data/app/views/tienda/delivery_service_prices/new.html.haml +6 -0
  106. data/app/views/tienda/delivery_services/_form.html.haml +38 -0
  107. data/app/views/tienda/delivery_services/edit.html.haml +9 -0
  108. data/app/views/tienda/delivery_services/index.html.haml +27 -0
  109. data/app/views/tienda/delivery_services/new.html.haml +6 -0
  110. data/app/views/tienda/order_mailer/accepted.text.erb +12 -0
  111. data/app/views/tienda/order_mailer/received.text.erb +12 -0
  112. data/app/views/tienda/order_mailer/rejected.text.erb +10 -0
  113. data/app/views/tienda/order_mailer/shipped.text.erb +16 -0
  114. data/app/views/tienda/orders/_form.html.haml +55 -0
  115. data/app/views/tienda/orders/_order_details.html.haml +57 -0
  116. data/app/views/tienda/orders/_order_items.html.haml +38 -0
  117. data/app/views/tienda/orders/_order_items_form.html.haml +61 -0
  118. data/app/views/tienda/orders/_payments_form.html.haml +15 -0
  119. data/app/views/tienda/orders/_payments_table.html.haml +37 -0
  120. data/app/views/tienda/orders/_search_form.html.haml +24 -0
  121. data/app/views/tienda/orders/_status_bar.html.haml +35 -0
  122. data/app/views/tienda/orders/despatch_note.html.haml +45 -0
  123. data/app/views/tienda/orders/edit.html.haml +21 -0
  124. data/app/views/tienda/orders/index.html.haml +39 -0
  125. data/app/views/tienda/orders/new.html.haml +14 -0
  126. data/app/views/tienda/orders/show.html.haml +25 -0
  127. data/app/views/tienda/payments/refund.html.haml +13 -0
  128. data/app/views/tienda/product_categories/_form.html.haml +26 -0
  129. data/app/views/tienda/product_categories/edit.html.haml +6 -0
  130. data/app/views/tienda/product_categories/index.html.haml +19 -0
  131. data/app/views/tienda/product_categories/new.html.haml +6 -0
  132. data/app/views/tienda/products/_form.html.haml +118 -0
  133. data/app/views/tienda/products/_table.html.haml +42 -0
  134. data/app/views/tienda/products/edit.html.haml +8 -0
  135. data/app/views/tienda/products/import.html.haml +63 -0
  136. data/app/views/tienda/products/index.html.haml +9 -0
  137. data/app/views/tienda/products/new.html.haml +7 -0
  138. data/app/views/tienda/sessions/new.html.haml +12 -0
  139. data/app/views/tienda/sessions/reset.html.haml +12 -0
  140. data/app/views/tienda/settings/edit.html.haml +19 -0
  141. data/app/views/tienda/shared/error.html.haml +6 -0
  142. data/app/views/tienda/stock_level_adjustments/index.html.haml +40 -0
  143. data/app/views/tienda/tax_rates/form.html.haml +28 -0
  144. data/app/views/tienda/tax_rates/index.html.haml +17 -0
  145. data/app/views/tienda/user_mailer/new_password.text.erb +9 -0
  146. data/app/views/tienda/users/_form.html.haml +27 -0
  147. data/app/views/tienda/users/edit.html.haml +5 -0
  148. data/app/views/tienda/users/index.html.haml +17 -0
  149. data/app/views/tienda/users/new.html.haml +7 -0
  150. data/app/views/tienda/variants/form.html.haml +66 -0
  151. data/app/views/tienda/variants/index.html.haml +33 -0
  152. data/config/locales/en.yml +650 -0
  153. data/config/locales/pl.yml +650 -0
  154. data/config/locales/pt-BR.yml +643 -0
  155. data/config/routes.rb +42 -0
  156. data/db/countries.txt +252 -0
  157. data/db/migrate/20150124094549_create_tienda_initial_schema.rb +184 -0
  158. data/db/seeds.rb +128 -0
  159. data/db/seeds_data/poe400.jpg +0 -0
  160. data/db/seeds_data/snom-870-blk.jpg +0 -0
  161. data/db/seeds_data/snom-870-grey.jpg +0 -0
  162. data/db/seeds_data/snom-mm2.jpg +0 -0
  163. data/db/seeds_data/spa303.jpg +0 -0
  164. data/db/seeds_data/t18p.jpg +0 -0
  165. data/db/seeds_data/t20p.jpg +0 -0
  166. data/db/seeds_data/t22p.jpg +0 -0
  167. data/db/seeds_data/t26p.jpg +0 -0
  168. data/db/seeds_data/t41pn.jpg +0 -0
  169. data/db/seeds_data/t46gn.jpg +0 -0
  170. data/db/seeds_data/w52p.jpg +0 -0
  171. data/db/seeds_data/yhs32.jpg +0 -0
  172. data/lib/tasks/tienda.rake +29 -0
  173. data/lib/tienda/associated_countries.rb +20 -0
  174. data/lib/tienda/country_importer.rb +14 -0
  175. data/lib/tienda/default_navigation.rb +20 -0
  176. data/lib/tienda/engine.rb +48 -0
  177. data/lib/tienda/error.rb +21 -0
  178. data/lib/tienda/errors/inappropriate_delivery_service.rb +6 -0
  179. data/lib/tienda/errors/insufficient_stock_to_fulfil.rb +15 -0
  180. data/lib/tienda/errors/invalid_configuration.rb +6 -0
  181. data/lib/tienda/errors/not_enough_stock.rb +15 -0
  182. data/lib/tienda/errors/payment_declined.rb +6 -0
  183. data/lib/tienda/errors/refund_failed.rb +6 -0
  184. data/lib/tienda/errors/unorderable_item.rb +6 -0
  185. data/lib/tienda/navigation_manager.rb +81 -0
  186. data/lib/tienda/orderable_item.rb +39 -0
  187. data/lib/tienda/settings.rb +26 -0
  188. data/lib/tienda/settings_loader.rb +16 -0
  189. data/lib/tienda/setup_generator.rb +10 -0
  190. data/lib/tienda/version.rb +3 -0
  191. data/lib/tienda/view_helpers.rb +16 -0
  192. data/lib/tienda.rb +59 -0
  193. metadata +604 -0
@@ -0,0 +1,229 @@
1
+ module Tienda
2
+ class Order < ActiveRecord::Base
3
+
4
+ # The associated delivery service
5
+ #
6
+ # @return [Tienda::DeliveryService]
7
+ belongs_to :delivery_service, :class_name => 'Tienda::DeliveryService'
8
+
9
+ # The country where this order is being delivered to (if one has been provided)
10
+ #
11
+ # @return [Tienda::Country]
12
+ belongs_to :delivery_country, :class_name => 'Tienda::Country', :foreign_key => 'delivery_country_id'
13
+
14
+ # The user who marked the order has shipped
15
+ #
16
+ # @return [Tienda::User]
17
+ belongs_to :shipper, :class_name => 'Tienda::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 Tienda::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 Tienda::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 Tienda:DeliveryServicePrice objects
138
+ def delivery_service_prices
139
+ if delivery_required?
140
+ prices = Tienda::DeliveryServicePrice.joins(:delivery_service).where(:tienda_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 [Tienda::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
+ Tienda::OrderMailer.shipped(self).deliver
225
+ end
226
+ end
227
+
228
+ end
229
+ end
@@ -0,0 +1,69 @@
1
+ module Tienda
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 Tienda::User who accepted the order
8
+ #
9
+ # @return [Tienda::User]
10
+ belongs_to :accepter, :class_name => 'Tienda::User', :foreign_key => 'accepted_by'
11
+
12
+ # The Tienda::User who rejected the order
13
+ #
14
+ # @return [Tienda::User]
15
+ belongs_to :rejecter, :class_name => 'Tienda::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,93 @@
1
+ module Tienda
2
+ class Order < ActiveRecord::Base
3
+
4
+ self.table_name = 'tienda_orders'
5
+
6
+ # Orders can have properties
7
+ key_value_store :properties
8
+
9
+ # Require dependencies
10
+ require_dependency 'tienda/order/states'
11
+ require_dependency 'tienda/order/actions'
12
+ require_dependency 'tienda/order/billing'
13
+ require_dependency 'tienda/order/delivery'
14
+
15
+ # All items which make up this order
16
+ has_many :order_items, :dependent => :destroy, :class_name => 'Tienda::OrderItem'
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 => 'Tienda::Product', :source => :ordered_item, :source_type => 'Tienda::Product'
21
+
22
+ # Validations
23
+ validates :token, :presence => true
24
+ with_options :if => Proc.new { |o| !o.building? } do |order|
25
+ order.validates :email_address, :format => {:with => /\A\b[A-Z0-9\.\_\%\-\+]+@(?:[A-Z0-9\-]+\.)+[A-Z]{2,6}\b\z/i}
26
+ order.validates :phone_number, :format => {:with => /\A[\d\ \-x\(\)]{7,}\z/}
27
+ end
28
+
29
+ # Set some defaults
30
+ before_validation { self.token = SecureRandom.uuid if self.token.blank? }
31
+
32
+ # The order number
33
+ #
34
+ # @return [String] - the order number padded with at least 5 zeros
35
+ def number
36
+ id ? id.to_s.rjust(6, '0') : nil
37
+ end
38
+
39
+ # The length of time the customer spent building the order before submitting it to us.
40
+ # The time from first item in basket to received.
41
+ #
42
+ # @return [Float] - the length of time
43
+ def build_time
44
+ return nil if self.received_at.blank?
45
+ self.created_at - self.received_at
46
+ end
47
+
48
+ # The name of the customer in the format of "Company (First Last)" or if they don't have
49
+ # company specified, just "First Last".
50
+ #
51
+ # @return [String]
52
+ def customer_name
53
+ company.blank? ? full_name : "#{company} (#{full_name})"
54
+ end
55
+
56
+ # The full name of the customer created by concatinting the first & last name
57
+ #
58
+ # @return [String]
59
+ def full_name
60
+ "#{first_name} #{last_name}"
61
+ end
62
+
63
+ # Is this order empty? (i.e. doesn't have any items associated with it)
64
+ #
65
+ # @return [Boolean]
66
+ def empty?
67
+ order_items.empty?
68
+ end
69
+
70
+ # Does this order have items?
71
+ #
72
+ # @return [Boolean]
73
+ def has_items?
74
+ total_items > 0
75
+ end
76
+
77
+ # Return the number of items in the order?
78
+ #
79
+ # @return [Integer]
80
+ def total_items
81
+ order_items.inject(0) { |t,i| t + i.quantity }
82
+ end
83
+
84
+ def self.ransackable_attributes(auth_object = nil)
85
+ ["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
86
+ end
87
+
88
+ def self.ransackable_associations(auth_object = nil)
89
+ []
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,239 @@
1
+ module Tienda
2
+ class OrderItem < ActiveRecord::Base
3
+
4
+ self.table_name = 'tienda_order_items'
5
+
6
+ # The associated order
7
+ #
8
+ # @return [Tienda::Order]
9
+ belongs_to :order, :class_name => 'Tienda::Order', :touch => true
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 => 'Tienda::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_hight_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 Tienda::OrderableItem protocol
46
+ # @param quantity [Fixnum] the number of items to order
47
+ # @return [Tienda::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 [Tienda::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 Tienda::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,80 @@
1
+ module Tienda
2
+ class Payment < ActiveRecord::Base
3
+
4
+ # The associated order
5
+ #
6
+ # @return [Tienda::Order]
7
+ belongs_to :order, :class_name => 'Tienda::Order'
8
+
9
+ # An associated payment (only applies to refunds)
10
+ #
11
+ # @return [Tienda::Payment]
12
+ belongs_to :parent, :class_name => "Tienda::Payment", :foreign_key => "parent_payment_id"
13
+
14
+ # Validatiosn
15
+ validates :amount, :numericality => true
16
+ validates :reference, :presence => true
17
+ validates :method, :presence => true
18
+
19
+ # Payments can have associated properties
20
+ key_value_store :properties
21
+
22
+ # Callbacks
23
+ after_create :cache_amount_paid
24
+ after_destroy :cache_amount_paid
25
+ before_destroy { self.parent.update_attribute(:amount_refunded, self.parent.amount_refunded + amount) if self.parent }
26
+
27
+ # Is this payment a refund?
28
+ #
29
+ # @return [Boolean]
30
+ def refund?
31
+ self.amount < BigDecimal(0)
32
+ end
33
+
34
+ # Has this payment had any refunds taken from it?
35
+ #
36
+ # @return [Boolean]
37
+ def refunded?
38
+ self.amount_refunded > BigDecimal(0)
39
+ end
40
+
41
+ # How much of the payment can be refunded
42
+ #
43
+ # @return [BigDecimal]
44
+ def refundable_amount
45
+ refundable? ? (self.amount - self.amount_refunded) : BigDecimal(0)
46
+ end
47
+
48
+ # Process a refund from this payment.
49
+ #
50
+ # @param amount [String] the amount which should be refunded
51
+ # @return [Boolean]
52
+ def refund!(amount)
53
+ amount = BigDecimal(amount)
54
+ if refundable_amount >= amount
55
+ transaction do
56
+ self.class.create(:parent => self, :order_id => self.order_id, :amount => 0-amount, :method => self.method, :reference => reference)
57
+ self.update_attribute(:amount_refunded, self.amount_refunded + amount)
58
+ true
59
+ end
60
+ else
61
+ raise Tienda::Errors::RefundFailed, :message => I18n.t('.refund_failed', refundable_amount: refundable_amount)
62
+ end
63
+ end
64
+
65
+ # Return a transaction URL for viewing further information about this
66
+ # payment.
67
+ #
68
+ # @return [String]
69
+ def transaction_url
70
+ nil
71
+ end
72
+
73
+ private
74
+
75
+ def cache_amount_paid
76
+ self.order.update_attribute(:amount_paid, self.order.payments.sum(:amount))
77
+ end
78
+
79
+ end
80
+ end