shoppe 0.0.14 → 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/app/assets/javascripts/shoppe/application.coffee +57 -1
  2. data/app/assets/javascripts/shoppe/mousetrap.js +9 -0
  3. data/app/assets/stylesheets/shoppe/application.scss +70 -59
  4. data/app/assets/stylesheets/shoppe/dialog.scss +10 -0
  5. data/app/assets/stylesheets/shoppe/sub.scss +15 -0
  6. data/app/controllers/shoppe/application_controller.rb +13 -1
  7. data/app/controllers/shoppe/attachments_controller.rb +10 -8
  8. data/app/controllers/shoppe/dashboard_controller.rb +6 -4
  9. data/app/controllers/shoppe/delivery_service_prices_controller.rb +33 -31
  10. data/app/controllers/shoppe/delivery_services_controller.rb +34 -32
  11. data/app/controllers/shoppe/orders_controller.rb +40 -38
  12. data/app/controllers/shoppe/product_categories_controller.rb +34 -32
  13. data/app/controllers/shoppe/products_controller.rb +32 -44
  14. data/app/controllers/shoppe/sessions_controller.rb +24 -22
  15. data/app/controllers/shoppe/stock_level_adjustments_controller.rb +40 -0
  16. data/app/controllers/shoppe/tax_rates_controller.rb +35 -33
  17. data/app/controllers/shoppe/users_controller.rb +40 -33
  18. data/app/controllers/shoppe/variants_controller.rb +50 -0
  19. data/app/helpers/shoppe/shoppe_helper.rb +2 -2
  20. data/app/mailers/shoppe/order_mailer.rb +20 -18
  21. data/app/models/shoppe/country.rb +1 -1
  22. data/app/models/shoppe/delivery_service.rb +17 -15
  23. data/app/models/shoppe/delivery_service_price.rb +18 -16
  24. data/app/models/shoppe/order.rb +293 -290
  25. data/app/models/shoppe/order_item.rb +115 -113
  26. data/app/models/shoppe/product.rb +76 -54
  27. data/app/models/shoppe/product/product_attributes.rb +12 -10
  28. data/app/models/shoppe/product/variants.rb +26 -0
  29. data/app/models/shoppe/product_attribute.rb +40 -38
  30. data/app/models/shoppe/product_category.rb +16 -14
  31. data/app/models/shoppe/stock_level_adjustment.rb +1 -2
  32. data/app/models/shoppe/tax_rate.rb +2 -2
  33. data/app/models/shoppe/user.rb +34 -32
  34. data/app/views/shoppe/orders/index.html.haml +3 -4
  35. data/app/views/shoppe/orders/show.html.haml +5 -5
  36. data/app/views/shoppe/products/_form.html.haml +28 -27
  37. data/app/views/shoppe/products/_table.html.haml +42 -0
  38. data/app/views/shoppe/products/edit.html.haml +2 -1
  39. data/app/views/shoppe/products/index.html.haml +1 -24
  40. data/app/views/shoppe/shared/error.html.haml +4 -0
  41. data/app/views/shoppe/{products/stock_levels.html.haml → stock_level_adjustments/index.html.haml} +12 -6
  42. data/app/views/shoppe/variants/form.html.haml +64 -0
  43. data/app/views/shoppe/variants/index.html.haml +33 -0
  44. data/config/routes.rb +2 -1
  45. data/config/shoppe.example.yml +16 -2
  46. data/db/migrate/20131022090919_refactor_order_items_to_allow_any_product.rb +6 -0
  47. data/db/migrate/20131022092904_rename_product_title_to_name.rb +5 -0
  48. data/db/migrate/20131022093538_stock_level_adjustments_should_be_polymorphic.rb +6 -0
  49. data/db/migrate/20131022135331_add_parent_id_to_products.rb +5 -0
  50. data/db/migrate/20131022145653_cost_price_should_be_default_to_zero.rb +9 -0
  51. data/db/seeds.rb +20 -20
  52. data/lib/shoppe.rb +2 -0
  53. data/lib/shoppe/errors/not_enough_stock.rb +1 -1
  54. data/lib/shoppe/errors/unorderable_item.rb +11 -0
  55. data/lib/shoppe/orderable_item.rb +39 -0
  56. data/lib/shoppe/version.rb +1 -1
  57. data/test/dummy/db/schema.rb +14 -11
  58. data/test/dummy/log/development.log +75 -0
  59. metadata +37 -5
