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,51 +1,53 @@
1
- class Shoppe::ProductAttribute < ActiveRecord::Base
1
+ module Shoppe
2
+ class ProductAttribute < ActiveRecord::Base
2
3
 
3
- # Set the table name
4
- self.table_name = 'shoppe_product_attributes'
4
+ # Set the table name
5
+ self.table_name = 'shoppe_product_attributes'
5
6
 
6
- # Validations
7
- validates :key, :presence => true
7
+ # Validations
8
+ validates :key, :presence => true
8
9
 
9
- # Relationships
10
- belongs_to :product, :class_name => 'Shoppe::Product'
10
+ # Relationships
11
+ belongs_to :product, :class_name => 'Shoppe::Product'
11
12
 
12
- # Scopes
13
- scope :searchable, -> { where(:searchable => true) }
14
- scope :public, -> { where(:public => true) }
13
+ # Scopes
14
+ scope :searchable, -> { where(:searchable => true) }
15
+ scope :public, -> { where(:public => true) }
15
16
 
16
- # Return the the available options as a hash
17
- def self.grouped_hash
18
- all.group_by(&:key).inject(Hash.new) do |h, (key, attributes)|
19
- h[key] = attributes.map(&:value).uniq
20
- h
17
+ # Return the the available options as a hash
18
+ def self.grouped_hash
19
+ all.group_by(&:key).inject(Hash.new) do |h, (key, attributes)|
20
+ h[key] = attributes.map(&:value).uniq
21
+ h
22
+ end
21
23
  end
22
- end
23
24
 
24
- # Create/update attributes for a product based on the provided hash of
25
- # keys & values
26
- def self.update_from_array(array)
27
- existing_keys = self.pluck(:key)
28
- index = 0
29
- array.each do |hash|
30
- next if hash['key'].blank?
31
- index += 1
32
- params = hash.merge({
33
- :searchable => hash['searchable'].to_s == '1',
34
- :public => hash['public'].to_s == '1',
35
- :position => index
36
- })
37
- if existing_attr = self.where(:key => hash['key']).first
38
- if hash['value'].blank?
39
- existing_attr.destroy
40
- index -= 1
25
+ # Create/update attributes for a product based on the provided hash of
26
+ # keys & values
27
+ def self.update_from_array(array)
28
+ existing_keys = self.pluck(:key)
29
+ index = 0
30
+ array.each do |hash|
31
+ next if hash['key'].blank?
32
+ index += 1
33
+ params = hash.merge({
34
+ :searchable => hash['searchable'].to_s == '1',
35
+ :public => hash['public'].to_s == '1',
36
+ :position => index
37
+ })
38
+ if existing_attr = self.where(:key => hash['key']).first
39
+ if hash['value'].blank?
40
+ existing_attr.destroy
41
+ index -= 1
42
+ else
43
+ existing_attr.update_attributes(params)
44
+ end
41
45
  else
42
- existing_attr.update_attributes(params)
46
+ attribute = self.create(params)
43
47
  end
44
- else
45
- attribute = self.create(params)
46
48
  end
49
+ self.where(:key => existing_keys - array.map { |h| h['key']}).delete_all
47
50
  end
48
- self.where(:key => existing_keys - array.map { |h| h['key']}).delete_all
49
- end
50
51
 
52
+ end
51
53
  end
@@ -1,22 +1,24 @@
1
- class Shoppe::ProductCategory < ActiveRecord::Base
1
+ module Shoppe
2
+ class ProductCategory < ActiveRecord::Base
2
3
 
3
- # Set the table name
4
- self.table_name = 'shoppe_product_categories'
4
+ # Set the table name
5
+ self.table_name = 'shoppe_product_categories'
5
6
 
6
- # Attachments
7
- attachment :image
7
+ # Attachments
8
+ attachment :image
8
9
 
9
- # Relationships
10
- has_many :products, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Product'
10
+ # Relationships
11
+ has_many :products, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Product'
11
12
 
12
- # Validations
13
- validates :name, :presence => true
14
- validates :permalink, :presence => true, :uniqueness => true
13
+ # Validations
14
+ validates :name, :presence => true
15
+ validates :permalink, :presence => true, :uniqueness => true
15
16
 
16
- # Scopes
17
- scope :ordered, -> { order(:name) }
17
+ # Scopes
18
+ scope :ordered, -> { order(:name) }
18
19
 
19
- # Set the permalink
20
- before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
20
+ # Set the permalink
21
+ before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
21
22
 
23
+ end
22
24
  end
