class Accounts

Attributes

check_progress[R]
check_state[R]

Public Class Methods

archive(month_start = 1, this_year = nil, only_account = nil) click to toggle source
# File lib/africompta/entities/account.rb, line 292
def self.archive(month_start = 1, this_year = nil, only_account = nil)
  if not this_year
    now = Time.now
    this_year = now.year
    now.month < month_start and this_year -= 1
  end

  root = AccountRoot.actual
  if only_account
    root = only_account
  elsif not root
    dputs(0) { 'Error: Root-account not available!' }
    return false
  elsif (root.account_id > 0)
    dputs(0) { "Error: Can't archive with Root is not in root: #{root.account_id.inspect}!" }
    return false
  end

  archive = AccountRoot.archive
  if not archive
    archive = self.create('Archive')
  end

  years_archived = {}
  archive.accounts.each { |a| years_archived[a.name.to_i] = a }

  dputs(2) { 'Got root and archive' }
  # For every account we search the most-used year, so
  # that we can move the account to that archive. This way
  # we omit as many as possible updates for the clients, as
  # every displacement of a movement will have to be updated,
  # while the displacement of an account is much simpler
  root.get_tree_depth { |acc|
    dputs(2) { "Looking at account #{acc.path}" }
    years = search_account(acc, month_start)

    if years.size > 0
      most_used = last_used = this_year
      if acc.accounts.count == 0
        most_used = years.key(years.values.max)
        last_used = years.keys.max
        years.delete most_used
      end
      acc_path = acc.path

      dputs(3) { "most_used: #{most_used} - last_used: #{last_used}" +
          "- acc_path: #{acc_path}" }

      # First move all other movements around
      if years.keys.size > 0
        create_accounts(acc, years, years_archived, this_year)

        move_movements(acc, years, month_start)
      end

      if most_used != this_year
        # Now move account to archive-year of most movements
        parent = archive_parent(acc, years_archived, most_used)
        if double = Accounts.get_by_path("#{parent.get_path}::#{acc.name}")
          dputs(3) { "Account #{acc.path_id} already exists in #{parent.path_id}" }
          # Move all movements
          acc.movements.each { |m|
            dputs(4) { "Moving movement #{m.to_json}" }
            value = m.value
            m.value = 0
            if m.account_src == acc
              m.account_src_id = double
            else
              m.account_dst_id = double
            end
            m.value = value
          }
          # Delete acc
          acc.delete
        else
          dputs(3) { "Moving account #{acc.path_id} to #{parent.path_id}" }
          acc.parent = parent
        end
      end

      # Check whether we need to add the account to the current year
      if (last_used >= this_year - 1) and
          (most_used != this_year)
        dputs(3) { "Adding #{acc_path} to this year with mult #{acc.multiplier}" }
        Accounts.create_path(acc_path, 'Copied from archive', false,
                             acc.multiplier, acc.keep_total)
      end

      if acc.keep_total
        dputs(2) { "Keeping total for #{acc_path}" }
        # And create a trail so that every year contains the previous
        # years worth of "total"
        sum_up_total(acc_path, years_archived, month_start)
        dputs(5) { "acc_path is now #{acc_path}" }
      else
        dputs(2) { "Not keeping for #{acc_path}" }
      end
    else
      dputs(3) { "Empty account #{acc.movements.count} - #{acc.accounts.count}" }
    end

    if acc.accounts.count == 0
      movs = acc.movements
      case movs.count
        when 0
          dputs(3) { "Deleting empty account #{acc.path}" }
          if acc.path != 'Root'
            acc.delete
          else
            dputs(2) { 'Not deleting root!' }
          end
        when 1
          dputs(3) { "Found only one movement for #{acc.path}" }
          if movs.first.desc =~ /^-- Sum of/
            dputs(3) { 'Deleting account which has only a sum' }
            movs.first.delete
            if acc.path != 'Root'
              acc.delete
            else
              dputs(2) { 'Not deleting root!' }
            end
          end
      end
    end
  }
  if DEBUG_LVL >= 3
    self.dump
  end
