Class: Analytic::BudgetFact

Inherits:
ApplicationRecord
  • Object
show all
Includes:
Utility
Defined in:
app/models/analytic/budget_fact.rb,
app/models/analytic/budget_fact/refresher.rb

Overview

== Schema Information

Table name: analytic_budget_facts
Database name: primary

id :integer not null, primary key
actual_accumulated :integer
actual_accumulated_previous :integer
actual_accumulated_previous_diff :integer
actual_accumulated_previous_percent :decimal(12, 2)
actual_month :integer
actual_month_previous :integer
actual_month_previous_diff :integer
actual_month_previous_percent :decimal(12, 2)
budget_accumulated :integer
budget_accumulated_diff :integer
budget_accumulated_percent :decimal(12, 2)
budget_month :integer
budget_month_diff :integer
budget_month_percent :decimal(12, 2)
is_revenue :boolean
month :integer
year :integer
created_at :datetime not null
updated_at :datetime not null
budget_dimension_id :integer

Indexes

idx_budget_facts_year_month_dimension_unique (year,month,budget_dimension_id) UNIQUE

Defined Under Namespace

Classes: Refresher

Belongs to collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.build_percentage(diff, original) ⇒ Numeric

Build a signed percentage change for a diff against an original amount.

Parameters:

  • diff (Numeric)

    the change (actual − budget, year-over-year, …)

  • original (Numeric)

    the base amount the diff is measured against

Returns:

  • (Numeric)

    0 when unchanged, 100 when growing from a zero base, else the percentage



68
69
70
71
72
73
74
# File 'app/models/analytic/budget_fact.rb', line 68

def self.build_percentage(diff, original)
  if diff.zero?
    0
  else
    (original.zero? ? 100 : ((diff.to_f / original) * 100))
  end
end

.refresh_data_for_month(year = Date.current.year, month, incremental: false, &progress_block) ⇒ Boolean

Refresh budget facts for a given year/month.

The heavy lifting — building the in-memory ledger/budget indexes and writing
the fact rows — lives in Refresher, a fresh instance
per call. That keeps every run's indexes on its own object, so concurrent
refreshes (the :budgets Sidekiq queue runs at concurrency 16, and
BudgetRefresherAllWorker fans out 12 monthly jobs) can't read or overwrite
each other's data. Previously these were class instance variables guarded
only by a process-local mutex, which serialized refreshes without curing the
cross-report data bleed. See
doc/tasks/202606252210_MEMOIZATION_THREAD_SAFETY_AUDIT.md §3e.

Parameters:

  • year (Integer) (defaults to: Date.current.year)

    the year to refresh

  • month (Integer)

    the month to refresh

  • incremental (Boolean) (defaults to: false)

    if true, uses upsert (update or insert) instead of delete-all-reinsert

  • progress_block (Proc)

    optional block called as ->(percent, message) for progress reporting

Returns:

  • (Boolean)

    true on completion



59
60
61
# File 'app/models/analytic/budget_fact.rb', line 59

def self.refresh_data_for_month(year = Date.current.year, month, incremental: false, &progress_block)
  Refresher.new(year, month, incremental: incremental, progress_block: progress_block).run
end

Instance Method Details

#budget_dimensionAnalytic::BudgetDimension?

Returns the company / ledger-account /
business-unit / project / supplier slice this fact row aggregates.

Returns:

  • (Analytic::BudgetDimension, nil)

    the company / ledger-account /
    business-unit / project / supplier slice this fact row aggregates



40
# File 'app/models/analytic/budget_fact.rb', line 40

belongs_to :budget_dimension, optional: true

#is_zero?Boolean

Returns true when every monetary measure on this fact row is zero.

Returns:

  • (Boolean)

    true when every monetary measure on this fact row is zero



77
78
79
# File 'app/models/analytic/budget_fact.rb', line 77

def is_zero?
  [budget_month, actual_month, budget_accumulated, actual_accumulated, actual_month_previous, actual_accumulated_previous].all?(&:zero?)
end