Merge pull request #2869 from Matt-Yorkley/bi/bulk_invoices

[Bulk Invoice Printing] Bulk invoices
This commit is contained in:
Pau Pérez Fabregat
2019-01-18 17:49:34 +01:00
committed by GitHub
23 changed files with 329 additions and 19 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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')

View File

@@ -1 +1 @@
angular.module("admin.orders", ['admin.indexUtils', 'ngResource'])
angular.module("admin.orders", ['admin.indexUtils', 'ngResource', 'mm.foundation'])

View File

@@ -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)

View File

@@ -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')

View File

@@ -17,4 +17,3 @@
@import 'variables';
@import 'components/*';
@import '*';
@import 'pages/*';

View File

@@ -1,6 +1,6 @@
@import '../variables';
.reveal-modal.product-image-upload {
.reveal-modal.simple-modal {
width: 300px;
.close-reveal-modal {

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -30,13 +30,14 @@
&nbsp;
%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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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