stall 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -23
  3. data/app/assets/javascripts/para/stall.coffee +1 -0
  4. data/app/assets/javascripts/para/stall/inputs/variant-select.coffee +62 -0
  5. data/app/assets/javascripts/para/stall/inputs/variants-matrix.coffee +12 -0
  6. data/app/assets/javascripts/para/stall/inputs/variants-matrix/helpers.coffee +40 -0
  7. data/app/assets/javascripts/para/stall/inputs/variants-matrix/input.coffee +133 -0
  8. data/app/assets/javascripts/para/stall/inputs/variants-matrix/nested-fields.coffee +38 -0
  9. data/app/assets/javascripts/para/stall/inputs/variants-matrix/properties_select.coffee +45 -0
  10. data/app/assets/javascripts/para/stall/inputs/variants-matrix/variant.coffee +59 -0
  11. data/app/assets/javascripts/stall.coffee +1 -0
  12. data/app/assets/javascripts/stall/add-to-cart-form.coffee +53 -28
  13. data/app/assets/javascripts/stall/cart-form.coffee +7 -2
  14. data/app/assets/stylesheets/para/stall.sass +28 -0
  15. data/app/controllers/para/stall/admin/carts_controller.rb +27 -0
  16. data/app/controllers/stall/cart_credits_controller.rb +27 -0
  17. data/app/controllers/stall/carts_controller.rb +1 -1
  18. data/app/controllers/stall/checkout/steps_controller.rb +22 -12
  19. data/app/controllers/stall/checkouts_controller.rb +1 -0
  20. data/app/controllers/stall/line_items_controller.rb +1 -0
  21. data/app/controllers/stall/payments_controller.rb +16 -1
  22. data/app/helpers/stall/credit_notes_helper.rb +25 -0
  23. data/app/helpers/stall/customers_helper.rb +3 -1
  24. data/app/models/billing_address.rb +2 -0
  25. data/app/models/cart_credit_note_adjustment.rb +3 -0
  26. data/app/models/credit_note.rb +3 -0
  27. data/app/models/credit_note_adjustment.rb +3 -0
  28. data/app/models/credit_note_usage.rb +3 -0
  29. data/app/models/product.rb +3 -0
  30. data/app/models/product_category.rb +3 -0
  31. data/app/models/product_detail.rb +3 -0
  32. data/app/models/property.rb +3 -0
  33. data/app/models/property_value.rb +3 -0
  34. data/app/models/shipping_address.rb +2 -0
  35. data/app/models/stall/models/address.rb +2 -2
  36. data/app/models/stall/models/cart.rb +2 -15
  37. data/app/models/stall/models/cart_credit_note_adjustment.rb +11 -0
  38. data/app/models/stall/models/credit_note.rb +50 -0
  39. data/app/models/stall/models/credit_note_adjustment.rb +13 -0
  40. data/app/models/stall/models/credit_note_usage.rb +16 -0
  41. data/app/models/stall/models/customer.rb +20 -8
  42. data/app/models/stall/models/line_item.rb +9 -0
  43. data/app/models/stall/models/product.rb +45 -0
  44. data/app/models/stall/models/product_category.rb +31 -0
  45. data/app/models/stall/models/product_detail.rb +17 -0
  46. data/app/models/stall/models/product_list.rb +9 -45
  47. data/app/models/stall/models/property.rb +20 -0
  48. data/app/models/stall/models/property_value.rb +23 -0
  49. data/app/models/stall/models/variant.rb +34 -0
  50. data/app/models/stall/models/variant_property_value.rb +18 -0
  51. data/app/models/variant.rb +3 -0
  52. data/app/models/variant_property_value.rb +3 -0
  53. data/app/services/stall/cart_credit_note_creation_service.rb +40 -0
  54. data/app/services/stall/cart_payment_validation_service.rb +28 -0
  55. data/app/services/stall/cart_update_service.rb +17 -6
  56. data/app/services/stall/credit_usage_service.rb +102 -0
  57. data/app/services/stall/payment_notification_service.rb +4 -8
  58. data/app/services/stall/product_list_staleness_handling_service.rb +33 -0
  59. data/app/views/admin/addresses/_fields.html.haml +9 -0
  60. data/app/views/admin/carts/_filters.html.haml +17 -0
  61. data/app/views/admin/carts/_form.html.haml +43 -0
  62. data/app/views/admin/carts/_table.html.haml +17 -0
  63. data/app/views/admin/customers/_fields.html.haml +1 -0
  64. data/app/views/admin/line_items/_fields.html.haml +15 -0
  65. data/app/views/admin/products/_table.html.haml +12 -0
  66. data/app/views/admin/properties/_form.html.haml +6 -0
  67. data/app/views/admin/properties/_table.html.haml +8 -0
  68. data/app/views/admin/property_values/_fields.html.haml +1 -0
  69. data/app/views/admin/shipments/_fields.html.haml +7 -0
  70. data/app/views/checkout/steps/_informations.html.haml +3 -1
  71. data/app/views/checkout/steps/_payment.html.haml +2 -0
  72. data/app/views/checkout/steps/_payment_return.html.haml +0 -1
  73. data/app/views/para/admin/resources/_variant_row.html.haml +24 -0
  74. data/app/views/para/stall/inputs/_variant_select.html.haml +14 -0
  75. data/app/views/para/stall/inputs/_variants_matrix.html.haml +41 -0
  76. data/app/views/stall/addresses/_fields.html.haml +6 -12
  77. data/app/views/stall/carts/_cart.html.haml +45 -37
  78. data/app/views/stall/carts/_widget.html.haml +28 -0
  79. data/app/views/stall/carts/show.html.haml +2 -0
  80. data/app/views/stall/checkout/steps/_navigation.html.haml +13 -0
  81. data/app/views/stall/credit_note_adjustments/_form.html.haml +28 -0
  82. data/app/views/stall/line_items/_added.html.haml +2 -2
  83. data/app/views/stall/line_items/_form.html.haml +1 -1
  84. data/app/views/stall/payments/manual_payment_gateway/_form.html.haml +10 -0
  85. data/app/views/stall/shared/mailers/_cart.html.haml +1 -1
  86. data/config/locales/stall.fr.yml +82 -2
  87. data/db/migrate/20161129101956_add_type_to_stall_address_ownerships.rb +52 -0
  88. data/db/migrate/20161202080218_add_reference_to_product_lists.rb +17 -0
  89. data/db/migrate/20170118103916_create_credit_notes.rb +17 -0
  90. data/db/migrate/20170118144047_create_credit_note_adjustments.rb +13 -0
  91. data/db/migrate/20170123123115_create_stall_product_categories.rb +12 -0
  92. data/db/migrate/20170123123326_create_stall_products.rb +17 -0
  93. data/db/migrate/20170123125030_create_stall_variants.rb +13 -0
  94. data/db/migrate/20170123131748_create_stall_product_category_hierarchies.rb +16 -0
  95. data/db/migrate/20170123143704_create_stall_product_details.rb +14 -0
  96. data/db/migrate/20170125152622_convert_all_money_fields_to_decimal_to_use_infinite_precision.rb +27 -0
  97. data/db/migrate/20170131162537_add_data_to_stall_adjustments.rb +5 -0
  98. data/db/migrate/20170202165514_create_stall_properties.rb +9 -0
  99. data/db/migrate/20170202165516_create_stall_property_values.rb +13 -0
  100. data/db/migrate/20170202165518_create_stall_variant_property_values.rb +13 -0
  101. data/lib/generators/stall/install/templates/initializer.rb +21 -0
  102. data/lib/generators/stall/view/view_generator.rb +41 -19
  103. data/lib/para/stall.rb +32 -0
  104. data/lib/para/stall/inputs.rb +13 -0
  105. data/lib/para/stall/inputs/variant_input_helper.rb +34 -0
  106. data/lib/para/stall/inputs/variant_select_input.rb +79 -0
  107. data/lib/para/stall/inputs/variants_matrix_input.rb +72 -0
  108. data/lib/para/stall/routes.rb +11 -0
  109. data/lib/para/stall/variants_property_config.rb +78 -0
  110. data/lib/stall.rb +10 -0
  111. data/lib/stall/addressable.rb +11 -59
  112. data/lib/stall/addresses.rb +1 -0
  113. data/lib/stall/addresses/copier_base.rb +3 -1
  114. data/lib/stall/addresses/copy.rb +10 -0
  115. data/lib/stall/addresses/copy_source_to_target.rb +10 -24
  116. data/lib/stall/addresses/prefill_target_from_source.rb +8 -16
  117. data/lib/stall/adjustable.rb +20 -0
  118. data/lib/stall/archived_paid_cart_helper.rb +36 -0
  119. data/lib/stall/cart_helper.rb +15 -7
  120. data/lib/stall/checkout/informations_checkout_step.rb +47 -50
  121. data/lib/stall/checkout/payment_return_checkout_step.rb +4 -1
  122. data/lib/stall/checkout/step.rb +24 -3
  123. data/lib/stall/checkout/step_form.rb +11 -5
  124. data/lib/stall/checkout/wizard.rb +7 -6
  125. data/lib/stall/config.rb +9 -0
  126. data/lib/stall/default_currency_manager.rb +27 -0
  127. data/lib/stall/engine.rb +14 -3
  128. data/lib/stall/payments.rb +2 -0
  129. data/lib/stall/payments/gateway_request.rb +15 -0
  130. data/lib/stall/payments/gateway_response.rb +33 -0
  131. data/lib/stall/payments/manual_payment_gateway.rb +86 -0
  132. data/lib/stall/priceable.rb +4 -0
  133. data/lib/stall/reference_manager.rb +17 -0
  134. data/lib/stall/routes.rb +1 -0
  135. data/lib/stall/shippable.rb +18 -0
  136. data/lib/stall/total_prices_manager.rb +40 -0
  137. data/lib/stall/version.rb +1 -1
  138. metadata +120 -5
  139. data/app/models/address_ownership.rb +0 -3
  140. data/app/models/stall/models/address_ownership.rb +0 -26
