Files
finreport-analyzer/app/services/report_parsers/form11_parser.rb
T

157 lines
5.3 KiB
Ruby

module ReportParsers
class Form11Parser < BaseParser
SHEET_NAME = 'Бухгалтерский баланс'
FORM_CODE = '1.1'
IGNORED_KEYS = [2].freeze
def call
return {} unless sheet_exists?(SHEET_NAME)
sheet = spreadsheet.sheet(SHEET_NAME)
extract_year(sheet)
build_payloads(sheet)
end
private
def build_payloads(sheet)
header_row = find_header_row(sheet)
columns = detect_columns(sheet, header_row)
# year читается по оригинальному col заголовка, данные — по откалиброванному
value_headers = columns[:value_columns].zip(columns[:data_value_columns]).each_with_object({}) do |(header_col, data_col), hash|
header_text = read_cell(sheet, header_row, header_col).to_s
year = extract_year_from_header(header_text)
hash[data_col] = year
end
all_rows = (header_row + 1).upto(sheet.last_row).filter_map do |row|
line_name = normalize_line_name(read_cell(sheet, row, columns[:name_column]))
line_code = read_cell(sheet, row, columns[:code_column]).to_s.strip.presence
next if ignored_row?(line_name, line_code)
values = columns[:data_value_columns].each_with_object({}) do |col, hash|
value = read_cell(sheet, row, col)
year = value_headers[col]
hash[year.to_s] = value if value.present? && year.present?
end.compact_blank
next if line_name.blank? && line_code.blank? && values.blank?
{
line_name: line_name,
line_code: line_code,
values: values
}
end
payloads_by_year = {}
value_headers.each do |_data_col, year|
next unless year.present?
rows_for_year = all_rows.map do |row|
value = row[:values][year.to_s]
next unless value.present?
{
line_name: row[:line_name],
line_code: row[:line_code],
value: value
}
end.compact
payloads_by_year[year] = {
form_code: FORM_CODE,
sheet_name: SHEET_NAME,
header_row: header_row,
data_year: year,
rows: rows_for_year
}
end
payloads_by_year
end
def extract_year_from_header(header_text)
year_match = header_text[/\b(20\d{2})\b/]
year_match.to_i if year_match
end
def find_header_row(sheet)
1.upto(sheet.last_row) do |row|
normalized_cells = 1.upto(sheet.last_column).map { |col| normalize_label(sheet.cell(row, col)) }
has_name = normalized_cells.any? { |text| text.include?('наименование показателя') }
has_code = normalized_cells.any? { |text| text == 'код' || text.start_with?('код ') || text.start_with?('код:') }
return row if has_name && has_code
end
1
end
def detect_columns(sheet, header_row)
columns = 1.upto(sheet.last_column).each_with_object({}) do |col, hash|
hash[col] = normalize_label(sheet.cell(header_row, col))
end
name_column = columns.key(columns.values.find { |text| text.include?('наименование показателя') }) || 1
code_column = columns.key(columns.values.find { |text|
text == 'код строки' || text.start_with?('код строки ') ||
(text == 'код' && columns[name_column + 1] == text)
}) || columns.key(columns.values.find { |text| text == 'код' || text.start_with?('код ') }) || [name_column + 1, sheet.last_column].min
value_columns = columns.keys.select do |col|
text = columns[col]
text.include?('на 31') ||
text.include?('на ') ||
text.match?(/\bна \d+ /) ||
text.match?(/\b20\d{2}\b/) ||
(col > code_column && text.include?('декабря')) ||
(col > code_column && text.match?(/\d+/)) # fallback: numeric values
end
if value_columns.empty?
value_columns = columns.keys.select { |col| col > code_column && columns[col].present? }
end
original_cols = value_columns.sort.first(3)
{
name_column: name_column,
code_column: code_column,
value_columns: original_cols,
data_value_columns: calibrate_value_columns(sheet, header_row, original_cols, code_column)
}
end
def calibrate_value_columns(sheet, header_row, value_columns, code_column)
data_row = (header_row + 2).upto(sheet.last_row).find do |row|
sheet.cell(row, code_column).to_s.strip.match?(/\A\d{4}\z/)
end
return value_columns unless data_row
value_columns.map do |col|
next col unless sheet.cell(data_row, col).nil? || sheet.cell(data_row, col).to_s.strip.empty?
shifted = col + 1
shifted <= sheet.last_column && !sheet.cell(data_row, shifted).to_s.strip.empty? ? shifted : col
end
end
def extract_year(sheet)
period_text = sheet.cell(4, 1).to_s
year = period_text[/\d{4}/].to_i
yearly_file.update!(year: year) if year.positive?
end
def ignored_row?(line_name, line_code)
keys = [line_code, line_name].compact.map { |value| normalize_label(value) }
keys.any? { |key| ignored_keys.include?(key) }
end
def ignored_keys
@ignored_keys ||= IGNORED_KEYS.map { |value| normalize_label(value) }.to_set
end
end
end