SU2-30 | добавлена фикс таблица для кодов, убрана таблица DocumentTypes, изменена структура jsonb

This commit is contained in:
Dmitry
2026-04-18 17:05:19 +00:00
parent c74ffc2aea
commit a03839a834
10 changed files with 595 additions and 84 deletions
+114 -42
View File
@@ -1,6 +1,13 @@
class OrganisationsController < ApplicationController
before_action :set_organisation, only: [:show]
DOC_TYPES = {
0 => { title: 'Бухгалтерский баланс', form_code: '1.1' },
1 => { title: 'Отчёт о финансовых результатах', form_code: '1.2' },
2 => { title: 'Отчёт об изменении капитала', form_code: '1.3' },
3 => { title: 'Отчёт о движении денежных средств', form_code: '1.4' }
}.freeze
def index
organizations = Organization.all
@@ -19,53 +26,18 @@ class OrganisationsController < ApplicationController
end
def show
doc_types = {
0 => 'Бухгалтерский баланс',
1 => 'Отчёт о финансовых результатах',
2 => 'Отчёт об изменении капитала',
3 => 'Отчет о движении денежных средств'
}
@type_doc = params[:type_doc].present? ? params[:type_doc].to_i : 0
data = doc_types.each_with_object({}) do |(key, value), hash|
hash[value] = Hash.new()
end
statements_by_year_and_form = build_statements_by_year_and_form
years = statements_by_year_and_form.keys.sort.reverse
grouped_rows = build_grouped_rows(statements_by_year_and_form, years)
doc_types.keys.each do |key|
financial_statements = @org.financial_statements.order(sheet_number: :desc).where(document_type_id: key + 1)
financial_statements.each do |financial_statement|
financial_statement.data['rows'].each_with_index do |row, index|
if index != 0
if data[doc_types[key]][[row['line_name'], row['line_code']]].nil?
data[doc_types[key]][[row['line_name'], row['line_code']]] = []
end
data[doc_types[key]][[row['line_name'], row['line_code']]] << row['value']
end
end
end
end
@info_about_organisation = build_organization_payload(years)
@info_about_organisation = {
"Сведения об организации" => {
"ИНН" => @org.inn,
"Полное наименование юридического лица" => @org.full_name,
"КПП" => @org.kpp,
"Код по ОКПО" => @org.okpo,
"Форма собственности (по ОКФС)" => @org.okfs,
"Организационно-правовая форма (по ОКОПФ)" => @org.okopf,
"Вид экономической деятельности по ОКВЭД 2" => @org.okved2,
"Местонахождение (адрес)" => @org.address
},
"Бухгалтерский баланс" => ["Наименование показателя", "Код", "На 31 декабря 2024 г.", "На 31 декабря 2023 г.", "На 31 декабря 2022 г.", "На 31 декабря 2021 г.", "На 31 декабря 2020 г.", "На 31 декабря 2019 г."],
"Отчёт о финансовых результатах" => ["Наименование показателя", "Код", "За 2024 г.", "За 2023 г.", "За 2022 г.", "За 2021 г.", "За 2020 г."]
}
@doc_name = doc_types[@type_doc]
selected_doc = DOC_TYPES.fetch(@type_doc, DOC_TYPES[0])
@doc_name = selected_doc[:title]
@current_doc_info = @info_about_organisation[@doc_name]
@current_doc_info1 = data[@doc_name]
@current_doc_info1 = grouped_rows[@doc_name]
end
@@ -74,4 +46,104 @@ class OrganisationsController < ApplicationController
def set_organisation
@org = Organization.find(params[:id])
end
def build_organization_payload(years)
payload = {
'Сведения об организации' => {
'ИНН' => @org.inn,
'Полное наименование юридического лица' => @org.full_name,
'КПП' => @org.kpp,
'Код по ОКПО' => @org.okpo,
'Форма собственности (по ОКФС)' => @org.okfs,
'Организационно-правовая форма (по ОКОПФ)' => @org.okopf,
'Вид экономической деятельности по ОКВЭД 2' => @org.okved2,
'Местонахождение (адрес)' => @org.address
}
}
DOC_TYPES.each_value do |doc_type|
payload[doc_type[:title]] = ['Наименование показателя', 'Код'] + years.map do |year|
column_header_for(doc_type[:form_code], year)
end
end
payload
end
def build_grouped_rows(statements_by_year_and_form, years)
data = DOC_TYPES.each_with_object({}) do |(_, doc_type), hash|
hash[doc_type[:title]] = {}
end
years.each do |year|
DOC_TYPES.each_value do |doc_type|
statement = statements_by_year_and_form.dig(year, doc_type[:form_code])
rows = statement ? extract_rows(statement, doc_type[:form_code]) : []
rows.each do |row|
line_name = row['line_name'] || row[:line_name]
line_code = row['line_code'] || row[:line_code]
next if line_name.blank? && line_code.blank?
key = [line_name, line_code]
data[doc_type[:title]][key] ||= []
data[doc_type[:title]][key] << (row['value'] || row[:value] || '-')
end
end
end
data
end
def build_statements_by_year_and_form
grouped = Hash.new { |hash, key| hash[key] = {} }
@org.financial_statements.includes(:yearly_file).each do |statement|
year = statement.sheet_number.to_i
next unless year.positive?
next unless unified_statement?(statement)
DOC_TYPES.each_value do |doc_type|
form_code = doc_type[:form_code]
next if extract_rows(statement, form_code).blank?
current = grouped[year][form_code]
grouped[year][form_code] = statement if better_statement?(statement, current)
end
end
grouped
end
def better_statement?(candidate, current)
return true if current.nil?
candidate_source_year = candidate.yearly_file&.year.to_i
current_source_year = current.yearly_file&.year.to_i
return true if candidate_source_year > current_source_year
return false if candidate_source_year < current_source_year
candidate.updated_at > current.updated_at
end
def unified_statement?(statement)
forms = statement.data.is_a?(Hash) ? (statement.data['forms'] || statement.data[:forms]) : nil
forms.is_a?(Hash)
end
def extract_rows(statement, form_code)
forms = statement.data['forms'] || statement.data[:forms] || {}
form_payload = forms[form_code] || forms[form_code.to_sym] || {}
rows = form_payload['rows'] || form_payload[:rows] || []
return rows if rows.is_a?(Array)
[]
end
def column_header_for(form_code, year)
return "На 31 декабря #{year} г." if form_code == '1.1'
"За #{year} г."
end
end
-5
View File
@@ -1,5 +0,0 @@
class DocumentType < ApplicationRecord
has_many :financial_statements, dependent: :restrict_with_error
validates :name, presence: true
end
-1
View File
@@ -1,6 +1,5 @@
class FinancialStatement < ApplicationRecord
belongs_to :yearly_file
belongs_to :document_type
validates :data, presence: true
end
+4
View File
@@ -0,0 +1,4 @@
class ReportCode < ApplicationRecord
validates :code, presence: true, uniqueness: true
validates :name, presence: true
end
+126 -25
View File
@@ -1,6 +1,27 @@
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
@@ -9,13 +30,8 @@ class ReportParser
ActiveRecord::Base.transaction do
ReportParsers::OrganizationInfoParser.new(@yearly_file, spreadsheet: spreadsheet).call
form_11_data = ReportParsers::Form11Parser.new(@yearly_file, spreadsheet: spreadsheet).call
form_12_data = ReportParsers::Form12Parser.new(@yearly_file, spreadsheet: spreadsheet).call
form_14_data = ReportParsers::Form14Parser.new(@yearly_file, spreadsheet: spreadsheet).call
save_form_statements(form_11_data, 1) # 1 - Бух. баланс
save_form_statements(form_12_data, 2) # 2 - Отчет о фин. результатах
save_form_statements(form_14_data, 4) # 4 - Отчет о движении денежных средств
parsed_forms = parse_forms
save_aggregated_statements(parsed_forms)
end
end
@@ -31,34 +47,119 @@ class ReportParser
end
end
def save_form_statements(form_data, form_id)
document_type = DocumentType.find_by(id: form_id)
raise "DocumentType with id=#{form_id} not found!" unless document_type
form_data.each do |year, data|
rows = data[:rows] || data['rows'] || []
next if rows.blank?
statement = find_or_build_statement(document_type, year)
payload = {
data_year: year,
rows: rows
}
statement.assign_attributes(data: payload)
statement.save!
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 find_or_build_statement(document_type, year)
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(document_type: document_type, sheet_number: year)
.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(document_type: document_type, sheet_number: year)
@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