Class: ItemLedgerEntriesController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- ItemLedgerEntriesController
- Includes:
- Controllers::Showable
- Defined in:
- app/controllers/item_ledger_entries_controller.rb
Overview
== Schema Information
Table name: item_ledger_entries
id :integer not null, primary key
store_id :integer
item_id :integer
category :string(255)
gl_date :date
quantity :integer
currency :string(255)
unit_cost :decimal(10, 4)
total_cost :decimal(10, 4)
description :string(255)
creator_id :integer
updater_id :integer
created_at :datetime
updated_at :datetime
shipment_receipt_item_id :integer
landed_cost_id :integer
old_unit_cogs :decimal(10, 4)
new_unit_cogs :decimal(10, 4)
location :string(255)
ledger_company_account_id :integer
ledger_transaction_id :integer
ledger_detail_project_id :integer
invoice_id :integer
rma_item_id :integer
old_qty_on_hand :integer
new_qty_on_hand :integer
business_unit_id :integer
quantity_eval :integer
cycle_count_item_id :integer
item_kit_id :integer
Constant Summary
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
Constants included from Www::SeoHelper
Www::SeoHelper::AWARDS, Www::SeoHelper::CA_ADDRESS, Www::SeoHelper::CA_BUSINESS_HOURS, Www::SeoHelper::CA_CONTACT_POINT, Www::SeoHelper::CA_CURRENCIES, Www::SeoHelper::CA_DESCRIPTION, Www::SeoHelper::CA_FOUNDING_DATE, Www::SeoHelper::CA_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::CA_LEGAL_NAME, Www::SeoHelper::CA_LOCAL_BUSINESS, Www::SeoHelper::CA_ONLINE_STORE, Www::SeoHelper::CA_RETURN_POLICY, Www::SeoHelper::CA_SALES_DEPARTMENT, Www::SeoHelper::CA_SERVICE_AREA, Www::SeoHelper::CA_URL, Www::SeoHelper::CA_VAT_ID, Www::SeoHelper::CA_WAREHOUSE_DEPARTMENT, Www::SeoHelper::CA_WAREHOUSE_HOURS, Www::SeoHelper::COMPANY_EMAIL, Www::SeoHelper::COMPANY_LOGO, Www::SeoHelper::COMPANY_NAME, Www::SeoHelper::COMPANY_SLOGAN, Www::SeoHelper::EXPERTISE, Www::SeoHelper::FAX_NUMBER, Www::SeoHelper::GS1_COMPANY_PREFIX, Www::SeoHelper::ISO6523_CODE, Www::SeoHelper::PAYMENT_METHODS, Www::SeoHelper::PHONE_NUMBER, Www::SeoHelper::PRIMARY_NAICS, Www::SeoHelper::REFUND_TYPE, Www::SeoHelper::RETURN_FEES, Www::SeoHelper::RETURN_METHOD, Www::SeoHelper::RETURN_POLICY_CATEGORY, Www::SeoHelper::SECONDARY_NAICS, Www::SeoHelper::SOCIAL_PROFILES, Www::SeoHelper::US_ADDRESS, Www::SeoHelper::US_BUSINESS_HOURS, Www::SeoHelper::US_CONTACT_POINT, Www::SeoHelper::US_CURRENCIES, Www::SeoHelper::US_DESCRIPTION, Www::SeoHelper::US_FOUNDING_DATE, Www::SeoHelper::US_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::US_IMAGE, Www::SeoHelper::US_LEGAL_NAME, Www::SeoHelper::US_LOCAL_BUSINESS, Www::SeoHelper::US_ONLINE_STORE, Www::SeoHelper::US_RETURN_POLICY, Www::SeoHelper::US_SALES_DEPARTMENT, Www::SeoHelper::US_SERVICE_AREA, Www::SeoHelper::US_TAX_ID, Www::SeoHelper::US_URL, Www::SeoHelper::US_WAREHOUSE_DEPARTMENT, Www::SeoHelper::US_WAREHOUSE_HOURS
Constants included from IconHelper
IconHelper::CUSTOM_ICON_MAP, IconHelper::CUSTOM_SVG_DIR, IconHelper::DEFAULT_FAMILY
Instance Method Summary collapse
- #check_for_errors(line_item_params) ⇒ Object
- #check_for_item_committed_errors(attrs) ⇒ Object
- #check_for_serial_number_errors(attrs) ⇒ Object
-
#cogs_adjustment ⇒ Object
GET /item_ledger_entries/cogs_adjustment — render the form for re-stating an item's per-unit cost (cost-of-goods-sold).
-
#do_cogs_adjustment ⇒ Object
POST /item_ledger_entries/do_cogs_adjustment — execute the cost re-statement via ItemLedgerEntry.cogs_adjustment.
-
#do_import_from_excel ⇒ Object
POST /item_ledger_entries/do_import_from_excel — pre-populate the #store_transfer form from a Roo-readable spreadsheet.
-
#do_inventory_adjustment ⇒ Object
POST /item_ledger_entries/do_inventory_adjustment — execute the adjustment.
-
#do_issue_to_account ⇒ Object
POST /item_ledger_entries/do_issue_to_account — post the write-off.
-
#do_item_reclassification ⇒ Object
POST /item_ledger_entries/do_item_reclassification — validate the from / to lists and post the paired ledger entries (qty out of source SKU, qty in to destination SKU) so the GL stays balanced and on-hand quantities match the physical recount.
-
#do_location_transfer ⇒ Object
POST /item_ledger_entries/do_location_transfer — execute the within-warehouse move.
-
#do_store_transfer ⇒ Object
POST /item_ledger_entries/do_store_transfer — execute the cross-warehouse transfer.
-
#do_total_cost_adjustment ⇒ Object
POST /item_ledger_entries/do_total_cost_adjustment — execute the lump adjustment via ItemLedgerEntry.total_cost_adjustment.
-
#download_sample_excel ⇒ Object
GET /item_ledger_entries/download_sample_excel — serve the canonical store-transfer template so users start from the right column layout.
-
#index ⇒ Object
GET /item_ledger_entries — landing page for the inventory ledger.
-
#inventory_adjustment ⇒ Object
GET /item_ledger_entries/inventory_adjustment — render the generic inventory-adjustment form (positive or negative qty delta against a store/location).
-
#issue_to_account ⇒ Object
GET /item_ledger_entries/issue_to_account — render the form for writing inventory off to an arbitrary GL account (samples, write-offs, donations).
-
#item_reclassification ⇒ Object
GET /item_ledger_entries/item_reclassification — render the form for converting one SKU into another (e.g. a recall or rev-change where stock-on-hand carries over to a new SKU).
-
#location_transfer ⇒ Object
GET /item_ledger_entries/location_transfer — render the location-to-location move form (e.g. moving stock from Receiving to Mezzanine within one warehouse).
-
#store_transfer ⇒ Object
GET /item_ledger_entries/store_transfer — render the cross-warehouse store-transfer form.
-
#total_cost_adjustment ⇒ Object
GET /item_ledger_entries/total_cost_adjustment — render the form for adjusting the on-hand value by an absolute dollar amount (rather than a per-unit cogs change).
Methods included from Controllers::Showable
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
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?, #stamp_impersonation_context, #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, #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 ApplicationHelper
#better_number_to_currency, #check_force_logout, #check_or_cross, #check_or_times, #embedded_tab_frame_id, #error_messages, #general_disclaimer_on_product_installation_and_local_codes, #gridjs_from_html_table, #gridjs_table, #is_wy_ip, #line_break, #parent_layout, #pass_or_fail, #render_error_messages_list, #render_video_card, #resolved_auth_form_turbo_frame, #return_path_or, #safe_css_color, #set_return_path_if_present, #set_section_if_present, #tab_frame_id, #to_underscore, #track_page?, #turbo_section_wrapper, #turbo_tabs_request?, #url_on_same_domain_as_request, #widget_index_daily_focus_index_path, #working_hours?, #yes_or_no, #yes_or_no_highlighted, #yes_or_no_with_check_or_cross, #youtube_video
Methods included from UppyUploaderHelper
#file_uploader, #image_uploader, #large_file_uploader_s3, #lead_sketch_uploader, #rma_image_uploader, #rma_image_uploader_s3, #uppy_uploader, #video_uploader
Methods included from Www::ImagesHelper
#image_asset_tag, #image_asset_url
Methods included from Www::SeoHelper
#add_page_schema, #add_webpage_schema, #canada?, #company_social_links, #ensure_context_json, #json_ld_script_tag, #local_business_schema, #online_store_id, #online_store_schema, #page_main_entity, #page_main_entity_json, #render_auto_collection_page_schema, #render_collection_page_schema, #render_local_business_schema, #render_online_store_schema, #render_page_schemas, #render_page_video_schemas, #render_webpage_schema, #render_webpage_schema_with_collections, #usa?
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?
Methods included from IconHelper
#account_nav_icon, #fa_icon, #star_rating_html
Instance Method Details
#check_for_errors(line_item_params) ⇒ Object
558 559 560 561 562 563 564 565 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 558 def check_for_errors(line_item_params) errors = 0 line_item_params.each do |_index, attrs| errors += 1 if attrs[:store_item_id].blank? || attrs[:qty].blank? || (attrs[:qty].to_i <= 0) || (attrs[:qty].to_i > attrs[:qty_on_hand].to_i) errors += check_for_serial_number_errors(attrs) end errors end |
#check_for_item_committed_errors(attrs) ⇒ Object
583 584 585 586 587 588 589 590 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 583 def check_for_item_committed_errors(attrs) errors = 0 if attrs[:store_item_id].present? si = StoreItem.find(attrs[:store_item_id]) errors += 1 if si.qty_committed > (si.qty_on_hand + attrs[:qty].to_i) end errors end |
#check_for_serial_number_errors(attrs) ⇒ Object
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 567 def check_for_serial_number_errors(attrs) errors = 0 if attrs[:store_item_id].present? si = StoreItem.find(attrs[:store_item_id]) if si.item.require_reservation? if attrs[:serial_number_id].blank? errors += 1 else serial_number_qty = si.serial_numbers.where(id: attrs[:serial_number_id]).first.qty errors += 1 if serial_number_qty != attrs[:qty].to_i.abs end end end errors end |
#cogs_adjustment ⇒ Object
GET /item_ledger_entries/cogs_adjustment — render the form
for re-stating an item's per-unit cost (cost-of-goods-sold).
Affects on-hand valuation but not quantity.
416 417 418 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 416 def cogs_adjustment @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { location: nil }) end |
#do_cogs_adjustment ⇒ Object
POST /item_ledger_entries/do_cogs_adjustment — execute the
cost re-statement via ItemLedgerEntry.cogs_adjustment.
Refuses negative new-cogs values.
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 423 def do_cogs_adjustment @line_items = params[:line_items] store_id = params.dig(:adjustment, :store) if store_id.blank? flash.now[:error] = 'You must specify a valid store to continue.' render :cogs_adjustment, status: :unprocessable_content else store = Store.find(store_id) date = begin Date.strptime(params.dig(:adjustment, :date), '%Y-%m-%d') rescue StandardError Date.current end description = params.dig(:adjustment, :description).presence errors = 0 params[:line_items].each do |_index, attrs| errors += 1 if attrs[:store_item_id].blank? || attrs[:new_cogs].blank? || BigDecimal(attrs[:new_cogs]).negative? end if errors.positive? flash.now[:error] = 'You must specify a valid item and new cogs of 0 or more on each line.' render :cogs_adjustment, status: :unprocessable_content else ItemLedgerEntry.cogs_adjustment(store, params[:line_items], date, description) flash[:info] = 'COGS adjustment has been processed successfully.' redirect_to item_ledger_entries_path end end end |
#do_import_from_excel ⇒ Object
POST /item_ledger_entries/do_import_from_excel — pre-populate
the #store_transfer form from a Roo-readable spreadsheet.
Skips unknown SKUs with a warning rather than failing the
whole import (operators were getting locked out of the screen
by single-row typos).
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 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 119 def do_import_from_excel = {} items_list = [] imported_store_transfer_items_excel = params.dig(:upload, :imported_store_transfer_items_excel) if imported_store_transfer_items_excel file_path = imported_store_transfer_items_excel.tempfile.path.to_s require 'roo' st_items_xlsx = Roo::Spreadsheet.open(file_path) skipped_skus = [] st_items_xlsx.parse(headers: true, clean: true).each_with_index do |row, index| next unless index >= 1 next if row.map { |_k, v| v }.uniq.compact.blank? [:store_id] = row['store_id'].to_i if row['store_id'].present? && index == 1 next unless row['sku'].present? && row['quantity'].present? item = Item.find_by(sku: row['sku']) unless item skipped_skus << row['sku'] next end items_list << [item.id, row['quantity'].to_i] end [:items_list] = items_list.to_h flash[:warning] = "Skipped unknown SKU(s): #{skipped_skus.join(', ')}" if skipped_skus.present? redirect_to store_transfer_item_ledger_entries_path(options: .stringify_keys) else flash.now[:error] = 'No file was uploaded' render :import_from_excel, status: :unprocessable_content end end |
#do_inventory_adjustment ⇒ Object
POST /item_ledger_entries/do_inventory_adjustment — execute the
adjustment. For negative deltas it additionally enforces
serial-number coverage and that the new on-hand wouldn't go
below committed (we don't want to let a write-off orphan a
picked order). override_kit_restriction=true lets the user
adjust component-level quantities on a kit SKU.
369 370 371 372 373 374 375 376 377 378 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 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 369 def do_inventory_adjustment @adjustment = OpenStruct.new({ date: Date.current }.merge(params[:adjustment] || {})) @line_items = params[:line_items] store_id = @adjustment.store if store_id.blank? flash.now[:error] = 'You must specify a valid store to continue.' render :inventory_adjustment, status: :unprocessable_content else store = Store.find(store_id) date = begin Date.strptime(@adjustment.date, '%Y-%m-%d') rescue StandardError Date.current end description = @adjustment.description.presence override_kit_restriction = @adjustment.override_kit_restriction.to_b errors = 0 qty_committed_errors = 0 @line_items.each do |_index, attrs| errors += 1 if attrs[:store_item_id].blank? || attrs[:qty].blank? if attrs[:qty].present? && attrs[:qty].to_i.negative? errors += check_for_serial_number_errors(attrs) qty_committed_errors += check_for_item_committed_errors(attrs) end end if errors.positive? || qty_committed_errors.positive? flash.now[:error] = 'You must specify a valid item and quantity on each line. You must specify a serial number with matching positive qty if item requested requires reservation and has a negative quantity.' if errors.positive? flash.now[:error] = "You cannot reduce an item's quantity below its available quantity (on hand - committed)." if qty_committed_errors.positive? render :inventory_adjustment, status: :unprocessable_content else begin ItemLedgerEntry.inventory_adjustment(store, @line_items, date, description, override_kit_restriction) flash[:info] = 'Inventory adjustment has been processed successfully. Please note that kit might take up to 30 seconds to have their new stock updated.' redirect_to_return_path_or_default item_ledger_entries_path rescue ItemLedgerEntry::ItemLedgerEntryError => e flash.now[:error] = e render :inventory_adjustment, status: :unprocessable_content end end end end |
#do_issue_to_account ⇒ Object
POST /item_ledger_entries/do_issue_to_account — post the
write-off. Enforces the standard P&L-account → business-unit
rule (required, and must match the store's company) before
delegating to #check_for_errors for per-line validation.
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 296 def do_issue_to_account @line_items = params[:line_items] adjustment_params = params.fetch(:adjustment, {}) store_id = adjustment_params[:store] company_account_id = adjustment_params[:company_account_id] if store_id.blank? || company_account_id.blank? flash.now[:error] = 'You must specify a valid store and account.' render :issue_to_account, status: :unprocessable_content else store = Store.find(store_id) company_account = LedgerCompanyAccount.find(company_account_id) business_unit = BusinessUnit.where(id: adjustment_params[:business_unit_id]).first project = LedgerDetailProject.where(id: adjustment_params[:project_id]).first date = begin Date.strptime(adjustment_params[:date], '%Y-%m-%d') rescue StandardError Date.current end if company_account.requires_business_unit? && business_unit.nil? flash.now[:error] = 'You must specify a business unit if issuing to a profit and loss account' render(:issue_to_account, status: :unprocessable_content) and return elsif company_account.requires_business_unit? && business_unit.company_id != store.company_id flash.now[:error] = 'Business unit must belong to the same company as the store' render(:issue_to_account, status: :unprocessable_content) and return end errors = check_for_errors(params[:line_items]) if errors.positive? flash.now[:error] = 'You must specify a valid item with a quantity greater than 0 and not more than current quantity. You must specify a serial number with matching qty if item requested requires reservation.' render :issue_to_account, status: :unprocessable_content else begin ItemLedgerEntry.issue_to_account(store, params[:line_items], company_account, business_unit, date, project, adjustment_params[:description]) flash[:info] = 'Item have been issued to account successfully.' redirect_to item_ledger_entries_path rescue ActiveRecord::RecordInvalid => e flash.now[:error] = e.record.errors..join(', ') render :issue_to_account, status: :unprocessable_content end end end end |
#do_item_reclassification ⇒ Object
POST /item_ledger_entries/do_item_reclassification —
validate the from / to lists and post the paired ledger
entries (qty out of source SKU, qty in to destination SKU)
so the GL stays balanced and on-hand quantities match the
physical recount.
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 513 def do_item_reclassification adjustment_params = params.fetch(:adjustment, {}) @from_line_items = params[:from_line_items] @to_line_items = params[:to_line_items] date = begin Date.strptime(adjustment_params[:date], '%Y-%m-%d') rescue StandardError Date.current end if params[:from_line_items].nil? || params[:to_line_items].nil? || adjustment_params[:store].blank? flash.now[:error] = 'You must specify a valid store and items to reclassify.' render :item_reclassification, status: :unprocessable_content else = [] from_errors = check_for_errors(params[:from_line_items]) << 'You must specify valid item(s) with a quantity greater than 0 and not more than current quantity. You must specify a serial number with matching qty if item requested requires reservation.' if from_errors.positive? to_errors = 0 params[:to_line_items].each do |_index, attrs| to_errors += 1 if attrs[:item_id].blank? || attrs[:qty].blank? || attrs[:qty].to_i <= 0 || attrs[:location].blank? end << 'You must specify item(s) to reclassify to with a destination location and quantity greater than 0.' if to_errors.positive? kit_errors = 0 params[:from_line_items].merge(params[:to_line_items]).each do |_index, attrs| kit_errors += 1 if attrs[:item_id] && Item.find(attrs[:item_id]).is_kit? end << 'Kits cannot be reclassified.' if kit_errors.positive? if .length.positive? flash.now[:error] = .join(', ') render :item_reclassification, status: :unprocessable_content else store = Store.find(adjustment_params[:store]) ItemLedgerEntry.item_reclassification(store, params[:from_line_items], params[:to_line_items], date, adjustment_params[:description]) flash[:info] = 'Item transfer has been initiated.' redirect_to_return_path_or_default item_ledger_entries_path end end end |
#do_location_transfer ⇒ Object
POST /item_ledger_entries/do_location_transfer — execute the
within-warehouse move. Per-line validation runs through
#check_for_errors before delegating to
ItemLedgerEntry.location_transfer; surfaces any failure as a
combined flash and re-renders the form so the user can fix
in-place.
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 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 87 def do_location_transfer adjustment_params = params.fetch(:adjustment, {}) @line_items = params[:line_items] if adjustment_params[:store].blank? flash.now[:error] = 'You must specify a valid store to continue.' render :location_transfer, status: :unprocessable_content else store = Store.find(adjustment_params[:store]) date = begin Date.strptime(adjustment_params[:date], '%Y-%m-%d') rescue StandardError Date.current end errors = check_for_errors(params[:line_items]) if errors.positive? flash.now[:error] = 'You must specify a valid item with a quantity greater than 0 and not more than current quantity. You must specify a serial number with matching qty if item requested requires reservation.' render :location_transfer, status: :unprocessable_content else ItemLedgerEntry.location_transfer(store, params[:line_items], date, adjustment_params[:description]) flash[:info] = 'Items have been transferred successfully.' redirect_to_return_path_or_default item_ledger_entries_path end end end |
#do_store_transfer ⇒ Object
POST /item_ledger_entries/do_store_transfer — execute the
cross-warehouse transfer. Validates per-line (item present,
quantity in stock, serial-number availability if reservable,
and that the item is in both the source and destination
store's primary catalog). Creates a transfer order via
ItemLedgerEntry.store_transfer so the warehouse pipeline
picks up the move like any other order, and lands the user on
the inventory ledger with a link to the new transfer order.
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 282 283 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 208 def do_store_transfer adjustment_params = params.to_h.fetch(:adjustment, {}) new_address_params = params.to_h.fetch(:address, {}) @adjustment = OpenStruct.new(adjustment_params) @line_items = params.fetch(:line_items, {}).to_h.with_indifferent_access @new_address = Address.new(new_address_params) @enter_new_address = adjustment_params[:enter_new_address] @addresses = [] # Needed by the store_transfer template's destination_address_id input so # the user's selection survives a re-render after validation failure. @preselected_destination_address = adjustment_params[:destination_address_id].present? ? Address.find_by(id: adjustment_params[:destination_address_id]) : nil if adjustment_params[:store].blank? || adjustment_params[:new_store_id].blank? || adjustment_params[:store] == adjustment_params[:new_store_id] flash.now[:error] = 'You must specify valid stores to continue.' render :store_transfer, status: :unprocessable_content elsif adjustment_params[:destination_address_id].blank? && new_address_params.dig(:street1).blank? flash.now[:error] = 'You must choose an existing destination address or enter a new one to continue.' render :store_transfer, status: :unprocessable_content else store = Store.find(adjustment_params[:store]) new_store = Store.find(adjustment_params[:new_store_id]) date = begin Date.strptime(adjustment_params[:date], '%Y-%m-%d') rescue StandardError Date.current end new_store_consignee_party_id = StoreTransfer.where(from_store_id: store.id).where(to_store_id: new_store.id).first.consignee_party_id @addresses = Address.where(party_id: new_store_consignee_party_id).order(state_code: :asc).map { |a| [a.to_s, a.id] } if @enter_new_address.to_b @new_address.party_id = new_store_consignee_party_id unless @new_address.save flash.now[:error] = 'New address is invalid.' render(:store_transfer, status: :unprocessable_content) and return end address_id = @new_address.id else address_id = adjustment_params[:destination_address_id] end errors = [] params[:line_items].each do |index, attrs| line_prefix = "Line #{index.to_i + 1}: " errors << "#{line_prefix} Store Item missing" if attrs[:store_item_id].blank? errors << "#{line_prefix} Quantity to transfer missing" if attrs[:qty].blank? qty_requested = attrs[:qty].to_i errors << "#{line_prefix} Quantity to transfer must be > 0" if qty_requested <= 0 qty_on_hand = attrs[:qty_on_hand].to_i errors << "#{line_prefix} Quantity to transfer exceeds stock on hand" if qty_requested > qty_on_hand # Serial number check if attrs[:serial_number_id].present? && (sn = SerialNumber.find(attrs[:serial_number_id])) && qty_requested > sn.qty_available errors << "#{line_prefix} Quantity to transfer must not be higher than available qty of serial number #{attrs[:serial_number_id]}" end store_item = StoreItem.find(attrs[:store_item_id]) item = store_item.item cat_item = store.primary_catalog.catalog_items.joins(:store_item).find_by(store_items: { item_id: item.id }) errors << "Unable to find given item SKU: #{item.sku} / ID: #{item.id} in origin store's primary catalog NAME: #{store.primary_catalog.name} / ID: #{store.primary_catalog_id}." if cat_item.nil? dest_cat_item = new_store.primary_catalog.catalog_items.joins(:store_item).find_by(store_items: { item_id: item.id }) errors << "Unable to find given item SKU: #{item.sku} / ID: #{item.id} in destination store's primary catalog NAME: #{new_store.primary_catalog.name} / ID: #{new_store.primary_catalog_id}." if dest_cat_item.nil? end if errors.present? flash.now[:error] = errors.to_sentence.capitalize render :store_transfer, status: :unprocessable_content else order = ItemLedgerEntry.store_transfer( store: store, line_items: params[:line_items], new_store: new_store, shipping_option_id: params[:shipping_option_id], gl_date: date, description: adjustment_params[:description], address_id: address_id, fulfillment_order_reference: adjustment_params[:fulfillment_order_reference] ) link = view_context.link_to("Order #{order.reference_number}", order) flash[:info] = "Item transfer has been initiated as #{link}" redirect_to item_ledger_entries_path end end end |
#do_total_cost_adjustment ⇒ Object
POST /item_ledger_entries/do_total_cost_adjustment — execute
the lump adjustment via
ItemLedgerEntry.total_cost_adjustment. Honors
override_kit_restriction so kit components can be adjusted
in place when needed.
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 467 def do_total_cost_adjustment adjustment_params = params.fetch(:adjustment, {}) @line_items = params[:line_items] if adjustment_params[:store].blank? flash.now[:error] = 'You must specify a valid store to continue.' render :total_cost_adjustment, status: :unprocessable_content else store = Store.find(adjustment_params[:store]) date = begin Date.strptime(adjustment_params[:date], '%Y-%m-%d') rescue StandardError Date.current end errors = 0 params[:line_items].each do |_index, attrs| errors += 1 if attrs[:store_item_id].blank? || attrs[:total_cost].blank? end if errors.positive? flash.now[:error] = 'You must specify a valid item and an amount to adjust by on each line.' render :total_cost_adjustment, status: :unprocessable_content else ItemLedgerEntry.total_cost_adjustment(store, params[:line_items], date, params[:description], adjustment_params[:override_kit_restriction].to_b) flash[:info] = 'Total cost adjustment has been processed successfully.' redirect_to item_ledger_entries_path end end end |
#download_sample_excel ⇒ Object
GET /item_ledger_entries/download_sample_excel — serve the
canonical store-transfer template so users start from the
right column layout.
156 157 158 159 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 156 def download_sample_excel file_path = Rails.root.join('data/import_templates/store_transfers/store_transfer_items_template.xlsx') send_file_accelerated(file_path, download: true, preserve_source: true) end |
#index ⇒ Object
GET /item_ledger_entries — landing page for the inventory ledger.
Builds a default ItemLedgerEntrySearch with sensible columns
so the user immediately sees the most-recent inventory movements.
params[:links] carries deep-link URLs the helper renders into
the entry/document/gl columns.
49 50 51 52 53 54 55 56 57 58 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 49 def index @search = ItemLedgerEntrySearch.new(set_limit: 50, employee: @context_user, query_params: {}, selected_columns: %w[entry_link category store_id item_link quantity quantity_eval currency unit_cost total_cost old_qty new_qty old_cogs new_cogs location description document_link account business_unit project gl_link created_at cycle_count_id], sort_columns: ['created_at desc']) @links = params[:links] || {} end |
#inventory_adjustment ⇒ Object
GET /item_ledger_entries/inventory_adjustment — render the
generic inventory-adjustment form (positive or negative qty
delta against a store/location). Pre-fills from
item_id/store/from deep-link or a single
store_item_id so the user lands on the right SKU directly.
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 346 def inventory_adjustment @adjustment = OpenStruct.new({ date: Date.current }.merge(params[:adjustment] || {})) if @adjustment.item_id && @adjustment.store && @adjustment.from if (store_item = StoreItem.where(store_id: @adjustment.store, item_id: @adjustment.item_id, location: @adjustment.from).first) @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { 'location' => params[:from], 'store_item_id' => store_item.id, 'store_item_name' => store_item.sku, 'qty_on_hand' => store_item.qty_on_hand, 'qty_available' => store_item.qty_available }) end elsif params[:store_item_id] store_item = StoreItem.find(params[:store_item_id]) @adjustment.store = store_item.store_id @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { 'location' => store_item.location, 'store_item_id' => store_item.id, 'store_item_name' => store_item.sku, 'qty_on_hand' => store_item.qty_on_hand, 'qty_available' => store_item.qty_available }) else @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { 'location' => nil }) end end |
#issue_to_account ⇒ Object
GET /item_ledger_entries/issue_to_account — render the form
for writing inventory off to an arbitrary GL account (samples,
write-offs, donations). Empty form with one blank line item.
288 289 290 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 288 def issue_to_account @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { location: nil }) end |
#item_reclassification ⇒ Object
GET /item_ledger_entries/item_reclassification — render the
form for converting one SKU into another (e.g. a recall or
rev-change where stock-on-hand carries over to a new SKU).
Two parallel line-item lists — from_* reduces the source
SKU; to_* increases the destination — both pre-fillable
via deep-link params.
503 504 505 506 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 503 def item_reclassification @from_line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { location: params[:from].presence, store_item_id: params[:from_store_item_id].presence, qty: 1 }) @to_line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { location: params[:to].presence, item_id: params[:to_item_id].presence, qty: 1 }) end |
#location_transfer ⇒ Object
GET /item_ledger_entries/location_transfer — render the
location-to-location move form (e.g. moving stock from
Receiving to Mezzanine within one warehouse). Query params
item_id, store_id, from, to deep-link from the
store-item show page.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 65 def location_transfer if params[:item_id] && params[:store_id] && params[:from] store_item = StoreItem.where(store_id: params[:store_id], item_id: params[:item_id], location: params[:from]).first @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { 'location' => params[:from], 'store_item_id' => store_item.id, 'store_item_name' => store_item.sku, 'qty_on_hand' => store_item.qty_on_hand, 'qty_available' => store_item.qty_available, 'new_location' => params[:to] }) else @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { 'location' => nil }) end end |
#store_transfer ⇒ Object
GET /item_ledger_entries/store_transfer — render the
cross-warehouse store-transfer form. Pre-fills from the
options hash deep-link param (set by either the spreadsheet
importer or a deep-link from the store-item show page).
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 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 165 def store_transfer = params[:options].present? ? params[:options].to_hash.symbolize_keys : {} items_list = [:items_list].presence s = [:store_id].present? ? Store.find_by(id: [:store_id]) : nil new_store = [:new_store_id].present? ? Store.find_by(id: [:new_store_id]) : nil @preselected_destination_address = [:destination_address_id].present? ? Address.find_by(id: [:destination_address_id]) : nil adjustment_attrs = { date: Date.current } adjustment_attrs[:store] = [s.name, s.id, { data: { company_id: s.company_id } }] if s adjustment_attrs[:new_store_id] = new_store.id if new_store adjustment_attrs[:destination_address_id] = @preselected_destination_address.id if @preselected_destination_address if s && items_list @adjustment = OpenStruct.new(adjustment_attrs) line_items = {} items_list.each_with_index do |(k, v), index| i = Item.find_by(id: k) next unless i si = StoreItem.where(store_id: s.id, item_id: i.id).first next unless si line_items[index.to_s] = { location: 'AVAILABLE', store_item_id: [i.sku, si.id], qty_on_hand: si.qty_on_hand, qty_available: si.qty_available, qty: v.to_i } end @line_items = line_items.presence || { '0' => { location: nil } }.with_indifferent_access else @adjustment = OpenStruct.new(adjustment_attrs) @line_items = { '0' => { location: nil } }.with_indifferent_access end @new_address = Address.new @enter_new_address = nil @addresses = [] end |
#total_cost_adjustment ⇒ Object
GET /item_ledger_entries/total_cost_adjustment — render the
form for adjusting the on-hand value by an absolute dollar
amount (rather than a per-unit cogs change). Used for landed-
cost catch-ups and similar lump adjustments.
458 459 460 |
# File 'app/controllers/item_ledger_entries_controller.rb', line 458 def total_cost_adjustment @line_items = ActiveSupport::HashWithIndifferentAccess.new('0' => { location: nil }) end |