@@ -1,146 +1,148 @@
1
- class Shoppe::OrderItem < ActiveRecord::Base
1
+ module Shoppe
2
+ class OrderItem < ActiveRecord::Base
2
3
 
3
- # Set the table name
4
- self.table_name = 'shoppe_order_items'
4
+ # Set the table name
5
+ self.table_name = 'shoppe_order_items'
5
6
 
6
- # Relationships
7
- belongs_to :order, :class_name => 'Shoppe::Order'
8
- belongs_to :product, :class_name => 'Shoppe::Product'
9
- has_many :stock_level_adjustments, :as => :parent, :dependent => :nullify
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
- # Validations
12
- validates :quantity, :numericality => true
12
+ # Validations
13
+ validates :quantity, :numericality => true
13
14
 
14
- # Set some values based on the selected product on validation
15
- before_validation do
16
- self.weight = self.quantity * self.product.weight
17
- end
15
+ before_validation do
16
+ self.weight = self.quantity * self.ordered_item.weight
17
+ end
18
18
 
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_product(product, quantity = 1)
23
- transaction do
24
- if existing = self.where(:product_id => product.id).first
25
- existing.increase!(quantity)
26
- existing
27
- else
28
- item = self.create(:product => product, :quantity => 0)
29
- item.increase!(quantity)
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
- # This allows you to remove a product from an order. It will also ensure that the order's
35
- # custom delivery service is updated.
36
- def remove
37
- transaction do
38
- self.destroy!
39
- self.order.remove_delivery_service_if_invalid
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
- # Increase the quantity of items in the order by the number provided. Will raise an error if we don't have
45
- # the stock to do this.
46
- def increase!(amount = 1)
47
- transaction do
48
- self.quantity += amount
49
- if self.product.stock_control? && self.product.stock < self.quantity
50
- raise Shoppe::Errors::NotEnoughStock, :product => self.product, :requested_stock => self.quantity
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
- # Decreases the quantity of items in the order by the number provided.
58
- def decrease!(amount = 1)
59
- transaction do
60
- self.quantity -= amount
61
- self.quantity == 0 ? self.destroy : self.save!
62
- self.order.remove_delivery_service_if_invalid
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
- # Return the unit price for the item
67
- def unit_price
68
- @unit_price ||= read_attribute(:unit_price) || product.try(:price) || 0.0
69
- end
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
- # Return the cost price for the item
72
- def unit_cost_price
73
- @unit_cost_price ||= read_attribute(:unit_cost_price) || product.try(:cost_price) || 0.0
74
- end
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
- # Return the tax rate for the item
77
- def tax_rate
78
- @tax_rate ||= read_attribute(:tax_rate) || product.try(:tax_rate).try(:rate_for, self.order) || 0.0
79
- end
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
- # Return the total tax for the item
82
- def tax_amount
83
- @tax_amount ||= read_attribute(:tax_amount) || (self.sub_total / BigDecimal(100)) * self.tax_rate
84
- end
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
- # Return the total cost for the product
87
- def total_cost
88
- quantity * unit_cost_price
89
- end
87
+ # Return the total cost for the product
88
+ def total_cost
89
+ quantity * unit_cost_price
90
+ end
90
91
 
91
- # Return the sub total for the product
92
- def sub_total
93
- quantity * unit_price
94
- end
92
+ # Return the sub total for the product
93
+ def sub_total
94
+ quantity * unit_price
95
+ end
95
96
 
96
- # Return the total price including tax for the order line
97
- def total
98
- tax_amount + sub_total
99
- end
97
+ # Return the total price including tax for the order line
98
+ def total
99
+ tax_amount + sub_total
100
+ end
100
101
 
101
- # This method will be triggered when the parent order is confirmed. This should automatically
102
- # update the stock levels on the source product.
103
- def confirm!
104
- write_attribute :unit_price, self.unit_price
105
- write_attribute :unit_cost_price, self.unit_cost_price
106
- write_attribute :tax_rate, self.tax_rate
107
- write_attribute :tax_amount, self.tax_amount
108
- save!
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
- if self.product.stock_control?
111
- self.product.stock_level_adjustments.create(:parent => self, :adjustment => 0 - self.quantity, :description => "Order ##{self.order.number} deduction")
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
- # This method will be trigger when the parent order is accepted.
116
- def accept!
117
- end
116
+ # This method will be trigger when the parent order is accepted.
117
+ def accept!
118
+ end
118
119
 