end
create(name, desc = 'Too lazy', parent = nil, global_id = '', mult = nil) click to toggle source
Calls superclass method
# File lib/africompta/entities/account.rb, line 26
def self.create(name, desc = 'Too lazy', parent = nil, global_id = '', mult = nil)
  dputs(5) { "Parent is #{parent.inspect}" }
  if parent
    if parent.class != Account && parent != AccountRoot
      parent = Accounts.matches_by_id(parent).first
    end
    mult ||= parent.multiplier
    a = super(:name => name, :desc => desc, :account_id => parent.id,
              :global_id => global_id.to_s, :multiplier => mult,
              :deleted => false, :keep_total => parent.keep_total)
  else
    mult ||= 1
    a = super(:name => name, :desc => desc, :account_id => 0,
              :global_id => global_id.to_s, :multiplier => mult,
              :deleted => false, :keep_total => false)
  end
  a.total = 0
  if global_id == ''
    a.global_id = Users.match_by_name('local').full + '-' + a.id.to_s
  end
  a.new_index
  dputs(2) { "Created account #{a.path_id} - #{a.inspect}" }
  a
end
create_path(path, desc = '', double_last = false, mult = 1, keep_total = false) click to toggle source
# File lib/africompta/entities/account.rb, line 51
def self.create_path(path, desc = '', double_last = false, mult = 1,
    keep_total = false)
  dputs(3) { "Path: #{path.inspect}, mult: #{mult}" }
  elements = path.split('::')
  parent = AccountRoot
  while elements.size > 0
    name = elements.shift
    dputs(4) { "Working on element #{name} with base of #{parent.path_id}" }
    child = parent.accounts.find { |a|
      dputs(5) { "Searching child #{a.name} - #{a.path_id}" }
      a.name == name
    }
    child and dputs(4) { "Found existing child #{child.path_id}" }
    if (not child) or (elements.size == 0 and double_last)
      dputs(4) { "Creating child #{name}" }
      child = Accounts.create(name, desc, parent)
    end
    parent = child
  end
  parent.set_nochildmult(name, desc, nil, mult, [], keep_total)
end
dump(mov = false) click to toggle source
# File lib/africompta/entities/account.rb, line 428
def self.dump(mov = false)
  dputs(1) { 'Root-tree is now' }
  AccountRoot.actual.dump_rec(mov)
  if archive = AccountRoot.archive
    dputs(1) { 'Archive-tree is now' }
    archive.dump_rec(mov)
  else
    dputs(1) { 'No archive-tree' }
  end
  AccountRoot.accounts.each { |a|
    dputs(1) { "Root-Account: #{a.inspect}" }
  }
end
dump_raw(mov = false) click to toggle source
# File lib/africompta/entities/account.rb, line 422
def self.dump_raw(mov = false)
  Accounts.search_all.each { |a|
    a.dump(mov)
  }
end
find_by_path(parent) click to toggle source
# File lib/africompta/entities/account.rb, line 145
def self.find_by_path(parent)
  return Accounts.get_by_path(parent)
end
from_s(str) click to toggle source

Gets an account from a string, if it doesn't exist yet, creates it. It will update it anyway.

# File lib/africompta/entities/account.rb, line 75
def self.from_s(str)
  str.force_encoding(Encoding::UTF_8)
  desc, str = str.split("\r")
  if not str
    dputs(0) { "Error: Invalid account found: #{desc}" }
    return [-1, nil]
  end
  global_id, total, name, multiplier, par,
      deleted_s, keep_total_s = str.split("\t")
  total, multiplier = total.to_f, multiplier.to_f
  deleted = deleted_s == 'true'
  keep_total = keep_total_s == 'true'
  dputs(3) { [global_id, total, name, multiplier].inspect }
  dputs(3) { [par, deleted_s, keep_total_s].inspect }
  dputs(5) { "deleted, keep_total is #{deleted.inspect}, #{keep_total.inspect}" }
  dputs(3) { 'Here comes the account: ' + global_id.to_s }
  dputs(3) { "global_id: #{global_id}" }

  if par.to_s.length > 0
    parent = Accounts.match_by_global_id(par)
    parent_id = parent.id
    dputs(3) { "Parent is #{parent.inspect}" }
  else
    parent = nil
    parent_id = 0
  end

  # Does the account already exist?
  our_a = nil
  if not (our_a = Accounts.match_by_global_id(global_id))
    # Create it
    dputs(3) { "Creating account #{name} - #{desc} - #{parent} - #{global_id}" }
    our_a = Accounts.create(name, desc, parent, global_id)
  end
  # And update it
  our_a.deleted = deleted
  our_a.set_nochildmult(name, desc, parent_id, multiplier, [], keep_total)
  our_a.global_id = global_id
  dputs(2) { "Saved account #{name} with index #{our_a.rev_index} and global_id #{our_a.global_id}" }
  dputs(4) { "Account is now #{our_a.inspect}" }
  return our_a
