SU2-30 | добавлена фикс таблица для кодов, убрана таблица DocumentTypes, изменена структура jsonb
This commit is contained in:
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
class DocumentType < ApplicationRecord
|
||||
has_many :financial_statements, dependent: :restrict_with_error
|
||||
|
||||
validates :name, presence: true
|
||||
end
|
||||
@@ -1,6 +1,5 @@
|
||||
class FinancialStatement < ApplicationRecord
|
||||
belongs_to :yearly_file
|
||||
belongs_to :document_type
|
||||
|
||||
validates :data, presence: true
|
||||
end
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
class ReportCode < ApplicationRecord
|
||||
validates :code, presence: true, uniqueness: true
|
||||
validates :name, presence: true
|
||||
end
|
||||
+126
-25
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user