@@ -0,0 +1,20 @@
1
+ module Stall
2
+ module Models
3
+ module Property
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_properties'
8
+
9
+ has_many :property_values, dependent: :destroy,
10
+ inverse_of: :property
11
+ accepts_nested_attributes_for :property_values, allow_destroy: true
12
+
13
+ has_many :variant_property_values, through: :property_values
14
+ has_many :variants, through: :variant_property_values
15
+
16
+ validates :name, presence: true
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module Stall
2
+ module Models
3
+ module PropertyValue
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_property_values'
8
+
9
+ acts_as_orderable
10
+
11
+ belongs_to :property
12
+
13
+ has_many :variant_property_values, dependent: :destroy,
14
+ inverse_of: :property_value
15
+ has_many :variants, through: :variant_property_values
16
+
17
+ validates :property, :value, presence: true
18
+
19
+ alias_attribute :name, :value
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module Stall
2
+ module Models
3
+ module Variant
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_variants'
8
+
9
+ include Stall::Sellable
10
+
11
+ belongs_to :product
12
+
13
+ has_many :variant_property_values, dependent: :destroy,
14
+ inverse_of: :variant
15
+ accepts_nested_attributes_for :variant_property_values,
16
+ allow_destroy: true,
17
+ reject_if: :all_blank
18
+
19
+ has_many :property_values, through: :variant_property_values
20
+ has_many :properties, through: :property_values
21
+
22
+ monetize :price_cents, with_model_currency: :currency, allow_nil: false
23
+
24
+ delegate :name, :image, :image?, :vat_rate, to: :product, allow_nil: true
25
+
26
+ scope :published, -> { where(published: true) }
27
+
28
+ def currency
29
+ @currency ||= Money::Currency.new(Stall.config.default_currency)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ module Stall
2
+ module Models
3
+ module VariantPropertyValue
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_variant_property_values'
8
+
9
+ belongs_to :property_value
10
+ has_one :property, through: :property_value
11
+
12
+ belongs_to :variant
13
+
14
+ validates :property_value, :variant, presence: true
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ class Variant < ActiveRecord::Base
2
+ include Stall::Models::Variant
3
+ end
@@ -0,0 +1,3 @@
1
+ class VariantPropertyValue < ActiveRecord::Base
2
+ include Stall::Models::VariantPropertyValue
3
+ end
@@ -0,0 +1,40 @@
1
+ module Stall
2
+ class CartCreditNoteCreationService < Stall::BaseService
3
+ attr_reader :cart
4
+
5
+ def initialize(cart)
6
+ @cart = cart
7
+ end
8
+
9
+ def call
10
+ if cart.remainder? && !adjustment_exists?
11
+ credit_note = create_credit_note!
12
+ create_adjustment_for!(credit_note)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def adjustment_exists?
19
+ cart.adjustments.any? { |adj| CartCreditNoteAdjustment === adj }
20
+ end
21
+
22
+ def create_credit_note!
23
+ cart.customer.credit_notes.create!(amount: cart.remainder)
24
+ end
25
+
26
+ def create_adjustment_for!(credit_note)
27
+ name = I18n.t(
28
+ 'stall.credit_notes.source_cart_adjustment_label',
29
+ ref: credit_note.reference
30
+ )
31
+
32
+ cart.adjustments.create!(
33
+ type: 'CartCreditNoteAdjustment',
34
+ name: name,
35
+ price: credit_note.amount,
36
+ credit_note: credit_note
37
+ )
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ module Stall
2
+ class CartPaymentValidationService < Stall::BaseService
3
+ attr_reader :cart
4
+
5
+ def initialize(cart)
6
+ @cart = cart
7
+ end
8
+
9
+ def call
10
+ cart.payment.pay!
11
+ send_payment_notification_emails!
12
+ create_credit_notes!
13
+ end
14
+
15
+ private
16
+
17
+ def send_payment_notification_emails!
18
+ Stall::CustomerMailer.order_paid_email(cart).deliver
19
+ Stall::AdminMailer.order_paid_email(cart).deliver
20
+ end
21
+
22
+ def create_credit_notes!
23
+ if cart.remainder?
24
+ Stall.config.service_for(:cart_credit_note_creation).new(cart).call
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,7 +2,7 @@ module Stall
2
2
  class CartUpdateService < Stall::BaseService
