Class: Quote::CombinedPdfGenerator

Inherits:
BaseService show all
Defined in:
app/services/quote/combined_pdf_generator.rb

Overview

Service object: combined pdf generator.

Defined Under Namespace

Classes: Result

Constant Summary collapse

TZ_COMPARISON_PDF_SKUS =

Tz comparison pdf skus.

%w[TEMPZONE-FLOOR-HEATING-COMPARISON-CHART-USA TEMPZONE-FLOOR-HEATING-COMPARISON-CHART-CANADA].freeze
CUSTOM_AGREEMENT_PDF_SKU =

Custom agreement pdf sku.

'CUSTOM-PRODUCT-ORDER-AGREEMENT-FORM'
CHECK_LIST_FH_SKU =

Check list fh sku.

'PRO-ELECTRICIAN-FLOOR-HEATING-CHECKLIST'
CHECK_LIST_SM_SKU =

Check list sm sku.

'SNOW-MELT-CHECKLIST-MATERIALS-AND-TOOLS-CHECKLIST'
VERSION_SUFFIX =

Keys ending in this suffix carry per-option style versions ("v1"/"v2") set
from the select_pdf_template form. They control which generator class
(legacy vs modern) renders each section.

'_version'
LETTER_SECTION_VERSION_KEYS =

Letter-section options. Any of these set to v2 triggers the modern letter
generator for the whole letter (so a single letter never mixes v1 + v2).

%i[
  show_room_summary_version show_line_items_version show_discount_breakdown_version
  show_suggested_services_version show_suggested_add_ons_version
  show_ordering_information_version proforma_invoice_version condensed_text_version
].freeze

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger

Constructor Details

#initialize(options = {}) ⇒ CombinedPdfGenerator

Returns a new instance of CombinedPdfGenerator.

Parameters:

  • options (Hash) (defaults to: {})

    configurable options

Options Hash (options):

  • quote_letter_generator (Object, nil)

    injected letter generator (used by tests/callers to fake it out); when nil the generator is picked by version



52
53
54
# File 'app/services/quote/combined_pdf_generator.rb', line 52

def initialize(options = {})
  @injected_letter_generator = options[:quote_letter_generator]
end

Instance Method Details

#build_letter_generator(version) ⇒ Object

Internal: returns the letter generator instance to use for this run.
An injected generator (used by tests / callers that want to fake it out)
always wins; otherwise pick by version.



22
23
24
25
26
# File 'app/services/quote/combined_pdf_generator.rb', line 22

def build_letter_generator(version)
  return @injected_letter_generator if @injected_letter_generator

  version == 'v2' ? Quote::LetterPdfGenerator.new : Quote::LetterPdfGeneratorLegacy.new
end

#process(quote, options = nil) {|current_step, total_steps, "Building quote letter for #{quote.reference_number}"| ... } ⇒ Result

Returns the generated PDF data/file paths, file name, options used, and additional attachment ids.

