diff --git a/app/controllers/spree/admin/promotion_batches_controller.rb b/app/controllers/spree/admin/promotion_batches_controller.rb
new file mode 100644
index 0000000000..d60e3e83b5
--- /dev/null
+++ b/app/controllers/spree/admin/promotion_batches_controller.rb
@@ -0,0 +1,63 @@
+module Spree
+ module Admin
+ class PromotionBatchesController < ResourceController
+ def update
+ if @object.template_promotion_id
+ flash[:error] = Spree.t(:template_promotion_already_assigned)
+ respond_with(@object) do |format|
+ format.html { render action: :edit, status: :unprocessable_entity }
+ format.js { render layout: false, status: :unprocessable_entity }
+ end
+ return
+ end
+ super
+ end
+
+ def destroy
+ result = Spree::PromotionBatches::Destroy.call(promotion_batch: @promotion_batch)
+
+ if result.success?
+ flash[:success] = flash_message_for(@promotion_batch, :successfully_removed)
+ else
+ flash[:error] = @promotion_batch.errors.full_messages.join(', ')
+ end
+
+ respond_with(@promotion_batch) do |format|
+ format.html { redirect_to location_after_destroy }
+ format.js { render_js_for_destroy }
+ end
+ end
+
+ def csv_export
+ send_data Spree::PromotionBatches::PromotionCodesExporter.new(params).call,
+ filename: "promo_codes_from_batch_id_#{params[:id]}.csv",
+ disposition: :attachment,
+ type: 'text/csv'
+ end
+
+ def csv_import
+ file = params[:file]
+ Spree::PromotionBatches::PromotionCodesImporter.new(file: file, promotion_batch_id: params[:id]).call
+ redirect_back fallback_location: admin_promotions_path, notice: Spree.t('code_upload')
+ rescue Spree::PromotionBatches::PromotionCodesImporter::Error => e
+ redirect_back fallback_location: admin_promotions_path, alert: e.message
+ end
+
+ def populate
+ batch_id = params[:id]
+ options = {
+ batch_size: params[:batch_size].to_i,
+ affix: params.dig(:code, :affix)&.to_sym,
+ content: params[:affix_content],
+ deny_list: params[:forbidden_phrases].split,
+ random_part_bytes: params[:random_part_bytes].to_i
+ }
+
+ Spree::Promotions::PopulatePromotionBatch.new(batch_id, options).call
+
+ flash[:success] = Spree.t('promotion_batch_populated')
+ redirect_to spree.edit_admin_promotion_batch_url(@promotion_batch)
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/admin/promotions_controller.rb b/app/controllers/spree/admin/promotions_controller.rb
index 23a616cc90..4b66698654 100644
--- a/app/controllers/spree/admin/promotions_controller.rb
+++ b/app/controllers/spree/admin/promotions_controller.rb
@@ -40,6 +40,7 @@ def collection
params[:q][:s] ||= 'id desc'
@collection = super
+ @collection = @collection.non_batched
@search = @collection.ransack(params[:q])
@collection = @search.result(distinct: true).
includes(promotion_includes).
diff --git a/app/helpers/spree/admin/navigation_helper.rb b/app/helpers/spree/admin/navigation_helper.rb
index dfecab836c..7b6c81a7b8 100644
--- a/app/helpers/spree/admin/navigation_helper.rb
+++ b/app/helpers/spree/admin/navigation_helper.rb
@@ -377,6 +377,14 @@ def variants_actions
def product_properties_actions
Rails.application.config.spree_backend.actions[:product_properties]
end
+
+ def promotion_batch_actions
+ Rails.application.config.spree_backend.actions[:promotion_batch_actions]
+ end
+
+ def promotion_batches_actions
+ Rails.application.config.spree_backend.actions[:promotion_batches_actions]
+ end
# rubocop:enable Metrics/ModuleLength
end
end
diff --git a/app/models/spree/admin/actions/promotion_batch_default_actions_builder.rb b/app/models/spree/admin/actions/promotion_batch_default_actions_builder.rb
new file mode 100644
index 0000000000..f9c7979b9d
--- /dev/null
+++ b/app/models/spree/admin/actions/promotion_batch_default_actions_builder.rb
@@ -0,0 +1,27 @@
+module Spree
+ module Admin
+ module Actions
+ class PromotionBatchDefaultActionsBuilder
+ include Spree::Core::Engine.routes.url_helpers
+
+ def build
+ root = Root.new
+ export_codes_to_csv_action(root)
+ root
+ end
+
+ private
+
+ def export_codes_to_csv_action(root)
+ action =
+ ActionBuilder.new('csv_export', ->(resource) { csv_export_admin_promotion_batch_path(resource) }).
+ with_icon_key('download.svg').
+ with_style(::Spree::Admin::Actions::ActionStyle::PRIMARY).
+ build
+
+ root.add(action)
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/spree/admin/actions/promotion_batches_default_actions_builder.rb b/app/models/spree/admin/actions/promotion_batches_default_actions_builder.rb
new file mode 100644
index 0000000000..79a6ea955f
--- /dev/null
+++ b/app/models/spree/admin/actions/promotion_batches_default_actions_builder.rb
@@ -0,0 +1,28 @@
+module Spree
+ module Admin
+ module Actions
+ class PromotionBatchesDefaultActionsBuilder
+ include Spree::Core::Engine.routes.url_helpers
+
+ def build
+ root = Root.new
+ add_new_promotion_batch_action(root)
+ root
+ end
+
+ private
+
+ def add_new_promotion_batch_action(root)
+ action =
+ ActionBuilder.new('new_promotion_batch', new_admin_promotion_batch_path).
+ with_icon_key('add.svg').
+ with_style(::Spree::Admin::Actions::ActionStyle::PRIMARY).
+ with_create_ability_check(::Spree::PromotionBatch).
+ build
+
+ root.add(action)
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/spree/admin/main_menu/default_configuration_builder.rb b/app/models/spree/admin/main_menu/default_configuration_builder.rb
index 4ad4887506..b524eaada3 100644
--- a/app/models/spree/admin/main_menu/default_configuration_builder.rb
+++ b/app/models/spree/admin/main_menu/default_configuration_builder.rb
@@ -19,6 +19,7 @@ def build
add_integrations_section(root)
add_oauth_section(root)
add_settings_section(root)
+ add_promotion_batches_section(root)
root
end
@@ -255,6 +256,13 @@ def add_settings_section(root)
build
root.add(section)
end
+
+ def add_promotion_batches_section(root)
+ root.add(ItemBuilder.new('promotion_batches', admin_promotion_batches_path).
+ with_icon_key('stack.svg').
+ with_admin_ability_check(Spree::PromotionBatch).
+ build)
+ end
# rubocop:enable Metrics/AbcSize
end
# rubocop:enable Metrics/ClassLength
diff --git a/app/views/spree/admin/promotion_batches/_form.html.erb b/app/views/spree/admin/promotion_batches/_form.html.erb
new file mode 100644
index 0000000000..c831fea953
--- /dev/null
+++ b/app/views/spree/admin/promotion_batches/_form.html.erb
@@ -0,0 +1,6 @@
+
+ <%= f.field_container :template_promotion do %>
+ <%= f.label :template_promotion_id, Spree.t(:template_promotion) %>
+ <%= f.collection_select :template_promotion_id, Spree::Promotion.non_batched, :id, ->(promotion) { "#{promotion.name} # #{promotion.id}" }, { include_blank: true }, { class: 'select2-clear w-100' } %>
+ <% end %>
+
diff --git a/app/views/spree/admin/promotion_batches/_size.html.erb b/app/views/spree/admin/promotion_batches/_size.html.erb
new file mode 100644
index 0000000000..ac444014fb
--- /dev/null
+++ b/app/views/spree/admin/promotion_batches/_size.html.erb
@@ -0,0 +1,6 @@
+
+ <%= form_tag(duplicate_admin_promotion_batch_path(@promotion_batch), method: :post) do %>
+ <%= number_field_tag(:batch_size) %>
+ <%= submit_tag("Duplicate") %>
+ <% end %>
+
\ No newline at end of file
diff --git a/app/views/spree/admin/promotion_batches/edit.html.erb b/app/views/spree/admin/promotion_batches/edit.html.erb
new file mode 100644
index 0000000000..8c1f21c7f7
--- /dev/null
+++ b/app/views/spree/admin/promotion_batches/edit.html.erb
@@ -0,0 +1,68 @@
+<% content_for :page_title do %>
+ <%= link_to Spree.t(:promotion_batches), admin_promotion_batches_path %> /
+ <%= @promotion_batch.id %>
+<% end %>
+
+<% if @promotion_batch.template_promotion_id%>
+
+
+ <%= form_tag csv_import_admin_promotion_batch_path, method: :post, multipart: true do %>
+ <%= file_field_tag :file, accept: ".csv" %>
+ <%= submit_tag Spree.t('promotion_batch_form.import_codes') %>
+ <% end %>
+
+
+<% end %>
+
+<% unless @promotion_batch.template_promotion_id%>
+ <%= form_for @promotion_batch, url: object_url, method: :put do |f| %>
+
+
+ <%= render partial: 'form', locals: { f: f } %>
+ <%= render partial: 'spree/admin/shared/edit_resource_links' %>
+
+
+ <% end %>
+<% end %>
+
+<% if @promotion_batch.template_promotion_id %>
+ <%= form_with url: populate_admin_promotion_batch_path(@promotion_batch), method: :post do %>
+
+ <%= label_tag :batch_size do %>
+ <%= number_field_tag(:batch_size) %>
+ <%= Spree.t('promotion_batch_form.batch_size') %>
+ <% end %>
+
+
+ <%= label_tag :prefix do %>
+ <%= radio_button :code, :affix, 'prefix' %>
+ <%= Spree.t('promotion_batch_form.prefix') %>
+ <% end %>
+
+
+ <%= label_tag :suffix do %>
+ <%= radio_button :code, :affix, 'suffix' %>
+ <%= Spree.t('promotion_batch_form.suffix') %>
+ <% end %>
+
+
+ <%= label_tag :affix_content do %>
+ <%= text_field_tag(:affix_content) %>
+ <%= Spree.t('promotion_batch_form.affix_content') %>
+ <% end %>
+
+
+ <%= label_tag :forbidden_phrases do %>
+ <%= text_area_tag(:forbidden_phrases) %>
+ <%= Spree.t('promotion_batch_form.forbidden_phrases') %>
+ <% end %>
+
+
+ <%= label_tag :random_part_bytes do %>
+ <%= number_field_tag(:random_part_bytes, value = 4) %>
+ <%= Spree.t('promotion_batch_form.random_part_bytes') %>
+ <% end %>
+
+ <%= submit_tag(Spree.t('promotion_batch_form.populate')) %>
+ <% end %>
+<% end %>
diff --git a/app/views/spree/admin/promotion_batches/index.html.erb b/app/views/spree/admin/promotion_batches/index.html.erb
new file mode 100644
index 0000000000..60b460ae49
--- /dev/null
+++ b/app/views/spree/admin/promotion_batches/index.html.erb
@@ -0,0 +1,45 @@
+<% content_for :page_title do %>
+ <%= plural_resource_name(Spree::PromotionBatch) %>
+<% end %>
+
+<% content_for :page_actions do %>
+ <% promotion_batches_actions.items.each do |action| %>
+ <% next unless action.available?(current_ability) %>
+ <%= button_link_to(
+ Spree.t(action.label_translation_key),
+ action.url,
+ class: action.classes,
+ icon: action.icon_key
+ ) %>
+ <% end %>
+<% end %>
+
+<% if @promotion_batches.any? %>
+
+
+
+
+ <%= Spree.t(:id) %> |
+ <%= Spree.t(:size) %> |
+ <%= Spree.t(:template_promotion) %> |
+ |
+
+
+
+ <% @promotion_batches.each do |promotion_batch| %>
+
+ <%= link_to Spree::PromotionBatchPresenter.new(promotion_batch).call[:model_name_id], spree.admin_promotion_batch_path(promotion_batch) %> |
+ <%= promotion_batch.promotions.count %> |
+ <%= link_to Spree::PromotionBatchPresenter.new(promotion_batch).call[:template_promotion_name_id], spree.edit_admin_promotion_path(promotion_batch.template_promotion) if promotion_batch.template_promotion %> |
+
+
+ <%= link_to_edit promotion_batch, no_text: true if can?(:edit, promotion_batch) %>
+ <%= link_to_delete promotion_batch, no_text: true if can?(:delete, promotion_batch) %>
+
+ |
+
+ <% end %>
+
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/spree/admin/promotion_batches/new.html.erb b/app/views/spree/admin/promotion_batches/new.html.erb
new file mode 100644
index 0000000000..328fe5e0c0
--- /dev/null
+++ b/app/views/spree/admin/promotion_batches/new.html.erb
@@ -0,0 +1,13 @@
+<% content_for :page_title do %>
+ <%= link_to Spree.t(:promotion_batches), admin_promotion_batches_path %> /
+ <%= Spree.t(:new_promotion_batch) %>
+<% end %>
+
+
+
+ <%= form_for :promotion_batch, url: collection_url do |f| %>
+ <%= render partial: 'form', locals: { f: f } %>
+ <%= render partial: 'spree/admin/shared/new_resource_links' %>
+ <% end %>
+
+
diff --git a/app/views/spree/admin/promotion_batches/show.html.erb b/app/views/spree/admin/promotion_batches/show.html.erb
new file mode 100644
index 0000000000..2bad5bdcfe
--- /dev/null
+++ b/app/views/spree/admin/promotion_batches/show.html.erb
@@ -0,0 +1,37 @@
+<% content_for :page_title do %>
+ <%= link_to Spree.t(:promotion_batches), admin_promotion_batches_path %> /
+ <%= @promotion_batch.id %>
+<% end %>
+
+<% content_for :page_actions do %>
+ <% promotion_batch_actions.items.each do |action| %>
+ <% next unless action.available?(current_ability) %>
+ <%= button_link_to(
+ Spree.t(action.label_translation_key),
+ action.url(@promotion_batch),
+ class: action.classes,
+ icon: action.icon_key
+ ) %>
+ <% end %>
+<% end %>
+
+
+
+
+ <%= Spree.t(:code) %> |
+ <%= Spree.t(:description) %> |
+ <%= Spree.t(:redeemed) %> |
+ <%= Spree.t(:expiration) %> |
+
+
+
+ <% @promotion_batch.promotions.each do |promotion| %>
+
+ <%= promotion.code %> |
+ <%= promotion.description %> |
+ <%= promotion.credits_count == promotion.usage_limit ? Spree.t(:say_yes) : Spree.t(:say_no) %> |
+ <%= promotion.expires_at.to_date if promotion.expires_at %> |
+
+ <% end %>
+
+
diff --git a/config/routes.rb b/config/routes.rb
index e1facaf504..c1ee28aa19 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,6 +10,14 @@
resources :promotion_categories, except: [:show]
+ resources :promotion_batches do
+ member do
+ get :csv_export, to: 'promotion_batches#csv_export'
+ post :csv_import, to: 'promotion_batches#csv_import'
+ post :populate
+ end
+ end
+
resources :zones
resources :stores, except: %i[index show] do
diff --git a/lib/spree/backend/engine.rb b/lib/spree/backend/engine.rb
index 49887afbff..119358659a 100644
--- a/lib/spree/backend/engine.rb
+++ b/lib/spree/backend/engine.rb
@@ -48,6 +48,10 @@ class Engine < ::Rails::Engine
Rails.application.config.spree_backend.actions[:payments] = Spree::Admin::Actions::PaymentsDefaultActionsBuilder.new.build
Rails.application.config.spree_backend.actions[:variants] = Spree::Admin::Actions::VariantsDefaultActionsBuilder.new.build
Rails.application.config.spree_backend.actions[:product_properties] = Spree::Admin::Actions::ProductPropertiesDefaultActionsBuilder.new.build
+ Rails.application.config.spree_backend.actions[:promotion_batch_actions] =
+ Spree::Admin::Actions::PromotionBatchDefaultActionsBuilder.new.build
+ Rails.application.config.spree_backend.actions[:promotion_batches_actions] =
+ Spree::Admin::Actions::PromotionBatchesDefaultActionsBuilder.new.build
end
end
end
diff --git a/spec/models/spree/admin/actions/promotion_batch_default_actions_builder_spec.rb b/spec/models/spree/admin/actions/promotion_batch_default_actions_builder_spec.rb
new file mode 100644
index 0000000000..2f45ca3460
--- /dev/null
+++ b/spec/models/spree/admin/actions/promotion_batch_default_actions_builder_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+module Spree
+ module Admin
+ describe Actions::PromotionBatchDefaultActionsBuilder, type: :model do
+ let(:builder) { described_class.new }
+ let(:default_actions) do
+ %w(csv_export)
+ end
+
+ describe '#build' do
+ subject { builder.build }
+
+ it 'builds default tabs' do
+ expect(subject.items.map(&:key)).to match(default_actions)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/spree/admin/actions/promotion_batches_default_actions_builder_spec.rb b/spec/models/spree/admin/actions/promotion_batches_default_actions_builder_spec.rb
new file mode 100644
index 0000000000..848f284f15
--- /dev/null
+++ b/spec/models/spree/admin/actions/promotion_batches_default_actions_builder_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+module Spree
+ module Admin
+ describe Actions::PromotionBatchesDefaultActionsBuilder, type: :model do
+ let(:builder) { described_class.new }
+ let(:default_actions) do
+ %w(new_promotion_batch)
+ end
+
+ describe '#build' do
+ subject { builder.build }
+
+ it 'builds default tabs' do
+ expect(subject.items.map(&:key)).to match(default_actions)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb b/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb
index 353f05a9dc..455a3fec23 100644
--- a/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb
+++ b/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb
@@ -9,7 +9,7 @@ module Admin
subject { builder.build }
it 'builds a valid menu' do
- expect(subject.items.count).to eq(12)
+ expect(subject.items.count).to eq(13)
expect(subject.items.map(&:key)).to include('dashboard')
expect(subject.items.map(&:key)).to include('orders')
expect(subject.items.map(&:key)).to include('settings')