Class: CreditMemosController

Inherits:
CrmController show all
Includes:
Controllers::Destroyable, Controllers::RequireTurboFrame, Controllers::Workflowable
Defined in:
app/controllers/credit_memos_controller.rb

Overview

== Schema Information

Table name: credit_memos

id :integer not null, primary key
customer_id :integer
credit_order_id :integer
original_order_id :integer
total :decimal(8, 2)
currency :string(255)
discount :decimal(8, 2)
line_total :decimal(8, 2)
state :string(255)
primary_sales_rep_id :integer
secondary_sales_rep_id :integer
account_specialist_id :integer
billing_address :text
shipping_address :text
tax_total :decimal(8, 2)
shipping_cost :decimal(8, 2)
reference_number :string(255)
uploads_count :integer
company_id :integer
transmission_state :string(255)
purchase_order_id :integer
rma_id :integer
request_date :date
creator_id :integer
updater_id :integer
created_at :datetime
updated_at :datetime
category :string(255)
local_sales_rep_id :integer
billing_address_id :integer
shipping_address_id :integer
original_order_missing_reason :string(255)
remark :text
original_po_number :string(255)
tax_date :date
gl_date :date
document_date :date
shipping_coupon :decimal(8, 2)
taxable_total :decimal(12, 2)
legacy_taxes :boolean
line_total_discounted :decimal(8, 2)
billing_customer_id :integer
order_type :string(255)

Constant Summary collapse

TAB_ACTIONS =

Tab fragments never render standalone — non-frame hits redirect to show ?tab=.

%i[tab_line_items tab_attachments tab_payments tab_outgoing_payments tab_receipts tab_account_ledger].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

Methods included from Controllers::Destroyable

#destroy, #perform_destroy

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

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TurboSafeRedirect

#redirect_to

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

#address_lookupObject

GET /credit_memos/address_lookup
Search addresses for tom-select dropdown based on customer_id or invoice_ref



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
# File 'app/controllers/credit_memos_controller.rb', line 148

def address_lookup
  q = params[:q]&.strip
  customer_id = params[:customer_id].presence
  invoice_ref = params[:invoice_ref].presence

  addresses = []

  # If invoice_ref is provided, get ONLY the addresses from the invoice
  if invoice_ref.present?
    invoice = Invoice.find_by(reference_number: invoice_ref)
    if invoice
      addresses << invoice.billing_address if invoice.billing_address
      addresses << invoice.shipping_address if invoice.shipping_address
    end
  elsif customer_id.present?
    # Get all addresses for the customer
    customer = Customer.find_by(id: customer_id)
    addresses = customer.all_addresses.to_a if customer
  end

  # Remove duplicates and filter by search term
  addresses = addresses.compact.uniq
  addresses = addresses.select { |a| a.to_s.downcase.include?(q.downcase) } if q.present?

  json_result = {
    results: addresses.map { |a| { id: a.id, text: a.to_s } },
    pagination: { more: false }
  }
  render json: json_result
end

#approveObject

POST /credit_memos/:id/approve — fire the approve! state-machine
event, advancing the credit memo from draft/pending into a state
that can be transmitted to accounting.



283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'app/controllers/credit_memos_controller.rb', line 283

def approve
  @credit_memo = CreditMemo.find(params[:id])
  if @credit_memo.can_approve?
    @credit_memo.approve!
  else
    # A double-submit (or a stale button on an already-approved memo)
    # would otherwise raise StateMachines::InvalidTransition and 500
    # (AppSignal #5572). Guard with the state-machine `can_approve?`
    # predicate and surface a friendly message instead.
    flash[:error] = "Credit memo can't be approved from its current state (#{@credit_memo.state.humanize})."
  end
  redirect_to @credit_memo
end

#createObject

POST /credit_memos



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'app/controllers/credit_memos_controller.rb', line 224

