solidus_sale_pricing 2.0.1

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +25 -0
  6. data/LICENSE +26 -0
  7. data/README.md +173 -0
  8. data/Rakefile +21 -0
  9. data/app/assets/javascripts/spree/backend/solidus_sale_pricing.js +2 -0
  10. data/app/assets/javascripts/spree/frontend/solidus_sale_pricing.js +2 -0
  11. data/app/assets/stylesheets/spree/backend/solidus_sale_pricing.css +13 -0
  12. data/app/assets/stylesheets/spree/frontend/solidus_sale_pricing.css +4 -0
  13. data/app/controllers/spree/admin/product_controller_decorator.rb +10 -0
  14. data/app/controllers/spree/admin/sale_prices_controller.rb +73 -0
  15. data/app/helpers/spree/base_helper_decorator.rb +21 -0
  16. data/app/models/spree/calculator/dollar_amount_sale_price_calculator.rb +12 -0
  17. data/app/models/spree/calculator/percent_off_sale_price_calculator.rb +12 -0
  18. data/app/models/spree/price_decorator.rb +93 -0
  19. data/app/models/spree/product_decorator.rb +49 -0
  20. data/app/models/spree/sale_price.rb +55 -0
  21. data/app/models/spree/variant_decorator.rb +65 -0
  22. data/app/overrides/add_sale_price_to_product_view.rb +12 -0
  23. data/app/overrides/add_sale_product_admin_tabs.rb +6 -0
  24. data/app/views/spree/admin/products/_sale_products.html.erb +3 -0
  25. data/app/views/spree/admin/sale_prices/_form.html.erb +31 -0
  26. data/app/views/spree/admin/sale_prices/edit.html.erb +17 -0
  27. data/app/views/spree/admin/sale_prices/index.html.erb +54 -0
  28. data/app/views/spree/admin/sale_prices/new.html.erb +15 -0
  29. data/bin/rails +7 -0
  30. data/config/locales/en.yml +34 -0
  31. data/config/routes.rb +10 -0
  32. data/db/migrate/20160622203615_add_spree_create_sale_prices_table.rb +19 -0
  33. data/lib/generators/solidus_sale_pricing/install/install_generator.rb +31 -0
  34. data/lib/solidus_sale_pricing.rb +8 -0
  35. data/lib/solidus_sale_pricing/engine.rb +22 -0
  36. data/lib/solidus_sale_pricing/factories.rb +6 -0
  37. data/lib/solidus_sale_pricing/version.rb +18 -0
  38. data/solidus_sale_pricing.gemspec +38 -0
  39. data/spec/controllers/sale_prices_controller_spec.rb +56 -0
  40. data/spec/factories/sale_price.rb +14 -0
  41. data/spec/models/sale_price_spec.rb +5 -0
  42. data/spec/spec_helper.rb +95 -0
  43. metadata +262 -0
