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,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>