3
3
  attr_reader :cart, :params
4
4
 
5
- def initialize(cart, params)
5
+ def initialize(cart, params = {})
6
6
  @cart = cart
7
7
  @params = params
8
8
  end
@@ -10,17 +10,28 @@ module Stall
10
10
  def call
11
11
  cart.update(params).tap do |saved|
12
12
  return false unless saved
13
-
14
- # Recalculate shipping fee if available for calculation to ensure
15
- # that the fee us always up to date when displayed to the customer
16
- shipping_fee_service.call if shipping_fee_service.available?
13
+ refresh_associated_services!
17
14
  end
18
15
  end
19
16
 
17
+ def refresh_associated_services!
18
+ # Recalculate shipping fee if available for calculation to ensure
19
+ # that the fee us always up to date when displayed to the customer
20
+ shipping_fee_service.call if shipping_fee_service.available?
21
+
22
+ # Recalculate the credit usage amount if already used to avoid negative
23
+ # cart totals
24
+ credit_usage_service.ensure_valid_or_remove! if credit_usage_service.available? && credit_usage_service.credit_used?
25
+ end
26
+
20
27
  private
21
28
 
22
29
  def shipping_fee_service
23
- @shipping_fee_service ||= Stall::ShippingFeeCalculatorService.new(cart)
30
+ @shipping_fee_service ||= Stall.config.service_for(:shipping_fee_calculator).new(cart)
31
+ end
32
+
33
+ def credit_usage_service
34
+ @credit_usage_service ||= Stall.config.service_for(:credit_usage).new(cart)
24
35
  end