end
get_by_path(parent, elements = nil) click to toggle source
# File lib/africompta/entities/account.rb, line 123
def self.get_by_path(parent, elements = nil)
  if not elements
    if parent
      return get_by_path(AccountRoot, parent.split('::'))
    else
      return nil
    end
  end

  child = elements.shift
  parent.accounts.each { |a|
    if a.name == child
      if elements.length > 0
        return get_by_path(a, elements)
      else
        return a
      end
    end
  }
  return nil
end
get_by_path_or_create(p, desc = '', last = false, mult = 1, keep = false) click to toggle source
# File lib/africompta/entities/account.rb, line 118
def self.get_by_path_or_create(p, desc = '', last = false, mult = 1, keep = false)
  get_by_path(p) or
      create_path(p, desc, last, mult, keep)
end
get_id_by_path(p) click to toggle source
# File lib/africompta/entities/account.rb, line 149
def self.get_id_by_path(p)
  if a = get_by_path(p)
    return a.id.to_s
  else
    return nil
  end
end

Public Instance Methods

archive_parent(acc, years_archived, year) click to toggle source
# File lib/africompta/entities/account.rb, line 157
def archive_parent(acc, years_archived, year)
  dputs(3) { "years_archived is #{years_archived.inspect}" }
  if not years_archived.has_key? year
    dputs(2) { "Adding #{year}" }
    years_archived[year] =
        Accounts.create_path("Archive::#{year}", 'New archive')
  end
  # This means we're more than one level below root, so we can't
  # just copy easily
  if acc.path.split('::').count > 2
    dputs(3) { "Creating archive #{acc.path} with mult #{acc.multiplier}" }
    return Accounts.create_path("Archive::#{year}::"+
                                    "#{acc.parent.path.gsub(/^Root::/, '')}", 'New archive', false,
                                acc.multiplier, acc.keep_total)
  else
    return years_archived[year]
  end
end
bool_to_s(b) click to toggle source
# File lib/africompta/entities/account.rb, line 496
def bool_to_s(b)
  (b && b != 'f') ? 'true' : 'false'
