module ReportParsers class Form11Parser < BaseParser SHEET_NAME = 'Бухгалтерский баланс' FORM_CODE = '1.1' # Add line codes or names that should be skipped during parsing. 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) value_headers = columns[:value_columns].each_with_object({}) do |col, hash| header_text = read_cell(sheet, header_row, col).to_s year = extract_year_from_header(header_text) hash[col] = year end all_rows = (header_row + 1).upto(sheet.last_row).filter_map do |row| 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[: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 |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 { name_column: name_column, code_column: code_column, value_columns: value_columns.sort.first(3) } 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