diff --git a/Gemfile b/Gemfile index f36ddced18..e36e5cf274 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ gem 'paper_trail', '~> 5.2.3' gem 'diffy' gem 'skylight', '< 2.0' +gem 'combine_pdf' gem 'wicked_pdf' gem 'wkhtmltopdf-binary' diff --git a/Gemfile.lock b/Gemfile.lock index e4c65940d2..36c333e36d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -234,6 +234,8 @@ GEM execjs coffee-script-source (1.10.0) colorize (0.8.1) + combine_pdf (1.0.15) + ruby-rc4 (>= 0.1.5) compass (1.0.3) chunky_png (~> 1.2) compass-core (~> 1.0.2) @@ -677,6 +679,7 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-ole (1.2.12.1) ruby-progressbar (1.10.0) + ruby-rc4 (0.1.5) rubyzip (1.2.2) safe_yaml (1.0.4) sass (3.3.14) @@ -778,6 +781,7 @@ DEPENDENCIES capybara (>= 2.15.4) chromedriver-helper coffee-rails (~> 3.2.1) + combine_pdf compass-rails custom_error_message! daemons diff --git a/app/assets/javascripts/admin/orders/controllers/bulk_invoice_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/bulk_invoice_controller.js.coffee new file mode 100644 index 0000000000..0442564e2e --- /dev/null +++ b/app/assets/javascripts/admin/orders/controllers/bulk_invoice_controller.js.coffee @@ -0,0 +1,32 @@ +angular.module("admin.orders").controller "bulkInvoiceCtrl", ($scope, $http, $timeout) -> + $scope.createBulkInvoice = -> + $scope.invoice_id = null + $scope.poll = 1 + $scope.loading = true + $scope.message = null + $scope.error = null + $scope.poll_wait = 5 # 5 Seconds between each check + $scope.poll_retries = 80 # Maximum checks before stopping + + $http.post('/admin/orders/invoices', {order_ids: $scope.selected_orders}).success (data) -> + $scope.invoice_id = data + $scope.pollBulkInvoice() + + $scope.pollBulkInvoice = -> + $timeout($scope.nextPoll, $scope.poll_wait * 1000) + + $scope.nextPoll = -> + $http.get('/admin/orders/invoices/'+$scope.invoice_id+'/poll').success (data) -> + $scope.loading = false + $scope.message = t('js.admin.orders.index.bulk_invoice_created') + + .error (data) -> + $scope.poll++ + + if $scope.poll > $scope.poll_retries + $scope.loading = false + $scope.error = t('js.admin.orders.index.bulk_invoice_failed') + return + + $scope.pollBulkInvoice() + diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index ad5bfff97d..7adbba40c3 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, Orders, SortOptions) -> +angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, Orders, SortOptions, $window, $filter) -> $scope.RequestMonitor = RequestMonitor $scope.pagination = Orders.pagination $scope.orders = Orders.all @@ -8,6 +8,11 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, {id: 50, name: t('js.admin.orders.index.per_page', results: 50)}, {id: 100, name: t('js.admin.orders.index.per_page', results: 100)} ] + $scope.selected_orders = [] + $scope.checkboxes = {} + $scope.selected = false + $scope.select_all = false + $scope.poll = 0 $scope.initialise = -> $scope.per_page = 15 @@ -17,6 +22,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.fetchResults() $scope.fetchResults = (page=1) -> + $scope.resetSelected() Orders.index({ 'q[completed_at_lt]': $scope['q']['completed_at_lt'], 'q[completed_at_gt]': $scope['q']['completed_at_gt'], @@ -36,6 +42,26 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, page: page }) + $scope.resetSelected = -> + $scope.selected_orders.length = 0 + $scope.selected = false + $scope.select_all = false + $scope.checkboxes = {} + + $scope.toggleSelection = (id) -> + index = $scope.selected_orders.indexOf(id) + + if index == -1 + $scope.selected_orders.push(id) + else + $scope.selected_orders.splice(index, 1) + + $scope.toggleAll = -> + $scope.selected_orders.length = 0 + $scope.orders.forEach (order) -> + $scope.checkboxes[order.id] = $scope.select_all + $scope.selected_orders.push order.id if $scope.select_all + $scope.$watch 'sortOptions', (sort) -> if sort && sort.predicate != "" $scope.sorting = sort.predicate + ' desc' if sort.reverse diff --git a/app/assets/javascripts/admin/orders/directives/invoices_modal.js.coffee b/app/assets/javascripts/admin/orders/directives/invoices_modal.js.coffee new file mode 100644 index 0000000000..0f2eabe778 --- /dev/null +++ b/app/assets/javascripts/admin/orders/directives/invoices_modal.js.coffee @@ -0,0 +1,5 @@ +angular.module("admin.orders").directive "invoicesModal", ($modal) -> + restrict: 'C' + link: (scope, elem, attrs, ctrl) -> + elem.on "click", (ev) => + scope.uploadModal = $modal.open(templateUrl: 'admin/modals/bulk_invoice.html', controller: ctrl, scope: scope, windowClass: 'simple-modal') diff --git a/app/assets/javascripts/admin/orders/orders.js.coffee b/app/assets/javascripts/admin/orders/orders.js.coffee index 4e1ec754e3..1a8b267d30 100644 --- a/app/assets/javascripts/admin/orders/orders.js.coffee +++ b/app/assets/javascripts/admin/orders/orders.js.coffee @@ -1 +1 @@ -angular.module("admin.orders", ['admin.indexUtils', 'ngResource']) +angular.module("admin.orders", ['admin.indexUtils', 'ngResource', 'mm.foundation']) diff --git a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee index 526c14bf2a..69ca1731fd 100644 --- a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee +++ b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee @@ -2,5 +2,5 @@ angular.module("ofn.admin").directive "imageModal", ($modal, ProductImageService restrict: 'C' link: (scope, elem, attrs, ctrl) -> elem.on "click", (ev) => - scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'product-image-upload') + scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'simple-modal') ProductImageService.configure(scope.product) diff --git a/app/assets/javascripts/templates/admin/modals/bulk_invoice.html.haml b/app/assets/javascripts/templates/admin/modals/bulk_invoice.html.haml new file mode 100644 index 0000000000..16ddb39508 --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/bulk_invoice.html.haml @@ -0,0 +1,15 @@ +%h4.modal-title + = t('js.admin.orders.index.compiling_invoices') + +%p.message{ ng: { show: 'message' } } + {{message}} +%p.error{ ng: { show: 'error' } } + {{error}} + +%img.spinner{ src: "/assets/spinning-circles.svg", ng: { show: "loading" } } +%p{ ng: { show: "loading" } } + = t('js.admin.orders.index.please_wait') + +%a.button{ target: '_blank', ng: { click: 'showBulkInvoice()', href: '/admin/orders/invoices/{{invoice_id}}', show: "!loading && !error" } } + = t('js.admin.orders.index.view_file') + diff --git a/app/assets/stylesheets/admin/all.scss b/app/assets/stylesheets/admin/all.scss index 637f0b5d2c..4a16154bf4 100644 --- a/app/assets/stylesheets/admin/all.scss +++ b/app/assets/stylesheets/admin/all.scss @@ -17,4 +17,3 @@ @import 'variables'; @import 'components/*'; @import '*'; -@import 'pages/*'; diff --git a/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss b/app/assets/stylesheets/admin/components/simple_modal.css.scss similarity index 87% rename from app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss rename to app/assets/stylesheets/admin/components/simple_modal.css.scss index 4dffade73e..453fe6da4f 100644 --- a/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss +++ b/app/assets/stylesheets/admin/components/simple_modal.css.scss @@ -1,6 +1,6 @@ @import '../variables'; -.reveal-modal.product-image-upload { +.reveal-modal.simple-modal { width: 300px; .close-reveal-modal { diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 0955d9fd46..4858c0685a 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -103,3 +103,34 @@ table.index td.actions { font-size: 1.2em; } } + +.index-controls { + + button { + float: right; + + &:disabled { + background-color: $disabled-button; + } + } +} + +.simple-modal { + text-align: center; + + .modal-title { + margin-bottom: 1.5em; + } + + .message, .error { + margin-bottom: 1em; + } + + .spinner { + margin-bottom: 1em; + } + + .error { + color: $warning-red; + } +} diff --git a/app/controllers/spree/admin/invoices_controller.rb b/app/controllers/spree/admin/invoices_controller.rb new file mode 100644 index 0000000000..230d01322b --- /dev/null +++ b/app/controllers/spree/admin/invoices_controller.rb @@ -0,0 +1,31 @@ +module Spree + module Admin + class InvoicesController < Spree::Admin::BaseController + respond_to :json + + def create + invoice_service = BulkInvoiceService.new + invoice_service.start_pdf_job(params[:order_ids]) + + render json: invoice_service.id, status: :ok + end + + def show + invoice_id = params[:id] + invoice_pdf = BulkInvoiceService.new.filepath(invoice_id) + + send_file(invoice_pdf, type: 'application/pdf', disposition: :inline) + end + + def poll + invoice_id = params[:invoice_id] + + if BulkInvoiceService.new.invoice_created? invoice_id + render json: { created: true }, status: :ok + else + render json: { created: false }, status: :unprocessable_entity + end + end + end + end +end diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index f7e1b56929..84e66451ba 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -37,8 +37,10 @@ Spree::Admin::OrdersController.class_eval do end def invoice - template = if Spree::Config.invoice_style2? then "spree/admin/orders/invoice2" else "spree/admin/orders/invoice" end - pdf = render_to_string pdf: "invoice-#{@order.number}.pdf", template: template, formats: [:html], encoding: "UTF-8" + pdf = render_to_string pdf: "invoice-#{@order.number}.pdf", + template: invoice_template, + formats: [:html], encoding: "UTF-8" + Spree::OrderMailer.invoice_email(@order.id, pdf).deliver flash[:success] = t('admin.orders.invoice_email_sent') @@ -46,8 +48,7 @@ Spree::Admin::OrdersController.class_eval do end def print - template = if Spree::Config.invoice_style2? then "spree/admin/orders/invoice2" else "spree/admin/orders/invoice" end - render pdf: "invoice-#{@order.number}", template: template, encoding: "UTF-8" + render pdf: "invoice-#{@order.number}", template: invoice_template, encoding: "UTF-8" end def print_ticket @@ -60,6 +61,10 @@ Spree::Admin::OrdersController.class_eval do private + def invoice_template + Spree::Config.invoice_style2? ? "spree/admin/orders/invoice2" : "spree/admin/orders/invoice" + end + def require_distributor_abn unless @order.distributor.abn.present? flash[:error] = t(:must_have_valid_business_number, enterprise_name: @order.distributor.name) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 9d0941a220..2c68470840 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -210,7 +210,9 @@ class AbilityDecorator # during the order creation process from the admin backend order.distributor.nil? || user.enterprises.include?(order.distributor) || order.order_cycle.andand.coordinated_by?(user) end - can [:admin, :bulk_management, :managed], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor) + can [:admin, :bulk_management, :managed, :bulk_invoice], Spree::Order do + user.admin? || user.enterprises.any?(&:is_distributor) + end can [:admin, :visible], Enterprise can [:admin, :index, :create, :update, :destroy], :line_item can [:admin, :index, :create], Spree::LineItem diff --git a/app/services/bulk_invoice_service.rb b/app/services/bulk_invoice_service.rb new file mode 100644 index 0000000000..397020635e --- /dev/null +++ b/app/services/bulk_invoice_service.rb @@ -0,0 +1,56 @@ +class BulkInvoiceService + include WickedPdf::PdfHelper + attr_reader :id + + def initialize + @id = new_invoice_id + end + + def start_pdf_job(order_ids) + pdf = CombinePDF.new + orders = Spree::Order.where(id: order_ids) + + orders.each do |order| + invoice = renderer.render_to_string pdf: "invoice-#{order.number}.pdf", + template: invoice_template, + formats: [:html], encoding: "UTF-8", + locals: { :@order => order } + + pdf << CombinePDF.parse(invoice) + end + + pdf.save "#{file_directory}/#{@id}.pdf" + end + handle_asynchronously :start_pdf_job + + def invoice_created?(invoice_id) + File.exist? filepath(invoice_id) + end + + def filepath(invoice_id) + "#{directory}/#{invoice_id}.pdf" + end + + private + + def new_invoice_id + Time.zone.now.to_i.to_s + end + + def directory + 'tmp/invoices' + end + + def renderer + ApplicationController.new + end + + def invoice_template + Spree::Config.invoice_style2? ? "spree/admin/orders/invoice2" : "spree/admin/orders/invoice" + end + + def file_directory + Dir.mkdir(directory) unless File.exist?(directory) + directory + end +end diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 3c0efc3793..5d98bc4a22 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -16,14 +16,19 @@ - content_for :table_filter do = render partial: 'filters' -.row +.row.index-controls{'ng-show' => '!RequestMonitor.loading && orders.length > 0'} = render partial: 'per_page_controls' + %button.invoices-modal{'ng-controller' => 'bulkInvoiceCtrl', 'ng-click' => 'createBulkInvoice()', 'ng-disabled' => 'selected_orders.length == 0'} + = t('.print_invoices') + %table#listing_orders.index.responsive{width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } %colgroup - %col{style: "width: 10%"} + %col{style: "width: 3%"} %thead %tr + %th + %input{type: 'checkbox', 'ng-click' => 'toggleAll()', 'ng-model' => 'select_all'} %th = t(:products_distributor) %th @@ -39,6 +44,8 @@ %th.actions %tbody %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'"} + %td.align-center + %input{type: 'checkbox', 'ng-model' => 'checkboxes[order.id]', 'ng-change' => 'toggleSelection(order.id)'} %td.align-center {{order.distributor_name}} %td.align-center diff --git a/app/views/spree/admin/orders/invoice.html.haml b/app/views/spree/admin/orders/invoice.html.haml index 0982ccc21f..982ef43f59 100644 --- a/app/views/spree/admin/orders/invoice.html.haml +++ b/app/views/spree/admin/orders/invoice.html.haml @@ -30,13 +30,14 @@   %td{ :align => "right" } %strong= "#{t('.to')}: #{@order.ship_address.full_name}" - - if @order.customer.code.present? + - if @order.andand.customer.andand.code.present? %br = "#{t('.code')}: #{@order.customer.code}" %br = @order.ship_address.full_address %br - = "#{@order.customer.email}," + - if @order.andand.customer.andand.email.present? + = "#{@order.customer.email}," = "#{@order.bill_address.phone}" = render 'spree/admin/orders/invoice_table' diff --git a/app/views/spree/admin/orders/invoice2.html.haml b/app/views/spree/admin/orders/invoice2.html.haml index 89f28ab308..837b2be21a 100644 --- a/app/views/spree/admin/orders/invoice2.html.haml +++ b/app/views/spree/admin/orders/invoice2.html.haml @@ -45,7 +45,7 @@ = t :invoice_billing_address %br %strong= @order.ship_address.full_name - - if @order.customer.code.present? + - if @order.andand.customer.andand.code.present? %br = "Code: #{@order.customer.code}" %br diff --git a/config/locales/en.yml b/config/locales/en.yml index 54b47bc620..0bde4fb005 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2593,6 +2593,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using orders: index: per_page: "%{results} per page" + view_file: View File + compiling_invoices: Compiling Invoices + bulk_invoice_created: Bulk Invoice created + bulk_invoice_failed: Failed to create Bulk Invoice + please_wait: Please wait until the PDF is ready before closing this modal. resend_user_email_confirmation: resend: "Resend" sending: "Resend..." @@ -2698,6 +2703,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using no_orders_found: "No Orders Found" results_found: "%{number} Results found." viewing: "Viewing %{start} to %{end}." + print_invoices: "Print Invoices" invoice: issued_on: Issued on tax_invoice: TAX INVOICE diff --git a/config/routes/spree.rb b/config/routes/spree.rb index a203ab0fb0..2e9dc0b5c8 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -74,6 +74,12 @@ Spree::Core::Engine.routes.prepend do get :print, on: :member get :print_ticket, on: :member get :managed, on: :collection + + collection do + resources :invoices, only: [:create, :show] do + get :poll + end + end end end diff --git a/spec/controllers/spree/admin/invoices_controller_spec.rb b/spec/controllers/spree/admin/invoices_controller_spec.rb new file mode 100644 index 0000000000..1eff65ba2c --- /dev/null +++ b/spec/controllers/spree/admin/invoices_controller_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Spree::Admin::InvoicesController, type: :controller do + let(:order) { create(:order_with_totals_and_distribution) } + let(:user) { create(:admin_user) } + + before do + allow(controller).to receive(:spree_current_user) { user } + end + + describe "#create" do + it "enqueues a job to create a bulk invoice and returns the filename" do + expect do + spree_post :create, order_ids: [order.id] + end.to enqueue_job Delayed::PerformableMethod + + expect(Delayed::Job.last.payload_object.method_name).to eq :start_pdf_job_without_delay + end + end + + describe "#poll" do + let(:invoice_id) { '479186263' } + + context "when the file is available" do + it "returns true" do + allow(File).to receive(:exist?).and_return(true) + spree_get :poll, invoice_id: invoice_id + + expect(response.body).to eq({ created: true }.to_json) + expect(response.status).to eq 200 + end + end + + context "when the file is not available" do + it "returns false" do + spree_get :poll, invoice_id: invoice_id + + expect(response.body).to eq({ created: false }.to_json) + expect(response.status).to eq 422 + end + end + end +end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index eb2e4434f3..96e3792163 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -751,9 +751,9 @@ feature %q{ end # Shows upload modal - expect(page).to have_selector "div.reveal-modal.product-image-upload" + expect(page).to have_selector "div.reveal-modal" - within "div.reveal-modal.product-image-upload" do + within "div.reveal-modal" do # Shows preview of current image expect(page).to have_css "img.preview" @@ -765,7 +765,7 @@ feature %q{ expect(page).to have_no_css "img.spinner", visible: true end - expect(page).to have_no_selector "div.reveal-modal.product-image-upload" + expect(page).to have_no_selector "div.reveal-modal" within "table#listing_products tr#p_#{product.id}" do # New thumbnail is shown in image column @@ -775,7 +775,7 @@ feature %q{ page.find("a.image-modal").click end - expect(page).to have_selector "div.reveal-modal.product-image-upload" + expect(page).to have_selector "div.reveal-modal" end end end diff --git a/spec/services/bulk_invoice_service_spec.rb b/spec/services/bulk_invoice_service_spec.rb new file mode 100644 index 0000000000..4611a46846 --- /dev/null +++ b/spec/services/bulk_invoice_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe BulkInvoiceService do + let(:service) { BulkInvoiceService.new } + + describe "#start_pdf_job" do + it "starts a background process to create a pdf with multiple invoices" do + expect do + service.start_pdf_job [1, 2] + end.to enqueue_job Delayed::PerformableMethod + + expect(Delayed::Job.last.payload_object.method_name).to eq :start_pdf_job_without_delay + end + end + + describe "#invoice_created?" do + context "when the invoice has been created" do + it "returns true" do + allow(File).to receive(:exist?).and_return(true) + + created = service.invoice_created? '45891723' + expect(created).to be_truthy + end + end + + context "when the invoice has not been created" do + it "returns false" do + created = service.invoice_created? '1234567' + expect(created).to_not be_truthy + end + end + end + + describe "#filepath" do + it "returns the filepath of a given invoice" do + filepath = service.filepath '1234567' + expect(filepath).to eq 'tmp/invoices/1234567.pdf' + end + end +end