@@ -2,11 +2,10 @@ module Shoppe
2
2
  class StockLevelAdjustment < ActiveRecord::Base
3
3
 
4
4
  # Relationships
5
- belongs_to :product
5
+ belongs_to :item, :polymorphic => true
6
6
  belongs_to :parent, :polymorphic => true
7
7
 
8
8
  # Validations
9
- validates :product_id, :presence => true
10
9
  validates :description, :presence => true
11
10
  validates :adjustment, :numericality => true
12
11
  validate { errors.add(:adjustment, "must be greater or less than zero") if adjustment == 0 }
@@ -12,8 +12,8 @@ module Shoppe
12
12
  validates :rate, :numericality => true
13
13
 
14
14
  # Relationships
15
- has_many :products, :dependent => :restrict_with_exception
16
- has_many :delivery_service_prices, :dependent => :restrict_with_exception
15
+ has_many :products, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Product'
16
+ has_many :delivery_service_prices, :dependent => :restrict_with_exception, :class_name => 'Shoppe::DeliveryServicePrice'
17
17
 
18
18
  # Scopes
19
19
  scope :ordered, -> { order("shoppe_tax_rates.id")}
@@ -1,41 +1,43 @@
1
- class Shoppe::User < ActiveRecord::Base
1
+ module Shoppe
2
+ class User < ActiveRecord::Base
2
3
 
3
- # Set the table name
4
- self.table_name = 'shoppe_users'
4
+ # Set the table name
5
+ self.table_name = 'shoppe_users'
5
6
 
6
- # Self explanatory I think!
7
- has_secure_password
7
+ # Self explanatory I think!
8
+ has_secure_password
8
9
 
9
- # Validations
10
- validates :first_name, :presence => true
11
- validates :last_name, :presence => true
12
- validates :email_address, :presence => true
10
+ # Validations
11
+ validates :first_name, :presence => true
12
+ validates :last_name, :presence => true
13
+ validates :email_address, :presence => true
13
14
 
14
- # The user's first name & last name concatenated
15
- def full_name
16
- "#{first_name} #{last_name}"
17
- end
15
+ # The user's first name & last name concatenated
16
+ def full_name
17
+ "#{first_name} #{last_name}"
18
+ end
18
19
 
19
- # The user's first name & initial of last name concatenated
20
- def short_name
21
- "#{first_name} #{last_name[0,1]}"
22
- end
20
+ # The user's first name & initial of last name concatenated
21
+ def short_name
22
+ "#{first_name} #{last_name[0,1]}"
23
+ end
23
24
 
24
- # Reset the user's password to something random and e-mail it to them
25
- def reset_password!
26
- self.password = SecureRandom.hex(8)
27
- self.password_confirmation = self.password
28
- self.save!
29
- Shoppe::UserMailer.new_password(self).deliver
30
- end
25
+ # Reset the user's password to something random and e-mail it to them
26
+ def reset_password!
27
+ self.password = SecureRandom.hex(8)
28
+ self.password_confirmation = self.password
29
+ self.save!
30
+ Shoppe::UserMailer.new_password(self).deliver
31
+ end
31
32
 
32
- # Attempt to authenticate a user based on email & password. Returns the
33
- # user if successful otherwise returns false.
34
- def self.authenticate(email_address, password)
35
- user = self.where(:email_address => email_address).first
36
- return false if user.nil?
37
- return false unless user.authenticate(password)
38
- user
39
- end
33
+ # Attempt to authenticate a user based on email & password. Returns the
34
+ # user if successful otherwise returns false.
35
+ def self.authenticate(email_address, password)
36
+ user = self.where(:email_address => email_address).first
37
+ return false if user.nil?
38
+ return false unless user.authenticate(password)
39
+ user
40
+ end
40
41
 
42
+ end
41
43
  end
@@ -23,9 +23,8 @@
23
23
  &rarr;
24
24
  = f.text_field :received_at_lteq, :class => 'small'
25
25
  %dl.right
26
- %dt= f.label :products_title_cont, "Contains product"
27
- %dd= f.text_field :products_title_cont
28
-
26
+ %dt= f.label :products_name_cont, "Contains product"
27
+ %dd= f.text_field :products_name_cont
29
28
  %dt= f.label :email_address_cont, "E-Mail Address"
30
29
  %dd= f.text_field :email_address_cont
31
30
  %dt= f.label :phone_number_cont, "Phone Number"
@@ -57,7 +56,7 @@
57
56
  %td
58
57
  %ul
59
58
  - for item in order.order_items
