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