Skip to content

Commit 43fac0b

Browse files
anna-devKagemaru
authored andcommitted
Fix expenses export with multi page pdf (64085)
1 parent 070f12d commit 43fac0b

File tree

4 files changed

+102
-96
lines changed

4 files changed

+102
-96
lines changed

app/assets/javascripts/expenses.js.coffee

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@ $(document).on 'ready, turbolinks:load', ->
1313
toggle_project_display = () ->
1414

1515
if expense_kind_input.val() == 'project'
16-
# if order_input.value and not order_selectized_input.value
17-
# input[0].value = ' '
18-
1916
order_form_group.show()
2017
order_selectized_input.attr('disabled', false)
2118
else
2219
order_form_group.hide()
2320
order_selectized_input.attr('disabled', true)
2421

25-
check_file_type = (initial = false) ->
22+
check_file_type = () ->
2623
warning_popup.addClass('hidden')
2724

2825
return unless (typeof receipt_input != "undefined" && receipt_input != null) && receipt_input[0].files.length > 0
@@ -32,7 +29,7 @@ $(document).on 'ready, turbolinks:load', ->
3229
unless /^image/.test(file_type) or file_type == 'application/pdf'
3330
warning_popup.removeClass('hidden')
3431

35-
check_file_type(true)
32+
check_file_type()
3633
toggle_project_display()
3734

3835
expense_kind_input.change (e) ->

app/controllers/expenses_controller.rb

Lines changed: 26 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# frozen_string_literal: true
22

33
class ExpensesController < ManageController
4+
require 'ruby-vips'
45
include Filterable
6+
57
self.optional_nesting = [Employee]
68

79
self.permitted_attrs = %i[payment_date employee_id kind order_id description amount receipt]
@@ -117,69 +119,39 @@ def receipt_param
117119
params.dig(model_identifier, :receipt)
118120
end
119121

120-
def pdf_pages_amount(filepath_pdf)
121-
first_page = Vips::Image.new_from_file(filepath_pdf, page: 0, n: 1)
122-
first_page.get('n-pages')
123-
end
122+
def attach_resized_receipt
123+
return unless receipt_param
124124

125-
def get_pdf_pages_as_images(filepath_pdf)
126-
images = []
127-
total_pages = pdf_pages_amount(filepath_pdf)
128-
(0...total_pages).each do |page_num|
129-
image = Vips::Image.new_from_file(filepath_pdf, page: page_num)
130-
image = image.thumbnail_image(Settings.expenses.receipt.max_pixel)
131-
images << image
125+
case receipt_param.content_type
126+
when 'application/pdf' then attach_pdf
127+
when /image/ then attach_image
132128
end
133-
images
134129
end
135130

136-
def combine_images(images, rows, columns)
137-
rows_of_images = []
138-
(0...rows).each do |row_num|
139-
start_index = row_num * columns
140-
row_images = images[start_index, columns] || []
141-
# Ensure all rows are the same length by padding with blank images (if necessary)
142-
row_images += [Vips::Image.black(images.first.width, images.first.height)] * (columns - row_images.size)
131+
def attach_image
132+
resized = ::ImageProcessing::Vips
133+
.source(receipt_param.tempfile)
134+
.resize_to_limit(Settings.expenses.receipt.max_pixel, Settings.expenses.receipt.max_pixel)
135+
.saver(quality: Settings.expenses.receipt.quality)
136+
.convert('jpg')
137+
.loader(page: 0)
138+
.call
143139

144-
row_image = row_images.reduce { |a, b| a.join(b, :horizontal) }
145-
rows_of_images << row_image
146-
end
140+
target_filename = "#{File.basename(receipt_param.original_filename.to_s, '.*')}.jpg"
147141

148-
rows_of_images.reduce { |a, b| a.join(b, :vertical) }
142+
entry.receipt.attach(io: File.open(resized), filename: target_filename, content_type: 'image/jpeg')
149143
end
150144

151-
def attach_resized_receipt
152-
return unless receipt_param
153-
154-
if receipt_param.content_type == 'application/pdf'
155-
pdf_path = receipt_param.tempfile.path
156-
images = get_pdf_pages_as_images(pdf_path)
157-
basename = File.basename(receipt_param.original_filename.to_s, '.*')
145+
def attach_pdf
146+
tmp_file = receipt_param.tempfile
147+
filename = "#{File.basename(receipt_param.original_filename.to_s, '.*')}.pdf"
148+
page_count = pdf_pages_amount(tmp_file.path)
158149