@@ -0,0 +1,49 @@
1
+ Spree::Product.class_eval do
2
+
3
+ # Essentially all read values here are delegated to reading the value on the Master variant
4
+ # All write values will write to all variants (including the Master) unless that method's all_variants parameter is set to false, in which case it will only write to the Master variant
5
+
6
+ delegate_belongs_to :master, :active_sale_in, :current_sale_in, :next_active_sale_in, :next_current_sale_in, :sale_price_in, :on_sale_in?, :original_price_in, :discount_percent_in, :sale_price, :original_price, :on_sale?, :sale_prices
7
+
8
+ # attr_accessible :sale_price, :original_price
9
+ # TODO Should the all_variants flag be on option you set on creating the sale and then it always behaves as such? Seems unsafe to pass this flag one way during create and use a different value for it later (they can actively bypass by accessing each variant directly and changing the values)
10
+
11
+ # TODO also accept a class reference for calculator type instead of only a string
12
+ def put_on_sale(attrs={}, all_variants=true)
13
+ run_on_variants(all_variants) { |v| v.put_on_sale(attrs) }
14
+ end
15
+ alias :create_sale :put_on_sale
16
+
17
+ def enable_sale(all_variants = true)
18
+ run_on_variants(all_variants) { |v| v.enable_sale }
19
+ end
20
+
21
+ def disable_sale(all_variants = true)
22
+ run_on_variants(all_variants) { |v| v.disable_sale }
23
+ end
24
+
25
+ def start_sale(end_time = nil, all_variants = true)
26
+ run_on_variants(all_variants) { |v| v.start_sale(end_time) }
27
+ end
28
+
29
+ def stop_sale(all_variants = true)
30
+ run_on_variants(all_variants) { |v| v.stop_sale }
31
+ end
32
+
33
+ def update_sale(attrs={}, all_variants = true)
34
+ run_on_variants(all_variants) { |v| v.update_sale(attrs) }
35
+ end
36
+
37
+ def active_sale_prices?
38
+ sale_prices.where(enabled: true)
39
+ end
40
+
41
+ private
42
+
43
+ def run_on_variants(all_variants, &block)
44
+ if all_variants && variants.present?
45
+ variants.each { |v| block.call v }
46
+ end
47
+ block.call master
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ module Spree
2
+ class SalePrice < ActiveRecord::Base
3
+ # TODO validations
4
+ belongs_to :price, class_name: 'Spree::Price'
5
+ has_one :calculator, class_name: 'Spree::Calculator', as: :calculable, dependent: :destroy
6
+ accepts_nested_attributes_for :calculator
7
+ validates :value, presence: true
8
+ validates :calculator, presence: true
9
+
10
+ scope :active, lambda {
11
+ where("enabled = 't' AND (start_at <= ? OR start_at IS NULL) AND (end_at >= ? OR end_at IS NULL)", Time.now, Time.now)
12
+ }
13
+
14
+ # TODO make this work or remove it
15
+ #def self.calculators
16
+ # Rails.application.config.spree.calculators.send(self.to_s.tableize.gsub('/', '_').sub('spree_', ''))
17
+ #end
18
+
19
+ def calculator_type
20
+ calculator.class.to_s if calculator
21
+ end
22
+
23
+ def calculator_type=(calculator_type)
24
+ clazz = calculator_type.constantize if calculator_type
25
+ self.calculator = clazz.new if clazz and not self.calculator.is_a? clazz
26
+ end
27
+
28
+ def price
29
+ calculator.compute self
30
+ end
31
+
32
+ def enable
33
+ update_attributes(end_at: nil, enabled: true)
34
+ end
35
+
36
+ def disable
37
+ update_attribute(:enabled, false)
38
+ end
39
+
40
+ def start(end_time = nil)
41
+ end_time = nil if end_time.present? && end_time <= Time.now # if end_time is not in the future then make it nil (no end)
42
+ attr = { end_at: end_time, enabled: true }
43
+ attr[:start_at] = Time.now if self.start_at.present? && self.start_at > Time.now # only set start_at if it's not set in the past
44
+ update_attributes(attr)
45
+ end
46
+
47
+ def stop
48
+ update_attributes(end_at: Time.now, enabled: false)
49
+ end
50
+
51
+ def update(attrs={})
52
+ update_attributes(attrs)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,65 @@
1
+ Spree::Variant.class_eval do
2
+ delegate_belongs_to :default_price, :sale_price, :sale_prices, :original_price, :on_sale?
3
+
4
+ # TODO also accept a class reference for calculator type instead of only a string
5
+ def put_on_sale(attrs={}, all_currencies=true)
6
+ run_on_prices(all_currencies) { |p| p.put_on_sale(attrs) }
7
+ end
8
+ alias :create_sale :put_on_sale
9
+
10
+ def active_sale_in(currency)
11
+ price_in(currency).active_sale
12
+ end
13
+ alias :current_sale_in :active_sale_in
14
+
15
+ def next_active_sale_in(currency)
16
+ price_in(currency).next_active_sale
17
+ end
18
+ alias :next_current_sale_in :next_active_sale_in
19
+
20
+ def sale_price_in(currency)
21
+ Spree::Price.new variant_id: self.id, currency: currency, amount: price_in(currency).sale_price
22
+ end
23
+
24
+ def discount_percent_in(currency)
25
+ price_in(currency).discount_percent
26
+ end
27
+
28
+ def on_sale_in?(currency)
29
+ price_in(currency).on_sale?
30
+ end
31
+
32
+ def original_price_in(currency)
33
+ Spree::Price.new variant_id: self.id, currency: currency, amount: price_in(currency).original_price
34
+ end
35
+
36
+ def enable_sale(all_currencies = true)
37
+ run_on_prices(all_currencies) { |p| p.enable_sale }
38
+ end
39
+
40
+ def disable_sale(all_currencies = true)
41
+ run_on_prices(all_currencies) { |p| p.disable_sale }
42
+ end
43
+
44
+ def start_sale(end_time = nil, all_currencies = true)
45
+ run_on_prices(all_currencies) { |p| p.start_sale end_time }
46
+ end
47
+
48
+ def stop_sale(all_currencies=true)
49
+ run_on_prices(all_currencies) { |p| p.stop_sale }
50
+ end
51
+
52
+ def update_sale(attrs, all_currencies=true)
53
+ run_on_prices(all_currencies) { |p| p.update_sale(attrs) }
54
+ end
55
+
56
+ private
57
+
58
+ def run_on_prices(all_currencies, &block)
59
+ if all_currencies && prices.present?
60
+ prices.each { |p| block.call p }
61
+ else
62
+ block.call default_price
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,12 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/shared/_products',
3
+ name: 'add_sale_price_to_product',
4
+ replace: 'span.price'
5
+ ) do
6
+ "<% if product.on_sale? %>
7
+ <span class='price sale' itemprop='sale_price'><%= number_to_currency(product.sale_price) %></span>
8
+ <span class='price selling' itemprop='price'><%= display_price(product) %></span>
9
+ <% else %>
10
+ <span class='price selling' itemprop='price'><%= display_price(product) %></span>
11
+ <% end %>"
12
+ end
@@ -0,0 +1,6 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/admin/shared/_product_tabs',
3
+ name: 'add_sale_products_admin_tab',
4
+ insert_bottom: '[data-hook="admin_product_tabs"]',
5
+ partial: 'spree/admin/products/sale_products'
6
+ )
@@ -0,0 +1,3 @@
1
+ <li<%== ' class="active"' if current == "On Sale" %>>
2
+ <%= link_to Spree.t(:'admin.tab.sale_products'), admin_product_sale_prices_url(@product) %>
3
+ </li>
@@ -0,0 +1,31 @@
1
+ <div data-hook='admin_sale_price_form_fields' class='row'>
2
+ <div class='alpha four columns'>
3
+ <div class='field'>
4
+ <%= f.field_container :value do %>
5
+ <%= f.label :value, Spree.t(:value) %><br />
6
+ <%= f.text_field :value, class: 'fullwidth' %>
7
+ <%= f.error_message_on :value %>
8
+ <% end %>
9
+ </div>
10
+ </div>
11
+
12
+ <div class='alpha four columns'>
13
+ <div class='field'>
14
+ <%= f.field_container :start_at do %>
15
+ <%= f.label :start_at, Spree.t(:start_at) %><br />
16
+ <%= f.text_field :start_at, value: datepicker_field_value(@sale_price.start_at), class: 'datepicker fullwidth' %>
17
+ <%= f.error_message_on :start_at %>
18
+ <% end %>
19
+ </div>
20
+ </div>
21
+
22
+ <div class='alpha four columns'>
23
+ <div class='field'>
24
+ <%= f.field_container :end_at do %>
25
+ <%= f.label :end_at, Spree.t(:end_at) %><br />
26
+ <%= f.text_field :end_at, value: datepicker_field_value(@sale_price.end_at), class: 'datepicker fullwidth' %>
27
+ <%= f.error_message_on :end_at %>
28
+ <% end %>
29
+ </div>
30
+ </div>
31
+ </div>
@@ -0,0 +1,17 @@
1
+ <% content_for :page_actions do %>
2
+ <% if can?(:admin, Spree::SalePrice) %>
3
+ <li><%= button_link_to Spree.t(:back_to_sale_prices_list), admin_product_sale_prices_path(@product), icon: 'arrow-left' %></li>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <%= render partial: 'spree/admin/shared/product_tabs', locals: { current: 'On Sale' } %>
8
+ <%= render partial: 'spree/shared/error_messages', locals: { target: @sale_price } %>
9
+
10
+ <% content_for :page_title do %>
11
+ <%= Spree.t(:sale_price) %>
12
+ <% end %>
13
+
14
+ <%= form_for [:admin, @product, @sale_price], method: :put do |f| %>
15
+ <%= render 'form', f: f %>
16
+ <%= render 'spree/admin/shared/edit_resource_links', collection_url: admin_product_sale_prices_url %>
17
+ <% end %>
@@ -0,0 +1,54 @@
1
+ <% content_for :page_title do %>
2
+ <%= Spree.t(:listing_sale_prices) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= button_link_to Spree.t(:new_sale_price), new_admin_product_sale_price_url(@product), icon: 'add', class: 'btn-success', id: 'admin_new_sale_price_link' %>
7
+ <% end %>
8
+
9
+ <%= render partial: 'spree/admin/shared/product_tabs', locals: { current: 'On Sale' } %>
10
+
11
+ <% if @sale_prices.any? %>
12
+ <table class='table' id='listing_sale_prices'>
13
+ <thead>
14
+ <tr data-hook='admin_sale_prices_index_headers'>
15
+ <th class='text-center'><%= Spree.t(:original_price) %></th>
16
+ <th class='text-center'><%= Spree.t(:value) %></th>
17
+ <th class='text-center'><%= Spree.t(:start_at) %></th>
18
+ <th class='text-center'><%= Spree.t(:end_at) %></th>
19
+ <th class='text-center'><%= Spree.t(:enabled) %></th>
20
+ <th class='actions' data-hook='admin_sale_prices_index_header_actions'></th>
21
+ </tr>
22
+ </thead>
23
+
24
+ <tbody>
25
+ <% @sale_prices.each do |sale_price| %>
26
+ <tr id='<%= spree_dom_id sale_price %>' data-hook='admin_sale_prices_index_rows' class='<%= cycle('odd', 'even') %>'>
27
+ <td class='align-center'><%= number_to_currency(@product.original_price.to_f) %></td>
28
+ <td class='align-center'><%= number_to_currency(sale_price.value) %></td>
29
+ <td class='align-center'><%= format_date(sale_price.start_at) %></td>
30
+ <td class='align-center'><%= format_date(sale_price.end_at) %></td>
31
+ <td class='align-center'><%= sale_price.enabled %></td>
32
+ <td class='actions actions-3 text-right' data-hook='admin_sale_prices_index_row_actions'>
33
+ <%= link_to_edit sale_price, url: edit_admin_product_sale_price_url(@product, sale_price), no_text: true, class: 'edit' if can?(:edit, sale_price) %>
34
+ &nbsp;
35
+ <% if sale_price.enabled? %>
36
+ <%= link_to_with_icon 'remove', Spree.t(:stop), admin_product_sale_price_stop_path(@product, sale_price), no_text: true, class: 'stop' if can?(:edit, sale_price) %>
37
+ &nbsp;
38
+ <% else %>
39
+ <%= link_to_with_icon 'check', Spree.t(:enable), admin_product_sale_price_enable_path(@product, sale_price), no_text: true, class: 'enable' if can?(:edit, sale_price) %>
40
+ &nbsp;
41
+ <% end %>
42
+ <%= link_to_delete sale_price, url: admin_product_sale_price_path(@product, sale_price), no_text: true if can?(:delete, sale_price) %>
43
+ </td>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+ <% else %>
49
+ <div class='alpha twelve columns no-objects-found'>
50
+ <%= render 'spree/admin/shared/no_objects_found',
51
+ resource: Spree::SalePrice,
52
+ new_resource_url: spree.new_admin_product_sale_price_path(@product) %>
53
+ </div>
54
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <% content_for :page_actions do %>
2
+ <%= button_link_to Spree.t(:back_to_sale_prices_list), admin_product_sale_prices_path(@product), icon: 'arrow-left', class: 'btn-primary' %>
3
+ <% end %>
4
+
5
+ <% content_for :page_title do %>
6
+ <%= Spree.t(:new_sale_price) %>
7
+ <% end %>
8
+
9
+ <%= render partial: 'spree/admin/shared/product_tabs', locals: { current: 'On Sale' } %>
10
+ <%= render partial: 'spree/shared/error_messages', locals: { target: @sale_price } %>
11
+
12
+ <%= form_for [:admin, :product, @sale_price] do |f| %>
13
+ <%= render 'form', f: f %>
14
+ <%= render 'spree/admin/shared/new_resource_links', collection_url: admin_product_sale_prices_path(@product) %>
15
+ <% end %>
@@ -0,0 +1,7 @@
1
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/solidus_sale_pricing/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
@@ -0,0 +1,34 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ activerecord:
6
+ models:
7
+ spree/sale_price:
8
+ one: Sale Price
9
+ other: Sale Prices
10
+ spree:
11
+ admin:
12
+ tab:
13
+ sale_products: Sale Products
14
+ new_sale_price: New Sale Price
15
+ listing_sale_prices: Listing Sale Prices
16
+ creating_sale_price: Creating Sale Price
17
+ sale_price: Sale Price
18
+ sale_products: On Sale
19
+ price_id: Price ID
20
+ original_price: Original Price
21
+ value: Value
22
+ start_at: Start At
23
+ end_at: End At
24
+ enabled: Enabled
25
+ stop: Stop
26
+ enable: Enable
27
+ sale_price_stopped: The sale price was stopped successfully!
28
+ sale_price_enabled: The sale price was enabled successfully!
29
+ please_stop_other_sale_prices: Please stop the other sale prices
30
+ sale_price_successfully_created: The sale price was successfully created
31
+ error_on_create: Something went wrong, please check your data
32
+ sale_price_successfully_updated: The sale price was successfully updated
33
+ error_on_update: Something went wrong, please check your data
34
+ back_to_sale_prices_list: Back to Sale Prices List
@@ -0,0 +1,10 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ namespace :admin do
3
+ resources :products do
4
+ resources :sale_prices do
5
+ get :stop
6
+ get :enable
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ class AddSpreeCreateSalePricesTable < SolidusSupport::Migration[4.2]
2
+ def change
3
+ create_table :spree_sale_prices do |t|
4
+ t.integer :price_id
5
+ t.float :value
6
+ t.datetime :start_at
7
+ t.datetime :end_at
8
+ t.boolean :enabled
9
+ t.timestamps
10
+ end
11
+
12
+ # Getting active sale prices for a price
13
+ add_index :spree_sale_prices, [:price_id, :start_at, :end_at, :enabled], :name => "index_active_sale_prices_for_price"
14
+ # Getting all active sale prices for all prices
15
+ add_index :spree_sale_prices, [:start_at, :end_at, :enabled], :name => "index_active_sale_prices_for_all_variants"
16
+ # Getting all sale prices for a price
17
+ add_index :spree_sale_prices, :price_id, :name => "index_sale_prices_for_price"
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ module SolidusSalePricing
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ class_option :auto_run_migrations, type: :boolean, default: false
6
+
7
+ def add_javascripts
8
+ append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/solidus_sale_pricing\n"
9
+ append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/solidus_sale_pricing\n"
10
+ end
11
+
12
+ def add_stylesheets
13
+ inject_into_file 'vendor/assets/stylesheets/spree/frontend/all.css', " *= require spree/frontend/solidus_sale_pricing\n", before: /\*\//, verbose: true
14
+ inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/solidus_sale_pricing\n", before: /\*\//, verbose: true
15
+ end
16
+
17
+ def add_migrations
18
+ run 'bundle exec rake railties:install:migrations FROM=solidus_sale_pricing'
19
+ end
20
+
21
+ def run_migrations
22
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
23
+ if run_migrations
24
+ run 'bundle exec rake db:migrate'
25
+ else
26
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ require 'solidus_backend'
2
+ require 'solidus_core'
3
+ require 'solidus_support'
4
+ require 'solidus_sale_pricing/engine'
5
+ require 'solidus_sale_pricing/version'
6
+ require 'coffee_script'
7
+ require 'sass/rails'
8
+ require 'deface'
@@ -0,0 +1,22 @@
1
+ module SolidusSalePricing
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'solidus_sale_pricing'
6
+
7
+ config.autoload_paths += %W(#{config.root}/lib)
8
+
9
+ # use rspec for tests
10
+ config.generators do |g|
11
+ g.test_framework :rspec
12
+ end
13
+
14
+ def self.activate
15
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
16
+ Rails.configuration.cache_classes ? require(c) : load(c)
17
+ end
18
+ end
19
+
20
+ config.to_prepare &method(:activate).to_proc
21
+ end
22
+ end