end
check_against_db(file) click to toggle source
# File lib/africompta/entities/account.rb, line 500
def check_against_db(file)
  # First build
  # in_db - content of 'file' in .to_s format
  # in_local - content available locally in .to_s format
  in_db, diff, in_local = [], [], []
  dputs(3) { 'Searching all accounts' }
  @check_state = 'Collect local'
  @check_progress = 0.0
  in_local = Accounts.search_all_
  progress_step = 1.0 / (in_local.size + 1)
  dputs(3) { "Found #{in_local.size} accounts" }

  @check_state = 'Collect local'
  in_local = in_local.collect { |a|
    @check_progress += progress_step
    a.to_s
  }

  dputs(3) { 'Loading file-db' }
  @check_state = 'Collect file-DB'
  @check_progress = 0.0
  SQLite3::Database.new(file) do |db|
    db.execute('select id, account_id, name, desc, global_id, total, '+
                   'multiplier, "index", rev_index, deleted, keep_total '+
                   'from compta_accounts').sort_by { |a| a[4] }.each do |row|
      #dputs(3) { "Looking at #{row}" }
      @check_progress += progress_step

      _, acc_id_, name_, desc_, gid_, tot_, mult_, _, _, del_, keep_, = row
      parent = if acc_id_
                 acc_id_ == 0 ? '' :
                     db.execute("select * from compta_accounts where id=#{acc_id_}").first[4]
               else
                 ''
               end
      in_db.push "#{desc_}\r#{gid_}\t" +
                     "#{sprintf('%.3f', tot_.to_f.round(3))}\t#{name_.to_s}\t"+
                     "#{mult_.to_i.to_s}\t#{parent}" +
                     "\t#{bool_to_s(del_)}" + "\t#{bool_to_s(keep_)}"
    end
  end

  # Now compare what is available only in db and what is available only locally
  dputs(3) { 'Comparing local accounts with file-db accounts' }
  @check_state = 'On one side'
  @check_progress = 0.0
  in_db.delete_if { |a|
    @check_progress += progress_step
    in_local.delete(a)
  }

  # And search for accounts with same global-id but different content
  dputs(3) { 'Seaching mix-ups' }
  @check_state = 'Mixed-up'
  @check_progress = 0.0
  progress_step = 1.0 / (in_db.size + 1)
  (in_db + in_local).sort_by { |a| a.match(/\r(.*?)\t/)[1] }
  in_db.delete_if { |a|
    @check_progress += progress_step
    gid = a.match(/\r(.*?)\t/)[1]
    if c = in_local.find { |b| b =~ /\r#{gid}\t/ }
      diff.push [a, c]
      in_local.delete c
    end
  }

  @check_state = 'Done'
  [in_db, diff, in_local]
end
create_accounts(acc, years, years_archived, this_year) click to toggle source
# File lib/africompta/entities/account.rb, line 190
def create_accounts(acc, years, years_archived, this_year)
  years.keys.each { |y|
    if y == this_year
      dputs(3) { "Creating path #{acc.path} with mult #{acc.multiplier}" }
      years[y] = Accounts.create_path(acc.path, acc.desc,
                                      true, acc.multiplier, acc.keep_total)
    else
      path = "#{archive_parent(acc, years_archived, y).path}::" +
          acc.name
      dputs(3) { "Creating other path #{path} with mult #{acc.multiplier}" }
      years[y] = Accounts.create_path(path, acc.desc, false,
                                      acc.multiplier, acc.keep_total)
    end
    dputs(3) { "years[y] is #{years[y].path_id}" }
  }
end
init() click to toggle source
# File lib/africompta/entities/account.rb, line 442
def init
  root = Accounts.create('Root', 'Initialisation')
  %w( Income Outcome Lending Cash ).each { |a|
    Accounts.create(a, 'Initialisation', root)
  }
  %w( Lending Cash ).each { |a|
    acc = Accounts.match_by_name(a)
    acc.multiplier = -1
    acc.keep_total = true
  }
  Accounts.save
end
listp_path() click to toggle source
# File lib/africompta/entities/account.rb, line 488
def listp_path
  dputs(3) { 'Being called' }
  Accounts.search_all.select { |a| !a.deleted }.collect { |a| [a.id, a.path] }.
      sort { |a, b|
    a[1] <=> b[1]
  }
end
load() click to toggle source
Calls superclass method
# File lib/africompta/entities/account.rb, line 455
def load
  super
  if Accounts.search_by_name('Root').count == 0
    dputs(1) { "Didn't find 'Root' in database - creating base" }
    Accounts.init
  end
end
migration_1(a) click to toggle source
# File lib/africompta/entities/account.rb, line 463
def migration_1(a)
  dputs(4) { Accounts.storage[:SQLiteAC].db_class.inspect }
  a.deleted = false
  # As most of the accounts in Cash have -1 and shall be kept, this
  # gives a good first initialisation
  a.keep_total = (a.multiplier == -1.0) || (a.multiplier == -1)
  dputs(4) { "#{a.name}: #{a.deleted.inspect} - #{a.keep_total.inspect}" }
end
migration_2(a) click to toggle source
# File lib/africompta/entities/account.rb, line 472
def migration_2(a)
  a.rev_index = a.id
end
migration_3(m) click to toggle source
# File lib/africompta/entities/account.rb, line 476
def migration_3(m)
  # dp "Migrating #{m.inspect}"
  if m.id == 1 && m.account_id != nil
    dp 'Root-account has parent...'
    m.account_id = 0
    m.name = 'Root'
    m.desc = 'Root'
    m.global_id = Digest::MD5.hexdigest((rand 2**128).to_s).to_s + '-1'
    m.total = 0.0
  end
end
move_movements(acc, years, month_start) click to toggle source
# File lib/africompta/entities/account.rb, line 207
def move_movements(acc, years, month_start)
  acc.movements.each { |mov|
    dputs(5) { 'Start of each' }
    y, m, _ = mov.date.to_s.split('-').collect { |d| d.to_i }
    dputs(5) { "Date of #{mov.desc} is #{mov.date}" }
    m < month_start and y -= 1
    if years.has_key? y
      value = mov.value
      mov.value = 0
      dputs(5) { "Moving to #{years[y].inspect}: " +
          "#{mov.account_src.id} - #{mov.account_dst.id} - #{acc.id}" }
      if mov.account_src.id == acc.id
        dputs(5) { 'Moving src' }
        mov.account_src_id = years[y]
      else
        dputs(5) { 'Moving dst' }
        mov.account_dst_id = years[y]
      end
      mov.value = value
    end
  }
  dputs(5) { "Movements left in account #{acc.path}:" }
  acc.movements.each { |m|
    dputs(5) { m.desc }
  }
end
search_account(acc, month_start) click to toggle source
# File lib/africompta/entities/account.rb, line 176
def search_account(acc, month_start)
  years = Hash.new(0)
  acc.movements.each { |mov|
    if not mov.desc =~ /^-- Sum of/
      y, m, _ = mov.date.to_s.split('-').collect { |d| d.to_i }
      dputs(5) { "Date of #{mov.desc} is #{mov.date}" }
      m < month_start and y -= 1
      years[y] += 1
    end
  }
  dputs(3) { "years is #{years.inspect}" }
  years
end
setup_data() click to toggle source
# File lib/africompta/entities/account.rb, line 9
def setup_data
  @default_type = :SQLiteAC
  @data_field_id = :id
  value_int :index

  value_str :name
  value_str :desc
  value_str :global_id
  value_float :total
  value_int :multiplier
  value_int :rev_index
  value_bool :deleted
  value_bool :keep_total
  # This is the ID of the parent account
  value_int :account_id
end
sum_up_total(acc_path, years_archived, month_start) click to toggle source
# File lib/africompta/entities/account.rb, line 234
def sum_up_total(acc_path, years_archived, month_start)
  a_path = acc_path.sub(/[^:]*::/, '')
  dputs(2) { "Summing up account #{a_path}" }
  acc_sum = []
  years_archived.each { |y, a|
    dputs(5) { "Found archived year #{y.inspect} which is #{y.class.name}" }
    aacc = Accounts.get_by_path(a.get_path + '::' + a_path)
    acc_sum.push [y, a, aacc]
  }
  dputs(5) { 'Trying to add current year' }
  if curr_acc = Accounts.get_by_path(acc_path)
    dputs(4) { 'Adding current year' }
    acc_sum.push [9999, nil, curr_acc]
  end

  last_total = 0
  last_year_acc = nil
  last_year_acc_parent = nil
  last_year = 0
  dputs(5) { "Sorting account_sums #{acc_sum.length}" }
  acc_sum.sort { |a, b| a[0] <=> b[0] }.each { |y, a, aacc|
    dputs(5) { "y, a, aacc: #{y}, #{a.to_json}, #{aacc.to_json}" }
    if aacc
      last_year_acc_parent and last_year_acc_parent.dump true
      dputs(4) { "Found archived account #{aacc.get_path} for year #{y}" +
          " with last_total #{last_total}" }
      dputs(5) { 'And has movements' }
      aacc.movements.each { |m|
        dputs(5) { m.to_json }
      }
      if last_total != 0
        desc = "-- Sum of #{last_year} of #{last_year_acc.path}"
        date = "#{last_year + 1}-#{month_start.to_s.rjust(2, '0')}-01"
        dputs(3) { "Deleting old sums with date #{date.inspect}" }
        Movements.matches_by_desc("^#{desc}$").each { |m|
          dputs(3) { "Testing movement with date #{m.date.to_s.inspect}: #{m.to_json}" }
          if m.date.to_s == date.to_s
            dputs(3) { 'Deleting it' }
            m.delete
          end
        }
        dputs(3) { "Creating movement for the sum of last year: #{last_total}" }
        mov = Movements.create(desc, date, last_total,
                               last_year_acc_parent, aacc)
        last_year_acc_parent.dump true
        dputs(3) { "Movement is: #{mov.to_json}" }
      end
      aacc.update_total
      dputs(5) { "#{aacc.total} - #{aacc.multiplier}" }
      last_total = aacc.total * aacc.multiplier
    else
      dputs(4) { "Didn't find archived account for #{y}" }
      last_total = 0
    end
    last_year, last_year_acc, last_year_acc_parent = y, aacc, a
  }
end