shoppe 0.0.14 → 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- 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,51 +1,53 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class ProductAttribute < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_product_attributes'
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
# Validations
|
8
|
+
validates :key, :presence => true
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
# Relationships
|
11
|
+
belongs_to :product, :class_name => 'Shoppe::Product'
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
# Scopes
|
14
|
+
scope :searchable, -> { where(:searchable => true) }
|
15
|
+
scope :public, -> { where(:public => true) }
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
1
|
+
module Shoppe
|
2
|
+
class ProductCategory < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_product_categories'
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
# Attachments
|
8
|
+
attachment :image
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
# Relationships
|
11
|
+
has_many :products, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Product'
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
# Validations
|
14
|
+
validates :name, :presence => true
|
15
|
+
validates :permalink, :presence => true, :uniqueness => true
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
# Scopes
|
18
|
+
scope :ordered, -> { order(:name) }
|
18
19
|
|
19
|
-
|
20
|
-
|
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 :
|
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")}
|
data/app/models/shoppe/user.rb
CHANGED
@@ -1,41 +1,43 @@
|
|
1
|
-
|
1
|
+
module Shoppe
|
2
|
+
class User < ActiveRecord::Base
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
# Set the table name
|
5
|
+
self.table_name = 'shoppe_users'
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
# Self explanatory I think!
|
8
|
+
has_secure_password
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
# Validations
|
11
|
+
validates :first_name, :presence => true
|
12
|
+
validates :last_name, :presence => true
|
13
|
+
validates :email_address, :presence => true
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
# The user's first name & last name concatenated
|
16
|
+
def full_name
|
17
|
+
"#{first_name} #{last_name}"
|
18
|
+
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
→
|
24
24
|
= f.text_field :received_at_lteq, :class => 'small'
|
25
25
|
%dl.right
|
26
|
-
%dt= f.label :
|
27
|
-
%dd= f.text_field :
|
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.
|
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=
|
108
|
-
%td.sku= item.
|
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 :
|
11
|
-
%dd= f.text_field :
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
%
|
74
|
-
|
75
|
-
|
76
|
-
%
|
77
|
-
|
78
|
-
|
79
|
-
%
|
80
|
-
|
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
|
+
∞
|
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
|
+
∞
|
42
|
+
|
@@ -1,7 +1,8 @@
|
|
1
1
|
- @page_title = "Products"
|
2
2
|
= content_for :header do
|
3
3
|
%p.buttons
|
4
|
-
= link_to "
|
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
|
-
|
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
|