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