60
- %li #{item.quantity} x #{item.product.title}
59
+ %li #{item.quantity} x #{item.ordered_item.full_name}
61
60
  %td= number_to_currency order.total
62
61
  %td= boolean_tag order.paid?
63
62
 
@@ -36,10 +36,10 @@
36
36
  %dl.form
37
37
  %dt.padding= label_tag 'payment_method', 'Payment Method'
38
38
  %dd= text_field_tag 'payment_method', '', :class => 'text'
39
- %dl
39
+ %dl.form
40
40
  %dt.padding= label_tag 'payment_reference', 'Payment Reference'
41
41
  %dd= text_field_tag 'payment_reference', '', :class => 'text'
42
- %dl
42
+ %dl.form
43
43
  %dd= submit_tag "Mark as paid", :class => 'button green button-mini'
44
44
 
45
45
  - if @order.accepted? && !@order.shipped?
@@ -47,7 +47,7 @@
47
47
  %dl.form
48
48
  %dt.padding= label_tag 'consignment_number', 'Consignment Number'
49
49
  %dd= text_field_tag 'consignment_number', '', :class => 'text'
50
- %dl
50
+ %dl.form
51
51
  %dd= submit_tag "Mark as shipped", :class => 'button green button-mini'
52
52
 
53
53
  - if @order.paid? && !(@order.accepted? || @order.rejected?)
@@ -104,8 +104,8 @@
104
104
  - for item in @order.order_items
105
105
  %tr
106
106
  %td.qty= item.quantity
107
- %td.product= link_to item.product.title, [:edit, item.product]
108
- %td.sku= item.product.sku
107
+ %td.product= item.ordered_item.full_name
108
+ %td.sku= item.ordered_item.sku
109
109
  %td.money= number_to_currency item.total_cost
110
110
  %td.money= number_to_currency item.sub_total
111
111
  %td.money= number_to_currency item.tax_amount
@@ -7,8 +7,8 @@
7
7
 
8
8
  .splitContainer
9
9
  %dl.third
10
- %dt= f.label :title
11
- %dd= f.text_field :title, :class => 'text'
10
+ %dt= f.label :name
11
+ %dd= f.text_field :name, :class => 'text focus'
12
12
  %dl.third
13
13
  %dt= f.label :permalink
14
14
  %dd= f.text_field :permalink, :class => 'text'
@@ -66,22 +66,35 @@
66
66
  %dd
67
67
  = attachment_preview @product.data_sheet
68
68
  %p= f.file_field :data_sheet_file
69
-
70
- = field_set_tag "Pricing" do
71
- .splitContainer
72
- %dl.third
73
- %dt= f.label :price
74
- %dd= f.text_field :price, :class => 'text'
75
- %dl.third
76
- %dt= f.label :cost_price
77
- %dd= f.text_field :cost_price, :class => 'text'
78
- %dl.third
79
- %dt= f.label :tax_rate_id
80
- %dd= f.collection_select :tax_rate_id, Shoppe::TaxRate.ordered, :id, :description, {:include_blank => true}, {:class => 'chosen-with-deselect', :data => {:placeholder => "No tax"}}
69
+
70
+ - unless @product.has_variants?
71
+ = field_set_tag "Pricing" do
72
+ .splitContainer
73
+ %dl.third
74
+ %dt= f.label :price
75
+ %dd= f.text_field :price, :class => 'text'
76
+ %dl.third
77
+ %dt= f.label :cost_price
78
+ %dd= f.text_field :cost_price, :class => 'text'
79
+ %dl.third
80
+ %dt= f.label :tax_rate_id
81
+ %dd= f.collection_select :tax_rate_id, Shoppe::TaxRate.ordered, :id, :description, {:include_blank => true}, {:class => 'chosen-with-deselect', :data => {:placeholder => "No tax"}}
82
+
83
+ = field_set_tag "Stock Control" do
84
+ .splitContainer
85
+ %dl.half
86
+ %dt= f.label :weight
87
+ %dd= f.text_field :weight, :class => 'text'
81
88
 
89
+ %dl.half
90
+ %dt= f.label :stock_control
91
+ %dd.checkbox
92
+ = f.check_box :stock_control
93
+ = f.label :stock_control, "Enable stock control for this product?"
94
+
82
95
  = field_set_tag "Website Properties" do
83
96
  .splitContainer
84
-
97
+
85
98
  %dl.half
86
99
  %dt= f.label :active, "On sale?"
87
100
  %dd.checkbox
@@ -93,18 +106,6 @@
93
106
  = f.check_box :featured
94
107
  = f.label :featured, "If checked, this product will appear on your homepage"
