spree_core 2.3.13 → 2.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (232) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/spree.js.coffee.erb +1 -5
  3. data/app/helpers/spree/base_helper.rb +22 -11
  4. data/app/helpers/spree/products_helper.rb +8 -7
  5. data/app/mailers/spree/base_mailer.rb +1 -0
  6. data/app/mailers/spree/reimbursement_mailer.rb +10 -0
  7. data/app/mailers/spree/test_mailer.rb +2 -3
  8. data/app/models/concerns/spree/adjustment_source.rb +24 -0
  9. data/app/models/concerns/spree/calculated_adjustments.rb +33 -0
  10. data/app/models/concerns/spree/named_type.rb +12 -0
  11. data/app/models/concerns/spree/user_address.rb +30 -0
  12. data/app/models/concerns/spree/user_payment_source.rb +19 -0
  13. data/app/models/spree/address.rb +13 -6
  14. data/app/models/spree/adjustment.rb +5 -5
  15. data/app/models/spree/app_configuration.rb +8 -4
  16. data/app/models/spree/asset.rb +1 -1
  17. data/app/models/spree/base.rb +0 -3
  18. data/app/models/spree/calculator/flat_rate.rb +1 -5
  19. data/app/models/spree/calculator/returns/default_refund_amount.rb +36 -0
  20. data/app/models/spree/classification.rb +1 -1
  21. data/app/models/spree/credit_card.rb +18 -22
  22. data/app/models/spree/customer_return.rb +70 -0
  23. data/app/models/spree/exchange.rb +42 -0
  24. data/app/models/spree/gateway/bogus.rb +3 -3
  25. data/app/models/spree/image.rb +1 -1
  26. data/app/models/spree/inventory_unit.rb +32 -8
  27. data/app/models/spree/item_adjustments.rb +7 -11
  28. data/app/models/spree/legacy_user.rb +2 -2
  29. data/app/models/spree/line_item.rb +25 -12
  30. data/app/models/spree/option_type.rb +1 -1
  31. data/app/models/spree/option_value.rb +1 -8
  32. data/app/models/spree/order.rb +163 -145
  33. data/app/models/spree/order/checkout.rb +35 -23
  34. data/app/models/spree/order/payments.rb +66 -0
  35. data/app/models/spree/order_contents.rb +34 -24
  36. data/app/models/spree/order_populator.rb +6 -4
  37. data/app/models/spree/order_updater.rb +10 -1
  38. data/app/models/spree/payment.rb +19 -16
  39. data/app/models/spree/payment/processing.rb +40 -72
  40. data/app/models/spree/payment_method.rb +1 -1
  41. data/app/models/spree/payment_method/check.rb +0 -2
  42. data/app/models/spree/preference.rb +1 -1
  43. data/app/models/spree/preferences/preferable.rb +20 -0
  44. data/app/models/spree/price.rb +13 -3
  45. data/app/models/spree/product.rb +24 -29
  46. data/app/models/spree/product_property.rb +0 -2
  47. data/app/models/spree/promotion.rb +66 -24
  48. data/app/models/spree/promotion/actions/create_adjustment.rb +2 -2
  49. data/app/models/spree/promotion/actions/create_item_adjustments.rb +15 -11
  50. data/app/models/spree/promotion/actions/create_line_items.rb +2 -12
  51. data/app/models/spree/promotion/rules/first_order.rb +6 -2
  52. data/app/models/spree/promotion/rules/item_total.rb +42 -4
  53. data/app/models/spree/promotion/rules/one_use_per_user.rb +24 -0
  54. data/app/models/spree/promotion/rules/product.rb +13 -11
  55. data/app/models/spree/promotion/rules/taxon.rb +61 -0
  56. data/app/models/spree/promotion/rules/user.rb +1 -1
  57. data/app/models/spree/promotion/rules/user_logged_in.rb +4 -1
  58. data/app/models/spree/promotion_category.rb +6 -0
  59. data/app/models/spree/promotion_handler/cart.rb +14 -18
  60. data/app/models/spree/promotion_handler/coupon.rb +25 -16
  61. data/app/models/spree/promotion_rule.rb +13 -0
  62. data/app/models/spree/property.rb +1 -3
  63. data/app/models/spree/refund.rb +91 -0
  64. data/app/models/spree/refund_reason.rb +13 -0
  65. data/app/models/spree/reimbursement.rb +148 -0
  66. data/app/models/spree/reimbursement/credit.rb +25 -0
  67. data/app/models/spree/reimbursement/reimbursement_type_engine.rb +56 -0
  68. data/app/models/spree/reimbursement/reimbursement_type_validator.rb +12 -0
  69. data/app/models/spree/reimbursement_performer.rb +43 -0
  70. data/app/models/spree/reimbursement_tax_calculator.rb +38 -0
  71. data/app/models/spree/reimbursement_type.rb +16 -0
  72. data/app/models/spree/reimbursement_type/credit.rb +13 -0
  73. data/app/models/spree/reimbursement_type/exchange.rb +9 -0
  74. data/app/models/spree/reimbursement_type/original_payment.rb +13 -0
  75. data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +50 -0
  76. data/app/models/spree/return_authorization.rb +52 -68
  77. data/app/models/spree/return_authorization_reason.rb +7 -0
  78. data/app/models/spree/return_item.rb +230 -0
  79. data/app/models/spree/return_item/default_eligibility_validator.rb +27 -0
  80. data/app/models/spree/return_item/eligibility_validator/base_validator.rb +24 -0
  81. data/app/models/spree/return_item/eligibility_validator/rma_required.rb +17 -0
  82. data/app/models/spree/return_item/eligibility_validator/time_since_purchase.rb +16 -0
  83. data/app/models/spree/return_item/exchange_variant_eligibility/same_option_value.rb +34 -0
  84. data/app/models/spree/return_item/exchange_variant_eligibility/same_product.rb +10 -0
  85. data/app/models/spree/returns_calculator.rb +8 -0
  86. data/app/models/spree/shipment.rb +209 -154
  87. data/app/models/spree/shipment_handler.rb +43 -0
  88. data/app/models/spree/shipping_calculator.rb +1 -1
  89. data/app/models/spree/shipping_category.rb +2 -2
  90. data/app/models/spree/shipping_method.rb +1 -1
  91. data/app/models/spree/shipping_method_category.rb +1 -1
  92. data/app/models/spree/shipping_rate.rb +4 -0
  93. data/app/models/spree/stock/adjuster.rb +10 -11
  94. data/app/models/spree/stock/availability_validator.rb +6 -10
  95. data/app/models/spree/stock/content_item.rb +48 -0
  96. data/app/models/spree/stock/coordinator.rb +14 -7
  97. data/app/models/spree/stock/estimator.rb +1 -1
  98. data/app/models/spree/stock/inventory_unit_builder.rb +21 -0
  99. data/app/models/spree/stock/package.rb +38 -51
  100. data/app/models/spree/stock/packer.rb +13 -11
  101. data/app/models/spree/stock/prioritizer.rb +7 -7
  102. data/app/models/spree/stock/splitter/base.rb +2 -2
  103. data/app/models/spree/stock_item.rb +6 -9
  104. data/app/models/spree/stock_location.rb +17 -25
  105. data/app/models/spree/stock_movement.rb +1 -8
  106. data/app/models/spree/stock_transfer.rb +0 -2
  107. data/app/models/spree/store.rb +1 -1
  108. data/app/models/spree/tax_category.rb +2 -2
  109. data/app/models/spree/tax_rate.rb +16 -22
  110. data/app/models/spree/taxon.rb +1 -1
  111. data/app/models/spree/variant.rb +45 -23
  112. data/app/models/spree/zone.rb +17 -17
  113. data/app/models/spree/zone_member.rb +1 -1
  114. data/app/views/layouts/spree/base_mailer.html.erb +784 -0
  115. data/app/views/spree/order_mailer/cancel_email.html.erb +45 -0
  116. data/app/views/spree/order_mailer/cancel_email.text.erb +2 -2
  117. data/app/views/spree/order_mailer/confirm_email.html.erb +84 -0
  118. data/app/views/spree/order_mailer/confirm_email.text.erb +2 -2
  119. data/app/views/spree/reimbursement_mailer/reimbursement_email.text.erb +22 -0
  120. data/app/views/spree/shared/_base_mailer_footer.html.erb +20 -0
  121. data/app/views/spree/shared/_base_mailer_header.html.erb +31 -0
  122. data/app/views/spree/shipment_mailer/shipped_email.html.erb +34 -0
  123. data/app/views/spree/test_mailer/test_email.html.erb +40 -0
  124. data/app/views/spree/test_mailer/test_email.text.erb +2 -2
  125. data/config/initializers/friendly_id.rb +88 -0
  126. data/config/initializers/premailer_assets.rb +1 -0
  127. data/config/initializers/user_class_extensions.rb +9 -22
  128. data/config/locales/en.yml +180 -12
  129. data/db/default/spree/states.rb +73 -55
  130. data/db/migrate/20130213191427_create_default_stock.rb +1 -0
  131. data/db/migrate/20130807024301_upgrade_adjustments.rb +4 -5
  132. data/db/migrate/20140309033438_create_store_from_preferences.rb +0 -7
  133. data/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb +8 -0
  134. data/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb +1 -1
  135. data/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb +5 -3
  136. data/db/migrate/20140625214618_create_spree_refunds.rb +12 -0
  137. data/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb +12 -0
  138. data/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb +5 -0
  139. data/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb +10 -0
  140. data/db/migrate/20140710041921_recreate_spree_return_authorizations.rb +55 -0
  141. data/db/migrate/20140710181204_add_amount_fields_to_return_items.rb +7 -0
  142. data/db/migrate/20140710190048_drop_return_authorization_amount.rb +5 -0
  143. data/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb +28 -0
  144. data/db/migrate/20140713140527_create_spree_refund_reasons.rb +14 -0
  145. data/db/migrate/20140713142214_rename_return_authorization_reason.rb +5 -0
  146. data/db/migrate/20140715182625_create_spree_promotion_categories.rb +11 -0
  147. data/db/migrate/20140716204111_drop_received_at_on_return_items.rb +9 -0
  148. data/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb +6 -0
  149. data/db/migrate/20140717155155_create_default_refund_reason.rb +9 -0
  150. data/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb +5 -0
  151. data/db/migrate/20140718133010_create_spree_customer_returns.rb +9 -0
  152. data/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb +6 -0
  153. data/db/migrate/20140718195325_create_friendly_id_slugs.rb +15 -0
  154. data/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb +5 -0
  155. data/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb +13 -0
  156. data/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb +15 -0
  157. data/db/migrate/20140725131539_create_spree_reimbursements.rb +21 -0
  158. data/db/migrate/20140728225422_add_promotionable_to_spree_products.rb +5 -0
  159. data/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb +7 -0
  160. data/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb +5 -0
  161. data/db/migrate/20140731150017_create_spree_reimbursement_types.rb +20 -0
  162. data/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb +5 -0
  163. data/db/migrate/20140805171219_make_existing_credit_cards_default.rb +10 -0
  164. data/db/migrate/20140806144901_add_type_to_reimbursement_type.rb +9 -0
  165. data/db/migrate/20140808184039_create_spree_reimbursement_credits.rb +10 -0
  166. data/db/migrate/20140827170513_add_meta_title_to_spree_products.rb +7 -0
  167. data/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb +5 -0
  168. data/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb +5 -0
  169. data/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb +6 -0
  170. data/db/migrate/20141009204607_add_store_id_to_orders.rb +8 -0
  171. data/lib/generators/spree/install/install_generator.rb +7 -3
  172. data/lib/spree/core.rb +11 -10
  173. data/lib/spree/core/controller_helpers/common.rb +3 -10
  174. data/lib/spree/core/controller_helpers/order.rb +15 -12
  175. data/lib/spree/core/controller_helpers/strong_parameters.rb +9 -9
  176. data/lib/spree/core/engine.rb +8 -1
  177. data/lib/spree/core/importer/order.rb +5 -17
  178. data/lib/spree/core/search/base.rb +1 -1
  179. data/lib/spree/core/validators/email.rb +1 -1
  180. data/lib/spree/core/version.rb +1 -1
  181. data/lib/spree/instrumentation.rb +41 -0
  182. data/lib/spree/migrations.rb +3 -7
  183. data/lib/spree/money.rb +2 -2
  184. data/lib/spree/permitted_attributes.rb +10 -9
  185. data/lib/spree/testing_support/ability_helpers.rb +25 -25
  186. data/lib/spree/testing_support/authorization_helpers.rb +3 -5
  187. data/lib/spree/testing_support/capybara_ext.rb +2 -2
  188. data/lib/spree/testing_support/factories/calculator_factory.rb +0 -8
  189. data/lib/spree/testing_support/factories/credit_card_factory.rb +1 -1
  190. data/lib/spree/testing_support/factories/customer_return_factory.rb +31 -0
  191. data/lib/spree/testing_support/factories/inventory_unit_factory.rb +1 -0
  192. data/lib/spree/testing_support/factories/line_item_factory.rb +2 -1
  193. data/lib/spree/testing_support/factories/order_factory.rb +11 -6
  194. data/lib/spree/testing_support/factories/promotion_category_factory.rb +6 -0
  195. data/lib/spree/testing_support/factories/promotion_factory.rb +9 -7
  196. data/lib/spree/testing_support/factories/refund_factory.rb +14 -0
  197. data/lib/spree/testing_support/factories/reimbursement_factory.rb +16 -0
  198. data/lib/spree/testing_support/factories/reimbursement_type_factory.rb +7 -0
  199. data/lib/spree/testing_support/factories/return_authorization_factory.rb +9 -3
  200. data/lib/spree/testing_support/factories/return_item_factory.rb +10 -0
  201. data/lib/spree/testing_support/factories/shipment_factory.rb +1 -0
  202. data/lib/spree/testing_support/factories/shipping_method_factory.rb +3 -2
  203. data/lib/spree/testing_support/factories/stock_factory.rb +12 -11
  204. data/lib/spree/testing_support/flash.rb +2 -2
  205. data/lib/tasks/email.rake +7 -0
  206. data/lib/tasks/exchanges.rake +70 -0
  207. data/vendor/assets/javascripts/jquery.validate/localization/messages_et.js +23 -0
  208. data/vendor/assets/javascripts/jquery.validate/localization/messages_eu.js +25 -0
  209. data/vendor/assets/javascripts/jquery.validate/localization/messages_hr.js +25 -0
  210. data/vendor/assets/javascripts/jquery.validate/localization/messages_ka.js +25 -0
  211. data/vendor/assets/javascripts/jquery.validate/localization/messages_ko.js +25 -0
  212. data/vendor/assets/javascripts/jquery.validate/localization/messages_my.js +25 -0
  213. data/vendor/assets/javascripts/jquery.validate/localization/messages_pt_BR.js +26 -0
  214. data/vendor/assets/javascripts/jquery.validate/localization/messages_pt_PT.js +26 -0
  215. data/vendor/assets/javascripts/jquery.validate/localization/messages_sl.js +25 -0
  216. data/vendor/assets/javascripts/jquery.validate/localization/messages_sv.js +23 -0
  217. data/vendor/assets/javascripts/jquery.validate/localization/messages_uk.js +25 -0
  218. data/vendor/assets/javascripts/jquery.validate/localization/messages_zh.js +25 -0
  219. data/vendor/assets/javascripts/jquery.validate/localization/messages_zh_TW.js +26 -0
  220. metadata +163 -47
  221. data/app/models/concerns/spree/ransackable_attributes.rb +0 -19
  222. data/db/migrate/20141021194502_add_state_lock_version_to_order.rb +0 -5
  223. data/db/migrate/20141101231208_fix_adjustment_order_presence.rb +0 -13
  224. data/db/migrate/20141105213646_update_classifications_positions.rb +0 -9
  225. data/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb +0 -5
  226. data/db/migrate/20150515211137_fix_adjustment_order_id.rb +0 -70
  227. data/lib/spree/core/adjustment_source.rb +0 -26
  228. data/lib/spree/core/calculated_adjustments.rb +0 -35
  229. data/lib/spree/core/controller_helpers.rb +0 -20
  230. data/lib/spree/core/user_address.rb +0 -32
  231. data/lib/spree/core/user_payment_source.rb +0 -20
  232. data/lib/spree/localized_number.rb +0 -20
