stall 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/para/stall/inputs/variant-select.coffee +4 -3
  3. data/app/assets/javascripts/stall.coffee +3 -1
  4. data/app/assets/javascripts/stall/add-product-to-wish-list-button.coffee +99 -0
  5. data/app/assets/javascripts/stall/add-to-wish-list-button.coffee +17 -0
  6. data/app/assets/javascripts/stall/product-list-form.coffee +31 -0
  7. data/app/controllers/stall/cart_line_items_controller.rb +21 -0
  8. data/app/controllers/stall/curated_product_lists_controller.rb +6 -0
  9. data/app/controllers/stall/line_items_controller.rb +14 -7
  10. data/app/controllers/stall/products_breadcrumbs.rb +8 -1
  11. data/app/controllers/stall/products_controller.rb +12 -0
  12. data/app/controllers/stall/wish_list_line_items_controller.rb +52 -0
  13. data/app/controllers/stall/wish_lists_controller.rb +7 -0
  14. data/app/helpers/stall/add_to_cart_helper.rb +3 -2
  15. data/app/helpers/stall/add_to_wish_list_helper.rb +30 -0
  16. data/app/helpers/stall/products_helper.rb +13 -0
  17. data/app/models/stall/models/cart.rb +0 -6
  18. data/app/models/stall/models/customer.rb +9 -0
  19. data/app/models/stall/models/line_item.rb +13 -0
  20. data/app/models/stall/models/product.rb +1 -1
  21. data/app/models/stall/models/product_list.rb +4 -0
  22. data/app/models/stall/models/variant.rb +4 -0
  23. data/app/models/stall/models/wish_list.rb +18 -0
  24. data/app/models/wish_list.rb +3 -0
  25. data/app/services/stall/add_to_cart_service.rb +4 -47
  26. data/app/services/stall/add_to_product_list_service.rb +63 -0
  27. data/app/services/stall/add_to_wish_list_service.rb +17 -0
  28. data/app/services/stall/available_stocks_service.rb +1 -1
  29. data/app/services/stall/shipping_fee_calculator_service.rb +22 -9
  30. data/app/views/admin/products/_form.html.haml +5 -0
  31. data/app/views/checkout/steps/_informations.html.haml +1 -1
  32. data/app/views/stall/addresses/_fields.html.haml +4 -4
  33. data/app/views/stall/{line_items → cart_line_items}/_add_error.html.haml +0 -0
  34. data/app/views/stall/{line_items → cart_line_items}/_added.html.haml +2 -2
  35. data/app/views/stall/{line_items → cart_line_items}/_form.html.haml +1 -1
  36. data/app/views/stall/carts/_widget.html.haml +1 -1
  37. data/app/views/stall/carts/show.html.haml +4 -4
  38. data/app/views/stall/curated_product_lists/show.html.haml +1 -1
  39. data/app/views/stall/customers/_fields.html.haml +1 -1
  40. data/app/views/stall/customers/_sign_in.html.haml +2 -2
  41. data/app/views/stall/products/_list.html.haml +2 -0
  42. data/app/views/stall/products/_product.html.haml +2 -1
  43. data/app/views/stall/wish_list_line_items/_add_error.html.haml +17 -0
  44. data/app/views/stall/wish_list_line_items/_added.html.haml +19 -0
  45. data/app/views/stall/wish_list_line_items/_button.html.haml +3 -0
  46. data/app/views/stall/wish_list_line_items/_form.html.haml +12 -0
  47. data/app/views/stall/wish_lists/show.html.haml +21 -0
  48. data/config/locales/stall.fr.yml +26 -0
  49. data/db/migrate/20170425085606_add_weight_to_stall_products_and_variants.rb +6 -0
  50. data/db/migrate/20170426163450_add_vat_rate_to_stall_products.rb +5 -0
  51. data/db/migrate/20170522062334_change_variants_weight_default_to_nil.rb +11 -0
  52. data/lib/generators/stall/install/templates/initializer.rb +18 -0
  53. data/lib/stall.rb +1 -0
  54. data/lib/stall/addressable.rb +35 -4
  55. data/lib/stall/addresses/copy_source_to_target.rb +14 -10
  56. data/lib/stall/addresses/prefill_target_from_source.rb +10 -6
  57. data/lib/stall/config.rb +4 -0
  58. data/lib/stall/engine.rb +1 -0
  59. data/lib/stall/routes.rb +42 -19
  60. data/lib/stall/sellable.rb +1 -1
  61. data/lib/stall/shipping/calculator.rb +1 -1
  62. data/lib/stall/shipping/country_weight_table_calculator.rb +3 -2
  63. data/lib/stall/version.rb +1 -1
  64. data/lib/stall/wish_list_helper.rb +93 -0
  65. metadata +26 -6
  66. data/app/assets/javascripts/stall/cart-form.coffee +0 -28
