Class: QuotesController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- QuotesController
- Includes:
- Controllers::ActivitiesTab, Controllers::CommunicationsTab, Controllers::Crm::ShippingParams, Controllers::OpportunityBriefingLoadable, Controllers::RequireTurboFrame, Controllers::RoomsTab, Controllers::SmsTab, Controllers::UploadsTab, Controllers::Workflowable, CrmFormHelper
- Defined in:
- app/controllers/quotes_controller.rb
Overview
Controller: quotes.
Constant Summary collapse
- TAB_ACTIONS =
Tab fragments never render standalone — non-frame hits redirect to show ?tab=.
%i[tab_material_alerts tab_main tab_line_items tab_contacts tab_shipping tab_rooms tab_attachments tab_communications tab_sms tab_activities tab_tickets tab_inventory tab_revisions].freeze
Constants included from Controllers::ReferenceFindable
Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS
Constants included from Controllers::AnalyticsEvents
Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY
Constants included from Controllers::ErrorRendering
Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES
Instance Method Summary collapse
-
#activity_log ⇒ Object
Audit log of every server-recorded customer action on the /ql/ viewer for this quote.
-
#build_communication ⇒ void
Redirects to the new-communication form pre-filled to send this quote's selected PDFs (and, optionally, the online quote link).
-
#cancel ⇒ void
Cancels the quote, if cancelable.
-
#complete_upgrade ⇒ void
Applies the room/line-item upgrade selections submitted on
upgrade. -
#convert_to_order ⇒ Object
PUT /customers/1/opportunities/1/quotes/1/convert_to_order.
-
#copy ⇒ void
Renders the form for copying a quote to a different opportunity.
-
#create ⇒ Object
PUT /customers/1/opportunities/1/quotes.
-
#current_support_case ⇒ SupportCase?
Finds the support case referenced by
params[:support_case_id], used bylookup_rowto render the quotes table in support case context. -
#destroy ⇒ Object
DELETE /customers/1/opportunities/1/quotes/1.
-
#disable_auto_coupon ⇒ void
Disables automatic coupon application on this quote.
-
#do_copy ⇒ void
Queues
QuoteCopyWorkerto copy the quote to the opportunity selected oncopy. -
#do_move ⇒ void
Moves the quote to the opportunity selected on
move. -
#edit ⇒ Object
GET /customers/1/opportunities/1/quotes/1/edit.
-
#enable_auto_coupon ⇒ void
Re-enables automatic coupon application and recalculates the quote's discount.
-
#enter_rooms ⇒ void
Applies the room selection submitted on the rooms tab: redirects straight to a new room form, or queues
EnterQuoteRoomsWorkerfor the selected rooms. -
#enter_shipping ⇒ void
Applies the submitted shipping method to the quote via
Shipping::UpdateShippingMethod. -
#fix_catalog ⇒ void
Runs the quote's catalog-coherence fixups and reports what changed.
-
#generate_pdf ⇒ void
Queues async generation of the quote PDF with the selected letter/materials/installation/electrical template versions.
-
#index ⇒ void
Lists quotes for the current context object (customer or opportunity).
-
#link ⇒ void
Builds the encrypted, customer-facing quote-viewer URL for this quote.
-
#lookup ⇒ void
Returns quote-lookup autocomplete results as JSON, either a single quote (
params[:quote_id]) or a paginated search by reference/customer. -
#lookup_row ⇒ Object
Return a server-rendered quote row for the support case quotes table.
-
#lookup_suffix ⇒ void
Returns quote-suffix autocomplete results as JSON.
-
#move ⇒ void
Renders the form for moving a quote to a different opportunity.
-
#new ⇒ Object
GET /customers/1/opportunities/1/quotes/new.
-
#profit_margins ⇒ void
Renders the profit-margins review page for the quote.
-
#recalculate_discount ⇒ void
Recalculates the quote's discount and reports success/failure.
-
#recalculate_taxes ⇒ void
Recalculates the quote's tax rate and reports success/failure.
-
#requote ⇒ void
Queues
RequoteWorkerto rebuild this quote from an associated RMA. -
#restore ⇒ void
Restores a cancelled quote.
-
#rooms ⇒ void
Lists the opportunity's room configurations for the rooms tab (client-side filtering happens via Stimulus).
-
#select_pdf_template ⇒ void
Runs a final profit review before letting the PDF template be selected.
-
#set_shipping_address ⇒ void
Saves the shipping address chosen or entered on the shipping address form, then updates freight/shipping-rate state and redirects accordingly.
-
#shipping ⇒ void
Renders the shipping tab, surfacing the last shipping-rate-request result if the shipping method was just updated.
-
#shipping_address ⇒ void
Renders the shipping address form.
-
#show ⇒ Object
GET /customers/1/opportunities/1/quotes/1.
-
#tab_activities ⇒ void
Renders the activities tab fragment.
-
#tab_attachments ⇒ void
Renders the attachments tab fragment.
-
#tab_communications ⇒ void
Renders the communications tab fragment.
-
#tab_contacts ⇒ void
Renders the contacts tab fragment.
-
#tab_inventory ⇒ void
Renders the inventory tab fragment, showing remaining quoted items.
-
#tab_line_items ⇒ void
Renders the line items tab fragment.
-
#tab_main ⇒ Object
Tab actions for turbo-tabs lazy loading.
-
#tab_material_alerts ⇒ void
Renders the material alerts tab fragment.
-
#tab_revisions ⇒ void
Renders the revisions tab fragment.
-
#tab_rooms ⇒ void
Renders the rooms tab fragment.
-
#tab_shipping ⇒ void
Renders the shipping tab fragment.
-
#tab_sms ⇒ void
Renders the SMS tab fragment.
-
#tab_tickets ⇒ void
Renders the tickets tab fragment.
-
#unopened_quotes ⇒ void
Lists quotes the customer has not yet opened on the
/ql/<token>viewer. -
#update ⇒ Object
PUT /customers/1/opportunities/1/quotes/1.
-
#update_online_quote_options ⇒ Object
PATCH /quotes/:id/update_online_quote_options.
-
#update_profit_margins ⇒ void
Applies submitted profit-margin minimums and releases from profit review when the new margins clear the threshold.
-
#upgrade ⇒ void
Renders the room-upgrade form (view-only; submission is
complete_upgrade).
Methods included from CrmFormHelper
#blocking_submit_button, #boolean_select, #datalist, #edit_button, #form_style_attribute_display, #opportunity_local_sales_rep_input, #sales_support_rep_input, #submit_cancel, #technical_rep_input, #technical_rep_sec_input, #yes_no_blank_boolean_collection_for_select, #yes_no_boolean_collection_for_select
Methods included from Controllers::RequireTurboFrame
require_in_pane_tab_frames, require_turbo_frame
Methods included from Controllers::Workflowable
#render_workflow_error_stream, #render_workflow_success_stream, #workflow_action, #workflow_action_complete
Methods inherited from CrmController
#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen
Methods inherited from ApplicationController
#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y
Methods included from Controllers::ReturnPathHandling
#check_for_return_path, #redirect_to_return_path_or_default
Methods included from Controllers::AnalyticsEvents
#consume_queued_analytics_events, #registration_lead_type, #track_event
Methods included from Controllers::DeviceDetection
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
Methods included from Controllers::TrackingDetection
#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?
Methods included from Controllers::AcceleratedFileSending
#send_file_accelerated, #send_upload_accelerated
Methods included from Controllers::ErrorRendering
#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback
Methods included from Controllers::TurnstileVerification
#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!
Methods included from Controllers::CloudflareCaching
edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session
Methods included from Controllers::Webpackable
#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?
Methods included from Controllers::Localizable
#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale
Methods included from Controllers::Authenticable
#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak
Methods included from UrlsHelper
#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?
Instance Method Details
#activity_log ⇒ Object
Audit log of every server-recorded customer action on the
/ql/ viewer for this quote. Sourced from visit_events rows
tagged with name LIKE 'ql.%' and properties.quote_id = @quote.id;
see MyQuotesController#record_ql_event for the writer side.
249 250 251 252 253 254 |
# File 'app/controllers/quotes_controller.rb', line 249 def activity_log @events = VisitEvent.where("name LIKE 'ql.%'") .where("properties->>'quote_id' = ?", @quote.id.to_s) .order(time: :asc) .limit(500) end |
#build_communication ⇒ void
This method returns an undefined value.
Redirects to the new-communication form pre-filled to send this quote's
selected PDFs (and, optionally, the online quote link).
237 238 239 240 241 242 243 |
# File 'app/controllers/quotes_controller.rb', line 237 def build_communication pdfs = Upload.where(id: params[:upload_ids]) cb = CommunicationBuilder.new(resource: @quote, current_user:, uploads: pdfs, online_quote: params[:online_quote].to_b, recipient_contact_point_ids: params[:recipient_contact_point_ids]) redirect_to new_communication_path(cb.to_params) end |
#cancel ⇒ void
This method returns an undefined value.
Cancels the quote, if cancelable.
584 585 586 587 588 |
# File 'app/controllers/quotes_controller.rb', line 584 def cancel @quote.cancel if @quote.cancelable? flash[:error] = @quote.errors_to_s if @quote.errors.any? redirect_to_return_path_or_default quote_path(@quote) end |
#complete_upgrade ⇒ void
This method returns an undefined value.
Applies the room/line-item upgrade selections submitted on upgrade.
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 |
# File 'app/controllers/quotes_controller.rb', line 563 def complete_upgrade room_configs = [] params[:quote][:line_items].each do |li_id, li_attrs| if li_attrs[:resource_id].present? @quote.line_items.find(li_id).update(li_attrs) room_configs << li_attrs[:resource_id] end end room_configs = room_configs.uniq room_configs.each do |r| rc = @opportunity.room_configurations.find(r) @quote.room_configurations << rc unless @quote.room_configurations.include?(rc) rc.update(requires_upgrade: false) end @quote.update(requires_upgrade: false) redirect_to quote_path(@quote) end |
#convert_to_order ⇒ Object
PUT /customers/1/opportunities/1/quotes/1/convert_to_order
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 |
# File 'app/controllers/quotes_controller.rb', line 415 def convert_to_order if @quote.can_convert? @quote.refresh_tax_rate @quote.reload order = @quote.to_order(nil, params[:txid]) do |order| # If user cannot create this type of order we explode right here (:create, order) end if order.new_record? flash[:error] = "Unable to convert to order. #{order.errors_to_s}" redirect_to quote_path(@quote) else redirect_to(account_managers_order_path(order)) end else errors = @quote.convert_to_order_service.errors || [] flash[:error] = errors.to_sentence.capitalize redirect_to quote_path(@quote) end end |
#copy ⇒ void
This method returns an undefined value.
Renders the form for copying a quote to a different opportunity.
878 879 880 881 882 |
# File 'app/controllers/quotes_controller.rb', line 878 def copy @quote = Quote.find(params[:id]) @opportunity = @quote.opportunity @customer = @quote.customer end |
#create ⇒ Object
PUT /customers/1/opportunities/1/quotes
88 89 90 91 92 93 94 95 96 |
# File 'app/controllers/quotes_controller.rb', line 88 def create @quote = @opportunity.quotes.new(params[:quote]) @quote.single_origin = true if @opportunity.customer.is_direct_buy? # patch for now for Direct Buy, must be single origin if @quote.save redirect_to rooms_quote_path(@quote) else render action: :new, status: :unprocessable_content end end |
#current_support_case ⇒ SupportCase?
Finds the support case referenced by params[:support_case_id], used by
lookup_row to render the quotes table in support case context.
819 820 821 |
# File 'app/controllers/quotes_controller.rb', line 819 def current_support_case SupportCase.find_by(id: params[:support_case_id]) end |
#destroy ⇒ Object
DELETE /customers/1/opportunities/1/quotes/1
110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'app/controllers/quotes_controller.rb', line 110 def destroy if @quote.ok_to_delete? @quote.destroy redirect_to opportunity_path(@opportunity) else deps = [] deps += @quote.dependents[:orders].first(5).map { |o| view_context.link_to(o.reference_number, order_path(o)) } deps += @quote.dependents[:children].map { |q| view_context.link_to(q.reference_number, quote_path(q)) } deps += @quote.dependents[:communications].map { |c| view_context.link_to("Communication #{c.id}", communication_path(c)) } flash[:error] = "Quote cannot be deleted because it is referenced by: #{deps.join(', ').html_safe}" redirect_to quote_path(@quote) end end |
#disable_auto_coupon ⇒ void
This method returns an undefined value.
Disables automatic coupon application on this quote.
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 |
# File 'app/controllers/quotes_controller.rb', line 473 def disable_auto_coupon @quote = Quote.find(params[:id]) @quote.update_attribute!(:disable_auto_coupon, true) flash[:info] = 'Auto coupons are now disabled, if any auto coupons were applied you can delete them and they will not come back.' respond_to do |format| format.turbo_stream do render turbo_stream: turbo_stream.replace( 'discount_summary', partial: 'discounts/summary', locals: { itemizable: @quote } ) end format.html do redirect_to_return_path_or_default(quote_discounts_path(@quote)) end end end |
#do_copy ⇒ void
This method returns an undefined value.
Queues QuoteCopyWorker to copy the quote to the opportunity selected on copy.
887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 |
# File 'app/controllers/quotes_controller.rb', line 887 def do_copy if (opportunity_id = params.dig(:copy, :opportunity_id)).present? = Opportunity.find(opportunity_id) end if = { opportunity_id: .id, quote_id: @quote.id, notify_email: current_user.email }.stringify_keys job_id = QuoteCopyWorker.perform_async() if job_id.blank? && (job_id = BackgroundJobStatus.search(worker_klass: 'QuoteCopyWorker', args: ).first&.dig(:job_id)) flash[:warning] = 'A similar job was already enqueued, watching status of that job now' end if job_id.present? redirect_to job_path(job_id) else flash[:warning] = 'There was a problem queuing the job' redirect_to_return_path_or_default opportunity_path(@opportunity) end else flash.now[:error] = 'Invalid opportunity selected' @opportunity = @quote.opportunity @customer = @quote.customer render :copy, status: :unprocessable_content end end |
#do_move ⇒ void
This method returns an undefined value.
Moves the quote to the opportunity selected on move.
851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 |
# File 'app/controllers/quotes_controller.rb', line 851 def do_move if (opportunity_id = params.dig(:move, :opportunity_id)).present? opportunity_moving_to = Opportunity.find(opportunity_id) end if opportunity_moving_to qm = Quote::Mover.new(@quote) if qm.move_to(opportunity_moving_to) flash[:info] = 'Quote has been moved successfully' redirect_to opportunity_path(opportunity_moving_to) else flash.now[:error] = qm.results.join(',') @opportunity = @quote.opportunity @customer = @quote.customer render :move, status: :unprocessable_content end else flash.now[:error] = 'Invalid opportunity selected' @opportunity = @quote.opportunity @customer = @quote.customer render :move, status: :unprocessable_content end end |
#edit ⇒ Object
GET /customers/1/opportunities/1/quotes/1/edit
83 84 85 |
# File 'app/controllers/quotes_controller.rb', line 83 def edit @addresses = @opportunity.customer.addresses.map { |a| [a.full_address, a.id] } end |
#enable_auto_coupon ⇒ void
This method returns an undefined value.
Re-enables automatic coupon application and recalculates the quote's discount.
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 |
# File 'app/controllers/quotes_controller.rb', line 495 def enable_auto_coupon @quote = Quote.find(params[:id]) @quote.disable_auto_coupon = false @quote.save # Recalculate discounts after auto coupons are applied @quote.reload.reset_discount flash[:info] = 'Auto coupons are enabled, if any auto coupons qualified they have been added automatically.' respond_to do |format| format.turbo_stream do render turbo_stream: turbo_stream.replace( 'discount_summary', partial: 'discounts/summary', locals: { itemizable: @quote } ) end format.html do redirect_to_return_path_or_default(quote_discounts_path(@quote)) end end end |
#enter_rooms ⇒ void
This method returns an undefined value.
Applies the room selection submitted on the rooms tab: redirects straight
to a new room form, or queues EnterQuoteRoomsWorker for the selected rooms.
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'app/controllers/quotes_controller.rb', line 379 def enter_rooms params[:quote] ||= {} new_rc_ids = (params[:quote][:room_configuration_ids] || []).uniq.sort create_new_room = (params[:commit] == 'create_new_room') # If creating new room, skip job processing and just redirect # User will be warned on client-side if they have unsaved changes if create_new_room redirect_to new_opportunity_room_configuration_path(@opportunity, quote_id: @quote.id, return_path: rooms_quote_path(@quote)) return end # Process room changes via background job = { quote_id: @quote.id, room_configuration_ids: new_rc_ids, opportunity_name: params.dig(:quote, :opportunity_attributes, :name), quote_suffix: params.dig(:quote, :suffix), redirect_path: quote_path(@quote, tab: 'rooms') }.stringify_keys job_id = EnterQuoteRoomsWorker.perform_async() if job_id.blank? && (job_id = BackgroundJobStatus.search(worker_klass: 'EnterQuoteRoomsWorker', args: ).first&.dig(:job_id)) flash[:warning] = 'A similar job was already enqueued, watching status of that job now' end if job_id.present? redirect_to job_path(job_id) else flash[:warning] = 'There was a problem queuing the job' redirect_to quote_path(@quote, tab: 'rooms') end end |
#enter_shipping ⇒ void
This method returns an undefined value.
Applies the submitted shipping method to the quote via
Shipping::UpdateShippingMethod.
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'app/controllers/quotes_controller.rb', line 334 def enter_shipping # Use service to handle shipping updates and discount recalculation # Service handles the update internally # Get the quote-specific parameters (empty hash if no quote params) quote_params = shipping_params # If there are no quote parameters, just redirect (no shipping to update) if quote_params.empty? @quote.ready_to_transmit if @quote.can_ready_to_transmit? redirect_to_return_path_or_default quote_path(@quote) return end # Pass the quote parameters to the service result = Shipping::UpdateShippingMethod.new.update_from_params(@quote, { quote: quote_params }) if result.success? # Display any shipping change messages result..each { |msg| flash[:info] = msg } if result..present? @quote.ready_to_transmit if @quote.can_ready_to_transmit? redirect_to_return_path_or_default quote_path(@quote) else flash.now[:error] = result.errors @ship_quotable = @quote render :shipping, status: :unprocessable_content end end |
#fix_catalog ⇒ void
This method returns an undefined value.
Runs the quote's catalog-coherence fixups and reports what changed.
440 441 442 443 444 |
# File 'app/controllers/quotes_controller.rb', line 440 def fix_catalog @quote = Quote.find(params[:id]) flash[:info] = @quote.fix_catalog.join('<br>') redirect_to quote_path(@quote) end |
#generate_pdf ⇒ void
This method returns an undefined value.
Queues async generation of the quote PDF with the selected
letter/materials/installation/electrical template versions.
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 |
# File 'app/controllers/quotes_controller.rb', line 151 def generate_pdf # A single v1/v2 switch drives the whole PDF: pull it out, boolean-cast the # remaining section toggles, then apply the chosen version to every section # the generator versions (letter, materials, installation & electrical plans). raw = (params[:select_quote_template] || {}).to_unsafe_h version = raw.delete('version').to_s.presence_in(%w[v1 v2]) || 'v2' = raw.reject { |k, _| k.to_s.end_with?('_version') } .transform_values(&:to_b) .merge( 'letter_version' => version, 'materials_only_version' => version, 'show_installation_plans_version' => version, 'show_electrical_plans_version' => version ) job_id = GenerateQuoteWorker.perform_async({ quote_id: @quote.id, current_user_id: @context_user.id, quote_options: }.deep_stringify_keys) if job_id redirect_to job_path(job_id) else flash[:error] = 'Job could not be queued or did not provide status, perhaps a duplicate' redirect_to_return_path_or_default quote_path(@quote) end end |
#index ⇒ void
This method returns an undefined value.
Lists quotes for the current context object (customer or opportunity).
26 27 28 29 30 31 32 33 34 35 36 |
# File 'app/controllers/quotes_controller.rb', line 26 def index @context_object = context_object @quotes = @context_object&.quotes || Quote.none @q = @quotes.ransack(params[:q]) @q.sorts = 'created_at DESC' if @q.sorts.blank? @pagy, @quotes = pagy(@q.result) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#link ⇒ void
This method returns an undefined value.
Builds the encrypted, customer-facing quote-viewer URL for this quote.
221 222 223 224 |
# File 'app/controllers/quotes_controller.rb', line 221 def link token = Encryption.encrypt_string(@quote.id.to_s) @url = "https://#{WEB_HOSTNAME}/quote-viewer/#{token}" end |
#lookup ⇒ void
This method returns an undefined value.
Returns quote-lookup autocomplete results as JSON, either a single quote
(params[:quote_id]) or a paginated search by reference/customer.
776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 |
# File 'app/controllers/quotes_controller.rb', line 776 def lookup q = params[:q].try(:upcase)&.split&.first quote_id = params[:quote_id] if quote_id quote = Quote.find(quote_id) render json: { results: { id: quote_id, text: "[#{quote.reference_number}] #{quote.customer.full_name} (#{quote.created_at.to_fs(:crm_default)})" } } else page = params[:page] || 1 per_page = params[:per_page] || 10 @results = params[:with_wildcard].present? ? Quote.like_lookup(q) : Quote.lookup(q) @pagy, @results = pagy(@results.joins(opportunity: :customer).includes(opportunity: :customer), page: page, limit: per_page) total_entries = @pagy.count results_array = @results.map do |e| { id: e.id, text: "[#{e.reference_number}] #{e.customer.full_name} (#{e.created_at.to_fs(:crm_date_only)})", label: "[#{e.reference_number}] #{e.customer.full_name} (#{e.created_at.to_fs(:crm_date_only)})", reference: e.reference_number, quote_link: quote_path(e), state: e.state, created_at: e.created_at.to_fs(:crm_date_only), opportunity: e.opportunity&.name, customer: e.customer.full_name, customer_link: customer_path(e.customer) } end render json: { results: results_array, total: total_entries } end end |
#lookup_row ⇒ Object
Return a server-rendered quote row for the support case quotes table
806 807 808 809 810 811 812 813 |
# File 'app/controllers/quotes_controller.rb', line 806 def lookup_row q = Quote.find_by(id: params[:id]) if q.nil? render html: ''.html_safe, status: :not_found else render partial: 'support_cases/quotes_table_row', locals: { q: q, hide_checkbox: false, support_case: current_support_case } end end |
#lookup_suffix ⇒ void
This method returns an undefined value.
Returns quote-suffix autocomplete results as JSON.
826 827 828 829 830 831 832 833 834 835 836 837 |
# File 'app/controllers/quotes_controller.rb', line 826 def lookup_suffix q = params[:q] @pagy, @results = pagy(ViewQuoteSuffix.lookup(q), limit: params[:per_page]) total_entries = @pagy.count results_array = @results.map do |e| { id: e.suffix, text: e.suffix } end render json: { results: results_array, total: total_entries } end |
#move ⇒ void
This method returns an undefined value.
Renders the form for moving a quote to a different opportunity.
842 843 844 845 846 |
# File 'app/controllers/quotes_controller.rb', line 842 def move @quote = Quote.find(params[:id]) @opportunity = @quote.opportunity @customer = @quote.customer end |
#new ⇒ Object
GET /customers/1/opportunities/1/quotes/new
74 75 76 77 78 79 80 |
# File 'app/controllers/quotes_controller.rb', line 74 def new def_cp_ids = [] def_email = @opportunity.primary_party.contact_points.emails.first || @opportunity.primary_party.customer.contact_points.emails.first def_cp_ids << def_email.id if def_email.present? @quote = @opportunity.quotes.new(buying_group_id: @opportunity., contact_point_ids: def_cp_ids, hold_for_transmission: @customer.hold_quotes_for_transmission_default?) @quote.single_origin = true if @opportunity.customer.is_direct_buy? # patch for now for Direct Buy, must be single origin end |
#profit_margins ⇒ void
This method returns an undefined value.
Renders the profit-margins review page for the quote.
601 602 603 604 605 |
# File 'app/controllers/quotes_controller.rb', line 601 def profit_margins @quote = Quote.find(params[:id]) :manage_profit_margins, @quote render layout: 'crm/crm' end |
#recalculate_discount ⇒ void
This method returns an undefined value.
Recalculates the quote's discount and reports success/failure.
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 |
# File 'app/controllers/quotes_controller.rb', line 520 def recalculate_discount @quote.reset_discount if @quote.errors.present? flash.now[:error] = "Problem while recalculating discount: #{@quote.errors_to_s}" else flash.now[:success] = 'Discount recalculated.' end respond_to do |format| format.turbo_stream { render turbo_stream: itemizable_recalculate_streams(@quote) } format.html do redirect_to_return_path_or_default quote_path(@quote) end end end |
#recalculate_taxes ⇒ void
This method returns an undefined value.
Recalculates the quote's tax rate and reports success/failure.
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 |
# File 'app/controllers/quotes_controller.rb', line 539 def recalculate_taxes @quote.refresh_tax_rate if @quote.errors.present? flash.now[:error] = "Problem while recalculating taxes: #{@quote.errors_to_s}" else flash.now[:success] = 'Taxes recalculated.' end respond_to do |format| format.turbo_stream { render turbo_stream: itemizable_recalculate_streams(@quote) } format.html do redirect_to_return_path_or_default quote_path(@quote) end end end |
#requote ⇒ void
This method returns an undefined value.
Queues RequoteWorker to rebuild this quote from an associated RMA.
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 |
# File 'app/controllers/quotes_controller.rb', line 449 def requote = { quote_id: @quote.id, rma_id: params[:rma_id], redirect_path: quote_path(@quote) }.stringify_keys job_id = RequoteWorker.perform_async() if job_id.blank? && (job_id = BackgroundJobStatus.search(worker_klass: 'RequoteWorker', args: ).first&.dig(:job_id)) flash[:warning] = 'A similar job was already enqueued, watching status of that job now' end if job_id.present? redirect_to job_path(job_id) else flash[:warning] = 'There was a problem queuing the job' redirect_to quote_path(@quote) end end |
#restore ⇒ void
This method returns an undefined value.
Restores a cancelled quote.
593 594 595 596 |
# File 'app/controllers/quotes_controller.rb', line 593 def restore @quote.restore if @quote.cancelled? redirect_to_return_path_or_default quote_path(@quote) end |
#rooms ⇒ void
This method returns an undefined value.
Lists the opportunity's room configurations for the rooms tab
(client-side filtering happens via Stimulus).
367 368 369 370 371 372 373 |
# File 'app/controllers/quotes_controller.rb', line 367 def rooms # Load all room configurations (client-side filtering via Stimulus) # Use preload to avoid N+1 queries - loads associations in separate queries @room_configurations = @opportunity.room_configurations .order('room_configurations.updated_at DESC, room_configurations.name') .preload(:room_layout_images, :controlled_rooms, :controlled_by) end |
#select_pdf_template ⇒ void
This method returns an undefined value.
Runs a final profit review before letting the PDF template be selected.
135 136 137 138 139 140 141 142 143 144 145 |
# File 'app/controllers/quotes_controller.rb', line 135 def select_pdf_template # This simple check will make sure our quote in awaiting transmission get a final profit check @quote.review_profit if @quote.can_review_profit? return unless @quote.profit_review? flash[:error] = 'Profit margins not met, quote cannot be sent' redirect_to quote_path(@quote) # templates = @quote.template_options end |
#set_shipping_address ⇒ void
This method returns an undefined value.
Saves the shipping address chosen or entered on the shipping address form,
then updates freight/shipping-rate state and redirects accordingly.
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'app/controllers/quotes_controller.rb', line 267 def set_shipping_address set_new_address # Intercept address id quote_params = params[:quote].to_h address_params = params[:address].to_h address_id = quote_params.delete(:shipping_address_id) @quote.attributes = quote_params if address_id == 'on' # No shipping address @quote.shipping_address_id = nil elsif address_id.to_i > 0 # Address book pick @quote.shipping_address_id = address_id.to_i elsif @quote.shipping_address&.one_time_only # update the one time address @quote.shipping_address.attributes = address_params @new_address = @quote.shipping_address elsif address_params.present? address_params.delete(:party_id) unless address_params[:party_id].to_i > 0 @quote.build_shipping_address(address_params) @new_address = @quote.shipping_address end if @quote.save if @quote.ships_freight_but_address_not_freight_ready? check_freight_fields_and_redirect else result = begin eval(@quote.last_shipping_rate_request_result.to_s) # rubocop:disable Security/Eval rescue StandardError nil end handle_shipping_flash_messaging(result) if result.present? @quote.ready_to_transmit if @quote.is_warehouse_pickup? && @quote.pre_pack? if @customer.addresses.count <= 1 && !@new_address&.new_record? && @new_address.country_iso3 && (@new_address.country_iso3 != @customer.catalog.country_iso3) flash[:error] = "Customer's first address country of #{@new_address.country.printable_name} does not match customer's existing catalog country of #{@customer.catalog.country.printable_name}" redirect_to edit_catalog_customer_path(@customer, return_path: @return_path || shipping_quote_path(@quote)) else redirect_to_return_path_or_default shipping_quote_path(@quote) end end else flash.now[:error] = @quote.errors_to_s.presence || 'Could not update shipping address.' render :shipping_address, status: :unprocessable_content end end |
#shipping ⇒ void
This method returns an undefined value.
Renders the shipping tab, surfacing the last shipping-rate-request result
if the shipping method was just updated.
320 321 322 323 324 325 326 327 328 |
# File 'app/controllers/quotes_controller.rb', line 320 def shipping @ship_quotable = @quote @shipping_address = @customer.shipping_address if params[:shipping_updated] result = eval(@quote.last_shipping_rate_request_result.to_s) # rubocop:disable Security/Eval handle_shipping_flash_messaging(result) end redirect_to_return_path_or_default quote_path(@quote) if @quote.complete? end |
#shipping_address ⇒ void
This method returns an undefined value.
Renders the shipping address form.
259 260 261 |
# File 'app/controllers/quotes_controller.rb', line 259 def shipping_address set_new_address end |
#show ⇒ Object
GET /customers/1/opportunities/1/quotes/1
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'app/controllers/quotes_controller.rb', line 39 def show (:read, @quote) set_show_variables set_opportunity_briefing_variables respond_to do |format| format.html do flash.now[:error] = "#{flash.now[:error]}Quote cannot be converted to order. #{(@quote.convert_to_order_service.errors || []).join('. ')} " if @quote.complete? && !@quote.sold? && !@quote.can_convert? end format.pdf do if Rails.env.development? || params[:inline].to_b = { current_user_id: @context_user.id, output_to_file: false, show_installation_plans: true, materials_only: params[:materials_only].to_b, regenerate_plans: params[:regenerate_plans].to_b } combined_pdf_result = Quote::CombinedPdfGenerator.new.process(@quote, **) send_data combined_pdf_result.pdf_data, type: 'application/pdf', disposition: 'inline' else = { quote_id: @quote.id, current_user_id: @context_user.id, preview: true, quote_options: { materials_only: params[:materials_only].to_b, regenerate_plans: params[:regenerate_plans].to_b } }.deep_stringify_keys job_id = GenerateQuoteWorker.perform_async() # perform_async returns nil when sidekiq-unique-jobs rejects a duplicate # (Sidekiq.default_job_options: client on_conflict: :log) — e.g. a double # click while an identical PDF job for this quote is still running. # redirect_to job_path(nil) would 500 with UrlGenerationError # (AppSignal #6312). Same guard as #generate_pdf below. if job_id redirect_to job_path(job_id) else flash[:error] = 'A PDF is already being generated for this quote — please wait a moment and try again.' redirect_to_return_path_or_default quote_path(@quote) end end end end end |
#tab_activities ⇒ void
This method returns an undefined value.
Renders the activities tab fragment.
703 704 705 706 707 708 709 710 |
# File 'app/controllers/quotes_controller.rb', line 703 def tab_activities (:read, @quote) build_activities_tab_data(@quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_attachments ⇒ void
This method returns an undefined value.
Renders the attachments tab fragment.
667 668 669 670 671 672 673 674 |
# File 'app/controllers/quotes_controller.rb', line 667 def (:read, @quote) build_uploads_tab_data(@quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_communications ⇒ void
This method returns an undefined value.
Renders the communications tab fragment.
679 680 681 682 683 684 685 686 |
# File 'app/controllers/quotes_controller.rb', line 679 def tab_communications (:read, @quote) build_communications_tab_data(@quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_contacts ⇒ void
This method returns an undefined value.
Renders the contacts tab fragment.
630 631 632 633 634 635 636 |
# File 'app/controllers/quotes_controller.rb', line 630 def tab_contacts (:read, @quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_inventory ⇒ void
This method returns an undefined value.
Renders the inventory tab fragment, showing remaining quoted items.
726 727 728 729 730 731 732 733 |
# File 'app/controllers/quotes_controller.rb', line 726 def tab_inventory (:read, @quote) @item_groups = Inventory::QuotedRemainingItems.new.process(@quote, 1).item_groups respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_line_items ⇒ void
This method returns an undefined value.
Renders the line items tab fragment.
619 620 621 622 623 624 625 |
# File 'app/controllers/quotes_controller.rb', line 619 def tab_line_items (:read, @quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_main ⇒ Object
Tab actions for turbo-tabs lazy loading
608 609 610 611 612 613 614 |
# File 'app/controllers/quotes_controller.rb', line 608 def tab_main (:read, @quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_material_alerts ⇒ void
This method returns an undefined value.
Renders the material alerts tab fragment.
127 128 129 130 |
# File 'app/controllers/quotes_controller.rb', line 127 def tab_material_alerts @material_alerts = @quote.get_material_alerts if @quote && @quote.id.present? render partial: '/shared/material_alerts_tab' end |
#tab_revisions ⇒ void
This method returns an undefined value.
Renders the revisions tab fragment.
738 739 740 741 742 743 744 |
# File 'app/controllers/quotes_controller.rb', line 738 def tab_revisions (:read, @quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_rooms ⇒ void
This method returns an undefined value.
Renders the rooms tab fragment.
652 653 654 655 656 657 658 659 660 661 662 |
# File 'app/controllers/quotes_controller.rb', line 652 def tab_rooms (:read, @quote) @opportunity = @quote.opportunity @editing_locked = @quote.complete? @return_path = quote_path(@quote, tab: 'rooms') build_rooms_tab_data(@quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_shipping ⇒ void
This method returns an undefined value.
Renders the shipping tab fragment.
641 642 643 644 645 646 647 |
# File 'app/controllers/quotes_controller.rb', line 641 def tab_shipping (:read, @quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_sms ⇒ void
This method returns an undefined value.
Renders the SMS tab fragment.
691 692 693 694 695 696 697 698 |
# File 'app/controllers/quotes_controller.rb', line 691 def tab_sms (:read, @quote) build_sms_tab_data(@quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#tab_tickets ⇒ void
This method returns an undefined value.
Renders the tickets tab fragment.
715 716 717 718 719 720 721 |
# File 'app/controllers/quotes_controller.rb', line 715 def tab_tickets (:read, @quote) respond_to do |format| format.html { render layout: should_render_layout? } format.turbo_stream end end |
#unopened_quotes ⇒ void
This method returns an undefined value.
Lists quotes the customer has not yet opened on the /ql/<token> viewer.
229 230 231 |
# File 'app/controllers/quotes_controller.rb', line 229 def unopened_quotes @quotes = Quotes::UnopenedQuoteFinder.new.process end |
#update ⇒ Object
PUT /customers/1/opportunities/1/quotes/1
99 100 101 102 103 104 105 106 107 |
# File 'app/controllers/quotes_controller.rb', line 99 def update @quote.validate_opportunity_and_prepared_for = true @quote.need_to_recalculate_shipping if @quote.update(params[:quote]) redirect_to_return_path_or_default(quote_path(@quote)) else render action: :edit, status: :unprocessable_content end end |
#update_online_quote_options ⇒ Object
PATCH /quotes/:id/update_online_quote_options
Sister of #generate_pdf for the new "Online quote (/ql/)" tab on
the select_pdf_template page. Persists the rep's header-style +
auto-include checkboxes to Quote#online_quote_options; the
customer-facing _plans.html.erb reads the saved state when
rendering the "Forms & guides" section on /ql/<token>.
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 |
# File 'app/controllers/quotes_controller.rb', line 184 def # recipient_contact_point_ids[] rides along in the same `online_quote` hash # (checkboxes) but is consumed separately by #picked_recipient_contact_point_ids; # permit it here so it doesn't trip action_on_unpermitted_parameters (:raise). incoming = params.fetch(:online_quote, {}) .permit(*Quote::ONLINE_QUOTE_OPTION_KEYS, recipient_contact_point_ids: []).to_h # Coerce form's "1"/"0" / on/off into stored booleans so predicates # downstream stay tidy. Missing key (unchecked box) → false. cleaned = Quote::ONLINE_QUOTE_OPTION_KEYS.index_with do |k| ActiveModel::Type::Boolean.new.cast(incoming[k.to_s]) end.stringify_keys # The people the rep ticked are the recipients of the transmit email. picked_cp_ids = picked_recipient_contact_point_ids # Merge into the existing JSONB rather than replace it — customer-side # intent flags (Quote::CUSTOMER_QUOTE_INTENT_KEYS) live in the same # column and a wholesale replace would silently drop them. merged = @quote..merge(cleaned) if @quote.update(online_quote_options: merged) if params[:commit_send].to_b redirect_to build_communication_quote_path(@quote, online_quote: 1, recipient_contact_point_ids: picked_cp_ids.join(',')) else flash[:info] = 'Online quote options saved.' redirect_to select_pdf_template_quote_path(@quote) end else flash[:error] = "Could not save: #{@quote.errors..to_sentence}" redirect_to select_pdf_template_quote_path(@quote) end end |
#update_profit_margins ⇒ void
This method returns an undefined value.
Applies submitted profit-margin minimums and releases from profit review
when the new margins clear the threshold.
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 |
# File 'app/controllers/quotes_controller.rb', line 750 def update_profit_margins (:manage_profit_margins, @quote) if quote_profit_margins_params.to_hash['min_profit_markup'].present? if @quote.update(quote_profit_margins_params) if @quote.can_release_from_profit_review? flash[:info] = 'Quote released for transmission' @quote.release_from_profit_review # Try to release elsif @quote.profit_review? flash[:error] = 'Minimum profit margins not met to allow release from profit review' end redirect_to_return_path_or_default quote_path(@quote) else flash.now[:error] = @quote.errors_to_s.presence || 'Could not update profit margins.' render :profit_margins, status: :unprocessable_content end else flash.now[:error] = 'Minimum profit margins cannot be empty' render :profit_margins, status: :unprocessable_content end end |
#upgrade ⇒ void
This method returns an undefined value.
Renders the room-upgrade form (view-only; submission is complete_upgrade).
558 |
# File 'app/controllers/quotes_controller.rb', line 558 def upgrade; end |