25
36
  end
26
37
  end
@@ -0,0 +1,102 @@
1
+ module Stall
2
+ class CreditUsageService < Stall::BaseService
3
+ attr_reader :cart, :params
4
+
5
+ def initialize(cart, params = {})
6
+ @cart = cart
7
+ @params = params
8
+ end
9
+
10
+ def call
11
+ return false unless enough_credit?
12
+
13
+ clean_credit_note_adjustments!
14
+
15
+ available_credit_notes.reduce(amount) do |missing_amount, credit_note|
16
+ break true if missing_amount.to_d == 0
17
+
18
+ used_amount = [credit_note.remaining_amount, missing_amount].min
19
+ add_adjustment(used_amount, credit_note)
20
+
21
+ missing_amount - used_amount
22
+ end
23
+ end
24
+
25
+ def amount
26
+ @amount ||= begin
27
+ amount = if params[:amount]
28
+ cents = BigDecimal.new(params[:amount]) * 100
29
+ Money.new(cents, cart.currency)
30
+ else
31
+ credit
32
+ end
33
+
34
+ [amount, cart.total_price].min
35
+ end
36
+ end
37
+
38
+ def credit
39
+ @credit ||= begin
40
+ credit = cart.customer.try(:credit, cart.currency) || Money.new(0, cart.currency)
41
+ Money.new(credit + credit_note_adjustments.map(&:price).sum.abs, cart.currency)
42
+ end
43
+ end
44
+
45
+ def enough_credit?
46
+ amount <= credit
47
+ end
48
+
49
+ def clean_credit_note_adjustments!
50
+ credit_note_adjustments.each do |adjustment|
51
+ cart.adjustments.destroy(adjustment)
52
+ end
53
+ end
54
+
55
+ # FIXME: Ducktyping ShippingFeeCalculatorService and use this method in
56
+ # CartUpdateService#refresh_associated_services! to test if credit notes exists
57
+ #
58
+ def available?
59
+ cart.respond_to?(:adjustments)
60
+ end
61
+
62
+ def credit_used?
63
+ credit_note_adjustments.any?
64
+ end
65
+
66
+ def credit_used
67
+ credit_note_adjustments.map(&:price).sum
68
+ end
69
+
70
+ def ensure_valid_or_remove!
71
+ if !enough_credit?
72
+ clean_credit_note_adjustments!
73
+ elsif cart.total_price.to_d < 0
74
+ @amount = credit_used
75
+ call
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def available_credit_notes
82
+ @available_credit_notes ||= cart.customer.credit_notes
83
+ .for_currency(cart.currency)
84
+ .select(&:with_remaining_money?)
85
+ end
86
+
87
+ def add_adjustment(amount, credit_note)
88
+ cart.adjustments.create!(
89
+ type: 'CreditNoteAdjustment',
90
+ name: I18n.t('stall.credit_notes.adjustment_label', ref: credit_note.reference),
91
+ price: -amount,
92
+ credit_note: credit_note
93
+ )
94
+ end
95
+
96
+ def credit_note_adjustments
97
+ @credit_note_adjustments ||= cart.adjustments.select do |adjustment|
98
+ CreditNoteAdjustment === adjustment
99
+ end
100
+ end
101
+ end
102
+ end
@@ -11,7 +11,7 @@ module Stall
11
11
 