@@ -29,6 +29,7 @@ module Stall
29
29
 
30
30
  before_validation :restore_valid_quantity
31
31
  before_validation :refresh_total_prices
32
+ before_validation :refresh_weight
32
33
 
33
34
  scope :ordered, -> { order(created_at: :asc) }
34
35
  end
@@ -43,6 +44,14 @@ module Stall
43
44
  product_list.try(:currency) || Money.default_currency
44
45
  end
45
46
 
47
+ def weight
48
+ read_store_attribute(:data, :weight).presence || Stall.config.default_product_weight
49
+ end
50
+
51
+ def total_weight
52
+ weight * quantity
53
+ end
54
+
46
55
  private
47
56
 
48
57
  # TODO : Stocks availibility handling
@@ -61,6 +70,10 @@ module Stall
61
70
  restore_quantity!
62
71
  end
63
72
  end
73
+
74
+ def refresh_weight
75
+ self.weight = sellable.weight if sellable.respond_to?(:weight)
76
+ end
64
77
  end
65
78
  end
66
79
  end
@@ -43,7 +43,7 @@ module Stall
43
43
  end
44
44
 
45
45
  def vat_rate
46
- Stall.config.vat_rate
46
+ read_attribute(:vat_rate).presence || Stall.config.vat_rate
47
47
  end
48
48
 
49
49
  def price
@@ -67,6 +67,10 @@ module Stall
67
67
  line_items.map(&:quantity).compact.sum
68
68
  end
69
69
 
70
+ def total_weight
71
+ line_items.map(&:total_weight).sum
72
+ end
73
+
70
74
  def wizard
71
75
  @wizard ||= self.class.wizard
72
76
  end
@@ -32,6 +32,10 @@ module Stall
32
32
  @currency ||= Money::Currency.new(Stall.config.default_currency)
33
33
  end
34
34
 
35
+ def weight
36
+ read_attribute(:weight).presence || product.try(:weight)
37
+ end
38
+
35
39
  private
36
40
 
37
41
  def refresh_name
@@ -0,0 +1,18 @@
1
+ module Stall
2
+ module Models
3
+ module WishList
4
+ extend ActiveSupport::Concern
5
+
6
+ def includes_product?(product)
7
+ line_item_for_product(product).present?
8
+ end
9
+
10
+ def line_item_for_product(product)
11
+ line_items.find do |line_item|
12
+ line_item.sellable_type == 'Variant' &&
13
+ product.variant_ids.include?(line_item.sellable_id)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ class WishList < ProductList
2
+ include Stall::Models::WishList
3
+ end
@@ -1,18 +1,11 @@
1
1
  module Stall
2
- class AddToCartService < Stall::BaseService
3
- attr_reader :cart, :params
4
-
5
- def initialize(cart, params)
6
- @cart = cart
7
- @params = params
8
- end
2
+ class AddToCartService < Stall::AddToProductListService
3
+ alias_method :cart, :product_list
9
4
 