def create
  @credit_memo = CreditMemo.new(params[:credit_memo])
  @addresses = @credit_memo.customer.nil? ? [] : @credit_memo.customer.all_addresses.to_a
  @invoice = @credit_memo.original_invoice
  if @invoice
    @addresses << @invoice.shipping_address unless @addresses.include?(@invoice.shipping_address)
    @addresses << @invoice.billing_address unless @addresses.include?(@invoice.billing_address)
  end

  if @credit_memo.save
    redirect_to(@credit_memo, info: 'Credit Memo was successfully created.')
  else
    render action: 'new', status: :unprocessable_content
  end
end

#editObject

GET /credit_memos/1/edit



210
211
212
213
214
215
216
217
218
219
220
221
# File 'app/controllers/credit_memos_controller.rb', line 210

def edit
  @credit_memo = CreditMemo.find(params[:id])
  redirect_to(@credit_memo, error: 'You cannot edit this Credit Memo') and return if @credit_memo.editing_locked?

  @customer = @credit_memo.customer
  @invoice = @credit_memo.original_invoice
  @addresses = @customer.all_addresses.to_a
  return unless @invoice

  @addresses << @invoice.shipping_address unless @addresses.include?(@invoice.shipping_address)
  @addresses << @invoice.billing_address unless @addresses.include?(@invoice.billing_address)
end

#indexObject

GET /credit_memos



61
62
63
# File 'app/controllers/credit_memos_controller.rb', line 61

def index
  @pagy, @credit_memos = pagy(CreditMemo.includes(:rma, :original_order, :credit_order, :customer, :company).order(reference_number: :desc))
end

#invoice_lookupObject

GET /credit_memos/invoice_lookup
Search invoices for tom-select dropdown



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/controllers/credit_memos_controller.rb', line 129

def invoice_lookup
  q = params[:q]&.strip
  customer_id = params[:customer_id].presence

  results = Invoice.includes(:customer).order(id: :desc)
  results = results.where(customer_id: customer_id) if customer_id
  results = results.where('invoices.reference_number ILIKE ?', "%#{q}%") if q.present?

  json_result = TomSelect.format_json_results(self, results, params[:page].presence, params[:per_page].presence || 20) do |inv|
    {
      id: inv.reference_number,
      text: "#{inv.reference_number} - #{inv.customer.try(:full_name)} ($#{inv.total})"
    }
  end
  render json: json_result
end

#newObject

GET /credit_memos/new



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
# File 'app/controllers/credit_memos_controller.rb', line 180

def new
  @customer = Customer.find_by(id: params[:customer_id])
  @invoice = Invoice.find_by(id: params[:invoice_id])
  @company = @customer&.catalog&.store&.company
  @currency = @company&.currency
  @addresses = @customer&.all_addresses.to_a
  @credit_memo = CreditMemo.new(customer_id: params[:customer_id],
                                customer_name: @customer&.full_name,
                                original_invoice_ref: @invoice&.reference_number,
                                company_id: @company&.id,
                                currency: @currency,
                                request_date: Date.current,
                                gl_date: Date.current,
                                document_date: Date.current,
                                tax_date: Date.current)
  @credit_memo.line_items.build
  if @invoice
    @addresses << @invoice.shipping_address unless @addresses.include?(@invoice.shipping_address)
    @addresses << @invoice.billing_address unless @addresses.include?(@invoice.billing_address)
    @credit_memo.shipping_address = @invoice.shipping_address
    @credit_memo.billing_address = @invoice.billing_address
    @credit_memo.original_po_number = @invoice.po_number
  elsif @customer
    @credit_memo.shipping_address = @customer.shipping_address
    @credit_memo.billing_address = @customer.billing_address
  end
  @addresses.compact! # remove any nil addresses
end

#pdfObject

GET /credit_memos/:id/pdf — return the cached credit_memo_pdf
upload, regenerating only when the cache is empty.



313
314
315
316
317
318
# File 'app/controllers/credit_memos_controller.rb', line 313

def pdf
  @credit_memo = CreditMemo.find(params[:id])
  @pdf = @credit_memo.uploads.where(category: 'credit_memo_pdf').first
  @pdf ||= @credit_memo.generate_pdf
  redirect_to upload_path(@pdf)
end

#printedObject