159-
# Calculate the number of rows and columns to fit all images in a square grid
160-
total_pages = pdf_pages_amount(pdf_path)
161-
grid_size = Math.sqrt(total_pages).ceil
162-
rows = grid_size
163-
columns = (total_pages.to_f / grid_size).ceil
164-
165-
combined_image = combine_images(images, rows, columns)
166-
167-
output_path = Rails.root.join('tmp', "#{basename}.jpg")
168-
combined_image.write_to_file(output_path.to_s, Q: Settings.expenses.receipt.quality)
169-
170-
entry.receipt.attach(io: File.open(output_path), filename: "#{basename}.jpg", content_type: 'image/jpeg')
171-
else
172-
resized = ImageProcessing::Vips
173-
.source(receipt_param.tempfile)
174-
.resize_to_limit(Settings.expenses.receipt.max_pixel, Settings.expenses.receipt.max_pixel)
175-
.saver(quality: Settings.expenses.receipt.quality)
176-
.convert('jpg')
177-
.loader(page: 0)
178-
.call
179-
180-
target_filename = "#{File.basename(receipt_param.original_filename.to_s, '.*')}.jpg"
150+
entry.receipt.attach(io: File.open(tmp_file), filename:, content_type: 'application/pdf', metadata: { page_count: })
151+
end
181152

182-
entry.receipt.attach(io: File.open(resized), filename: target_filename, content_type: 'image/jpeg')
183-
end
153+
def pdf_pages_amount(filepath_pdf)
154+
first_page = ::Vips::Image.new_from_file(filepath_pdf, page: 0, n: 1)
155+
first_page.get('n-pages')
184156
end
185157
end

app/domain/expenses/pdf_export.rb

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,32 @@ def expenses
3232
def build
3333
validate
3434
setup_fonts
35-
expenses.each_with_index do |e, i|
36-
@expense = e
35+
pages.each_with_index do |e, i|
36+
@expense = e[:expense]
3737
pdf.start_new_page unless i.zero?
38-
add_header
39-
add_receipt
38+
add_header if e[:header]
39+
add_receipt(e[:page])
4040
reset_model_data
4141
end
4242
pdf.number_pages('Seite <page>/<total>', at: [pdf.bounds.right - 60, pdf.bounds.bottom + 5])
4343
end
4444

45+
def pages
46+
expenses.flat_map do |exp|
47+
if exp.receipt.content_type == 'application/pdf' && page_count(exp) > 1
48+
(1..exp.receipt.metadata[:page_count]).to_a.flat_map do |page_num|
49+
{ header: page_num == 1, expense: exp, page: page_num }
50+
end
51+
else
52+
{ header: true, expense: exp, page: 1 }
53+
end
54+
end
55+
end
56+
57+
def page_count(exp)
58+
exp.receipt.metadata[:page_count]
59+
end
60+
4561
def generate
4662
build
4763
pdf.render_file FILENAME
@@ -157,7 +173,7 @@ def attribute(title, value)
157173
end
158174

159175
def receipt_printable?
160-
receipt.attached? && receipt.image? # Currently, previewables will not be handled
176+
receipt.attached? && (receipt.image? || receipt.content_type == 'application/pdf') # Currently, previewables will not be handled
161177
end
162178

163179
def receipt
@@ -171,23 +187,45 @@ def receipt_text
171187
'Es wurde kein Beleg beigelegt.'
172188
end
173189

174-
def add_receipt
190+
def add_receipt(page)
175191
return unless receipt_printable?
176192

193+
case receipt.content_type
194+
when 'application/pdf' then add_pdf_receipt(page - 1)
195+
when /image/ then add_image_receipt
196+
end
197+
end
198+
199+
def add_image_receipt
177200
blob.open do |file|
178201
# Vips auto rotates by default
179202
image = ::Vips::Image.new_from_file(file.path)
180-
rotated = ImageProcessing::Vips.source(image)
203+
rotated = ::ImageProcessing::Vips.source(image)
181204
rotated.write_to_file(file.path)
182205
pdf.image file.path, position: :center, fit: [image_width, image_height]
183206
end
184207
rescue StandardError => e
185-
add_text "Error while adding picture for expense #{@expense.id}"
186-
add_text "Message: #{e.message}"
208+
add_receipt_adding_errors(e, 'image')
209+
end
210+
211+
def add_pdf_receipt(page_num)
212+
blob.open do |file|
213+
pdf_image = ::Vips::Image.new_from_file(file.path, page: page_num)
214+
image = pdf_image.thumbnail_image(Settings.expenses.receipt.max_pixel)
215+
source = ::ImageProcessing::Vips.source(image)
216+
pdf.image source.call, position: :center, fit: [image_width, image_height]
217+
end
218+
rescue StandardError => e
219+
add_receipt_adding_errors(e, 'pdf')
220+
end
221+
222+
def add_receipt_adding_errors(error, file_type)
223+
add_text "Error while adding #{file_type} for expense #{@expense.id}"
224+
add_text "Message: #{error.message}"
187225