10
5
  def call
11
6
  return false unless line_item_valid?
12
7
 
13
- unless assemble_identical_line_items
14
- cart.line_items << line_item
15
- end
8
+ cart.line_items << line_item unless assemble_identical_line_items
16
9
 
17
10
  cart.save.tap do |saved|
18
11
  return false unless saved
@@ -32,46 +25,10 @@ module Stall
32
25
  stock_service.on_hand?(line_item)
33
26
  end
34
27
 
35
- def line_item
36
- @line_item ||= sellable.to_line_item.tap do |line_item|
37
- line_item.quantity = line_item_params[:quantity]
38
- end
39
- end
40
-
41
28
  private
42
29
 
43
- def assemble_identical_line_items
44
- # Find an existing line item which is like our new one
45
- existing_line_item = cart.line_items.find do |li|
46
- line_item.like?(li)
47
- end
48
-
49
- # If we found one, we assemble both line items into the old one, to avoid
50
- # duplicating the same sellables in the product list
51
- if existing_line_item
52
- existing_line_item.quantity += line_item.quantity
53
- @line_item = existing_line_item
54
- else
55
- false
56
- end
57
- end
58
-
59
- def sellable
60
- @sellable ||= sellable_class.find(line_item_params[:sellable_id])
61
- end
62
-
63
- def sellable_class
64
- @sellable_class ||= line_item_params[:sellable_type].camelize.constantize
65
- end
66
-
67
30
  def shipping_fee_service
68
- @shipping_fee_service ||= Stall::ShippingFeeCalculatorService.new(cart)
69
- end
70
-
71
- def line_item_params
72
- @line_item_params ||= params.require(:line_item).permit(
73
- :sellable_type, :sellable_id, :quantity
74
- )
31
+ @shipping_fee_service ||= Stall.config.service_for(:shipping_fee_calculator).new(cart)
75
32
  end
76
33
  end
77
34
  end
@@ -0,0 +1,63 @@
1
+ module Stall
2
+ class AddToProductListService < Stall::BaseService
3
+ attr_reader :product_list, :params
4
+
5
+ def initialize(product_list, params)
6
+ @product_list = product_list
7
+ @params = params
8
+ end
9
+
10
+ def call
11
+ fail NotImplementedError,
12
+ 'Override #call in Stall::AddToProductListService subclasses'
13
+ end
14
+
15
+ def line_item
16
+ @line_item ||= sellable.to_line_item.tap do |line_item|
17
+ line_item.quantity = quantity
18
+ end
19
+ end
20
+
21
+ def line_item_params?
22
+ line_item_params.any?
23
+ end
24
+
25
+ private
26
+
27
+ def assemble_identical_line_items
28
+ # Find an existing line item which is like our new one
29
+ existing_line_item = product_list.line_items.find do |li|
30
+ line_item.like?(li)
31
+ end
32
+
33
+ # If we found one, we assemble both line items into the old one, to avoid
34
+ # duplicating the same sellables in the product list
35
+ if existing_line_item
36
+ existing_line_item.quantity += line_item.quantity
37
+ @line_item = existing_line_item
38
+ else
39
+ false
40
+ end
41
+ end
42
+
43
+ def sellable
44
+ @sellable ||= sellable_class.find(line_item_params[:sellable_id])
45
+ end
46
+
47
+ def sellable_class
48
+ @sellable_class ||= line_item_params[:sellable_type].camelize.constantize
49
+ end
50
+
51
+ def quantity
52
+ @quantity ||= line_item_params[:quantity]
53
+ end
54
+
55
+ def line_item_params
56
+ @line_item_params ||= params.require(:line_item).permit(
57
+ :sellable_type, :sellable_id, :quantity
58
+ )
59
+ rescue ActionController::ParameterMissing
60
+ {}
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ module Stall
2
+ class AddToWishListService < Stall::AddToProductListService
3
+ alias_method :wish_list, :product_list
4
+
5
+ def call
6
+ return false unless line_item.valid?
7
+ wish_list.line_items << line_item unless assemble_identical_line_items
8
+ wish_list.save
9
+ end
10
+
11
+ def add(variant)
12
+ @sellable = variant
13
+ @quantity = 1
14
+ call
15
+ end
16
+ end
17
+ end
@@ -2,7 +2,7 @@ module Stall
2
2
  class AvailableStocksService < Stall::BaseService
