Files
finreport-analyzer/app/services/report_parser.rb
T

166 lines
4.7 KiB
Ruby

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