188-
Rails.logger.info "Error while adding picture for expense #{@expense.id}"
189-
Rails.logger.info "Message: #{e.message}"
190-
Rails.logger.info "Backtrace: #{e.backtrace.inspect}"
226+
Rails.logger.info "Error while adding #{file_type} for expense #{@expense.id}"
227+
Rails.logger.info "Message: #{error.message}"
228+
Rails.logger.info "Backtrace: #{error.backtrace.inspect}"
191229
end
192230

193231
def blob

config/locales/views.de-CH.yml

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,46 @@ de-CH:
88
version:
99
model:
1010
create:
11-
employee: "Der Member #%{id} wurde erstellt."
12-
employment: "Die Anstellung #%{id} wurde erstellt."
13-
employmentrolesemployment: "Der Funktionsanteil \"%{role}\" der Anstellung #%{employment_id} wurde erstellt."
11+
employee: 'Der Member #%{id} wurde erstellt.'
12+
employment: 'Die Anstellung #%{id} wurde erstellt.'
13+
employmentrolesemployment: 'Der Funktionsanteil "%{role}" der Anstellung #%{employment_id} wurde erstellt.'
1414
update:
15-
employee: "Der Member #%{id} wurde bearbeitet."
16-
employment: "Die Anstellung #%{id} wurde bearbeitet."
17-
employmentrolesemployment: "Der Funktionsanteil \"%{role}\" der Anstellung #%{employment_id} wurde bearbeitet."
15+
employee: 'Der Member #%{id} wurde bearbeitet.'
16+
employment: 'Die Anstellung #%{id} wurde bearbeitet.'
17+
employmentrolesemployment: 'Der Funktionsanteil "%{role}" der Anstellung #%{employment_id} wurde bearbeitet.'
1818
destroy:
19-
employee: "Der Member #%{id} wurde gelöscht."
20-
employment: "Die Anstellung #%{id} wurde gelöscht."
21-
employmentrolesemployment: "Der Funktionsanteil \"%{role}\" der Anstellung #%{employment_id} wurde gelöscht."
19+
employee: 'Der Member #%{id} wurde gelöscht.'
20+
employment: 'Die Anstellung #%{id} wurde gelöscht.'
21+
employmentrolesemployment: 'Der Funktionsanteil "%{role}" der Anstellung #%{employment_id} wurde gelöscht.'
2222
model_reference:
23-
employee: "des Members"
24-
employment: "der Anstellung"
25-
employmentrolesemployment: "des Funktionsanteils"
23+
employee: 'des Members'
24+
employment: 'der Anstellung'
25+
employmentrolesemployment: 'des Funktionsanteils'
2626
attribute_change:
27-
from_to: "%{attr} %{model_ref} wurde von «%{from}» auf «%{to}» geändert."
28-
from: "%{attr} %{model_ref} «%{from}» wurde gelöscht."
29-
to: "%{attr} %{model_ref} wurde auf «%{to}» gesetzt."
27+
from_to: '%{attr} %{model_ref} wurde von «%{from}» auf «%{to}» geändert.'
28+
from: '%{attr} %{model_ref} «%{from}» wurde gelöscht.'
29+
to: '%{attr} %{model_ref} wurde auf «%{to}» gesetzt.'
3030

3131
expenses: &expenses
3232
new:
33-
title: "%{model} erfassen"
33+
title: '%{model} erfassen'
3434
create:
35-
title: "%{model} erfassen"
35+
title: '%{model} erfassen'
3636
flash:
37-
success: "%{model} wurden erfolgreich erfasst."
37+
success: '%{model} wurden erfolgreich erfasst.'
3838
update:
3939
flash:
40-
success: "%{model} wurden erfolgreich aktualisiert."
40+
success: '%{model} wurden erfolgreich aktualisiert.'
4141
destroy:
4242
flash:
43-
success: "%{model} wurden erfolgreich gelöscht."
44-
failure: "%{model} konnten nicht gelöscht werden."
43+
success: '%{model} wurden erfolgreich gelöscht.'
44+
failure: '%{model} konnten nicht gelöscht werden.'
4545
attachment:
46-
name: "Anhang"
47-
show: "Anzeigen"
48-
hint: "Der Beleg muss ein Bild und gut lesbar sein und den Kaufpreis, das Kaufdatum und die MWST-Nummer enthalten."
46+
name: 'Anhang'
47+
show: 'Anzeigen'
48+
hint: 'Der Beleg muss ein gut lesbares Bild oder PDF sein und den Kaufpreis, das Kaufdatum und die MWST-Nummer enthalten.'
4949
global:
5050
link:
5151
add: Erfassen
5252

5353
expenses_reviews: *expenses
54-
...

0 commit comments

Comments
 (0)