spree_price 3.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +83 -0
  3. data/app/assets/javascripts/spree/backend/price.js.coffee +13 -0
  4. data/app/assets/javascripts/spree/backend/price_book.js.coffee +104 -0
  5. data/app/assets/javascripts/spree/backend/spree_price.js +2 -0
  6. data/app/assets/javascripts/spree/frontend/spree_price.js +2 -0
  7. data/app/assets/stylesheets/spree/backend/spree_price.css +4 -0
  8. data/app/assets/stylesheets/spree/frontend/spree_price.css +4 -0
  9. data/app/controllers/spree/admin/currency_rates_controller.rb +11 -0
  10. data/app/controllers/spree/admin/price_books_controller.rb +41 -0
  11. data/app/controllers/spree/admin/price_types_controller.rb +6 -0
  12. data/app/controllers/spree/admin/prices_controller_decorator.rb +54 -0
  13. data/app/controllers/spree/admin/stores_controller_decorator.rb +16 -0
  14. data/app/helpers/spree/base_helper_decorator.rb +32 -0
  15. data/app/models/spree/currency_rate.rb +56 -0
  16. data/app/models/spree/line_item_decorator.rb +79 -0
  17. data/app/models/spree/line_item_price.rb +4 -0
  18. data/app/models/spree/order/currency_updater_decorator.rb +9 -0
  19. data/app/models/spree/order_decorator.rb +15 -0
  20. data/app/models/spree/order_price.rb +4 -0
  21. data/app/models/spree/order_updater_decorator.rb +30 -0
  22. data/app/models/spree/price_book.rb +88 -0
  23. data/app/models/spree/price_decorator.rb +39 -0
  24. data/app/models/spree/price_type.rb +15 -0
  25. data/app/models/spree/role_decorator.rb +15 -0
  26. data/app/models/spree/role_price_book.rb +5 -0
  27. data/app/models/spree/store_decorator.rb +4 -0
  28. data/app/models/spree/store_price_book.rb +7 -0
  29. data/app/models/spree/variant_decorator.rb +43 -0
  30. data/app/overrides/spree/admin/roles/_form/add_default_checkout_to_role.html.erb.deface +9 -0
  31. data/app/overrides/spree/admin/shared/sub_menu/_configuration/current_rates_link.html.erb.deface +6 -0
  32. data/app/overrides/spree/admin/shared/sub_menu/_product/add_price_books_tab.html.erb.deface +4 -0
  33. data/app/overrides/spree/admin/stores/_form/add_price_book_to_store.html.erb.deface +49 -0
  34. data/app/views/spree/admin/currency_rates/_form.html.erb +29 -0
  35. data/app/views/spree/admin/currency_rates/edit.html.erb +19 -0
  36. data/app/views/spree/admin/currency_rates/index.html.erb +50 -0
  37. data/app/views/spree/admin/currency_rates/new.html.erb +21 -0
  38. data/app/views/spree/admin/price_books/_add_price.html.erb +82 -0
  39. data/app/views/spree/admin/price_books/_form.html.erb +81 -0
  40. data/app/views/spree/admin/price_books/_price.html.erb +12 -0
  41. data/app/views/spree/admin/price_books/_price_book.html.erb +16 -0
  42. data/app/views/spree/admin/price_books/_price_list.html.erb +37 -0
  43. data/app/views/spree/admin/price_books/edit.html.erb +21 -0
  44. data/app/views/spree/admin/price_books/index.html.erb +65 -0
  45. data/app/views/spree/admin/price_books/new.html.erb +14 -0
  46. data/app/views/spree/admin/price_books/show.html.erb +39 -0
  47. data/app/views/spree/admin/price_types/_form.html.erb +25 -0
  48. data/app/views/spree/admin/price_types/edit.html.erb +18 -0
  49. data/app/views/spree/admin/price_types/index.html.erb +51 -0
  50. data/app/views/spree/admin/price_types/new.html.erb +14 -0
  51. data/app/views/spree/admin/prices/_variant_prices.html.erb +57 -0
  52. data/app/views/spree/admin/prices/index.html.erb +39 -0
  53. data/config/locales/en.yml +34 -0
  54. data/config/locales/validates_timeliness.en.yml +16 -0
  55. data/config/routes.rb +33 -0
  56. data/db/migrate/20180828044728_add_spree_price_book.rb +52 -0
  57. data/db/migrate/20180830053207_add_price_to_order_and_line_item.rb +19 -0
  58. data/db/migrate/20180830055224_add_default_for_role.rb +7 -0
  59. data/lib/generators/spree_price/install/install_generator.rb +31 -0
  60. data/lib/spree_price.rb +7 -0
  61. data/lib/spree_price/engine.rb +20 -0
  62. data/lib/spree_price/factories.rb +5 -0
  63. data/lib/spree_price/factories/currency_rate_factory.rb +15 -0
  64. data/lib/spree_price/factories/price_book_factory.rb +34 -0
  65. data/lib/spree_price/factories/price_type_factory.rb +21 -0
  66. data/lib/spree_price/version.rb +17 -0
  67. data/lib/tasks/price.rake +58 -0
  68. metadata +362 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 48cb223d2b3c405236743e13146782f898967245dd2ac3d25cf6e0869cfdfc66