119
- # This method will be trigger when the parent order is rejected.
120
- def reject!
121
- self.stock_level_adjustments.destroy_all
122
- end
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
- # Do we have the stock needed to fulfil this order?
125
- def in_stock?
126
- if self.product.stock_control?
127
- self.product.stock >= self.quantity
128
- else
129
- true
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
- # Validate the stock level against the product and update as appropriate. This method will be executed
134
- # before an order is completed. If we have run out of this product, we will update the quantity to an
135
- # appropriate level (or remove the order item) and return the object.
136
- def validate_stock_levels
137
- if in_stock?
138
- false
139
- else
140
- self.quantity = self.product.stock
141
- self.quantity == 0 ? self.destroy : self.save!
142
- self
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
- class Shoppe::Product < ActiveRecord::Base
1
+ module Shoppe
2
+ class Product < ActiveRecord::Base
2
3
 
3
- # Set the table name
4
- self.table_name = 'shoppe_products'
4
+ # Set the table name
5
+ self.table_name = 'shoppe_products'
5
6
 
6
- # Require some concerns
7
- require_dependency 'shoppe/product/product_attributes'
7
+ # Require some concerns
8
+ require_dependency 'shoppe/product/product_attributes'
9
+ require_dependency 'shoppe/product/variants'
8
10
 
9
- # Attachments
10
- attachment :default_image
11
- attachment :data_sheet
11
+ # Attachments
12
+ attachment :default_image
13
+ attachment :data_sheet
12
14
 
13
- # Relationships
14
- belongs_to :product_category, :class_name => 'Shoppe::ProductCategory'
15
- belongs_to :tax_rate, :class_name => "Shoppe::TaxRate"
16
- has_many :order_items, :dependent => :restrict_with_exception, :class_name => 'Shoppe::OrderItem'
17
- has_many :orders, :through => :order_items, :class_name => 'Shoppe::Order'
18
- has_many :stock_level_adjustments, :dependent => :destroy
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
- # Validations
21
- validates :product_category_id, :presence => true
22
- validates :title, :presence => true
23
- validates :permalink, :presence => true, :uniqueness => true
24
- validates :sku, :presence => true
25
- validates :description, :presence => true
26
- validates :short_description, :presence => true
27
- validates :weight, :numericality => true
28
- validates :price, :numericality => true
29
- validates :cost_price, :numericality => true, :allow_blank => true
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
- # Set the permalink
32
- before_validation { self.permalink = self.title.parameterize if self.permalink.blank? && self.title.is_a?(String) }
35
+ # Set the permalink
36
+ before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
33
37
 
34
- # Scopes
35
- scope :active, -> { where(:active => true) }
36
- scope :featured, -> {where(:featured => true)}
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
- # Is this product currently in stock?
39
- def in_stock?
40
- stock > 0
41
- end
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
- # Return the total number of items currently in stock
44
- def stock
45
- @stock ||= self.stock_level_adjustments.sum(:adjustment)
46
- end
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
- # Specify which attributes can be searched
49
- def self.ransackable_attributes(auth_object = nil)
50
- ["id", "title", "sku"] + _ransackers.keys
51
- end
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
- # Specify which associations can be searched
54
- def self.ransackable_associations(auth_object = nil)
55
- []
56
- end
74
+ # Specify which associations can be searched
75
+ def self.ransackable_associations(auth_object = nil)
76
+ []
77
+ end
57
78
 
58
- # Search for products which include the guven attributes and return an active record
59
- # scope of these products. Chainable with other scopes and with_attributes methods.
60
- # For example:
61
- #
62
- # Shoppe::Product.active.with_attribute('Manufacturer', 'Apple').with_attribute('Model', ['Macbook', 'iPhone'])
63
- def self.with_attributes(key, values)
64
- product_ids = Shoppe::ProductAttribute.searchable.where(:key => key, :value => values).pluck(:product_id).uniq
65
- where(:id => product_ids)
66
- end
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
- class Shoppe::Product
1
+ module Shoppe
2
+ class Product
2
3
 
3
- # Relationships
4
- has_many :product_attributes, -> { order(:position) }, :class_name => 'Shoppe::ProductAttribute'
4
+ # Relationships
5
+ has_many :product_attributes, -> { order(:position) }, :class_name => 'Shoppe::ProductAttribute'
5
6
 
6
- # Attribute for providing the hash
7
- attr_accessor :product_attributes_array
7
+ # Attribute for providing the hash
8
+ attr_accessor :product_attributes_array
8
9
 
9
- # Save the attributes after saving the record
10
- after_save do
11
- return unless product_attributes_array.is_a?(Array)
12
- self.product_attributes.update_from_array(product_attributes_array)
13
- end
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