12
12
  def call
13
13
  if gateway_response.valid?
14
- send_payment_notification_emails! if gateway_response.notify
14
+ validate_cart! if gateway_response.process
15
15
  else
16
16
  raise UnknownNotificationError,
17
17
  "The payment notification request does not seem to come from " +
@@ -25,8 +25,9 @@ module Stall
25
25
 
26
26
  private
27
27
 
28
- def cart
29
- gateway_response.cart
28
+ def validate_cart!
29
+ service = Stall.config.service_for(:cart_payment_validation)
30
+ service.new(gateway_response.cart).call
30
31
  end
31
32
 
32
33
  def gateway_class
@@ -36,10 +37,5 @@ module Stall
36
37
  def gateway_response
37
38
  @gateway_response ||= gateway_class.response(request)
38
39
  end
39
-
40
- def send_payment_notification_emails!
41
- Stall::CustomerMailer.order_paid_email(gateway_response.cart).deliver
42
- Stall::AdminMailer.order_paid_email(gateway_response.cart).deliver
43
- end
44
40
  end
45
41
  end
@@ -0,0 +1,33 @@
1
+ module Stall
2
+ class ProductListStalenessHandlingService < Stall::BaseService
3
+ attr_reader :cart
4
+
5
+ def initialize(cart)
6
+ @cart = cart
7
+ end
8
+
9
+ def call
10
+ handle_line_items
11
+ end
12
+
13
+ private
14
+
15
+ def handle_line_items
16
+ cart.line_items.each do |line_item|
17
+ if stale?(line_item)
18
+ cart.line_items.delete(line_item)
19
+ else
20
+ handle_valid_line_item(line_item)
21
+ end
22
+ end
23
+ end
24
+
25
+ def handle_valid_line_item line_item
26
+ # Override this method
27
+ end
28
+
29
+ def stale? line_item
30
+ line_item.sellable.nil? || !line_item.sellable.published?
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ = form.input :civility
2
+ = form.input :first_name
3
+ = form.input :last_name
4
+ = form.input :address
5
+ = form.input :address_details
6
+ = form.input :zip
7
+ = form.input :city
8
+ = form.input :country
9
+ = form.input :phone
@@ -0,0 +1,17 @@
1
+ = search_form_for @q, builder: SimpleForm::FormBuilder, url: @component.path, html: { data: { :'filters-form' => true } } do |form|
2
+ .row
3
+ .col-md-3
4
+ = form.input_field :payment_paid_at_null, as: :selectize, collection: [{ text: t('activerecord.enums.payment.state.paid'), value: '0' }, { text: t('activerecord.enums.payment.state.pending'), value: '1' }], placeholder: @component.model.human_attribute_name(:payment)
5
+
6
+ .col-md-3
7
+ = form.input_field :state_eq, as: :selectize, collection: DefaultCheckoutWizard.steps.map { |step| { text: t("activerecord.enums.cart.state.#{ step }"), value: step } }, placeholder: @component.model.human_attribute_name(:state)
8
+
9
+ .col-md-4
10
+ .input-group
11
+ %span.input-group-addon
12
+ %i.fa.fa-search
13
+
14
+ = form.input_field :reference_or_customer_email_cont, as: :string, placeholder: t('para.shared.search'), class: 'form-control'
15
+
16
+ .col-md-2
17
+ = form.submit t('para.shared.search'), class: 'btn btn-default btn-block'
@@ -0,0 +1,43 @@
1
+ = para_form_for(resource) do |form|
2
+ = form.tabs do |tabs|
3
+ = tabs.tab :informations do
4
+ = form.input :reference
5
+ = form.input :total_price do
6
+ .input-group
7
+ = form.input_field :total_price, class: 'form-control', disabled: 'disabled'
8
+ %span.input-group-addon
9
+ = form.object.total_price.symbol
10
+
11
+ = form.actions
12
+
13
+ = tabs.tab :billing do
14
+ = form.input :customer, :as => :nested_one
15
+
16
+ = form.input :billing_address do
17
+ .nested-one-field
18
+ = form.simple_fields_for :billing_address, (form.object.billing_address || form.object.build_billing_address), nested_attribute_name: :billing_address do |nested_form|
19
+ = render partial: find_partial_for(Address, :fields), locals: { form: nested_form, parent: form.object }
20
+
21
+ = form.actions
22
+
23
+ = tabs.tab :shipping do
24
+ = form.input :shipment, :as => :nested_one
25
+
26
+ = form.input :shipping_address do
27
+ .nested-one-field
28
+ = form.simple_fields_for :shipping_address, (form.object.shipping_address || form.object.build_shipping_address), nested_attribute_name: :shipping_address do |nested_form|
29
+ = render partial: find_partial_for(Address, :fields), locals: { form: nested_form, parent: form.object }
30
+
31
+ = form.actions
32
+
33
+ = tabs.tab :payment do
34
+ = form.input :payment, :as => :nested_one
35
+
36
+ = form.actions
37
+
38
+ = tabs.tab :line_items do
39
+ = form.input :line_items, :as => :nested_many
40
+
41
+ = form.actions
42
+
43
+