solidus_sale_pricing 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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