POST /credit_memos/:id/printed — fire the printed! state-machine
transition (used after the user has actually printed the cheque
for an outgoing-payment-style credit memo). Catches any error so a
state-machine guard surfaces as a flash, not a 500.



270
271
272
273
274
275
276
277
278
# File 'app/controllers/credit_memos_controller.rb', line 270

def printed
  @credit_memo = CreditMemo.find(params[:id])
  begin
    @credit_memo.printed!
  rescue StandardError => e
    flash[:error] = "Unable to print credit memo, message: #{e}"
  end
  redirect_to @credit_memo
end

#process_refundObject

POST /credit_memos/process_refund



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
# File 'app/controllers/credit_memos_controller.rb', line 98

def process_refund
  @credit_memo = CreditMemo.find(params[:id])
  error = false
  if @credit_memo.processing_refund?
    error = true
    error_message = 'A refund is already being processed for this credit memo'
  elsif params['payments'].nil? || params['payments'].to_h.all? { |_auth_id, attrs| attrs['refund_amount'].blank? || (BigDecimal(Heatwave::Normalizers.chain(attrs['refund_amount'], :currency)) == 0) }
    error = true
    error_message = 'You must specify at least one refund amount'
  else
    auths = {}
    params[:payments].each do |auth_id, attrs|
      auths[auth_id.to_i] = BigDecimal(Heatwave::Normalizers.chain(attrs['refund_amount'], :currency)) unless attrs['refund_amount'].blank? || (BigDecimal(Heatwave::Normalizers.chain(attrs['refund_amount'], :currency)) == 0)
    end
    result = @credit_memo.process_refund(auths)
    if result[:success] == false
      error = true
      error_message = result[:error_message]
    end
  end
  if error == true
    flash.now[:error] = error_message
    render :refund, status: :unprocessable_content
  else
    flash[:info] = 'Refund was successfully processed'
    redirect_to @credit_memo
  end
end

#refundObject

GET /credit_memos/refund



89
90
91
92
93
94
95
# File 'app/controllers/credit_memos_controller.rb', line 89

def refund
  @credit_memo = CreditMemo.find(params[:id])
  return if @credit_memo.can_be_refunded?

  flash[:error] = 'Credit memo not available to be refunded'
  redirect_to @credit_memo
end

#regenerate_pdfObject

POST /credit_memos/:id/regenerate_pdf — force a fresh PDF render
(after editing line items, addresses, etc.). Responds with a
turbo_stream open_url action when the request is a turbo
submission so the new tab opens cleanly without a redirect chain.



301
302
303
304
305
306
307
308
309
# File 'app/controllers/credit_memos_controller.rb', line 301

def regenerate_pdf
  @credit_memo = CreditMemo.find(params[:id])
  @pdf = @credit_memo.generate_pdf

  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.open_url(upload_url(@pdf)) }
    format.html { redirect_to upload_path(@pdf) }
  end
end

#send_edi_messageObject

POST /credit_memos/:id/send_edi_message — re-queue the credit memo
810 message via Edi::CreditMemoEventProcessor. Used to recover
when a transient EDI failure left the partner without our 810.



333
334
335
336
337
# File 'app/controllers/credit_memos_controller.rb', line 333

def send_edi_message
  @credit_memo = CreditMemo.find(params[:id])
  Edi::CreditMemoEventProcessor.new.process(@credit_memo, :created)
  redirect_to(@credit_memo, info: 'Credit memo 810 EDI Message was queued for processing and transmission.')
end

#showObject

GET /credit_memos/1



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/controllers/credit_memos_controller.rb', line 66

def show
  @credit_memo = CreditMemo.find(params[:id])
  @show_kits_and_sn = params[:show_kits_and_sn].to_b
  @attachments = @credit_memo.uploads
  @pagy_activities, @activities = pagy(@credit_memo.activities.natural_order)

  set_context(@credit_memo)

  respond_to do |format|
    format.html # show.html.erb
    format.pdf do
      store = @credit_memo.original_order&.store || @credit_memo.company.stores.first
      pdf = CreditMemo::PdfGenerator.new(@credit_memo, store: store, show_tax_info: true, show_kits_and_sn: @show_kits_and_sn).generate

      send_data pdf,
                filename: "#{@credit_memo.file_name}.pdf",
                type: 'application/pdf',
                disposition: 'inline'
    end
  end
