shoppe 0.0.14 → 0.0.15
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.
- data/app/assets/javascripts/shoppe/application.coffee +57 -1
- data/app/assets/javascripts/shoppe/mousetrap.js +9 -0
- data/app/assets/stylesheets/shoppe/application.scss +70 -59
- data/app/assets/stylesheets/shoppe/dialog.scss +10 -0
- data/app/assets/stylesheets/shoppe/sub.scss +15 -0
- data/app/controllers/shoppe/application_controller.rb +13 -1
- data/app/controllers/shoppe/attachments_controller.rb +10 -8
- data/app/controllers/shoppe/dashboard_controller.rb +6 -4
- data/app/controllers/shoppe/delivery_service_prices_controller.rb +33 -31
- data/app/controllers/shoppe/delivery_services_controller.rb +34 -32
- data/app/controllers/shoppe/orders_controller.rb +40 -38
- data/app/controllers/shoppe/product_categories_controller.rb +34 -32
- data/app/controllers/shoppe/products_controller.rb +32 -44
- data/app/controllers/shoppe/sessions_controller.rb +24 -22
- data/app/controllers/shoppe/stock_level_adjustments_controller.rb +40 -0
- data/app/controllers/shoppe/tax_rates_controller.rb +35 -33
- data/app/controllers/shoppe/users_controller.rb +40 -33
- data/app/controllers/shoppe/variants_controller.rb +50 -0
- data/app/helpers/shoppe/shoppe_helper.rb +2 -2
- data/app/mailers/shoppe/order_mailer.rb +20 -18
- data/app/models/shoppe/country.rb +1 -1
- data/app/models/shoppe/delivery_service.rb +17 -15
- data/app/models/shoppe/delivery_service_price.rb +18 -16
- data/app/models/shoppe/order.rb +293 -290
- data/app/models/shoppe/order_item.rb +115 -113
- data/app/models/shoppe/product.rb +76 -54
- data/app/models/shoppe/product/product_attributes.rb +12 -10
- data/app/models/shoppe/product/variants.rb +26 -0
- data/app/models/shoppe/product_attribute.rb +40 -38
- data/app/models/shoppe/product_category.rb +16 -14
- data/app/models/shoppe/stock_level_adjustment.rb +1 -2
- data/app/models/shoppe/tax_rate.rb +2 -2
- data/app/models/shoppe/user.rb +34 -32
- data/app/views/shoppe/orders/index.html.haml +3 -4
- data/app/views/shoppe/orders/show.html.haml +5 -5
- data/app/views/shoppe/products/_form.html.haml +28 -27
- data/app/views/shoppe/products/_table.html.haml +42 -0
- data/app/views/shoppe/products/edit.html.haml +2 -1
- data/app/views/shoppe/products/index.html.haml +1 -24
- data/app/views/shoppe/shared/error.html.haml +4 -0
- data/app/views/shoppe/{products/stock_levels.html.haml → stock_level_adjustments/index.html.haml} +12 -6
- data/app/views/shoppe/variants/form.html.haml +64 -0
- data/app/views/shoppe/variants/index.html.haml +33 -0
- data/config/routes.rb +2 -1
- data/config/shoppe.example.yml +16 -2
- data/db/migrate/20131022090919_refactor_order_items_to_allow_any_product.rb +6 -0
- data/db/migrate/20131022092904_rename_product_title_to_name.rb +5 -0
- data/db/migrate/20131022093538_stock_level_adjustments_should_be_polymorphic.rb +6 -0
- data/db/migrate/20131022135331_add_parent_id_to_products.rb +5 -0
- data/db/migrate/20131022145653_cost_price_should_be_default_to_zero.rb +9 -0
- data/db/seeds.rb +20 -20
- data/lib/shoppe.rb +2 -0
- data/lib/shoppe/errors/not_enough_stock.rb +1 -1
- data/lib/shoppe/errors/unorderable_item.rb +11 -0
- data/lib/shoppe/orderable_item.rb +39 -0
- data/lib/shoppe/version.rb +1 -1
- data/test/dummy/db/schema.rb +14 -11
- data/test/dummy/log/development.log +75 -0
- metadata +37 -5
@@ -0,0 +1,50 @@
|
|
1
|
+
module Shoppe
|
2
|
+
class VariantsController < ApplicationController
|
3
|
+
|
4
|
+
before_filter { @active_nav = :products }
|
5
|
+
before_filter { @product = Shoppe::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 => "Varient has been added successfully"
|
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 => "Varient has been updated successfully"
|
33
|
+
else
|
34
|
+
render :action => "form"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def destroy
|
39
|
+
@variant.destroy
|
40
|
+
redirect_to [@product, :variants], :notice => "Varient has been removed successfully"
|
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)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -4,7 +4,7 @@ module Shoppe::ShoppeHelper
|
|
4
4
|
content_tag :span, status, :class => "status-tag #{status}"
|
5
5
|
end
|
6
6
|
|
7
|
-
def attachment_preview(attachment)
|
7
|
+
def attachment_preview(attachment, options = {})
|
8
8
|
if attachment
|
9
9
|
String.new.tap do |s|
|
10
10
|
if attachment.image?
|
@@ -22,7 +22,7 @@ module Shoppe::ShoppeHelper
|
|
22
22
|
s << "</div>"
|
23
23
|
s << "</div>"
|
24
24
|
end.html_safe
|
25
|
-
|
25
|
+
elsif !options[:hide_if_blank]
|
26
26
|
"<div class='attachmentPreview'><div class='imgContainer'><div class='img none'></div></div><div class='desc none'>No attachment</div></div>".html_safe
|
27
27
|
end
|
28
28
|
end
|
@@ -1,25 +1,27 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class OrderMailer < ActionMailer::Base
|
2
3
|
|
3
|
-
|
4
|
+
default :from => "#{Shoppe.config[:store_name]} <#{Shoppe.config[:email_address]}>"
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
def received(order)
|
7
|
+
@order = order
|
8
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.subject', :default => "Order Confirmation")
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def accepted(order)
|
12
|
+
@order = order
|
13
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.accepted', :default => "Order Accepted")
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def rejected(order)
|
17
|
+
@order = order
|
18
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.rejected', :default => "Order Rejected")
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
def shipped(order)
|
22
|
+
@order = order
|
23
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.shipped', :default => "Order Shipped")
|
24
|
+
end
|
24
25
|
|
26
|
+
end
|
25
27
|
end
|
@@ -5,7 +5,7 @@ module Shoppe
|
|
5
5
|
self.table_name = 'shoppe_countries'
|
6
6
|
|
7
7
|
# Relationships
|
8
|
-
has_many :orders, :dependent => :restrict_with_exception
|
8
|
+
has_many :orders, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Order'
|
9
9
|
|
10
10
|
# Scopes
|
11
11
|
scope :ordered, -> { order('shoppe_countries.name asc') }
|
@@ -1,22 +1,24 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class DeliveryService < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_delivery_services'
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
# Validations
|
8
|
+
validates :name, :presence => true
|
9
|
+
validates :courier, :presence => true
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
# Relationships
|
12
|
+
has_many :orders, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Order'
|
13
|
+
has_many :delivery_service_prices, :dependent => :destroy, :class_name => 'Shoppe::DeliveryServicePrice'
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
# Scopes
|
16
|
+
scope :active, -> { where(:active => true)}
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
# Return the tracking URL for the given consignment number
|
19
|
+
def tracking_url_for(consignment_number)
|
20
|
+
tracking_url.gsub("{{consignment_number}}", consignment_number)
|
21
|
+
end
|
22
|
+
|
20
23
|
end
|
21
|
-
|
22
24
|
end
|
@@ -1,23 +1,25 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class DeliveryServicePrice < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_delivery_service_prices'
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
# Tax rates are associated with countries
|
8
|
+
include Shoppe::AssociatedCountries
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
# Relationships
|
11
|
+
belongs_to :delivery_service, :class_name => 'Shoppe::DeliveryService'
|
12
|
+
belongs_to :tax_rate, :class_name => "Shoppe::TaxRate"
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
# Validations
|
15
|
+
validates :price, :numericality => true
|
16
|
+
validates :cost_price, :numericality => true, :allow_blank => true
|
17
|
+
validates :min_weight, :numericality => true
|
18
|
+
validates :max_weight, :numericality => true
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
# Scopes
|
21
|
+
scope :ordered, -> { order('price asc')}
|
22
|
+
scope :for_weight, -> weight { where("min_weight <= ? AND max_weight >= ?", weight, weight) }
|
22
23
|
|
24
|
+
end
|
23
25
|
end
|
data/app/models/shoppe/order.rb
CHANGED
@@ -1,352 +1,355 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class Order < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_orders'
|
6
|
+
|
7
|
+
# An array of all the available statuses for an order
|
8
|
+
STATUSES = ['building', 'confirming', 'received', 'accepted', 'rejected', 'shipped']
|
9
|
+
|
10
|
+
# Order's implement a key value store for storing arbitary properties which
|
11
|
+
# may be useful (for example payment configuraiton)
|
12
|
+
key_value_store :properties
|
13
|
+
|
14
|
+
# These additional callbacks allow for applications to hook into other
|
15
|
+
# parts of the order lifecycle.
|
16
|
+
define_model_callbacks :confirmation, :payment, :acceptance, :rejection, :ship
|
17
|
+
|
18
|
+
# Relationships
|
19
|
+
belongs_to :delivery_service, :class_name => 'Shoppe::DeliveryService'
|
20
|
+
belongs_to :country, :class_name => 'Shoppe::Country'
|
21
|
+
belongs_to :accepter, :class_name => 'Shoppe::User', :foreign_key => 'accepted_by'
|
22
|
+
belongs_to :rejecter, :class_name => 'Shoppe::User', :foreign_key => 'rejected_by'
|
23
|
+
belongs_to :shipper, :class_name => 'Shoppe::User', :foreign_key => 'shipped_by'
|
24
|
+
has_many :order_items, :dependent => :destroy, :class_name => 'Shoppe::OrderItem'
|
25
|
+
has_many :products, :through => :order_items, :class_name => 'Shoppe::Product'
|
26
|
+
|
27
|
+
# Validations
|
28
|
+
validates :token, :presence => true
|
29
|
+
validates :status, :inclusion => {:in => STATUSES}
|
30
|
+
with_options :if => Proc.new { |o| !o.building? } do |order|
|
31
|
+
order.validates :first_name, :presence => true
|
32
|
+
order.validates :last_name, :presence => true
|
33
|
+
order.validates :address1, :presence => true
|
34
|
+
order.validates :address3, :presence => true
|
35
|
+
order.validates :address4, :presence => true
|
36
|
+
order.validates :postcode, :presence => true
|
37
|
+
order.validates :country, :presence => true
|
38
|
+
order.validates :email_address, :format => {:with => /\A\b[A-Z0-9\.\_\%\-\+]+@(?:[A-Z0-9\-]+\.)+[A-Z]{2,6}\b\z/i}
|
39
|
+
order.validates :phone_number, :format => {:with => /\A[\d\ \-x\(\)]{7,}\z/}
|
40
|
+
end
|
41
|
+
validate do
|
42
|
+
unless available_delivery_services.include?(self.delivery_service)
|
43
|
+
errors.add :delivery_service_id, "is not suitable for this order"
|
44
|
+
end
|
42
45
|
end
|
43
|
-
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
47
|
+
# Scopes
|
48
|
+
scope :received, -> {where("received_at is not null")}
|
49
|
+
scope :pending, -> { where(:status => 'received') }
|
50
|
+
scope :ordered, -> { order('id desc')}
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
# Set some defaults
|
53
|
+
before_validation do
|
54
|
+
self.status = 'building' if self.status.blank?
|
55
|
+
self.token = SecureRandom.uuid if self.token.blank?
|
56
|
+
end
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
# Is this order still being built by the user?
|
59
|
+
def building?
|
60
|
+
self.status == 'building'
|
61
|
+
end
|
60
62
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
63
|
+
# Is this order in the user confirmation step?
|
64
|
+
def confirming?
|
65
|
+
self.status == 'confirming'
|
66
|
+
end
|
65
67
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
68
|
+
# Has this order been rejected?
|
69
|
+
def rejected?
|
70
|
+
!!self.rejected_at
|
71
|
+
end
|
70
72
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
73
|
+
# Has this order been accepted?
|
74
|
+
def accepted?
|
75
|
+
!!self.accepted_at
|
76
|
+
end
|
75
77
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
78
|
+
# Has this order been shipped?
|
79
|
+
def shipped?
|
80
|
+
!!self.shipped_at?
|
81
|
+
end
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
83
|
+
# Has the order been received?
|
84
|
+
def received?
|
85
|
+
!!self.received_at?
|
86
|
+
end
|
85
87
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
88
|
+
# The order number
|
89
|
+
def number
|
90
|
+
id.to_s.rjust(6, '0')
|
91
|
+
end
|
90
92
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
93
|
+
# The length of time the customer spent building the order before submitting it to us.
|
94
|
+
# The time from first item in basket to received.
|
95
|
+
def build_time
|
96
|
+
return nil if self.received_at.blank?
|
97
|
+
self.created_at - self.received_at
|
98
|
+
end
|
97
99
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
100
|
+
# The name of the customer
|
101
|
+
def customer_name
|
102
|
+
company.blank? ? "#{first_name} #{last_name}" : "#{company} (#{first_name} #{last_name})"
|
103
|
+
end
|
102
104
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
105
|
+
# Is this order empty? (i.e. doesn't have any items associated with it)
|
106
|
+
def empty?
|
107
|
+
order_items.empty?
|
108
|
+
end
|
107
109
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
110
|
+
# Does this order have items?
|
111
|
+
def has_items?
|
112
|
+
total_items > 0
|
113
|
+
end
|
112
114
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
115
|
+
# Return the number of items in the order?
|
116
|
+
def total_items
|
117
|
+
@total_items ||= order_items.inject(0) { |t,i| t + i.quantity }
|
118
|
+
end
|
117
119
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
120
|
+
# The total cost of the order
|
121
|
+
def total_cost
|
122
|
+
self.delivery_cost_price +
|
123
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.total_cost }
|
124
|
+
end
|
123
125
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
126
|
+
# Return the price for the order
|
127
|
+
def profit
|
128
|
+
total_before_tax - total_cost
|
129
|
+
end
|
128
130
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
131
|
+
# The total price of the order before tax
|
132
|
+
def total_before_tax
|
133
|
+
self.delivery_price +
|
134
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.sub_total }
|
135
|
+
end
|
134
136
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
137
|
+
# The total amount of tax due on this order
|
138
|
+
def tax
|
139
|
+
self.delivery_tax_amount +
|
140
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.tax_amount }
|
141
|
+
end
|
140
142
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
143
|
+
# The total of the order including tax
|
144
|
+
def total
|
145
|
+
self.delivery_price +
|
146
|
+
self.delivery_tax_amount +
|
147
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.total }
|
148
|
+
end
|
147
149
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
150
|
+
# The total of the order including tax in pence
|
151
|
+
def total_in_pence
|
152
|
+
(total * BigDecimal(100)).to_i
|
153
|
+
end
|
152
154
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
155
|
+
# The total weight of the order
|
156
|
+
def total_weight
|
157
|
+
order_items.inject(BigDecimal(0)) { |t,i| t + i.weight}
|
158
|
+
end
|
157
159
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
160
|
+
# An array of all the delivery service prices which can be applied to this order.
|
161
|
+
def delivery_service_prices
|
162
|
+
@delivery_service_prices ||= begin
|
163
|
+
prices = Shoppe::DeliveryServicePrice.joins(:delivery_service).where(:shoppe_delivery_services => {:active => true}).order("`default` desc, price asc").for_weight(total_weight)
|
164
|
+
prices = prices.select { |p| p.countries.empty? || p.country?(self.country) }
|
165
|
+
prices
|
166
|
+
end
|
164
167
|
end
|
165
|
-
end
|
166
168
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
169
|
+
# An array of all the delivery services which are suitable for this order in it's
|
170
|
+
# current state (based on its current weight)
|
171
|
+
def available_delivery_services
|
172
|
+
@available_delivery_services ||= begin
|
173
|
+
delivery_service_prices.map(&:delivery_service).uniq
|
174
|
+
end
|
172
175
|
end
|
173
|
-
end
|
174
176
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
177
|
+
# The recommended delivery service for this order
|
178
|
+
def delivery_service
|
179
|
+
super || available_delivery_services.first
|
180
|
+
end
|
179
181
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
182
|
+
# Return the delivery price for this order in its current state
|
183
|
+
def delivery_service_price
|
184
|
+
@delivery_service_price ||= self.delivery_service && self.delivery_service.delivery_service_prices.for_weight(self.total_weight).first
|
185
|
+
end
|
184
186
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
187
|
+
# The price for delivering this order in its current state
|
188
|
+
def delivery_price
|
189
|
+
@delivery_price ||= read_attribute(:delivery_price) || delivery_service_price.try(:price) || 0.0
|
190
|
+
end
|
189
191
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
192
|
+
# The cost of delivering this order in its current state
|
193
|
+
def delivery_cost_price
|
194
|
+
@delivery_cost_price ||= read_attribute(:delivery_cost_price) || delivery_service_price.try(:cost_price) || 0.0
|
195
|
+
end
|
194
196
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
197
|
+
# The tax amount due for the delivery of this order in its current state
|
198
|
+
def delivery_tax_amount
|
199
|
+
@delivery_tax_amount ||= begin
|
200
|
+
read_attribute(:delivery_tax_amount) ||
|
201
|
+
delivery_price / BigDecimal(100) * delivery_tax_rate ||
|
202
|
+
0.0
|
203
|
+
end
|
201
204
|
end
|
202
|
-
end
|
203
205
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
206
|
+
# The tax rate for the delivery of this order in its current state
|
207
|
+
def delivery_tax_rate
|
208
|
+
@delivery_tax_rate ||= begin
|
209
|
+
read_attribute(:delivery_tax_rate) ||
|
210
|
+
delivery_service_price.try(:tax_rate).try(:rate_for, self) ||
|
211
|
+
0.0
|
212
|
+
end
|
210
213
|
end
|
211
|
-
end
|
212
214
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
215
|
+
# Is the currently assigned delivery service appropriate for this order?
|
216
|
+
def valid_delivery_service?
|
217
|
+
self.delivery_service && self.available_delivery_services.include?(self.delivery_service)
|
218
|
+
end
|
217
219
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
220
|
+
# Remove the associated delivery service if it's invalid
|
221
|
+
def remove_delivery_service_if_invalid
|
222
|
+
unless self.valid_delivery_service?
|
223
|
+
self.delivery_service = nil
|
224
|
+
self.save
|
225
|
+
end
|
223
226
|
end
|
224
|
-
end
|
225
227
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
228
|
+
# The URL which can be used to track the delivery of this order
|
229
|
+
def courier_tracking_url
|
230
|
+
return nil if self.shipped_at.blank? || self.consignment_number.blank?
|
231
|
+
@courier_tracking_url ||= self.delivery_service.tracking_url_for(self.consignment_number)
|
232
|
+
end
|
231
233
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
234
|
+
# Has this order been fully paid for?
|
235
|
+
def paid?
|
236
|
+
!paid_at.blank?
|
237
|
+
end
|
236
238
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
239
|
+
# This method is called by the customer when they submit their details in the first step of
|
240
|
+
# the checkout process. It will update the status to 'confirmed' as well as updating their
|
241
|
+
# details. Any issues with validation will cause false to be returned otherwise true. Any
|
242
|
+
# more serious issues will be raised as exceptions.
|
243
|
+
def proceed_to_confirm(params = {})
|
244
|
+
self.status = 'confirming'
|
245
|
+
if self.update(params)
|
246
|
+
true
|
247
|
+
else
|
248
|
+
false
|
249
|
+
end
|
247
250
|
end
|
248
|
-
end
|
249
251
|
|
250
|
-
|
251
|
-
|
252
|
-
|
252
|
+
# This method will confirm the order If there are any issues with the order an exception
|
253
|
+
# should be raised.
|
254
|
+
def confirm!
|
253
255
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
256
|
+
# Ensure that we have the stock to fulfil this order at the current time. We may have had it when
|
257
|
+
# it was placed int he basket and if we don't now, we should let the user know so they can
|
258
|
+
# rethink.
|
259
|
+
no_stock_of = self.order_items.select(&:validate_stock_levels)
|
260
|
+
unless no_stock_of.empty?
|
261
|
+
raise Shoppe::Errors::InsufficientStockToFulfil, :order => self, :out_of_stock_items => no_stock_of
|
262
|
+
end
|
261
263
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
264
|
+
# Ensure that before we confirm the order that the delivery service which has been selected
|
265
|
+
# is appropritae for the contents of the order.
|
266
|
+
unless self.valid_delivery_service?
|
267
|
+
raise Shoppe::Errors::InappropriateDeliveryService, :order => self
|
268
|
+
end
|
267
269
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
270
|
+
# Store the delivery prices with the order
|
271
|
+
if self.delivery_service
|
272
|
+
write_attribute :delivery_service_id, self.delivery_service.id
|
273
|
+
write_attribute :delivery_price, self.delivery_price
|
274
|
+
write_attribute :delivery_cost_price, self.delivery_cost_price
|
275
|
+
write_attribute :delivery_tax_amount, self.delivery_tax_amount
|
276
|
+
write_attribute :delivery_tax_rate, self.delivery_tax_rate
|
277
|
+
end
|
276
278
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
279
|
+
run_callbacks :confirmation do
|
280
|
+
# If we have successfully charged the card (i.e. no exception) we can go ahead and mark this
|
281
|
+
# order as 'received' which means it can be accepted by staff.
|
282
|
+
self.status = 'received'
|
283
|
+
self.received_at = Time.now
|
284
|
+
self.save!
|
283
285
|
|
284
|
-
|
286
|
+
self.order_items.each(&:confirm!)
|
285
287
|
|
286
|
-
|
287
|
-
|
288
|
-
|
288
|
+
# Send an email to the customer
|
289
|
+
Shoppe::OrderMailer.received(self).deliver
|
290
|
+
end
|
289
291
|
|
290
|
-
|
291
|
-
|
292
|
-
|
292
|
+
# We're all good.
|
293
|
+
true
|
294
|
+
end
|
293
295
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
296
|
+
# This method will mark an order as paid.
|
297
|
+
def pay!(reference, method)
|
298
|
+
run_callbacks :payment do
|
299
|
+
self.paid_at = Time.now.utc
|
300
|
+
self.payment_reference = reference
|
301
|
+
self.payment_method = method
|
302
|
+
self.save!
|
303
|
+
end
|
301
304
|
end
|
302
|
-
end
|
303
305
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
306
|
+
# This method will accept the this order. It is called by a user (which is the only
|
307
|
+
# parameter).
|
308
|
+
def accept!(user)
|
309
|
+
run_callbacks :acceptance do
|
310
|
+
self.accepted_at = Time.now
|
311
|
+
self.accepted_by = user.id
|
312
|
+
self.status = 'accepted'
|
313
|
+
self.save!
|
314
|
+
self.order_items.each(&:accept!)
|
315
|
+
Shoppe::OrderMailer.accepted(self).deliver
|
316
|
+
end
|
314
317
|
end
|
315
|
-
end
|
316
318
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
319
|
+
# This method will reject the order. It is called by a user (which is the only parameter).
|
320
|
+
def reject!(user)
|
321
|
+
run_callbacks :rejection do
|
322
|
+
self.rejected_at = Time.now
|
323
|
+
self.rejected_by = user.id
|
324
|
+
self.status = 'rejected'
|
325
|
+
self.save!
|
326
|
+
self.order_items.each(&:reject!)
|
327
|
+
Shoppe::OrderMailer.rejected(self).deliver
|
328
|
+
end
|
326
329
|
end
|
327
|
-
end
|
328
330
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
331
|
+
# This method will mark an order as shipped and store the given consignment number with the
|
332
|
+
# order for use later in tracking.
|
333
|
+
def ship!(user, consignment_number)
|
334
|
+
run_callbacks :ship do
|
335
|
+
self.shipped_at = Time.now
|
336
|
+
self.shipped_by = user.id
|
337
|
+
self.status = 'shipped'
|
338
|
+
self.consignment_number = consignment_number
|
339
|
+
self.save!
|
340
|
+
Shoppe::OrderMailer.shipped(self).deliver
|
341
|
+
end
|
339
342
|
end
|
340
|
-
end
|
341
343
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
344
|
+
# Specify which attributes can be searched
|
345
|
+
def self.ransackable_attributes(auth_object = nil)
|
346
|
+
["id", "postcode", "address1", "address2", "address3", "address4", "first_name", "last_name", "company", "email_address", "phone_number", "consignment_number", "status", "received_at"] + _ransackers.keys
|
347
|
+
end
|
346
348
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
349
|
+
# Specify which associations can be searched
|
350
|
+
def self.ransackable_associations(auth_object = nil)
|
351
|
+
['products']
|
352
|
+
end
|
351
353
|
|
354
|
+
end
|
352
355
|
end
|