spree_price 3.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +83 -0
- data/app/assets/javascripts/spree/backend/price.js.coffee +13 -0
- data/app/assets/javascripts/spree/backend/price_book.js.coffee +104 -0
- data/app/assets/javascripts/spree/backend/spree_price.js +2 -0
- data/app/assets/javascripts/spree/frontend/spree_price.js +2 -0
- data/app/assets/stylesheets/spree/backend/spree_price.css +4 -0
- data/app/assets/stylesheets/spree/frontend/spree_price.css +4 -0
- data/app/controllers/spree/admin/currency_rates_controller.rb +11 -0
- data/app/controllers/spree/admin/price_books_controller.rb +41 -0
- data/app/controllers/spree/admin/price_types_controller.rb +6 -0
- data/app/controllers/spree/admin/prices_controller_decorator.rb +54 -0
- data/app/controllers/spree/admin/stores_controller_decorator.rb +16 -0
- data/app/helpers/spree/base_helper_decorator.rb +32 -0
- data/app/models/spree/currency_rate.rb +56 -0
- data/app/models/spree/line_item_decorator.rb +79 -0
- data/app/models/spree/line_item_price.rb +4 -0
- data/app/models/spree/order/currency_updater_decorator.rb +9 -0
- data/app/models/spree/order_decorator.rb +15 -0
- data/app/models/spree/order_price.rb +4 -0
- data/app/models/spree/order_updater_decorator.rb +30 -0
- data/app/models/spree/price_book.rb +88 -0
- data/app/models/spree/price_decorator.rb +39 -0
- data/app/models/spree/price_type.rb +15 -0
- data/app/models/spree/role_decorator.rb +15 -0
- data/app/models/spree/role_price_book.rb +5 -0
- data/app/models/spree/store_decorator.rb +4 -0
- data/app/models/spree/store_price_book.rb +7 -0
- data/app/models/spree/variant_decorator.rb +43 -0
- data/app/overrides/spree/admin/roles/_form/add_default_checkout_to_role.html.erb.deface +9 -0
- data/app/overrides/spree/admin/shared/sub_menu/_configuration/current_rates_link.html.erb.deface +6 -0
- data/app/overrides/spree/admin/shared/sub_menu/_product/add_price_books_tab.html.erb.deface +4 -0
- data/app/overrides/spree/admin/stores/_form/add_price_book_to_store.html.erb.deface +49 -0
- data/app/views/spree/admin/currency_rates/_form.html.erb +29 -0
- data/app/views/spree/admin/currency_rates/edit.html.erb +19 -0
- data/app/views/spree/admin/currency_rates/index.html.erb +50 -0
- data/app/views/spree/admin/currency_rates/new.html.erb +21 -0
- data/app/views/spree/admin/price_books/_add_price.html.erb +82 -0
- data/app/views/spree/admin/price_books/_form.html.erb +81 -0
- data/app/views/spree/admin/price_books/_price.html.erb +12 -0
- data/app/views/spree/admin/price_books/_price_book.html.erb +16 -0
- data/app/views/spree/admin/price_books/_price_list.html.erb +37 -0
- data/app/views/spree/admin/price_books/edit.html.erb +21 -0
- data/app/views/spree/admin/price_books/index.html.erb +65 -0
- data/app/views/spree/admin/price_books/new.html.erb +14 -0
- data/app/views/spree/admin/price_books/show.html.erb +39 -0
- data/app/views/spree/admin/price_types/_form.html.erb +25 -0
- data/app/views/spree/admin/price_types/edit.html.erb +18 -0
- data/app/views/spree/admin/price_types/index.html.erb +51 -0
- data/app/views/spree/admin/price_types/new.html.erb +14 -0
- data/app/views/spree/admin/prices/_variant_prices.html.erb +57 -0
- data/app/views/spree/admin/prices/index.html.erb +39 -0
- data/config/locales/en.yml +34 -0
- data/config/locales/validates_timeliness.en.yml +16 -0
- data/config/routes.rb +33 -0
- data/db/migrate/20180828044728_add_spree_price_book.rb +52 -0
- data/db/migrate/20180830053207_add_price_to_order_and_line_item.rb +19 -0
- data/db/migrate/20180830055224_add_default_for_role.rb +7 -0
- data/lib/generators/spree_price/install/install_generator.rb +31 -0
- data/lib/spree_price.rb +7 -0
- data/lib/spree_price/engine.rb +20 -0
- data/lib/spree_price/factories.rb +5 -0
- data/lib/spree_price/factories/currency_rate_factory.rb +15 -0
- data/lib/spree_price/factories/price_book_factory.rb +34 -0
- data/lib/spree_price/factories/price_type_factory.rb +21 -0
- data/lib/spree_price/version.rb +17 -0
- data/lib/tasks/price.rake +58 -0
- 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,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,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,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 %>
|
data/app/overrides/spree/admin/shared/sub_menu/_configuration/current_rates_link.html.erb.deface
ADDED
@@ -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,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>
|