spree_price 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,79 @@
1
+ Spree::LineItem.class_eval do
2
+ has_many :line_item_prices, validate: true
3
+
4
+ def user_roles_for_price
5
+ order ? order.user_roles : Spree::Store.current_user.try(:roles) || [Spree::Role.default]
6
+ end
7
+
8
+ def store_for_price
9
+ order ? order.store : Spree::Store.current_store
10
+ end
11
+
12
+ def currency_for_price
13
+ order ? order.currency : Spree::Store.current_currency
14
+ end
15
+
16
+ def variant_price_by_price_type(price_type, currency = currency_for_price)
17
+ variant.price_in(currency, store_for_price, user_roles_for_price, price_type)
18
+ end
19
+
20
+ def line_item_price_by_price_type(price_type)
21
+ line_item_price = line_item_prices.find do |line_item_price|
22
+ line_item_price.price_type_id == price_type.id
23
+ end
24
+ line_item_price || line_item_prices.build({ price_type_id: price_type.id })
25
+ end
26
+
27
+ def default_line_item_price
28
+ line_item_prices.find { |line_item_price| line_item_price.price_type.default? }
29
+ end
30
+
31
+ def update_default_price
32
+ default_price = default_line_item_price
33
+
34
+ if default_price
35
+ self.currency = order.currency if order
36
+ self.price = default_price.amount
37
+ end
38
+ end
39
+
40
+ def update_line_item_prices
41
+ Spree::PriceType.all.each do |price_type|
42
+ yield(variant_price_by_price_type(price_type), price_type)
43
+ end
44
+ end
45
+
46
+ def update_price
47
+ update_line_item_prices do |variant_price, price_type|
48
+ line_item_price = line_item_price_by_price_type(price_type)
49
+
50
+ if order
51
+ line_item_price.amount = variant_price.price_including_vat_for(tax_zone: tax_zone)
52
+ else
53
+ line_item_price.amount = variant_price.amount
54
+ end
55
+ end
56
+
57
+ update_default_price
58
+ end
59
+
60
+ def update_price_from_modifier(currency, opts)
61
+ self.currency = currency if currency
62
+
63
+ update_line_item_prices do |variant_price, price_type|
64
+ line_item_price = line_item_price_by_price_type(price_type)
65
+
66
+ if currency
67
+ line_item_price.amount =
68
+ variant_price_by_price_type(price_type, currency).amount +
69
+ variant.price_modifier_amount_in(currency, opts)
70
+ else
71
+ line_item_price.amount =
72
+ variant_price_by_price_type(price_type).amount +
73
+ variant.price_modifier_amount(opts)
74
+ end
75
+ end
76
+
77
+ update_default_price
78
+ end
79
+ end
@@ -0,0 +1,4 @@
1
+ class Spree::LineItemPrice < Spree::Base
2
+ belongs_to :line_item
3
+ belongs_to :price_type
4
+ end
@@ -0,0 +1,9 @@
1
+ Spree::Order::CurrencyUpdater.class_eval do
2
+ def update_line_item_currencies!
3
+ line_items.where('currency != ?', currency).each do |line_item|
4
+ line_item.update_attributes!(currency: currency)
5
+ line_item.update_price
6
+ line_item.save!
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ Spree::Order.class_eval do
2
+ has_many :order_prices
3
+
4
+ def user_roles
5
+ user.try(:roles) || [Spree::Role.default]
6
+ end
7
+
8
+ def price_by_price_type(price_type)
9
+ price = order_prices.find do |order_price|
10
+ order_price.price_type_id == price_type.id
11
+ end
12
+
13
+ price || order_prices.build(price_type: price_type)
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ class Spree::OrderPrice < Spree::Base
2
+ belongs_to :order
3
+ belongs_to :price_type
4
+ end
@@ -0,0 +1,30 @@
1
+ Spree::OrderUpdater.class_eval do
2
+ def update_prices
3
+ Spree::PriceType.all.each do |price_type|
4
+ order_price = order.price_by_price_type(price_type)
5
+ order_price.amount = line_items.reduce(0) do |sum, line_item|
6
+ sum + (line_item.variant_price_by_price_type(price_type, order.currency).amount * line_item.quantity)
7
+ end
8
+ order_price.save!
9
+ end
10
+ end
11
+
12
+ def persist_totals
13
+ update_prices
14
+
15
+ order.update_columns(
16
+ payment_state: order.payment_state,
17
+ shipment_state: order.shipment_state,
18
+ item_total: order.item_total,
19
+ item_count: order.item_count,
20
+ adjustment_total: order.adjustment_total,
21
+ included_tax_total: order.included_tax_total,
22
+ additional_tax_total: order.additional_tax_total,
23
+ payment_total: order.payment_total,
24
+ shipment_total: order.shipment_total,
25
+ promo_total: order.promo_total,
26
+ total: order.total,
27
+ updated_at: Time.current,
28
+ )
29
+ end
30
+ end
@@ -0,0 +1,88 @@
1
+ class Spree::PriceBook < Spree::Base
2
+ acts_as_nested_set
3
+
4
+ has_many :role_price_books
5
+ has_many :roles, class_name: 'Spree::Role', through: :role_price_books
6
+ has_many :prices
7
+
8
+ has_many :store_price_books
9
+ has_many :stores, through: :store_price_books
10
+ has_many :variants, through: :prices
11
+
12
+ belongs_to :price_type
13
+
14
+ validates :active_to, timeliness: { after: :active_from, allow_blank: true }
15
+ validates :currency, presence: true
16
+
17
+ before_save :set_price_adjustment_factor
18
+ accepts_nested_attributes_for :prices
19
+
20
+ scope :active, -> {
21
+ where("(#{table_name}.active_from <= ? AND (#{table_name}.active_to IS NULL OR #{table_name}.active_to >= ?))",
22
+ Time.zone.now, Time.zone.now)
23
+ }
24
+ scope :by_currency, -> (currency_iso) { where(currency: currency_iso) }
25
+ scope :by_roles, lambda { |role_ids|
26
+ compacted_ids = role_ids.try(:compact)
27
+ unless compacted_ids.nil? || compacted_ids.empty?
28
+ includes(:role_price_books).where(spree_role_price_books: { role_id: compacted_ids })
29
+ else
30
+ where(nil)
31
+ end
32
+ }
33
+ scope :by_store, lambda { |store_id|
34
+ if store_id.present?
35
+ includes(:store_price_books).where(spree_store_price_books: { store_id: store_id })
36
+ else
37
+ where(nil)
38
+ end
39
+ }
40
+ scope :by_price_type, lambda { |price_type_id|
41
+ if price_type_id.present?
42
+ where(price_type_id: price_type_id)
43
+ else
44
+ where(nil)
45
+ end
46
+ }
47
+ scope :explicit, -> { where(parent_id: nil, price_adjustment_factor: nil) }
48
+
49
+ def explicit?
50
+ parent_id.blank?
51
+ end
52
+
53
+ def factored?
54
+ parent_id.present?
55
+ end
56
+
57
+ def active?
58
+ active_from <= Time.zone.now && (active_to.nil? || active_to >= Time.zone.now)
59
+ end
60
+
61
+ def load_prices_from_parent(force_update = false)
62
+ return if explicit?
63
+
64
+ parent.prices.each do |parent_price|
65
+ price = prices.find_or_initialize_by(
66
+ currency: currency,
67
+ variant_id: parent_price.variant_id,
68
+ )
69
+
70
+ if force_update || price.new_record?
71
+ price.amount = parent_price.amount * price_adjustment_factor
72
+ price.save!
73
+ end
74
+ end
75
+ end
76
+
77
+ def set_price_adjustment_factor
78
+ if price_adjustment_factor.blank?
79
+ if parent.present? && parent.currency != currency &&
80
+ self.price_adjustment_factor = Spree::CurrencyRate
81
+ .find_by(base_currency: parent.currency, currency: currency)
82
+ .try(:exchange_rate)
83
+ else
84
+ self.price_adjustment_factor = 1.0
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,39 @@
1
+ Spree::Price.class_eval do
2
+ default_scope { includes(:variant).order('spree_variants.sku') }
3
+
4
+ belongs_to :price_book
5
+ validate :ensure_proper_currency
6
+
7
+ scope :by_currency, -> (currency_iso) { where(currency: currency_iso) }
8
+ scope :by_roles, lambda { |role_ids|
9
+ compacted_ids = role_ids.try(:compact)
10
+ unless compacted_ids.nil? || compacted_ids.empty?
11
+ includes(price_book: :role_price_books)
12
+ .where(spree_role_price_books: { role_id: compacted_ids })
13
+ else
14
+ where(nil)
15
+ end
16
+ }
17
+ scope :by_store, lambda { |store_id|
18
+ if store_id.present?
19
+ includes(price_book: :store_price_books)
20
+ .where(spree_store_price_books: { store_id: store_id })
21
+ else
22
+ where(nil)
23
+ end
24
+ }
25
+ scope :by_price_type, lambda { |price_type_id|
26
+ if price_type_id.present?
27
+ includes(:price_book)
28
+ .where(spree_price_books: { price_type_id: price_type_id })
29
+ else
30
+ where(nil)
31
+ end
32
+ }
33
+
34
+ def ensure_proper_currency
35
+ if price_book && currency != price_book.currency
36
+ errors.add(:currency, :match_price_book)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ class Spree::PriceType < Spree::Base
2
+ def self.create_default
3
+ price_type = find_or_initialize_by(
4
+ name: "Selling Price #{Spree::Config[:currency]}",
5
+ code: 'selling'
6
+ )
7
+ price_type.default = true
8
+ price_type.save!
9
+ end
10
+
11
+ def self.default
12
+ default = where(default: true).first
13
+ default || create_default
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ Spree::Role.class_eval do
2
+ has_many :role_price_books
3
+ has_many :price_books, through: :role_price_books
4
+
5
+ def self.create_default
6
+ user = find_or_initialize_by(name: 'user')
7
+ user.default = true
8
+ user.save!
9
+ end
10
+
11
+ def self.default
12
+ default = where(default: true).first
13
+ default || create_default
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class Spree::RolePriceBook < Spree::Base
2
+ belongs_to :price_book
3
+ belongs_to :role
4
+ delegate :name, to: :price_book
5
+ end
@@ -0,0 +1,4 @@
1
+ Spree::Store.class_eval do
2
+ has_many :price_books, -> { order('spree_store_price_books.priority ASC') }, through: :store_price_books
3
+ has_many :store_price_books
4
+ end
@@ -0,0 +1,7 @@
1
+ class Spree::StorePriceBook < Spree::Base
2
+ default_scope { order(:priority) }
3
+
4
+ belongs_to :price_book
5
+ belongs_to :store
6
+ delegate :name, to: :price_book
7
+ end
@@ -0,0 +1,43 @@
1
+ Spree::Variant.class_eval do
2
+ has_many :price_books, -> { active }, through: :prices
3
+ has_many :prices, dependent: :destroy, inverse_of: :variant
4
+
5
+ def price_in_price_book(
6
+ currency = Spree::Store.current_currency,
7
+ store = Spree::Store.current_store,
8
+ roles = Spree::Store.current_user.try(:roles) || [Spree::Role.default],
9
+ price_type = nil
10
+ )
11
+ prices
12
+ .by_currency(currency)
13
+ .by_roles(roles)
14
+ .by_store(store)
15
+ .by_price_type(price_type)
16
+ .first
17
+ end
18
+
19
+ def price_in(
20
+ currency = Spree::Store.current_currency,
21
+ store = Spree::Store.current_store,
22
+ roles = Spree::Store.current_user.try(:roles) || [Spree::Role.default],
23
+ price_type = nil
24
+ )
25
+ price_book_price = price_in_price_book(currency, store, roles, price_type)
26
+
27
+ logger.warn("Variant #{sku} price cannot be found in store #{store.try(:code)} "\
28
+ "in #{currency} with roles #{roles.try(:compact).try(:flat_map, &:name)}") if price_book_price.nil?
29
+
30
+ price_book_price ||
31
+ prices.by_currency(currency).by_price_type(price_type).first ||
32
+ prices.by_currency(currency).first
33
+ end
34
+
35
+ def price_by_price_type(price_type)
36
+ price_in(
37
+ Spree::Store.current_currency,
38
+ Spree::Store.current_store,
39
+ Spree::Store.current_user.try(:roles),
40
+ price_type
41
+ )
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ <!--
2
+ insert_bottom "div[data-hook='additional_role_fields']"
3
+ -->
4
+
5
+ <%= f.field_container :default, class: ["form-group"], 'data-hook' => "role_default" do %>
6
+ <%= f.label :default, Spree.t(:default) %> <span class="required">*</span>
7
+ <%= f.check_box :default, class: 'form-control' %>
8
+ <%= f.error_message_on :default %>
9
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <!-- insert_bottom "[data-hook='admin_configurations_sidebar_menu'], #admin_configurations_sidebar_menu[data-hook]" -->
2
+
3
+ <% if can? :manage, Spree::CurrencyRate %>
4
+ <%= configurations_sidebar_menu_item Spree::CurrencyRate.model_name.human(count: 1.1), spree.admin_currency_rates_path %>
5
+ <%= configurations_sidebar_menu_item Spree::PriceType.model_name.human(count: 1.1), spree.admin_price_types_path %>
6
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <!--
2
+ insert_bottom "[data-hook='admin_product_sub_tabs'], #sub_nav"
3
+ -->
4
+ <%= tab :price_books, label: Spree::PriceBook.model_name.human(count: 1.1) %>
@@ -0,0 +1,49 @@
1
+ <!--
2
+ insert_after "div.row:has([data-hook='stores'])"
3
+ -->
4
+ <div class='row'>
5
+ <div class='col-md-6'>
6
+ <h3><%= Spree::PriceBook.model_name.human(count: 2) %></h3>
7
+ <hr />
8
+ <%= f.field_container :price_books do %>
9
+ <%= f.collection_check_boxes :price_book_ids, Spree::PriceBook.all, :id, :name do |b| %>
10
+ <%= b.label { b.check_box + ' ' + b.text } %>
11
+ <br/>
12
+ <% end %>
13
+ <% end %>
14
+ </div>
15
+ <div class="col-md-6">
16
+ <h3><%= Spree.t :price_book_priority %></h3>
17
+ <% unless f.object.new_record? %>
18
+ <table
19
+ class="table sortable"
20
+ id="listing_price_book"
21
+ data-hook data-sortable-link="<%= update_price_book_positions_admin_store_url(@store) %>"
22
+ >
23
+ <thead>
24
+ <tr data-hook="price_book_header">
25
+ <th class="no-border"></th>
26
+ <th>
27
+ <%= Spree.t(:name) %>
28
+ </th>
29
+ </tr>
30
+ </thead>
31
+
32
+ <tbody>
33
+ <% f.object.price_books.each do |pb| %>
34
+ <tr id="<%= spree_dom_id pb %>" data-hook="price_book_row">
35
+ <td class="move-handle text-center">
36
+ <span class="icon icon-move handle"></span>
37
+ </td>
38
+ <td>
39
+ <%= pb.name %>
40
+ </td>
41
+ </tr>
42
+ <% end %>
43
+ </tbody>
44
+ </table>
45
+ <% else %>
46
+ <p><%= Spree.t :price_book_empty %></p>
47
+ <% end %>
48
+ </div>
49
+ </div>