95
108
 
96
- = field_set_tag "Stock Control" do
97
- .splitContainer
98
- %dl.half
99
- %dt= f.label :weight
100
- %dd= f.text_field :weight, :class => 'text'
101
-
102
- %dl.half
103
- %dt= f.label :stock_control
104
- %dd.checkbox
105
- = f.check_box :stock_control
106
- = f.label :stock_control, "Enable stock control for this product?"
107
-
108
109
  %p.submit
109
110
  - unless @product.new_record?
110
111
  %span.right= link_to "Delete", @product, :class => 'button purple', :method => :delete, :data => {:confirm => "Are you sure you wish to remove this product?"}
@@ -0,0 +1,42 @@
1
+ .table
2
+ %table.data
3
+ %thead
4
+ %tr
5
+ %th{:width => '20%'} SKU
6
+ %th{:width => '40%'} Name
7
+ %th{:width => '25%'} Price/Variants
8
+ %th{:width => '15%'} Stock
9
+ %tbody
10
+ - if products.empty?
11
+ %tr.empty
12
+ %td{:colspan => 4} No products to display.
13
+ - else
14
+ - for category, products in products
15
+ %tr
16
+ %th{:colspan => 4}= category.name
17
+ - for product in products
18
+ %tr
19
+ %td= product.sku
20
+ %td= link_to product.name, [:edit, product]
21
+ - if product.has_variants?
22
+ %td.table{:colspan => 2}
23
+ %table.data
24
+ - for variant in product.variants
25
+ %tr
26
+ %td{:width => '40%'}= link_to variant.name, edit_product_variant_path(product, variant)
27
+ %td{:width => '30%'}= number_to_currency variant.price
28
+ %td{:width => '30%'}
29
+ - if variant.stock_control?
30
+ %span.float-right= link_to "Edit", stock_level_adjustments_path(:item_type => variant.class, :item_id => variant.id), :class => 'edit', :rel => 'dialog', :data => {:dialog_width => 700, :dialog_behavior => 'stockLevelAdjustments'}
31
+ = boolean_tag(variant.in_stock?, nil, :true_text => variant.stock, :false_text => 'No stock')
32
+ - else
33
+ &#8734;
34
+ - else
35
+ %td= number_to_currency product.price
36
+ %td
37
+ - if product.stock_control?
38
+ %span.float-right= link_to "Edit", stock_level_adjustments_path(:item_type => product.class, :item_id => product.id), :class => 'edit', :rel => 'dialog', :data => {:dialog_width => 700, :dialog_behavior => 'stockLevelAdjustments'}
39
+ = boolean_tag(product.in_stock?, nil, :true_text => product.stock, :false_text => 'No stock')
40
+ - else
41
+ &#8734;
42
+
@@ -1,7 +1,8 @@
1
1
  - @page_title = "Products"
2
2
  = content_for :header do
3
3
  %p.buttons
4
- = link_to "Stock levels", [:stock_levels, @product], :class => 'button'
4
+ = link_to "Variants", [@product, :variants], :class => 'button'
5
+ = link_to "Stock levels", stock_level_adjustments_path(:item_id => @product.id, :item_type => @product.class), :class => 'button', :rel => 'dialog', :data => {:dialog_width => 700, :dialog_behavior => 'stockLevelAdjustments'}
5
6
  = link_to "Back to product list", :products, :class => 'button'
6
7
  %h2.products Products
7
8
  = render 'form'
@@ -4,27 +4,4 @@
4
4
  %p.buttons= link_to "New product", :new_product, :class => 'button green'
5
5
  %h2.products Products
6
6
 
7
- .table
8
- %table.data
9
- %thead
10
- %tr
11
- %th{:width => '20%'} SKU
12
- %th{:width => '50%'} Name
13
- %th{:width => '15%'} Price
14
- %th{:width => '15%'} Stock
15
- %tbody
16
- - if @products.empty?
17
- %tr.empty
18
- %td{:colspan => 4} No products to display.
19
- - else
20
- - for category, products in @products
21
- %tr
22
- %th{:colspan => 4}= category.name
23
- - for product in products
24
- %tr
25
- %td= product.sku
26
- %td= link_to product.title, [:edit, product]
27
- %td= number_to_currency product.price
28
- %td
29
- %span.float-right= link_to "Edit", stock_levels_product_path(product), :class => 'edit'
30
- = product.stock_control? ? boolean_tag(product.in_stock?, nil, :true_text => product.stock, :false_text => 'No stock') : ''
7
+ = render 'table', :products => @products