190 lines
5.6 KiB
Ruby
190 lines
5.6 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: normalized_statement_value(source_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 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.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
|