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