4
+ data.tar.gz: 99334096c7a58e696e3188098f5506c74aeabcfc2e47ade2ae1c01d8561f48e7
5
+ SHA512:
6
+ metadata.gz: 66027887b1514bb4658a575ad4e717233fd76f2ad28f22911164c35112cc6c866889f4fbee122955aa828e00eb6b13d1d99a5e62f6febd522e6fc9374cec0736
7
+ data.tar.gz: 24d3611554d5c320265685fcaff8d9de5697e1acb48e5b78d7a3c65fb32d0e41ad0386ae8c8706151e5fe6feddbf68cddc49347c2e952d644d13262f5f34ad00
@@ -0,0 +1,83 @@
1
+ # Spree Price
2
+
3
+ Heavily inspired by [spree-contrib/spree_price_book](https://github.com/spree-contrib/spree_price_books).
4
+ 1. Support multiple store price
5
+ 2. Support multiple type of prices (e.g. sales price, marked price, manufacturer's suggested retail price
6
+ 3. Support price book. Price book can be prioritized at store.
7
+ 4. Auto adjust prices according to exchange rate.
8
+ 5. Price is found by the following order.
9
+ - Manual set price
10
+ - Price book with higher priority in the same store
11
+ - First price matching the currency and price type
12
+ - First price matching the currency
13
+ TODO: Find the price from parent price type without sacrificing the performance too much
14
+
15
+ 6. The price will not be populated from parent price type by default. You can either fill nil prices from parent price book with adjustment factor or refresh all prices from parent price book in admin panel.
16
+
17
+ 7. Line item and order will save all prices for each price type.
18
+
19
+ ### Usage
20
+ Create your own price type
21
+ ![Price Type](/docs/price-type-1.png?raw=true "Price Type")
22
+
23
+ Create your own price book for each currency, price type, store, user roles
24
+ ![Price Book](/docs/price-book-1.png?raw=true "Price Book")
25
+
26
+ You can update the prices in price book from parent price book, either filling nil prices from parent price book with adjustment factor or refreshing all prices in price book.
27
+ ![Price Book](/docs/price-book-details-1.png?raw=true "Price Book")
28
+
29
+ Update the price from product page with store, price type and role filter. If the price is not set manually, it will show you the reference price.
30
+ ![Variant Prices](/docs/variant-prices.png?raw=true "Variant Prices")
31
+
32
+ ### Installation
33
+ Add spree_price_books to your Gemfile:
34
+
35
+ ```shell
36
+ gem 'spree_price', github: 'EONIQ/spree_price'
37
+ ```
38
+
39
+ Bundle your dependencies and run the installation generator:
40
+ ```shell
41
+ bundle
42
+ bundle exec rails g spree_price:install
43
+ ```
44
+
45
+ ### Configuration
46
+ Once installed you can seed default currency exchange rates via open exchange rate
47
+
48
+ Get your app id from https://openexchangerates.org/signup
49
+
50
+ Add open_exchange_rate.rb to config/initializers
51
+ ```ruby
52
+ Rails.application.config.openExchangeRate = {
53
+ appId: 'YOUR APP ID HERE',
54
+ }
55
+ ```
56
+
57
+ ```shell
58
+ bundle exec rake spree_price:currency_rates
59
+ ```
60
+
61
+ ### Testing
62
+ TODO: Need to work on rspec
63
+
64
+ First bundle your dependencies, then run rake. rake will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using rake test_app.
65
+
66
+ On the first run you may need to create the Postgresql role (ex. createuser -d postgres)
67
+
68
+ ```
69
+ bundle
70
+ bundle exec rake
71
+ ```
72
+
73
+ When testing your applications integration with this extension you may use it's factories. Simply add this require statement to your spec_helper:
74
+
75
+ ```
76
+ require 'spree_price/factories'
77
+ ```
78
+
79
+ ### Credit
80
+ [spree/spree](https://github.com/spree/spree)
81
+ [spree-contrib/spree_price_book](https://github.com/spree-contrib/spree_price_books)
82
+
83
+ Copyright (c) 2018 EONIQ (HK) LIMITED, released under the New BSD License
@@ -0,0 +1,13 @@
1
+ $ ->
2
+ appendParams = (key, value) ->
3
+ path = window.location.pathname;
4
+ url = Spree.url(path + window.location.search).deleteQueryParam(key).addQueryParam(key, value)
5
+
6
+ window.location.href = url.toString();
7
+
8
+ $('#select_variant_prices_store').on 'change', (event) ->
9
+ appendParams('store_id', event.val)
10
+ $('#select_variant_prices_price_type').on 'change', (event) ->
11
+ appendParams('price_type_id', event.val)
12
+ $('#select_variant_prices_role').on 'change', (event) ->
13
+ appendParams('role_id', event.val)
@@ -0,0 +1,104 @@
1
+ $ ->
2
+ class PriceBookVariant
3
+ constructor: (@variant) ->
4
+ @id = @variant.id
5
+ @name = "#{@variant.name} - #{@variant.sku}"
6
+ @price = 0.0
7
+
8
+ update: (price) ->
9
+ @price = price
10
+
11
+ class PriceBookVariants
12
+ constructor: ->
13
+ @build_select(Spree.url(Spree.routes.variants_api), 'product_name_or_sku_cont')
14
+
15
+ format_variant_result: (result) ->
16
+ "#{result.name} - #{result.sku}"
17
+
18
+ build_select: (url, query) ->
19
+ $('#price_book_variant').select2
20
+ minimumInputLength: 3
21
+ ajax:
22
+ url: url
23
+ datatype: "json"
24
+ data: (term, page) ->
25
+ query_object = {}
26
+ query_object[query] = term
27
+ q: query_object
28
+ token: Spree.api_key
29
+
30
+ results: (data, page) ->
31
+ result = data["variants"]
32
+ window.variants = result
33
+ results: result
34
+
35
+ formatResult: @format_variant_result
36
+ formatSelection: (variant) ->
37
+ if !!variant.options_text
38
+ variant.name + " (#{variant.options_text})" + " - #{variant.sku}"
39
+ else
40
+ variant.name + " - #{variant.sku}"
41
+
42
+ class PriceBookAddVariants
43
+ constructor: ->
44
+ @variants = []
45
+ if $('#price_book_variant_template').length > 0
46
+ @template = Handlebars.compile $('#price_book_variant_template').html()
47
+
48
+ $('button.price_book_add_variant').click (event) =>
49
+ event.preventDefault()
50
+ if $('#price_book_variant').select2('data')?
51
+ @add_variant()
52
+ else
53
+ alert('Please select a variant first')
54
+
55
+ $('#price_book-variants-table').on 'click', '.price_book_remove_variant', (event) =>
56
+ event.preventDefault()
57
+ @remove_variant $(event.target)
58
+
59
+ $('button.price_book_new_push').click =>
60
+ unless @variants.length > 0
61
+ alert('no variants to transfer')
62
+ false
63
+
64
+ add_variant: ->
65
+ variant = $('#price_book_variant').select2('data')
66
+ price = $('#price_book_variant_price').val()
67
+
68
+ variant = @find_or_add(variant)
69
+ variant.update(price)
70
+ @render()
71
+
72
+ find_or_add: (variant) ->
73
+ if existing = _.find(@variants, (v) -> v.id == variant.id)
74
+ return existing
75
+ else
76
+ variant = new PriceBookVariant($.extend({}, variant))
77
+ @variants.push variant
78
+ return variant
79
+
80
+ remove_variant: (target) ->
81
+ variant_id = parseInt(target.data('variantId'))
82
+ @variants = (v for v in @variants when v.id isnt variant_id)
83
+ @render()
84
+
85
+ clear_variants: ->
86
+ @variants = []
87
+ @render()
88
+
89
+ contains: (id) ->
90
+ _.contains(_.pluck(@variants, 'id'), id)
91
+
92
+ render: ->
93
+ if @variants.length is 0
94
+ $('#price_book-variants-table').hide()
95
+ $('.no-objects-found').show()
96
+ else
97
+ $('#price_book-variants-table').show()
98
+ $('.no-objects-found').hide()
99
+
100
+ rendered = @template { variants: @variants }
101
+ $('#price_book_variants_tbody').html(rendered)
102
+
103
+ price_book_add_variants = new PriceBookAddVariants
104
+ price_book_variants = new PriceBookVariants
@@ -0,0 +1,2 @@
1
+ //= require spree/backend/price_book
2
+ //= require spree/backend/price
@@ -0,0 +1,2 @@
1
+ // Placeholder manifest file.
2
+ // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js'
@@ -0,0 +1,4 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css'
4
+ */
@@ -0,0 +1,4 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4
+ */
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ module Admin
3
+ class CurrencyRatesController < Spree::Admin::ResourceController
4
+ def fetch
5
+ Spree::CurrencyRate.update_from_open_exchange
6
+ flash[:success] = Spree.t('notice_messages.currency_rate_fetched')
7
+ redirect_to admin_currency_rates_path
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ module Spree
2
+ module Admin
3
+ class PriceBooksController < Spree::Admin::ResourceController
4
+ def show
5
+ @prices = @price_book
6
+ .prices
7
+ .includes(variant: [{ option_values: :option_type }, :product])
8
+ .page(params[:page])
9
+ end
10
+
11
+ def update_price
12
+ @price_book.update_attributes(prices_params)
13
+ redirect_to admin_price_book_path(@price_book)
14
+ end
15
+
16
+ def add_price
17
+ prices_params[:prices_attributes].each do |price_param|
18
+ price = @price_book.prices.find_or_initialize_by(
19
+ currency: @price_book.currency,
20
+ variant_id: price_param[:variant_id].to_i,
21
+ )
22
+ price.amount = price_param[:amount].to_f
23
+ price.save!
24
+ end
25
+ redirect_to admin_price_book_path(@price_book)
26
+ end
27
+
28
+ def load_from_parent
29
+ @price_book.load_prices_from_parent(!params[:force_update].nil?)
30
+
31
+ flash[:success] = Spree.t('notice_messages.price_book_loaded_from_parent')
32
+ redirect_to admin_price_book_path(@price_book)
33
+ end
34
+
35
+ private
36
+ def prices_params
37
+ params.require(:price_book).permit(prices_attributes: [:id, :amount, :variant_id])
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ module Admin
3
+ class PriceTypesController < Spree::Admin::ResourceController
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,54 @@
1
+ Spree::Admin::PricesController.class_eval do
2
+ before_action :load_extra, only: [:index, :variant_prices]
3
+
4
+ def load_extra
5
+ @store = params[:store_id] ? Spree::Store.find(params[:store_id]) : Spree::Store.first
6
+ @price_type = params[:price_type_id] ? Spree::PriceType.find(params[:price_type_id]) : Spree::PriceType.first
7
+ @role = params[:role_id] ? Spree::Role.find(params[:role_id]) : Spree::Role.first
8
+ end
9
+
10
+ def variant_prices
11
+ @product = Spree::Product.friendly.find(params[:product_id])
12
+
13
+ variant_prices_params.each do |variant_id, price_params|
14
+ price_params.each do |currency, amount|
15
+ if amount.present?
16
+ price_book = @store
17
+ .price_books
18
+ .by_currency(currency.upcase)
19
+ .by_price_type(@price_type.try(:id))
20
+ .by_roles([@role.try(:id)])
21
+ .first
22
+ price_book = price_book || Spree::PriceBook
23
+ .by_currency(currency)
24
+ .by_price_type(@price_type.try(:id))
25
+ .first
26
+
27
+ price = Spree::Price.find_or_initialize_by(
28
+ currency: currency.upcase,
29
+ variant_id: variant_id,
30
+ price_book_id: price_book.try(:id)
31
+ )
32
+
33
+ price.amount = amount.to_f
34
+ price.save!
35
+ end
36
+ end
37
+ end
38
+
39
+ flash[:success] = Spree.t('notice_messages.variant_price_updated')
40
+ redirect_to admin_product_prices_path(
41
+ @product,
42
+ store_id: @store.try(:id),
43
+ role_id: @role.try(:id),
44
+ price_type_id: @price_type.try(:id)
45
+ )
46
+ end
47
+
48
+ private
49
+ def variant_prices_params
50
+ params.require(:vp).tap do |whitelisted|
51
+ whitelisted = params[:vp]
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,16 @@
1
+ Spree::Admin::StoresController.class_eval do
2
+ def update_price_book_positions
3
+ Spree::PriceBook.transaction do
4
+ params[:positions].each do |id, index|
5
+ Spree::StorePriceBook
6
+ .where(price_book_id: id, store_id: params[:id])
7
+ .update_all(priority: index)
8
+ end
9
+ end
10
+
11
+ respond_to do |format|
12
+ format.html { redirect_to admin_stores_url(params[:id]) }
13
+ format.js { render plain: 'Ok' }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ Spree::BaseHelper.class_eval do
2
+ def supported_currencies_options
3
+ supported_currencies.map do |currency|
4
+ iso = currency.iso_code
5
+ ["#{currency.name} (#{iso})", iso]
6
+ end
7
+ end
8
+
9
+ def price_types_options
10
+ Spree::PriceType.all.map do |price_type|
11
+ ["#{price_type.name} (#{price_type.code})", price_type.id]
12
+ end
13
+ end
14
+
15
+ def stores_options
16
+ Spree::Store.all.map do |store|
17
+ ["#{store.name} (#{store.code})", store.id]
18
+ end
19
+ end
20
+
21
+ def roles_options
22
+ Spree::Role.all.map do |role|
23
+ [role.name, role.id]
24
+ end
25
+ end
26
+
27
+ def roles_to_s(roles)
28
+ if roles && !roles.empty?
29
+ roles.map(&:name).join(', ')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,56 @@
1
+ class Spree::CurrencyRate < Spree::Base
2
+ validates :base_currency, presence: true
3
+ validates :currency, presence: true, uniqueness: { scope: :base_currency }
4
+ validates :exchange_rate, presence: true
5
+ validate :validate_single_default
6
+
7
+ def self.create_default
8
+ create(base_currency: Spree::Config[:currency], currency: Spree::Config[:currency], default: true)
9
+ end
10
+
11
+ def self.default
12
+ if default = where(default: true).first
13
+ default
14
+ else
15
+ create_default
16
+ end
17
+ end
18
+
19
+ def self.update_from_open_exchange
20
+ oxr = Money::Bank::OpenExchangeRatesBank.new
21
+ oxr.app_id = Rails.application.config.openExchangeRate[:appId]
22
+ oxr.update_rates
23
+ oxr.cache = 'tmp/cache.json'
24
+ oxr.ttl_in_seconds = 86400
25
+ oxr.source = Spree::CurrencyRate.default.currency
26
+ Money.default_bank = oxr
27
+
28
+ Spree::Config[:supported_currencies].split(',').each do |currencyCode|
29
+ logger.debug "Fetching currency #{currencyCode} from OpenExchange"
30
+ currency = Money::Currency.new(currencyCode)
31
+ rate = Money.default_bank.get_rate(Spree::CurrencyRate.default.currency, currency)
32
+ currencyRate = Spree::CurrencyRate.find_or_create_by(
33
+ base_currency: Spree::CurrencyRate.default.currency,
34
+ currency: currency.iso_code,
35
+ default: (Spree::Config[:currency] == currency.iso_code)
36
+ )
37
+ currencyRate.update_attribute(:exchange_rate, rate) if currencyRate
38
+ logger.debug "Currency #{currencyCode} rate updated: #{rate}"
39
+ end
40
+ end
41
+
42
+ private
43
+ def validate_single_default
44
+ return unless default?
45
+
46
+ matches = self.class.where(default: true)
47
+
48
+ if persisted?
49
+ matches = matches.where('id != ?', id)
50
+ end
51
+
52
+ if matches.exists?
53
+ errors.add(:default, 'cannot have multiple defaults.')
54
+ end
55
+ end
56
+ end