end

#tab_account_ledgerObject

GET /credit_memos/:id/tab_account_ledger — turbo-frame body for
the GL view of the credit memo's LedgerEntry postings.



397
398
399
400
401
402
403
404
# File 'app/controllers/credit_memos_controller.rb', line 397

def 
  @credit_memo = CreditMemo.find(params[:id])
  authorize!(:read, @credit_memo)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_attachmentsObject

GET /credit_memos/:id/tab_attachments — turbo-frame body for the
attachments tab.



352
353
354
355
356
357
358
359
# File 'app/controllers/credit_memos_controller.rb', line 352

def tab_attachments
  @credit_memo = CreditMemo.find(params[:id])
  authorize!(:read, @credit_memo)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_line_itemsObject

GET /credit_memos/:id/tab_line_items — turbo-frame body for the
line-items tab.



341
342
343
344
345
346
347
348
# File 'app/controllers/credit_memos_controller.rb', line 341

def tab_line_items
  @credit_memo = CreditMemo.find(params[:id])
  authorize!(:read, @credit_memo)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_outgoing_paymentsObject

GET /credit_memos/:id/tab_outgoing_payments — turbo-frame body
listing the OutgoingPayment rows that consumed this credit memo
(when the credit was paid out as a refund cheque).



375
376
377
378
379
380
381
382
# File 'app/controllers/credit_memos_controller.rb', line 375

def tab_outgoing_payments
  @credit_memo = CreditMemo.find(params[:id])
  authorize!(:read, @credit_memo)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_paymentsObject

GET /credit_memos/:id/tab_payments — turbo-frame body for the
incoming-payment tab.



363
364
365
366
367
368
369
370
# File 'app/controllers/credit_memos_controller.rb', line 363

def tab_payments
  @credit_memo = CreditMemo.find(params[:id])
  authorize!(:read, @credit_memo)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_receiptsObject

GET /credit_memos/:id/tab_receipts — turbo-frame body for the
receipts tab.



386
387
388
389
390
391
392
393
# File 'app/controllers/credit_memos_controller.rb', line 386

def tab_receipts
  @credit_memo = CreditMemo.find(params[:id])
  authorize!(:read, @credit_memo)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#transmitObject

GET /credit_memos/:id/transmit — kick off a "Send Credit Memo"
communication via CommunicationBuilder and route the user
through the editable communication-form, returning to the credit
memo's show page on completion.



324
325
326
327
328
# File 'app/controllers/credit_memos_controller.rb', line 324

def transmit
  @credit_memo = CreditMemo.find(params[:id])
  redir_params = CommunicationBuilder.new(resource: @credit_memo, current_user: current_user).to_params
  redirect_to new_communication_path(redir_params.merge({ return_path: credit_memo_path(@credit_memo) }))
end

#updateObject

PUT /credit_memos/1



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'app/controllers/credit_memos_controller.rb', line 241

def update
  @credit_memo = CreditMemo.find(params[:id])
  redirect_to(@credit_memo, error: 'You cannot edit this Credit Memo') and return if @credit_memo.editing_locked?

  tax_exempt = params[:credit_memo][:tax_exempt].to_i
  if tax_exempt.positive?
    resource_tax_rate = @credit_memo.resource_tax_rate
    @credit_memo.update(tax_exempt: true, resource_tax_rate_id: nil)
    resource_tax_rate.destroy
  end
  @customer = @credit_memo.customer
  @invoice = @credit_memo.original_invoice
  @addresses = @customer.all_addresses.to_a
  if @invoice
    @addresses << @invoice.shipping_address unless @addresses.include?(@invoice.shipping_address)
    @addresses << @invoice.billing_address unless @addresses.include?(@invoice.billing_address)
  end

  if @credit_memo.update(params[:credit_memo])
    redirect_to(@credit_memo, info: 'Credit memo was successfully updated.')
  else
    render action: 'edit', status: :unprocessable_content
  end
end