Parameters:

  • quote (Quote)

    the quote to generate the combined PDF for

  • options (Hash, nil) (defaults to: nil)

    generation options (defaults to #default_pdf_options); keys ending in _version select the per-section style version ('v1'/'v2')

Options Hash (options):

  • output_to_file (Boolean)

    write the PDF to a file instead of streaming (defaults to true)

  • output_to_file_path (String, nil)

    explicit output path when output_to_file is set

  • show_line_items (Boolean)

    include the quote letter line items section (defaults to true)

  • show_room_summary (Boolean)

    include the room summary section

  • show_suggested_services (Boolean)

    include suggested services

  • show_suggested_add_ons (Boolean)

    include suggested add-ons

  • show_ordering_information (Boolean)

    include ordering information

  • show_discount_breakdown (Boolean)

    include the discount breakdown

  • condensed_text (Boolean)

    use condensed letter text

  • show_installation_plans (Boolean)

    build and attach installation plans (defaults to true)

  • show_electrical_plans (Boolean)

    build and attach electrical plans (defaults to true)

  • show_electrical_install_plans (Boolean)

    include electrical install plans

  • copy_of_plans (Boolean)

    also produce a separate copy-of-plans PDF

  • regenerate_plans (Boolean)

    force-rebuild every room plan from current data instead of serving the cached upload

  • materials_only (Boolean)

    generate only the materials list document

  • include_custom_order_agreement (Boolean)

    attach the custom product order agreement form

  • include_tempzone_comparison (Boolean)

    attach the TempZone comparison chart

  • include_addendums (Boolean)

    attach the quote addendums (defaults to true)

  • include_last_heat_loss (Boolean)

    attach the last heat loss report per room

  • check_list_fh (Boolean)

    attach the floor heating checklist

  • check_list_sm (Boolean)

    attach the snow melting checklist

  • letter_version (String)

    letter generator version ('v1'/'v2'); defaults to 'v2' when any letter-section *_version key is 'v2'

  • materials_only_version (String)

    materials document version (defaults to 'v2')

  • show_installation_plans_version (String)

    installation plan version (defaults to 'v2')

  • show_electrical_plans_version (String)

    electrical plan version (defaults to 'v2')

Yields:

  • (current_step, total_steps, "Building quote letter for #{quote.reference_number}")

Returns:

  • (Result)

    the generated PDF data/file paths, file name, options used, and additional attachment ids



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'app/services/quote/combined_pdf_generator.rb', line 84

def process(quote, options = nil)
  options ||= default_pdf_options
  options = options.deep_symbolize_keys if options
  options[:output_to_file] = true if options[:output_to_file].nil?
  options[:show_line_items] = true if options[:show_line_items].nil?
  options[:show_installation_plans] = true if options[:show_installation_plans].nil?
  options[:show_electrical_plans] = true if options[:show_electrical_plans].nil?

  # Resolve per-option versions. Any v2 in a letter-section toggle promotes the
  # whole letter to v2 so the document looks like a single style.
  options[:letter_version] ||= LETTER_SECTION_VERSION_KEYS.any? { |k| options[k] == 'v2' } ? 'v2' : 'v2'
  options[:materials_only_version]      ||= 'v2'
  options[:show_installation_plans_version] ||= 'v2'
  options[:show_electrical_plans_version]   ||= 'v2'

  all_suggested_items = options[:show_suggested_add_ons] ? quote.all_suggested_items : {}

  build_plans = options[:show_installation_plans] || options[:show_electrical_plans] || options[:copy_of_plans]
  # Non-default: force-rebuild every room plan from current data instead of
  # serving the cached upload. Used after a plan-template change.
  regenerate_plans = options[:regenerate_plans].to_b

  total_steps = 1
  if build_plans
    number_of_rooms = quote.room_configurations.count
    total_steps += number_of_rooms
  end
  current_step = 1
  yield(current_step, total_steps, "Building quote letter for #{quote.reference_number}") if block_given?

  additional_attachments = []
  combined_pdf = PdfCombinator.new
  copy_of_plans_pdf = PdfCombinator.new
  combined_pdf_files = []
  copy_of_plans_pdf_files = []
  Dir.mktmpdir do |dir|
    if options[:materials_only].to_b
      materials_klass = options[:materials_only_version] == 'v2' ? Pdf::Document::LetterMaterials : Pdf::Document::LetterMaterialsLegacy
      materials_pdf_blob = materials_klass.new(quote, options).call.pdf
      materials_file_path = "#{dir}/quote_materials.pdf"
      File.open(materials_file_path, 'wb') do |f|
        f.write materials_pdf_blob
        f.flush
        f.fsync
      end
      combined_pdf_files << materials_file_path
    elsif options[:show_line_items] || options[:show_room_summary]
      options[:suggested_goods] = all_suggested_items[:goods]
      options[:suggested_services] = all_suggested_items[:services]
      letter_generator = build_letter_generator(options[:letter_version])
      quote_pdf_blob = letter_generator.process(quote, options).pdf
      letter_file_path = "#{dir}/quote_letter.pdf"
      File.open(letter_file_path, 'wb') do |f|
        f.write quote_pdf_blob
        f.flush
        f.fsync
      end
      combined_pdf_files << letter_file_path
    end

    if options[:include_custom_order_agreement] && (pub = Item.find_publication(CUSTOM_AGREEMENT_PDF_SKU, quote.store))
      total_steps += 1
      current_step += 1
      yield(current_step, total_steps, "Attaching customer order agreement to #{quote.reference_number}") if block_given?
      additional_attachments << pub.literature.id
    end

    if options[:check_list_fh].to_b && (pub = Item.find_publication(CHECK_LIST_FH_SKU, quote.store))
      total_steps += 1
      current_step += 1
      yield(current_step, total_steps, "Attaching checklist floor heating to #{quote.reference_number}") if block_given?
      additional_attachments << pub.literature.id
    end

    if options[:check_list_sm].to_b && (pub = Item.find_publication(CHECK_LIST_SM_SKU, quote.store))
      total_steps += 1
      current_step += 1
      yield(current_step, total_steps, "Attaching checklist snow melting to #{quote.reference_number}") if block_given?
      additional_attachments << pub.literature.id
    end

    if options[:include_addendums] && quote.addendums.present?
      total_steps += quote.addendums.size
      quote.addendums.each do |p|
        current_step += 1
        yield(current_step, total_steps, "Attaching Quote Addendum #{p.id} to #{quote.reference_number}") if block_given?
        additional_attachments << p.id
      end
    end

    if build_plans
      # build plans for each room
      room_configurations = quote.room_configurations.order(:name)
      total_steps += room_configurations.size

      Rails.logger.debug { "master_rooms: room_configurations.map{|rc| rc.controlled_by}.compact.uniq.map{|r| r.id} #{room_configurations.filter_map(&:controlled_by).uniq.map(&:id).join(', ')}" }
      if (master_rooms = room_configurations.filter_map(&:controlled_by).compact.uniq).present?
        master_rooms.each_with_index do |rc, _i|
          current_step += 1
          Rails.logger.debug { "Building plans for room #{rc.reference_number}" }
          yield(current_step, total_steps, "Building plans for room #{rc.reference_number}") if block_given?

          next unless options[:show_installation_plans]

          multizone_plan = rc.get_or_generate_multizone_plan_pdf(version: options[:show_installation_plans_version], force: regenerate_plans)
          next unless multizone_plan&.attachment

          yield(current_step, total_steps, "Pulling multizone plan for room #{rc.reference_number}") if block_given?
          Rails.logger.debug { "Pulling multizone plan for room #{rc.reference_number}" }
          multizone_plan_file_path = "#{dir}/multizone_plan_#{rc.reference_number}.pdf"
          multizone_plan.attachment.to_file(multizone_plan_file_path)
          combined_pdf_files << multizone_plan_file_path
          copy_of_plans_pdf_files << multizone_plan_file_path if options[:copy_of_plans].to_b
        end
      end

      room_configurations.each_with_index do |rc, _i|
        current_step += 1
        yield(current_step, total_steps, "Building plans for room #{rc.reference_number}") if block_given?

        if options[:show_installation_plans]
          install_plan = rc.get_or_generate_installation_plan_pdf(version: options[:show_installation_plans_version], force: regenerate_plans)
          if install_plan&.attachment
            yield(current_step, total_steps, "Pulling installation plan for room #{rc.reference_number}") if block_given?
            install_plan_file_path = "#{dir}/install_plan_#{rc.reference_number}.pdf"
            install_plan.attachment.to_file(install_plan_file_path)
            combined_pdf_files << install_plan_file_path
            copy_of_plans_pdf_files << install_plan_file_path if options[:copy_of_plans].to_b
          end
        end

        if options[:show_electrical_plans].to_b
          electrical_plan = rc.get_or_generate_electrical_plan_pdf(version: options[:show_electrical_plans_version], force: regenerate_plans)
          if electrical_plan&.attachment
            yield(current_step, total_steps, "Pulling electrical plan for room #{rc.reference_number}") if block_given?
            electrical_plan_file_path = "#{dir}/electrical_plan_#{rc.reference_number}.pdf"
            electrical_plan.attachment.to_file(electrical_plan_file_path)
            combined_pdf_files << electrical_plan_file_path
            copy_of_plans_pdf_files << electrical_plan_file_path if options[:copy_of_plans].to_b
          end
        end

        next unless options[:include_last_heat_loss].to_b

        next unless (heat_loss_upload = rc.heat_loss_report_pdf && heat_loss_upload.attachment)

        yield(current_step, total_steps, "Pulling heat loss for room #{rc.reference_number}") if block_given?
        heat_loss_file_path = "#{dir}/heat_loss_#{rc.reference_number}.pdf"
        heat_loss_upload.attachment.to_file(heat_loss_file_path)
        combined_pdf_files << heat_loss_file_path
        copy_of_plans_pdf_files << heat_loss_file_path if options[:copy_of_plans].to_b
      end

    end

    # Time to combine
    yield(current_step, total_steps, 'Assembling files and creating pdf') if block_given?
    combined_pdf_files.each do |file_path|
      combined_pdf.load(file_path)
    end
    combined_pdf.number_pages(location: [:bottom_right], margin_from_height: -4, font_size: 9)

    # Repeat for plan copy
    if copy_of_plans_pdf_files.present?
      copy_of_plans_pdf_files.each do |file_path|
        copy_of_plans_pdf.load(file_path)
      end
      copy_of_plans_pdf.number_pages(location: [:bottom_right], margin_from_height: -4, font_size: 9)
    end
  end

  file_name = "Quote_#{quote.reference_number}.pdf"

  result_hsh = {
    options:,
    file_name:,
    additional_attachments:
  }
  if options[:output_to_file]
    base_name = "Quote_#{quote.reference_number}_#{SecureRandom.hex(6)}"
    temp_path = Upload.temp_location("#{base_name}.pdf")
    result_hsh[:pdf_file_path] = options[:output_to_file_path] || temp_path
    combined_pdf.save result_hsh[:pdf_file_path]
    result_hsh[:pdf_file_path] = Pdf::Compressor.new(input_path: result_hsh[:pdf_file_path]).compress.output_path.to_s
    if copy_of_plans_pdf_files.present?
      extra_plans_file_name = "#{base_name}_plans.pdf"
      extra_plans_temp_path = Upload.temp_location(extra_plans_file_name)
      extra_plans_temp_path = Pdf::Compressor.new(input_path: extra_plans_temp_path).compress.output_path
      result_hsh[:copy_of_plans_pdf_file_path] = extra_plans_temp_path
      result_hsh[:copy_of_plans_file_name] = extra_plans_file_name
      copy_of_plans_pdf.save result_hsh[:copy_of_plans_pdf_file_path]
    end
  else # Stream
    result_hsh[:pdf_data] = Pdf::Compressor.new(input_blob: combined_pdf.to_pdf).compress.output_blob
  end

  Result.new(**result_hsh)
end