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,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