@@ -0,0 +1,27 @@
1
+ module Spree
2
+ class ReturnItem::DefaultEligibilityValidator < Spree::ReturnItem::EligibilityValidator::BaseValidator
3
+ class_attribute :permitted_eligibility_validators
4
+ self.permitted_eligibility_validators = [
5
+ ReturnItem::EligibilityValidator::TimeSincePurchase,
6
+ ReturnItem::EligibilityValidator::RMARequired,
7
+ ]
8
+
9
+ def eligible_for_return?
10
+ validators.all? {|v| v.eligible_for_return? }
11
+ end
12
+
13
+ def requires_manual_intervention?
14
+ validators.any? {|v| v.requires_manual_intervention? }
15
+ end
16
+
17
+ def errors
18
+ validators.map(&:errors).reduce({}, :merge)
19
+ end
20
+
21
+ private
22
+
23
+ def validators
24
+ @validators ||= permitted_eligibility_validators.map{|v| v.new(@return_item) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ module Spree
2
+ class Spree::ReturnItem::EligibilityValidator::BaseValidator
3
+ attr_reader :errors
4
+
5
+ def initialize(return_item)
6
+ @return_item = return_item
7
+ @errors = {}
8
+ end
9
+
10
+ def eligible_for_return?
11
+ raise 'Implement me'
12
+ end
13
+
14
+ def requires_manual_intervention?
15
+ raise 'Implement me'
16
+ end
17
+
18
+ private
19
+
20
+ def add_error(key, error)
21
+ @errors[key] = error
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module Spree
2
+ class ReturnItem::EligibilityValidator::RMARequired < Spree::ReturnItem::EligibilityValidator::BaseValidator
3
+ def eligible_for_return?
4
+ if @return_item.return_authorization.present?
5
+ return true
6
+ else
7
+ add_error(:rma_required, Spree.t('return_item_rma_ineligible'))
8
+ return false
9
+ end
10
+ end
11
+
12
+ def requires_manual_intervention?
13
+ false
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ class ReturnItem::EligibilityValidator::TimeSincePurchase < Spree::ReturnItem::EligibilityValidator::BaseValidator
3
+ def eligible_for_return?
4
+ if (@return_item.inventory_unit.created_at + Spree::Config[:return_eligibility_number_of_days].days) > Time.now
5
+ return true
6
+ else
7
+ add_error(:number_of_days, Spree.t('return_item_time_period_ineligible'))
8
+ return false
9
+ end
10
+ end
11
+
12
+ def requires_manual_intervention?
13
+ false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ module ReturnItem::ExchangeVariantEligibility
3
+ class SameOptionValue
4
+ class_attribute :option_type_restrictions
5
+ self.option_type_restrictions = []
6
+ # This can be configured in an initializer, e.g.:
7
+ # Spree::ReturnItem::ExchangeVariantEligibility::SameOptionValue.option_type_restrictions = ["size", "color"]
8
+ #
9
+ # This restriction causes only variants that share the same option value for the
10
+ # specified option types to be returned. e.g.:
11
+ #
12
+ # option_type_restrictions = ["color", "waist"]
13
+ # Variant: blue pants with 32 waist and 30 inseam
14
+ #
15
+ # can be exchanged for:
16
+ # blue pants with 32 waist and 31 inseam
17
+ #
18
+ # cannot be exchanged for:
19
+ # green pants with 32 waist and 30 inseam
20
+ # blue pants with 34 waist and 32 inseam
21
+
22
+ def self.eligible_variants(variant)
23
+ product_variants = SameProduct.eligible_variants(variant).includes(option_values: :option_type)
24
+
25
+ relevant_option_values = variant.option_values.select { |ov| option_type_restrictions.include? ov.option_type.name }
26
+ if relevant_option_values.present?
27
+ product_variants.select { |v| (relevant_option_values & v.option_values) == relevant_option_values }
28
+ else
29
+ product_variants
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module Spree
2
+ module ReturnItem::ExchangeVariantEligibility
3
+ class SameProduct
4
+
5
+ def self.eligible_variants(variant)
6
+ Spree::Variant.where.not(id: variant.id).where(product_id: variant.product_id, is_master: false)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ module Spree
2
+ class ReturnsCalculator < Calculator
3
+
4
+ def compute(return_item)
5
+ raise NotImplementedError, "Please implement 'compute(return_item)' in your calculator: #{self.class.name}"
6
+ end
7
+ end
8
+ end
@@ -2,15 +2,15 @@ require 'ostruct'
2
2
 
3
3
  module Spree
4
4
  class Shipment < Spree::Base
5
- belongs_to :order, class_name: 'Spree::Order', touch: true, inverse_of: :shipments
6
5
  belongs_to :address, class_name: 'Spree::Address', inverse_of: :shipments
6
+ belongs_to :order, class_name: 'Spree::Order', touch: true, inverse_of: :shipments
7
7
  belongs_to :stock_location, class_name: 'Spree::StockLocation'
8
8
 
9
+ has_many :adjustments, as: :adjustable, dependent: :delete_all
10
+ has_many :inventory_units, dependent: :delete_all, inverse_of: :shipment
9
11
  has_many :shipping_rates, -> { order('cost ASC') }, dependent: :delete_all
10
12
  has_many :shipping_methods, through: :shipping_rates
11
13
  has_many :state_changes, as: :stateful
12
- has_many :inventory_units, dependent: :delete_all, inverse_of: :shipment
13
- has_many :adjustments, as: :adjustable, dependent: :delete_all
14
14
 
15
15
  after_save :update_adjustments
16
16
 
@@ -23,11 +23,13 @@ module Spree
23
23
 
24
24
  make_permalink field: :number, length: 11, prefix: 'H'
25
25
 
26
- scope :shipped, -> { with_state('shipped') }
27
- scope :ready, -> { with_state('ready') }
28
26
  scope :pending, -> { with_state('pending') }
29
- scope :with_state, ->(*s) { where(state: s) }
27
+ scope :ready, -> { with_state('ready') }
28
+ scope :shipped, -> { with_state('shipped') }
30
29
  scope :trackable, -> { where("tracking IS NOT NULL AND tracking != ''") }
30
+ scope :with_state, ->(*s) { where(state: s) }
31
+ # sort by most recent shipped_at, falling back to created_at. add "id desc" to make specs that involve this scope more deterministic.
32
+ scope :reverse_chronological, -> { order('coalesce(spree_shipments.shipped_at, spree_shipments.created_at) desc', id: :desc) }
31
33
 
32
34
  # shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
33
35
  state_machine initial: :pending, use_transactions: false do
@@ -43,7 +45,7 @@ module Spree
43
45
  end
44
46
 
45
47
  event :ship do
46
- transition from: :ready, to: :shipped
48
+ transition from: [:ready, :canceled], to: :shipped
47
49
  end
48
50
  after_transition to: :shipped, do: :after_ship
49
51
 
@@ -61,7 +63,7 @@ module Spree
61
63
  }
