require 'roo' class ReportParser FORM_DEFINITIONS = { '1.1' => { parser: ReportParsers::Form11Parser, sheet_name: 'Бухгалтерский баланс' }, '1.2' => { parser: ReportParsers::Form12Parser, sheet_name: 'Отчет о финансовых результатах' }, '1.3' => { parser: nil, sheet_name: 'Отчет об изменениях капитала' }, '1.4' => { parser: ReportParsers::Form14Parser, sheet_name: 'Отчет о движении денежных средств' } }.freeze DEFAULT_MISSING_VALUE = '-' def initialize(yearly_file) @yearly_file = yearly_file end def call ActiveRecord::Base.transaction do ReportParsers::OrganizationInfoParser.new(@yearly_file, spreadsheet: spreadsheet).call parsed_forms = parse_forms save_aggregated_statements(parsed_forms) end end private def spreadsheet @spreadsheet ||= begin tempfile = Tempfile.new(['report', '.xlsx']) tempfile.binmode @yearly_file.file.download { |chunk| tempfile.write(chunk) } tempfile.rewind Roo::Excelx.new(tempfile.path) end end def parse_forms FORM_DEFINITIONS.each_with_object({}) do |(form_code, config), hash| parser_class = config.fetch(:parser) hash[form_code] = parser_class ? parser_class.new(@yearly_file, spreadsheet: spreadsheet).call : {} end end def save_aggregated_statements(parsed_forms) codes = report_codes return if codes.blank? target_years = extract_target_years(parsed_forms) return if target_years.blank? target_years.each do |year| statement = find_or_build_statement(year) if should_update_statement?(statement) statement.assign_attributes( yearly_file: @yearly_file, sheet_number: year, data: build_payload_for_year(year, parsed_forms, codes) ) statement.save! end cleanup_duplicate_statements(year, statement.id) end end def extract_target_years(parsed_forms) years = parsed_forms.values.flat_map(&:keys).map(&:to_i).select(&:positive?).uniq years = [@yearly_file.year.to_i] if years.blank? && @yearly_file.year.to_i.positive? years.sort.reverse end def build_payload_for_year(year, parsed_forms, codes) forms_payload = FORM_DEFINITIONS.each_with_object({}) do |(form_code, config), hash| form_data = parsed_forms.fetch(form_code, {})[year] || {} source_rows = form_data[:rows] || form_data['rows'] || [] rows_by_code = index_rows_by_code(source_rows) hash[form_code] = { form_code: form_code, sheet_name: form_data[:sheet_name] || form_data['sheet_name'] || config.fetch(:sheet_name), rows: codes.map do |report_code| source_row = rows_by_code[report_code.code] source_value = source_row && (source_row[:value] || source_row['value']) { line_name: report_code.name, line_code: report_code.code, value: source_value.presence || DEFAULT_MISSING_VALUE } end } end { data_year: year, forms: forms_payload } end def index_rows_by_code(rows) rows.each_with_object({}) do |row, hash| normalized_code = normalize_code(row[:line_code] || row['line_code']) next if normalized_code.blank? # Keep first non-blank value for duplicated codes inside one form/year. existing = hash[normalized_code] next if existing.present? && (existing[:value] || existing['value']).present? hash[normalized_code] = row end end def find_or_build_statement(year) existing = FinancialStatement .joins(:yearly_file) .where(sheet_number: year) .where(yearly_files: { organization_id: @yearly_file.organization_id }) .order('yearly_files.year DESC NULLS LAST, financial_statements.updated_at DESC') .first return existing if existing @yearly_file.financial_statements.new(sheet_number: year) end def should_update_statement?(statement) return true if statement.new_record? existing_source_year = statement.yearly_file&.year.to_i current_source_year = @yearly_file.year.to_i existing_source_year <= current_source_year end def normalize_code(value) value.to_s.scan(/\d+/).join.presence end def report_codes @report_codes ||= ReportCode.order(:code).select(:code, :name).to_a end def cleanup_duplicate_statements(year, keeper_id) FinancialStatement .joins(:yearly_file) .where(sheet_number: year) .where(yearly_files: { organization_id: @yearly_file.organization_id }) .where.not(id: keeper_id) .delete_all end end