Files

238 lines
7.1 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: ReportParsers::Form13Parser,
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
ensure
@tempfile&.close!
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)
previous_data = statement.data.deep_dup if statement.persisted?
statement.assign_attributes(
yearly_file: @yearly_file,
sheet_number: year,
data: build_payload_for_year(year, parsed_forms, codes, previous_data: previous_data)
)
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, previous_data: nil)
forms_payload = FORM_DEFINITIONS.each_with_object({}) do |(form_code, config), hash|
form_present, form_data = parsed_form_for_year(parsed_forms, form_code, year)
form_data ||= {}
unless form_present
previous_payload = previous_form_payload(previous_data, form_code)
if previous_payload.present?
hash[form_code] = previous_payload
next
end
end
source_rows = form_data[:rows] || form_data['rows'] || []
rows_by_code = index_rows_by_code(source_rows)
form_codes = report_codes_for_form(form_code, codes)
hash[form_code] = {
form_code: form_code,
sheet_name: form_data[:sheet_name] || form_data['sheet_name'] || config.fetch(:sheet_name),
rows: form_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: normalized_statement_value(source_value)
}
end
}
end
{
data_year: year,
forms: forms_payload
}
end
def parsed_form_for_year(parsed_forms, form_code, year)
form_results = parsed_forms.fetch(form_code, {})
return [true, form_results[year]] if form_results.key?(year)
return [true, form_results[year.to_s]] if form_results.key?(year.to_s)
[false, {}]
end
def previous_form_payload(previous_data, form_code)
forms = previous_data && (previous_data[:forms] || previous_data['forms'])
return unless forms.is_a?(Hash)
forms[form_code] || forms[form_code.to_sym]
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 normalized_statement_value(value)
return DEFAULT_MISSING_VALUE if value.nil?
return value if value.is_a?(Numeric)
text = value.to_s.strip
return DEFAULT_MISSING_VALUE if text.blank?
numeric_text?(text) ? text : DEFAULT_MISSING_VALUE
end
def numeric_text?(text)
normalized = text.tr("\u00A0", ' ').squeeze(' ')
integer_or_decimal = /\A[+-]?\d+(?:[.,]\d+)?\z/
integer_or_decimal_grouped = /\A[+-]?\d{1,3}(?: \d{3})+(?:[.,]\d+)?\z/
parenthesized_integer_or_decimal = /\A\(\d+(?:[.,]\d+)?\)\z/
parenthesized_integer_or_decimal_grouped = /\A\(\d{1,3}(?: \d{3})+(?:[.,]\d+)?\)\z/
normalized.match?(integer_or_decimal) ||
normalized.match?(integer_or_decimal_grouped) ||
normalized.match?(parenthesized_integer_or_decimal) ||
normalized.match?(parenthesized_integer_or_decimal_grouped)
end
def report_codes
@report_codes ||= ReportCode.select(:code, :name).order(:id).to_a
end
def report_codes_for_form(form_code, codes)
codes.select { |report_code| report_code_belongs_to_form?(report_code.code, form_code) }
end
def report_code_belongs_to_form?(code, form_code)
normalized_code = normalize_code(code)
return false if normalized_code.blank?
case form_code
when '1.1' then normalized_code.start_with?('1')
when '1.2' then normalized_code.start_with?('2')
when '1.3' then normalized_code.start_with?('3')
when '1.4' then normalized_code.start_with?('4')
else
true
end
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)
.destroy_all
end
end