3
3
  def on_hand?(line_item)
4
4
  if Stall.config.manage_inventory
5
- line_item.variant.stock >= line_item.quantity
5
+ line_item.sellable.stock >= line_item.quantity
6
6
  else
7
7
  true
8
8
  end
@@ -9,24 +9,37 @@ module Stall
9
9
  def call
10
10
  return unless cart.shipment && cart.shipment.shipping_method
11
11
 
12
- calculator_identifier = cart.shipment.shipping_method.identifier
13
- calculator_class = Stall::Shipping::Calculator.for(calculator_identifier)
12
+ update_price_for(cart.shipment, calculator) if calculator
13
+ end
14
+
15
+ def available?
16
+ cart.line_items.length > 0 &&
17
+ cart.try(:shipping_address) &&
18
+ cart.try(:shipment).try(:shipping_method)
19
+ end
14
20
 
15
- if calculator_class
16
- calculator = calculator_class.new(cart, cart.shipment.shipping_method)
21
+ private
17
22
 
18
- cart.shipment.update_attributes(
23
+ def update_price_for(shipment, calculator)
24
+ if calculator.price
25
+ shipment.update_attributes(
19
26
  price: calculator.price,
20
27
  eot_price: calculator.eot_price,
21
28
  vat_rate: calculator.vat_rate
22
29
  )
30
+ else
31
+ shipment.errors.add(:price, :unavailable)
23
32
  end
24
33
  end
25
34
 
26
- def available?
27
- cart.line_items.length > 0 &&
28
- cart.try(:shipping_address) &&
29
- cart.try(:shipment).try(:shipping_method)
35
+ def calculator
36
+ @calculator ||= calculator_class && calculator_class.new(cart, cart.shipment.shipping_method)
37
+ end
38
+
39
+ def calculator_class
40
+ @calculator_class ||= Stall::Shipping::Calculator.for(
41
+ cart.shipment.shipping_method.identifier
42
+ ) if cart.shipment.try(:shipping_method)
30
43
  end
31
44
  end
32
45
  end
@@ -8,6 +8,8 @@
8
8
  = form.input :product_category, as: :selectize, collection: Para.components.product_categories.resources.leaves
9
9
  = form.input :manufacturer, as: :selectize
10
10
 
11
+ = form.input :vat_rate, hint: t('para.stall.products.vat_rate_hint', rate: number_to_percentage(Stall.config.vat_rate), precision: 2).html_safe
12
+
11
13
  = form.input :suggested_products, as: :selectize
12
14
 
13
15
  = tabs.tab :images, icon: 'picture-o' do
@@ -20,4 +22,7 @@
20
22
  = tabs.tab :variants, icon: 'list' do
21
23
  = form.input :variants, as: :variants_matrix
22
24
 
25
+ = tabs.tab :shipping, icon: 'truck' do
26
+ = form.input :weight, hint: t('para.stall.products.weight_hint').html_safe
27
+
23
28
  = form.actions
@@ -25,7 +25,7 @@
25
25
  = form.input :terms, label: false do
26
26
  = form.check_box :terms, id: 'terms-checkbox'
27
27
  = form.label :terms, t('stall.checkout.informations.accept_the')
28
- = link_to Stall.config.terms_path, target: '_blank' do
28
+ = link_to Stall.config.terms_path, target: '_blank', rel: 'nofollow' do
29
29
  = form.object.class.human_attribute_name(:terms)
30
30
 
31
31
  %button.btn.btn-primary.btn-lg.btn-block{ type: 'submit' }
@@ -1,10 +1,10 @@
1
1
  .addresses-fields{ data: { :'addresses-fields' => true } }
2
2
  %fieldset{ data: { :'address-form' => :shipping } }
3
- = form.fields_for :shipping_address do |address_form|
3
+ = form.fields_for :shipping_address, form.object.shipping_address(force_actual_address: true) do |address_form|
4
4
  = render partial: 'stall/addresses/nested_fields', locals: { form: address_form }
5
5
 
6
- = form.input :use_another_address_for_billing, as: :boolean, input_html: { checked: (params[:use_another_address_for_billing] || form.object.billing_address?), data: { :'use-another-address-for-billing' => true } }
6
+ = form.input :use_another_address_for_billing, as: :boolean, input_html: { checked: (params[:use_another_address_for_billing] == '1' || form.object.billing_address?), data: { :'use-another-address-for-billing' => true } }
7
7
 
8
- %fieldset{ class: ('hidden' unless params[:use_another_address_for_billing] == '1'), data: { :'address-form' => :billing } }
9
- = form.fields_for :billing_address do |address_form|
8
+ %fieldset{ class: ('hidden' unless params[:use_another_address_for_billing] == '1' || form.object.billing_address?), data: { :'address-form' => :billing } }
9
+ = form.fields_for :billing_address, form.object.billing_address(force_actual_address: true) do |address_form|
10
10
  = render partial: 'stall/addresses/nested_fields', locals: { form: address_form }
@@ -1,4 +1,4 @@
1
- .modal.fade{ role: 'dialog', tabindex: '-1', data: { :'cart' => { token: current_cart.token, :'total-quantity' => current_cart.total_quantity, :'widget-markup' => @widget_partial } } }
1
+ .modal.fade{ role: 'dialog', tabindex: '-1', data: { :'cart' => { token: @product_list.token, :'total-quantity' => @product_list.total_quantity, :'widget-markup' => @widget_partial } } }
2
2
  .modal-dialog
3
3
  .modal-content
4
4
  .modal-header
@@ -19,5 +19,5 @@
19
19
  %button.btn.btn-default{ type: 'button', data: { dismiss: 'modal' } }
20
20
  = t('stall.line_items.added.continue_shopping')
21
21
 
22
- = link_to cart_path(@cart), class: 'btn btn-primary' do
22
+ = link_to cart_path(@product_list), class: 'btn btn-primary', rel: 'nofollow' do
23
23
  = t('stall.carts.actions.view_cart')
@@ -1,6 +1,6 @@
1
1
  = simple_form_for line_item, as: :line_item, url: cart_line_items_path(cart), remote: true, data: { :'add-to-cart-form' => true, :'error-messages' => { choose: t('stall.line_items.errors.choose'), quantity: t('stall.line_items.errors.quantity') } } do |form|
2
2
  = form.hidden_field :sellable_type, value: 'Variant'
3
- = form.input_field :sellable, as: :variant_select, product: @product
3
+ = form.input_field :sellable, as: :variant_select, product: product
4
4
 
5
5
  .input-group
6
6
  = form.input_field :quantity, spinner: false, value: 1, class: 'form-group', data: { :'quantity-field' => true }
@@ -24,5 +24,5 @@
24
24
  = number_to_currency(cart.total_price)
25
25
  %tr
26
26
  %td{ colspan: 2 }
27
- = link_to cart_path(cart), class: 'btn btn-primary btn-block' do
27
+ = link_to cart_path(cart), class: 'btn btn-primary btn-block', rel: 'nofollow' do
28
28
  = t('stall.carts.actions.view_cart')
@@ -1,5 +1,5 @@
1
- .cart-recap{ data: { :'cart-recap' => true } }
2
- = simple_form_for @cart, url: cart_path(@cart), as: :cart, remote: true, data: { :'cart-form' => true } do |form|
1
+ .cart-recap{ data: { :'product-list-recap' => true } }
2
+ = simple_form_for @cart, url: cart_path(@cart), as: :cart, remote: true, data: { :'product-list-form' => true } do |form|
3
3
  %table.table.table-striped
4
4
  %thead
5
5
  %tr
@@ -56,10 +56,10 @@
56
56
  %td
57
57
 
58
58
  .form-actions
59
- %button.btn.btn-default{ type: 'submit', data: { :'cart-update-button' => true } }
59
+ %button.btn.btn-default{ type: 'submit', data: { :'product-list-update-button' => true } }
60
60
  = t('stall.carts.recap.update')
61
61
 
62
- = link_to checkout_path(current_cart.identifier), class: 'btn btn-primary' do
62
+ = link_to checkout_path(current_cart.identifier), class: 'btn btn-primary', rel: 'nofollow' do
63
63
  = t('stall.carts.recap.validate')
64
64
 
65
65
 
@@ -5,4 +5,4 @@
5
5
  = render partial: 'stall/products/filters', locals: { search: @search, products: @filterable_products }
6
6
 
7
7
  .col-md-8
8
- = render partial: 'stall/products/list', locals: { products: @products }
8
+ = render partial: 'stall/products/list', locals: { products: @products, parent: @curated_product_list }
@@ -26,7 +26,7 @@
26
26
  %legend.omniauth-sign-up= t('stall.checkout.sign_in.omniauth_sign_up')
27
27
 
28
28
  - Stall.config.omniauth_providers.each do |provider|
29
- = link_to user_omniauth_redirect_path(provider.name, _redirect_to: request.path), class: 'btn btn-default' do
29
+ = link_to user_omniauth_redirect_path(provider.name, _redirect_to: request.path), class: 'btn btn-default', rel: 'nofollow' do
30
30
  = fa_icon provider.icon
31
31
  = t('stall.omniauth.sign_in_with', provider: provider.display_name)
32
32
 
@@ -2,7 +2,7 @@
2
2
  .panel-heading
3
3
  %i.fa.fa-user
4
4
  = t('stall.checkout.sign_in.already_have_an_account')
5
- = link_to '#sign-in-form', data: { toggle: 'collapse' } do
5
+ = link_to '#sign-in-form', data: { toggle: 'collapse' }, rel: 'nofollow' do
6
6
  = t('stall.checkout.sign_in.click_to_sign_in')
7
7
 
8
8
  #sign-in-form.panel-body.collapse{ data: { :'sign-in-form' => true } }
@@ -27,6 +27,6 @@
27
27
  %legend.omniauth-sign-in= t('stall.checkout.sign_in.omniauth_sign_in')
28
28
 
29
29
  - Stall.config.omniauth_providers.each do |provider|
30
- = link_to user_omniauth_redirect_path(provider.name, _redirect_to: request.path), class: 'btn btn-default' do
30
+ = link_to user_omniauth_redirect_path(provider.name, _redirect_to: request.path), class: 'btn btn-default', rel: 'nofollow' do
31
31
  = fa_icon provider.icon
32
32
  = t('stall.omniauth.sign_in_with', provider: provider.display_name)
@@ -1,3 +1,5 @@
1
+ - unless defined?(parent) then parent = nil; end
2
+
1
3
  .products-list
2
4
  .row
3
5
  - products.each do |product|
@@ -1,4 +1,5 @@
1
- = link_to product_path(product), class: 'product' do
1
+ = link_to product_path(product), class: 'product', data: { :'product-id' => product.id } do
2
2
  %span.product-name= product.name
3
3
  = image_tag product.image.url(:thumb), class: 'img-responsive'
4
4
  = number_to_currency(product.price)
5
+ = add_to_wish_list_button(product)