tienda 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,53 @@
1
+ module Tienda
2
+ class UsersController < Tienda::ApplicationController
3
+
4
+ before_filter { @active_nav = :users }
5
+ before_filter { params[:id] && @user = Tienda::User.find(params[:id]) }
6
+ before_filter(only: [:create, :update, :destroy]) do
7
+ if Tienda.settings.demo_mode?
8
+ raise Tienda::Error, t('tienda.users.demo_mode_error')
9
+ end
10
+ end
11
+
12
+ def index
13
+ @users = Tienda::User.all
14
+ end
15
+
16
+ def new
17
+ @user = Tienda::User.new
18
+ end
19
+
20
+ def create
21
+ @user = Tienda::User.new(safe_params)
22
+ if @user.save
23
+ redirect_to :users, flash: { notice: t('tienda.users.create_notice') }
24
+ else
25
+ render action: "new"
26
+ end
27
+ end
28
+
29
+ def edit
30
+ end
31
+
32
+ def update
33
+ if @user.update(safe_params)
34
+ redirect_to [:edit, @user], flash: { notice t('tienda.users.update_notice') }
35
+ else
36
+ render action: "edit"
37
+ end
38
+ end
39
+
40
+ def destroy
41
+ raise Tienda::Error, t('tienda.users.self_remove_error') if @user == current_user
42
+ @user.destroy
43
+ redirect_to :users, flash: { notice: t('tienda.users.destroy_notice') }
44
+ end
45
+
46
+ private
47
+
48
+ def safe_params
49
+ params[:user].permit(:first_name, :last_name, :email_address, :password, :password_confirmation)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ module Tienda
2
+ class VariantsController < ApplicationController
3
+
4
+ before_filter { @active_nav = :products }
5
+ before_filter { @product = Tienda::Product.find(params[:product_id]) }
6
+ before_filter { params[:id] && @variant = @product.variants.find(params[:id]) }
7
+
8
+ def index
9
+ @variants = @product.variants.ordered
10
+ end
11
+
12
+ def new
13
+ @variant = @product.variants.build
14
+ render action: "form"
15
+ end
16
+
17
+ def create
18
+ @variant = @product.variants.build(safe_params)
19
+ if @variant.save
20
+ redirect_to [@product, :variants], notice: t('tienda.variants.create_notice')
21
+ else
22
+ render action: "form"
23
+ end
24
+ end
25
+
26
+ def edit
27
+ render action: "form"
28
+ end
29
+
30
+ def update
31
+ if @variant.update(safe_params)
32
+ redirect_to edit_product_variant_path(@product, @variant), notice: t('tienda.variants.update_notice')
33
+ else
34
+ render action: "form"
35
+ end
36
+ end
37
+
38
+ def destroy
39
+ @variant.destroy
40
+ redirect_to [@product, :variants], notice: t('tienda.variants.destroy_notice')
41
+ end
42
+
43
+ private
44
+
45
+ def safe_params
46
+ params[:product].permit(:name, :permalink, :sku, :default_image_file, :price, :cost_price, :tax_rate_id, :weight, :stock_control, :active, :default)
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,60 @@
1
+ module Tienda
2
+ module ApplicationHelper
3
+
4
+ def navigation_manager_link(item)
5
+ link_to item.description, item.url(self), item.link_options.merge(:class => item.active?(self) ? 'active' : 'inactive')
6
+ end
7
+
8
+ def status_tag(status)
9
+ content_tag :span, t("tienda.orders.statuses.#{status}"), :class => "status-tag #{status}"
10
+ end
11
+
12
+ def attachment_preview(attachment, options = {})
13
+ if attachment
14
+ String.new.tap do |s|
15
+ if attachment.image?
16
+ style = "style='background-image:url(#{attachment.path})'"
17
+ else
18
+ style = ''
19
+ end
20
+ s << "<div class='attachmentPreview #{attachment.image? ? 'image' : 'doc'}'>"
21
+ s << "<div class='imgContainer'><div class='img' #{style}></div></div>"
22
+ s << "<div class='desc'>"
23
+ s << "<span class='filename'><a href='#{attachment_path(attachment)}'>#{attachment.file_name}</a></span>"
24
+ s << "<span class='delete'>"
25
+ s << link_to(t('helpers.attachment_preview.delete', :default => 'Delete this file?'), attachment_path(attachment), :method => :delete, :data => {:confirm => t('helpers.attachment_preview.delete_confirm', :default => "Are you sure you wish to remove this attachment?")})
26
+ s << "</span>"
27
+ s << "</div>"
28
+ s << "</div>"
29
+ end.html_safe
30
+ elsif !options[:hide_if_blank]
31
+ "<div class='attachmentPreview'><div class='imgContainer'><div class='img none'></div></div><div class='desc none'>#{t('helpers.attachment_preview.no_attachment')},</div></div>".html_safe
32
+ end
33
+ end
34
+
35
+ def settings_label(field)
36
+ "<label for='settings_#{field}'>#{t("tienda.settings.labels.#{field}")}</label>".html_safe
37
+ end
38
+
39
+ def settings_field(field, options = {})
40
+ default = I18n.t("tienda.settings.defaults")[field.to_sym]
41
+ value = (params[:settings] && params[:settings][field]) || Tienda.settings[field.to_s]
42
+ type = I18n.t("tienda.settings.types")[field.to_sym] || 'string'
43
+ case type
44
+ when 'boolean'
45
+ String.new.tap do |s|
46
+ value = default if value.blank?
47
+ s << "<div class='radios'>"
48
+ s << radio_button_tag("settings[#{field}]", 'true', value == true, :id => "settings_#{field}_true")
49
+ s << label_tag("settings_#{field}_true", t("tienda.settings.options.#{field}.affirmative", :default => 'Yes'))
50
+ s << radio_button_tag("settings[#{field}]", 'false', value == false, :id => "settings_#{field}_false")
51
+ s << label_tag("settings_#{field}_false", t("tienda.settings.options.#{field}.negative", :default => 'No'))
52
+ s << "</div>"
53
+ end.html_safe
54
+ else
55
+ text_field_tag "settings[#{field}]", value, options.merge(:placeholder => default, :class => 'text')
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module Tienda
2
+ class OrderMailer < ActionMailer::Base
3
+
4
+ def received(order)
5
+ @order = order
6
+ mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.subject', :default => "Order Confirmation")
7
+ end
8
+
9
+ def accepted(order)
10
+ @order = order
11
+ mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.accepted', :default => "Order Accepted")
12
+ end
13
+
14
+ def rejected(order)
15
+ @order = order
16
+ mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.rejected', :default => "Order Rejected")
17
+ end
18
+
19
+ def shipped(order)
20
+ @order = order
21
+ mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.shipped', :default => "Order Shipped")
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module Tienda
2
+ class UserMailer < ActionMailer::Base
3
+
4
+ def new_password(user)
5
+ @user = user
6
+ mail :from => Tienda.settings.outbound_email_address, :to => user.email_address, :subject => "Your new Tienda password"
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ module Tienda
2
+
3
+ # The Tienda::Country model stores countries which can be used for delivery & billing
4
+ # addresses for orders.
5
+ #
6
+ # You can use the Tienda::CountryImporter to import a pre-defined list of countries
7
+ # into your database. This automatically happens when you run the 'tienda:setup'
8
+ # rake task.
9
+
10
+ class Country < ActiveRecord::Base
11
+
12
+ self.table_name = 'tienda_countries'
13
+
14
+ # All orders which have this country set as their billing country
15
+ has_many :billed_orders, :dependent => :restrict_with_exception, :class_name => 'Tienda::Order', :foreign_key => 'billing_country_id'
16
+
17
+ # All orders which have this country set as their delivery country
18
+ has_many :delivered_orders, :dependent => :restrict_with_exception, :class_name => 'Tienda::Order', :foreign_key => 'delivery_country_id'
19
+
20
+ # All countries ordered by their name asending
21
+ scope :ordered, -> { order(:name => :asc) }
22
+
23
+ # Validations
24
+ validates :name, :presence => true
25
+
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ module Tienda
2
+ class DeliveryService < ActiveRecord::Base
3
+
4
+ self.table_name = 'tienda_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 => 'Tienda::Order'
12
+
13
+ # Prices for the different levels of service within this delivery service
14
+ has_many :delivery_service_prices, :dependent => :destroy, :class_name => 'Tienda::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 [Tienda::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 Tienda
2
+ class DeliveryServicePrice < ActiveRecord::Base
3
+
4
+ # Set the table name
5
+ self.table_name = 'tienda_delivery_service_prices'
6
+
7
+ include Tienda::AssociatedCountries
8
+
9
+ # The delivery service which this price belongs to
10
+ belongs_to :delivery_service, :class_name => 'Tienda::DeliveryService'
11
+
12
+ # The tax rate which should be applied
13
+ belongs_to :tax_rate, :class_name => "Tienda::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,98 @@
1
+ module Tienda
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 Tienda::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 [Tienda::User] the user who carried out this action
56
+ def accept!(user = nil)
57
+ transaction do
58
+ run_callbacks :acceptance do
59
+ self.accepted_at = Time.now
60
+ self.accepter = user if user
61
+ self.status = 'accepted'
62
+ self.save!
63
+ self.order_items.each(&:accept!)
64
+ deliver_accepted_order_email
65
+ end
66
+ end
67
+ end
68
+
69
+ # Mark order as rejected
70
+ #
71
+ # @param user [Tienda::User] the user who carried out the action
72
+ def reject!(user = nil)
73
+ transaction do
74
+ run_callbacks :rejection do
75
+ self.rejected_at = Time.now
76
+ self.rejecter = user if user
77
+ self.status = 'rejected'
78
+ self.save!
79
+ self.order_items.each(&:reject!)
80
+ deliver_rejected_order_email
81
+ end
82
+ end
83
+ end
84
+
85
+ def deliver_accepted_order_email
86
+ Tienda::OrderMailer.accepted(self).deliver
87
+ end
88
+
89
+ def deliver_rejected_order_email
90
+ Tienda::OrderMailer.rejected(self).deliver
91
+ end
92
+
93
+ def deliver_received_order_email
94
+ Tienda::OrderMailer.received(self).deliver
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,105 @@
1
+ module Tienda
2
+ class Order < ActiveRecord::Base
3
+
4
+ # The country which this order should be billed to
5
+ #
6
+ # @return [Tienda::Country]
7
+ belongs_to :billing_country, :class_name => 'Tienda::Country', :foreign_key => 'billing_country_id'
8
+
9
+ # Payments which have been stored for the order
10
+ has_many :payments, :dependent => :destroy, :class_name => 'Tienda::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