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