shoppe 0.0.14 → 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -1,146 +1,148 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class OrderItem < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_order_items'
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
# Relationships
|
8
|
+
belongs_to :order, :class_name => 'Shoppe::Order'
|
9
|
+
belongs_to :ordered_item, :polymorphic => true
|
10
|
+
has_many :stock_level_adjustments, :as => :parent, :dependent => :nullify, :class_name => 'Shoppe::StockLevelAdjustment'
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
# Validations
|
13
|
+
validates :quantity, :numericality => true
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
15
|
+
before_validation do
|
16
|
+
self.weight = self.quantity * self.ordered_item.weight
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
existing.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
19
|
+
# This allows you to add a product to the scoped order. For example Order.first.order_items.add_product(...).
|
20
|
+
# This will either increase the quantity of the value in the order or create a new item if one does not
|
21
|
+
# exist already.
|
22
|
+
def self.add_item(ordered_item, quantity = 1)
|
23
|
+
raise Errors::UnorderableItem, :ordered_item => ordered_item unless ordered_item.orderable?
|
24
|
+
transaction do
|
25
|
+
if existing = self.where(:ordered_item_id => ordered_item.id, :ordered_item_type => ordered_item.class.to_s).first
|
26
|
+
existing.increase!(quantity)
|
27
|
+
existing
|
28
|
+
else
|
29
|
+
new_item = self.create(:ordered_item => ordered_item, :quantity => 0)
|
30
|
+
new_item.increase!(quantity)
|
31
|
+
end
|
30
32
|
end
|
31
33
|
end
|
32
|
-
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
# This allows you to remove a product from an order. It will also ensure that the order's
|
36
|
+
# custom delivery service is updated.
|
37
|
+
def remove
|
38
|
+
transaction do
|
39
|
+
self.destroy!
|
40
|
+
self.order.remove_delivery_service_if_invalid
|
41
|
+
end
|
40
42
|
end
|
41
|
-
end
|
42
43
|
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
45
|
+
# Increase the quantity of items in the order by the number provided. Will raise an error if we don't have
|
46
|
+
# the stock to do this.
|
47
|
+
def increase!(amount = 1)
|
48
|
+
transaction do
|
49
|
+
self.quantity += amount
|
50
|
+
unless self.in_stock?
|
51
|
+
raise Shoppe::Errors::NotEnoughStock, :ordered_item => self.ordered_item, :requested_stock => self.quantity
|
52
|
+
end
|
53
|
+
self.save!
|
54
|
+
self.order.remove_delivery_service_if_invalid
|
51
55
|
end
|
52
|
-
self.save!
|
53
|
-
self.order.remove_delivery_service_if_invalid
|
54
56
|
end
|
55
|
-
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
# Decreases the quantity of items in the order by the number provided.
|
59
|
+
def decrease!(amount = 1)
|
60
|
+
transaction do
|
61
|
+
self.quantity -= amount
|
62
|
+
self.quantity == 0 ? self.destroy : self.save!
|
63
|
+
self.order.remove_delivery_service_if_invalid
|
64
|
+
end
|
63
65
|
end
|
64
|
-
end
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
# Return the unit price for the item
|
68
|
+
def unit_price
|
69
|
+
@unit_price ||= read_attribute(:unit_price) || ordered_item.try(:price) || 0.0
|
70
|
+
end
|
70
71
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
# Return the cost price for the item
|
73
|
+
def unit_cost_price
|
74
|
+
@unit_cost_price ||= read_attribute(:unit_cost_price) || ordered_item.try(:cost_price) || 0.0
|
75
|
+
end
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
# Return the tax rate for the item
|
78
|
+
def tax_rate
|
79
|
+
@tax_rate ||= read_attribute(:tax_rate) || ordered_item.try(:tax_rate).try(:rate_for, self.order) || 0.0
|
80
|
+
end
|
80
81
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
82
|
+
# Return the total tax for the item
|
83
|
+
def tax_amount
|
84
|
+
@tax_amount ||= read_attribute(:tax_amount) || (self.sub_total / BigDecimal(100)) * self.tax_rate
|
85
|
+
end
|
85
86
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
87
|
+
# Return the total cost for the product
|
88
|
+
def total_cost
|
89
|
+
quantity * unit_cost_price
|
90
|
+
end
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
92
|
+
# Return the sub total for the product
|
93
|
+
def sub_total
|
94
|
+
quantity * unit_price
|
95
|
+
end
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
97
|
+
# Return the total price including tax for the order line
|
98
|
+
def total
|
99
|
+
tax_amount + sub_total
|
100
|
+
end
|
100
101
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
102
|
+
# This method will be triggered when the parent order is confirmed. This should automatically
|
103
|
+
# update the stock levels on the source product.
|
104
|
+
def confirm!
|
105
|
+
write_attribute :unit_price, self.unit_price
|
106
|
+
write_attribute :unit_cost_price, self.unit_cost_price
|
107
|
+
write_attribute :tax_rate, self.tax_rate
|
108
|
+
write_attribute :tax_amount, self.tax_amount
|
109
|
+
save!
|
109
110
|
|
110
|
-
|
111
|
-
|
111
|
+
if self.ordered_item.stock_control?
|
112
|
+
self.ordered_item.stock_level_adjustments.create(:parent => self, :adjustment => 0 - self.quantity, :description => "Order ##{self.order.number} deduction")
|
113
|
+
end
|
112
114
|
end
|
113
|
-
end
|
114
115
|
|
115
|
-
|
116
|
-
|
117
|
-
|
116
|
+
# This method will be trigger when the parent order is accepted.
|
117
|
+
def accept!
|
118
|
+
end
|
118
119
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
120
|
+
# This method will be trigger when the parent order is rejected.
|
121
|
+
def reject!
|
122
|
+
self.stock_level_adjustments.destroy_all
|
123
|
+
end
|
123
124
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
125
|
+
# Do we have the stock needed to fulfil this order?
|
126
|
+
def in_stock?
|
127
|
+
if self.ordered_item.stock_control?
|
128
|
+
self.ordered_item.stock >= self.quantity
|
129
|
+
else
|
130
|
+
true
|
131
|
+
end
|
130
132
|
end
|
131
|
-
end
|
132
133
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
134
|
+
# Validate the stock level against the product and update as appropriate. This method will be executed
|
135
|
+
# before an order is completed. If we have run out of this product, we will update the quantity to an
|
136
|
+
# appropriate level (or remove the order item) and return the object.
|
137
|
+
def validate_stock_levels
|
138
|
+
if in_stock?
|
139
|
+
false
|
140
|
+
else
|
141
|
+
self.quantity = self.ordered_item.stock
|
142
|
+
self.quantity == 0 ? self.destroy : self.save!
|
143
|
+
self
|
144
|
+
end
|
143
145
|
end
|
144
|
-
end
|
145
146
|
|
147
|
+
end
|
146
148
|
end
|
@@ -1,68 +1,90 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class Product < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_products'
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
# Require some concerns
|
8
|
+
require_dependency 'shoppe/product/product_attributes'
|
9
|
+
require_dependency 'shoppe/product/variants'
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
# Attachments
|
12
|
+
attachment :default_image
|
13
|
+
attachment :data_sheet
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
# Relationships
|
16
|
+
belongs_to :product_category, :class_name => 'Shoppe::ProductCategory'
|
17
|
+
belongs_to :tax_rate, :class_name => "Shoppe::TaxRate"
|
18
|
+
has_many :order_items, :dependent => :restrict_with_exception, :class_name => 'Shoppe::OrderItem', :as => :ordered_item
|
19
|
+
has_many :orders, :through => :order_items, :class_name => 'Shoppe::Order'
|
20
|
+
has_many :stock_level_adjustments, :dependent => :destroy, :class_name => 'Shoppe::StockLevelAdjustment', :as => :item
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
22
|
+
# Validations
|
23
|
+
with_options :if => Proc.new { |p| p.parent.nil? } do |product|
|
24
|
+
product.validates :product_category_id, :presence => true
|
25
|
+
product.validates :description, :presence => true
|
26
|
+
product.validates :short_description, :presence => true
|
27
|
+
end
|
28
|
+
validates :name, :presence => true
|
29
|
+
validates :permalink, :presence => true, :uniqueness => true
|
30
|
+
validates :sku, :presence => true
|
31
|
+
validates :weight, :numericality => true
|
32
|
+
validates :price, :numericality => true
|
33
|
+
validates :cost_price, :numericality => true, :allow_blank => true
|
30
34
|
|
31
|
-
|
32
|
-
|
35
|
+
# Set the permalink
|
36
|
+
before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
# Scopes
|
39
|
+
scope :active, -> { where(:active => true) }
|
40
|
+
scope :featured, -> {where(:featured => true)}
|
41
|
+
scope :ordered, -> {order('name asc')}
|
42
|
+
|
43
|
+
# Return the name of the product
|
44
|
+
def full_name
|
45
|
+
self.parent ? "#{self.parent.name} (#{name})" : name
|
46
|
+
end
|
47
|
+
|
48
|
+
# Is this product actually orderable?
|
49
|
+
def orderable?
|
50
|
+
return false if self.has_variants?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return the price for the product
|
55
|
+
def price
|
56
|
+
self.default_variant ? self.default_variant.price : read_attribute(:price)
|
57
|
+
end
|
37
58
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
59
|
+
# Is this product currently in stock?
|
60
|
+
def in_stock?
|
61
|
+
self.default_variant ? self.default_variant.in_stock? : stock > 0
|
62
|
+
end
|
42
63
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
64
|
+
# Return the total number of items currently in stock
|
65
|
+
def stock
|
66
|
+
@stock ||= self.stock_level_adjustments.sum(:adjustment)
|
67
|
+
end
|
47
68
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
69
|
+
# Specify which attributes can be searched
|
70
|
+
def self.ransackable_attributes(auth_object = nil)
|
71
|
+
["id", "name", "sku"] + _ransackers.keys
|
72
|
+
end
|
52
73
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
74
|
+
# Specify which associations can be searched
|
75
|
+
def self.ransackable_associations(auth_object = nil)
|
76
|
+
[]
|
77
|
+
end
|
57
78
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
79
|
+
# Search for products which include the guven attributes and return an active record
|
80
|
+
# scope of these products. Chainable with other scopes and with_attributes methods.
|
81
|
+
# For example:
|
82
|
+
#
|
83
|
+
# Shoppe::Product.active.with_attribute('Manufacturer', 'Apple').with_attribute('Model', ['Macbook', 'iPhone'])
|
84
|
+
def self.with_attributes(key, values)
|
85
|
+
product_ids = Shoppe::ProductAttribute.searchable.where(:key => key, :value => values).pluck(:product_id).uniq
|
86
|
+
where(:id => product_ids)
|
87
|
+
end
|
67
88
|
|
89
|
+
end
|
68
90
|
end
|
@@ -1,15 +1,17 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class Product
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Relationships
|
5
|
+
has_many :product_attributes, -> { order(:position) }, :class_name => 'Shoppe::ProductAttribute'
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
# Attribute for providing the hash
|
8
|
+
attr_accessor :product_attributes_array
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
# Save the attributes after saving the record
|
11
|
+
after_save do
|
12
|
+
return unless product_attributes_array.is_a?(Array)
|
13
|
+
self.product_attributes.update_from_array(product_attributes_array)
|
14
|
+
end
|
14
15
|
|
16
|
+
end
|
15
17
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Shoppe
|
2
|
+
class Product
|
3
|
+
|
4
|
+
# Relationships
|
5
|
+
has_many :variants, :class_name => 'Shoppe::Product', :foreign_key => 'parent_id', :dependent => :destroy
|
6
|
+
belongs_to :parent, :class_name => 'Shoppe::Product', :foreign_key => 'parent_id'
|
7
|
+
|
8
|
+
# Validations
|
9
|
+
validate do
|
10
|
+
errors.add :base, "can only belong to a root product" if self.parent && self.parent.parent
|
11
|
+
end
|
12
|
+
|
13
|
+
# Scopes
|
14
|
+
scope :root, -> { where(:parent_id => nil) }
|
15
|
+
|
16
|
+
def has_variants?
|
17
|
+
!variants.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_variant
|
21
|
+
return nil if self.parent
|
22
|
+
@default_variant ||= self.variants.first
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|