62
64
  transition from: :canceled, to: :pending
63
65
  end
64
- after_transition from: :canceled, to: [:pending, :ready], do: :after_resume
66
+ after_transition from: :canceled, to: [:pending, :ready, :shipped], do: :after_resume
65
67
 
66
68
  after_transition do |shipment, transition|
67
69
  shipment.state_changes.create!(
@@ -72,113 +74,96 @@ module Spree
72
74
  end
73
75
  end
74
76
 
75
- def to_param
76
- number
77
- end
78
-
79
- def backordered?
80
- inventory_units.any? { |inventory_unit| inventory_unit.backordered? }
77
+ def add_shipping_method(shipping_method, selected = false)
78
+ shipping_rates.create(shipping_method: shipping_method, selected: selected, cost: cost)
81
79
  end
82
80
 
83
- def ready_or_pending?
84
- self.ready? || self.pending?
81
+ def after_cancel
82
+ manifest.each { |item| manifest_restock(item) }
85
83
  end
86
84
 
87
- def shipped=(value)
88
- return unless value == '1' && shipped_at.nil?
89
- self.shipped_at = Time.now
85
+ def after_resume
86
+ manifest.each { |item| manifest_unstock(item) }
90
87
  end
91
88
 
92
- def shipping_method
93
- selected_shipping_rate.try(:shipping_method) || shipping_rates.first.try(:shipping_method)
89
+ def backordered?
90
+ inventory_units.any? { |inventory_unit| inventory_unit.backordered? }
94
91
  end
95
92
 
96
- self.whitelisted_ransackable_attributes = ['number']
97
-
98
- def add_shipping_method(shipping_method, selected = false)
99
- shipping_rates.create(shipping_method: shipping_method, selected: selected, cost: cost)
93
+ def currency
94
+ order ? order.currency : Spree::Config[:currency]
100
95
  end
101
96
 
102
- def selected_shipping_rate
103
- shipping_rates.where(selected: true).first
97
+ # Determines the appropriate +state+ according to the following logic:
98
+ #
99
+ # pending unless order is complete and +order.payment_state+ is +paid+
100
+ # shipped if already shipped (ie. does not change the state)
101
+ # ready all other cases
102
+ def determine_state(order)
103
+ return 'canceled' if order.canceled?
104
+ return 'pending' unless order.can_ship?
105
+ return 'pending' if inventory_units.any? &:backordered?
106
+ return 'shipped' if state == 'shipped'
107
+ order.paid? || Spree::Config[:auto_capture_on_dispatch] ? 'ready' : 'pending'
104
108
  end
105
109
 
106
- def selected_shipping_rate_id
107
- selected_shipping_rate.try(:id)
110
+ def discounted_cost
111
+ cost + promo_total
108
112
  end
113
+ alias discounted_amount discounted_cost
109
114
 
110
- def selected_shipping_rate_id=(id)
111
- shipping_rates.update_all(selected: false)
112
- shipping_rates.update(id, selected: true)
113
- self.save!
115
+ def display_cost
116
+ Spree::Money.new(cost, { currency: currency })
114
117
  end
118
+ alias display_amount display_cost
115
119
 
116
- def tax_category
117
- selected_shipping_rate.try(:tax_rate).try(:tax_category)
120
+ def display_discounted_cost
121
+ Spree::Money.new(discounted_cost, { currency: currency })
118
122
  end
119
123
 
120
- def refresh_rates
121
- return shipping_rates if shipped?
122
- return [] unless can_get_rates?
123
-
124
- # StockEstimator.new assigment below will replace the current shipping_method
125
- original_shipping_method_id = shipping_method.try(:id)
126
-
127
- self.shipping_rates = Stock::Estimator.new(order).shipping_rates(to_package)
128
-
129
- if shipping_method
130
- selected_rate = shipping_rates.detect { |rate|
131
- rate.shipping_method_id == original_shipping_method_id
132
- }
133
- self.selected_shipping_rate_id = selected_rate.id if selected_rate
134
- end
135
-
136
- shipping_rates
124
+ def display_final_price
125
+ Spree::Money.new(final_price, { currency: currency })
137
126
  end
138
127
 
139
- def currency
140
- order ? order.currency : Spree::Config[:currency]
128
+ def display_item_cost
129
+ Spree::Money.new(item_cost, { currency: currency })
141
130
  end
142
131
 
143
- def display_cost
144
- Spree::Money.new(cost, { currency: currency })
132
+ def editable_by?(user)
133
+ !shipped?
145
134
  end
146
- alias display_amount display_cost
147
135
 
148
- def item_cost
149
- line_items.map(&:amount).sum
136
+ def final_price
137
+ cost + adjustment_total
150
138
  end
151
139
 
152
- def discounted_cost
153
- cost + promo_total
140
+ def final_price_with_items
141
+ item_cost + final_price
154
142
  end
155
- alias discounted_amount discounted_cost
156
143
 
157
- # Only one of either included_tax_total or additional_tax_total is set
158
- # This method returns the total of the two. Saves having to check if
159
- # tax is included or additional.
160
- def tax_total
161
- included_tax_total + additional_tax_total
144
+ def finalize!
145
+ InventoryUnit.finalize_units!(inventory_units)
146
+ manifest.each { |item| manifest_unstock(item) }
162
147
  end
163
148
 
164
- def final_price
165
- cost + adjustment_total
149
+ def include?(variant)
150
+ inventory_units_for(variant).present?
166
151
  end
167
152
 
168
- def display_discounted_cost
169
- Spree::Money.new(discounted_cost, { currency: currency })
153
+ def inventory_units_for(variant)
154
+ inventory_units.where(variant_id: variant.id)
170
155
  end
171
156
 
172
- def display_final_price
173
- Spree::Money.new(final_price, { currency: currency })
157
+ def inventory_units_for_item(line_item, variant = nil)
158
+ inventory_units.where(line_item_id: line_item.id, variant_id: line_item.variant.id || variant.id)
174
159
  end
175
160
 
176
- def display_item_cost
177
- Spree::Money.new(item_cost, { currency: currency })
161
+ def item_cost
162
+ line_items.map(&:amount).sum
178
163
  end
179
164
 
180
- def editable_by?(user)
181
- !shipped?
165
+ def line_items
166
+ inventory_units.includes(:line_item).map(&:line_item).uniq
182
167
  end
183
168
 
184
169
  ManifestItem = Struct.new(:line_item, :variant, :quantity, :states)
@@ -199,84 +184,119 @@ module Spree
199
184
  end.flatten
200
185
  end
201
186
 
202
- def line_items
203
- inventory_units.includes(:line_item).map(&:line_item).uniq
187
+ def process_order_payments
188
+ pending_payments = order.pending_payments
189
+ .sort_by(&:uncaptured_amount).reverse
190
+
191
+ # NOTE Do we really need to force orders to have pending payments on dispatch?
192
+ if pending_payments.empty?
193
+ raise Spree::Core::GatewayError, Spree.t(:no_pending_payments)
194
+ else
195
+ shipment_to_pay = final_price_with_items
196
+ payments_amount = 0
197
+
198
+ payments_pool = pending_payments.each_with_object([]) do |payment, pool|
199
+ next if payments_amount >= shipment_to_pay
200
+ payments_amount += payment.uncaptured_amount
201
+ pool << payment
202
+ end
203
+
204
+ payments_pool.each do |payment|
205
+ capturable_amount = if payment.amount >= shipment_to_pay
206
+ shipment_to_pay
207
+ else
208
+ payment.amount
209
+ end
210
+ cents = (capturable_amount * 100).to_i
211
+ payment.capture!(cents)
212
+ shipment_to_pay -= capturable_amount
213
+ end
214
+ end
215
+ rescue Spree::Core::GatewayError => e
216
+ errors.add(:base, e.message)
217
+ return !!Spree::Config[:allow_checkout_on_gateway_error]
204
218
  end
205
219
 
206
- def finalize!
207
- InventoryUnit.finalize_units!(inventory_units)
208
- manifest.each { |item| manifest_unstock(item) }
220
+ def ready_or_pending?
221
+ self.ready? || self.pending?
209
222
  end
210
223
 
211
- def after_cancel
212
- manifest.each { |item| manifest_restock(item) }
224
+ def refresh_rates
225
+ return shipping_rates if shipped?
226
+ return [] unless can_get_rates?
227
+
228
+ # StockEstimator.new assigment below will replace the current shipping_method
229
+ original_shipping_method_id = shipping_method.try(:id)
230
+
231
+ self.shipping_rates = Stock::Estimator.new(order).shipping_rates(to_package)
232
+
233
+ if shipping_method
234
+ selected_rate = shipping_rates.detect { |rate|
235
+ rate.shipping_method_id == original_shipping_method_id
236
+ }
237
+ self.selected_shipping_rate_id = selected_rate.id if selected_rate
238
+ end
239
+
240
+ shipping_rates
213
241
  end
214
242
 
215
- def after_resume
216
- manifest.each { |item| manifest_unstock(item) }
243
+ def selected_shipping_rate
244
+ shipping_rates.where(selected: true).first
217
245
  end
218
246
 
219
- # Updates various aspects of the Shipment while bypassing any callbacks. Note that this method takes an explicit reference to the
220
- # Order object. This is necessary because the association actually has a stale (and unsaved) copy of the Order and so it will not
221
- # yield the correct results.
222
- def update!(order)
223
- old_state = state
224
- new_state = determine_state(order)
225
- update_columns(
226
- state: new_state,
227
- updated_at: Time.now,
228
- )
229
- after_ship if new_state == 'shipped' and old_state != 'shipped'
247
+ def selected_shipping_rate_id
248
+ selected_shipping_rate.try(:id)
230
249
  end
231
250
 
232
- # Determines the appropriate +state+ according to the following logic:
233
- #
234
- # pending unless order is complete and +order.payment_state+ is +paid+
235
- # shipped if already shipped (ie. does not change the state)
236
- # ready all other cases
237
- def determine_state(order)
238
- return 'canceled' if order.canceled?
239
- return 'pending' unless order.can_ship?
240
- return 'pending' if inventory_units.any? &:backordered?
241
- return 'shipped' if state == 'shipped'
242
- order.paid? ? 'ready' : 'pending'
251
+ def selected_shipping_rate_id=(id)
252
+ shipping_rates.update_all(selected: false)
253
+ shipping_rates.update(id, selected: true)
254
+ self.save!
243
255
  end
244
256
 
245
- def tracking_url
246
- @tracking_url ||= shipping_method.build_tracking_url(tracking)
257
+ def set_up_inventory(state, variant, order, line_item)
258
+ self.inventory_units.create(
259
+ state: state,
260
+ variant_id: variant.id,
261
+ order_id: order.id,
262
+ line_item_id: line_item.id
263
+ )
247
264
  end
248
265
 
249
- def include?(variant)
250
- inventory_units_for(variant).present?
266
+ def shipped=(value)
267
+ return unless value == '1' && shipped_at.nil?
268
+ self.shipped_at = Time.now
251
269
  end
252
270
 
253
- def inventory_units_for(variant)
254
- inventory_units.where(variant_id: variant.id)
271
+ def shipping_method
272
+ selected_shipping_rate.try(:shipping_method) || shipping_rates.first.try(:shipping_method)
255
273
  end
256
274
 
257
- def inventory_units_for_item(line_item, variant = nil)
258
- inventory_units.where(line_item_id: line_item.id, variant_id: line_item.variant.id || variant.id)
275
+ def tax_category
276
+ selected_shipping_rate.try(:tax_rate).try(:tax_category)
259
277
  end
260
278
 
261
- def to_package
262
- package = Stock::Package.new(stock_location, order)
263
- grouped_inventory_units = inventory_units.includes(:line_item).group_by do |iu|
264
- [iu.line_item, iu.state_name]
265
- end
279
+ # Only one of either included_tax_total or additional_tax_total is set
280
+ # This method returns the total of the two. Saves having to check if
281
+ # tax is included or additional.
282
+ def tax_total
283
+ included_tax_total + additional_tax_total
284
+ end
266
285
 
267
- grouped_inventory_units.each do |(line_item, state_name), inventory_units|
268
- package.add line_item, inventory_units.count, state_name
286
+ def to_package
287
+ package = Stock::Package.new(stock_location)
288
+ inventory_units.group_by(&:state).each do |state, state_inventory_units|
289
+ package.add_multiple state_inventory_units, state.to_sym
269
290
  end
270
291
  package
271
292
  end
272
293
 
273
- def set_up_inventory(state, variant, order, line_item)
274
- self.inventory_units.create(
275
- state: state,
276
- variant_id: variant.id,
277
- order_id: order.id,
278
- line_item_id: line_item.id
279
- )
294
+ def to_param
295
+ number
296
+ end
297
+
298
+ def tracking_url
299
+ @tracking_url ||= shipping_method.build_tracking_url(tracking)
280
300
  end
281
301
 
282
302
  def update_amounts
@@ -319,10 +339,63 @@ module Spree
319
339
  end
320
340
  end
321
341
 
342
+ # Updates various aspects of the Shipment while bypassing any callbacks. Note that this method takes an explicit reference to the
343
+ # Order object. This is necessary because the association actually has a stale (and unsaved) copy of the Order and so it will not
344
+ # yield the correct results.
345
+ def update!(order)
346
+ old_state = state
347
+ new_state = determine_state(order)
348
+ update_columns(
349
+ state: new_state,
350
+ updated_at: Time.now,
351
+ )
352
+ after_ship if new_state == 'shipped' and old_state != 'shipped'
353
+ end
354
+
355
+ def transfer_to_location(variant, quantity, stock_location)
356
+ if quantity <= 0
357
+ raise ArgumentError
358
+ end
359
+
360
+ transaction do
361
+ new_shipment = order.shipments.create!(stock_location: stock_location)
362
+
363
+ order.contents.remove(variant, quantity, {shipment: self})
364
+ order.contents.add(variant, quantity, {shipment: new_shipment})
365
+
366
+ refresh_rates
367
+ save!
368
+ new_shipment.save!
369
+ end
370
+ end
371
+
372
+ def transfer_to_shipment(variant, quantity, shipment_to_transfer_to)
373
+ quantity_already_shipment_to_transfer_to = shipment_to_transfer_to.manifest.find{|mi| mi.line_item.variant == variant}.try(:quantity) || 0
374
+ final_quantity = quantity + quantity_already_shipment_to_transfer_to
375
+
376
+ if (quantity <= 0 || self == shipment_to_transfer_to)
377
+ raise ArgumentError
378
+ end
379
+
380
+ transaction do
381
+ order.contents.remove(variant, quantity, {shipment: self})
382
+ order.contents.add(variant, quantity, {shipment: shipment_to_transfer_to})
383
+
384
+ refresh_rates
385
+ save!
386
+ shipment_to_transfer_to.refresh_rates
387
+ shipment_to_transfer_to.save!
388
+ end
389
+ end
390
+
322
391
  private
323
392
 
324
- def manifest_unstock(item)
325
- stock_location.unstock item.variant, item.quantity, self
393
+ def after_ship
394
+ ShipmentHandler.factory(self).perform
395
+ end
396
+
397
+ def can_get_rates?
398
+ order.ship_address && order.ship_address.valid?
326
399
  end
327
400
 
328
401
  def manifest_restock(item)
@@ -335,23 +408,12 @@ module Spree
335
408
  end
336
409
  end
337
410
 
338
- def description_for_shipping_charge
339
- "#{Spree.t(:shipping)} (#{shipping_method.name})"
340
- end
341
-
342
- def after_ship
343
- inventory_units.each &:ship!
344
- send_shipped_email
345
- touch :shipped_at
346
- update_order_shipment_state
411
+ def manifest_unstock(item)
412
+ stock_location.unstock item.variant, item.quantity, self
347
413
  end
348
414
 
349
- def update_order_shipment_state
350
- new_state = OrderUpdater.new(order).update_shipment_state
351
- order.update_columns(
352
- shipment_state: new_state,
353
- updated_at: Time.now,
354
- )
415
+ def recalculate_adjustments
416
+ Spree::ItemAdjustments.new(self).update
355
417
  end
356
418
 
357
419
  def send_shipped_email
@@ -368,12 +430,5 @@ module Spree
368
430
  end
369
431
  end
370
432
 
371
- def recalculate_adjustments
372
- Spree::ItemAdjustments.new(self).update
373
- end
374
-
375
- def can_get_rates?
376
- order.ship_address && order.ship_address.valid?
377
- end
378
433
  end
379
434
  end