Merge branch 'features/models' of DAAntipenko/finreport-analyzer into features/layout

This commit is contained in:
Дмитрий Антипенко
2026-03-12 13:28:32 +03:00
committed by Gogs
17 changed files with 290 additions and 43 deletions
+1
View File
@@ -9,6 +9,7 @@ gem "rails", "~> 7.1.3", ">= 7.1.3.2"
gem "sidekiq" # background jobs processing gem "sidekiq" # background jobs processing
gem 'httparty' # for http requests gem 'httparty' # for http requests
gem 'rubyzip' # for zip file processing gem 'rubyzip' # for zip file processing
gem 'roo', '~> 2.10' # for xlsx file parsing
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails" gem "sprockets-rails"
+4
View File
@@ -335,6 +335,9 @@ GEM
request_store (1.7.0) request_store (1.7.0)
rack (>= 1.4) rack (>= 1.4)
rexml (3.3.9) rexml (3.3.9)
roo (2.10.1)
nokogiri (~> 1)
rubyzip (>= 1.3.0, < 3.0.0)
rspec-core (3.13.2) rspec-core (3.13.2)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.3)
@@ -496,6 +499,7 @@ DEPENDENCIES
rack-cas! rack-cas!
rails (~> 7.1.3, >= 7.1.3.2) rails (~> 7.1.3, >= 7.1.3.2)
redis (>= 4.0.1) redis (>= 4.0.1)
roo (~> 2.10)
rspec-rails (~> 6.1.0) rspec-rails (~> 6.1.0)
rubyzip rubyzip
selenium-webdriver selenium-webdriver
+9 -13
View File
@@ -1,17 +1,13 @@
class AuditorsController < ApplicationController class AuditorsController < ApplicationController
def index def index
@info_about_auditors = {"A" => {"Наименование аудиторской организации/ФИО индивидуального аудитора" => "ООО \"Инвест-Аудит\"", auditors = Auditor.all
"ИНН" => 7606073440,
"ОГРН/ОРГНИП" => 1097606001250 @info_about_auditors = auditors.each_with_object({}) do |auditor, hash|
}, hash[auditor.id] = {
"B" => {"Наименование аудиторской организации/ФИО индивидуального аудитора" => "ООО \"Инвест-Аудит\"", "Наименование аудиторской организации/ФИО индивидуального аудитора" => auditor.name,
"ИНН" => 7606073440, "ИНН" => auditor.inn,
"ОГРН/ОРГНИП" => 1097606001250 "ОГРН/ОРГНИП" => auditor.ogrn
}, }
"C" => {"Наименование аудиторской организации/ФИО индивидуального аудитора" => "ООО \"Инвест-Аудит\"", end
"ИНН" => 7606073440,
"ОГРН/ОРГНИП" => 1097606001250
}
}
end end
end end
+14 -28
View File
@@ -1,33 +1,19 @@
class WelcomeController < ApplicationController class WelcomeController < ApplicationController
def index def index
@info_about_organisations = {"A" => {"ИНН" => 7707602010, organizations = Organization.all
"Полное наименование юридического лица" =>"Публичное акционерное общество \"М.видео\"",
"КПП" => 770101001, @info_about_organisations = organizations.each_with_object({}) do |org, hash|
"Код по ОКПО" => 71630621, hash[org.id] = {
"Форма собственности (по ОКФС)" => "34 Совместная частная и иностранная собственность", "ИНН" => org.inn,
"Организационно-правовая форма (по ОКОПФ)" => "12247 Публичные акционерные общества", "Полное наименование юридического лица" => org.full_name,
"Вид экономической деятельности по ОКВЭД 2" => "70.10.1", "КПП" => org.kpp,
"Местонахождение (адрес)" => "105066, г. Москва, ул. Красносельская Нижн., д. 40/12, к. 20, пом. II" "Код по ОКПО" => org.okpo,
}, "Форма собственности (по ОКФС)" => org.okfs,
"B" => {"ИНН" => 7707602010, "Организационно-правовая форма (по ОКОПФ)" => org.okopf,
"Полное наименование юридического лица" =>"Публичное акционерное общество \"М.видео\"", "Вид экономической деятельности по ОКВЭД 2" => org.okved2,
"КПП" => 770101001, "Местонахождение (адрес)" => org.address
"Код по ОКПО" => 71630621, }
"Форма собственности (по ОКФС)" => "34 Совместная частная и иностранная собственность", end
"Организационно-правовая форма (по ОКОПФ)" => "12247 Публичные акционерные общества",
"Вид экономической деятельности по ОКВЭД 2" => "70.10.1",
"Местонахождение (адрес)" => "105066, г. Москва, ул. Красносельская Нижн., д. 40/12, к. 20, пом. II"
},
"C" => {"ИНН" => 7707602010,
"Полное наименование юридического лица" =>"Публичное акционерное общество \"М.видео\"",
"КПП" => 770101001,
"Код по ОКПО" => 71630621,
"Форма собственности (по ОКФС)" => "34 Совместная частная и иностранная собственность",
"Организационно-правовая форма (по ОКОПФ)" => "12247 Публичные акционерные общества",
"Вид экономической деятельности по ОКВЭД 2" => "70.10.1",
"Местонахождение (адрес)" => "105066, г. Москва, ул. Красносельская Нижн., д. 40/12, к. 20, пом. II"
}
}
end end
end end
+3
View File
@@ -0,0 +1,3 @@
class Auditor < ApplicationRecord
has_many :yearly_files, dependent: :nullify
end
+5
View File
@@ -0,0 +1,5 @@
class DocumentType < ApplicationRecord
has_many :financial_statements, dependent: :restrict_with_error
validates :name, presence: true
end
+6
View File
@@ -0,0 +1,6 @@
class FinancialStatement < ApplicationRecord
belongs_to :yearly_file
belongs_to :document_type
validates :data, presence: true
end
+6
View File
@@ -0,0 +1,6 @@
class Organization < ApplicationRecord
has_many :yearly_files, dependent: :destroy
has_many :financial_statements, through: :yearly_files
validates :inn, presence: true
end
+10
View File
@@ -0,0 +1,10 @@
class YearlyFile < ApplicationRecord
belongs_to :organization
belongs_to :auditor, optional: true
has_many :financial_statements, dependent: :destroy
has_one_attached :file
validates :year, presence: true
end
+98
View File
@@ -0,0 +1,98 @@
require 'roo'
class ReportParser
SHEET_NAME = 'Сведения об организации'
CELL_MAP = {
full_name: { row: 6, col: 13 },
inn: { row: 10, col: 13 },
kpp: { row: 11, col: 13 },
okpo: { row: 12, col: 13 },
okfs: { row: 13, col: 13 },
okopf: { row: 14, col: 13 },
okved2: { row: 15, col: 13 },
address: { row: 16, col: 13 }
}.freeze
AUDITOR_MAP = {
name: { row: 19, col: 13 },
inn: { row: 20, col: 13 },
ogrn: { row: 21, col: 13 }
}.freeze
def initialize(yearly_file)
@yearly_file = yearly_file
end
def call
ActiveRecord::Base.transaction do
parse_organization
parse_auditor
extract_year
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 info_sheet
@info_sheet ||= spreadsheet.sheet(SHEET_NAME)
end
def read_cell(sheet, row, col)
value = sheet.cell(row, col)
value.is_a?(String) ? value.strip.presence : value
end
def parse_organization
org_data = CELL_MAP.each_with_object({}) do |(attr, pos), hash|
hash[attr] = read_cell(info_sheet, pos[:row], pos[:col])
end
organization = @yearly_file.organization
organization.update!(org_data)
organization
end
def parse_auditor
auditor_data = AUDITOR_MAP.each_with_object({}) do |(attr, pos), hash|
hash[attr] = read_cell(info_sheet, pos[:row], pos[:col])
end
return if auditor_data[:name].blank?
auditor_inn = auditor_data[:inn].to_s.strip
auditor = if auditor_inn.present?
Auditor.find_or_initialize_by(inn: auditor_inn)
else
Auditor.find_or_initialize_by(name: auditor_data[:name])
end
auditor.assign_attributes(
name: auditor_data[:name],
inn: auditor_inn.presence,
ogrn: auditor_data[:ogrn].to_s.strip.presence
)
auditor.save!
@yearly_file.update!(auditor: auditor)
auditor
end
def extract_year
balance_sheet = spreadsheet.sheet('Бухгалтерский баланс')
period_text = balance_sheet.cell(4, 1).to_s
year = period_text.match(/(\d{4})/)&.captures&.first&.to_i
@yearly_file.update!(year: year) if year
year
end
end
+15
View File
@@ -15,11 +15,26 @@ class FileProcessorJob
download_inn(inn, temp_dir.to_s) download_inn(inn, temp_dir.to_s)
# Найти или создать организацию по ИНН
organization = Organization.find_or_create_by!(inn: inn)
Dir.glob("#{temp_dir}/**/*").select { |f| File.file?(f) }.each do |file_path| Dir.glob("#{temp_dir}/**/*").select { |f| File.file?(f) }.each do |file_path|
# Прикрепляем файл к Report (обратная совместимость)
report.files.attach( report.files.attach(
io: File.open(file_path), io: File.open(file_path),
filename: File.basename(file_path) filename: File.basename(file_path)
) )
# Создаём YearlyFile и прикрепляем xlsx
yearly_file = organization.yearly_files.build(year: 0)
yearly_file.file.attach(
io: File.open(file_path),
filename: File.basename(file_path)
)
yearly_file.save!
# Парсим данные из xlsx
ReportParser.new(yearly_file).call
end end
# Удали временные файлы # Удали временные файлы
@@ -0,0 +1,18 @@
class CreateOrganizations < ActiveRecord::Migration[7.1]
def change
create_table :organizations do |t|
t.string :inn, null: false
t.string :full_name
t.string :kpp
t.string :okpo
t.string :okfs
t.string :okopf
t.string :okved2
t.string :address
t.timestamps
end
add_index :organizations, :inn
end
end
@@ -0,0 +1,13 @@
class CreateAuditors < ActiveRecord::Migration[7.1]
def change
create_table :auditors do |t|
t.string :name
t.string :inn
t.string :ogrn
t.timestamps
end
add_index :auditors, :inn
end
end
@@ -0,0 +1,9 @@
class CreateDocumentTypes < ActiveRecord::Migration[7.1]
def change
create_table :document_types do |t|
t.string :name, null: false
t.timestamps
end
end
end
@@ -0,0 +1,11 @@
class CreateYearlyFiles < ActiveRecord::Migration[7.1]
def change
create_table :yearly_files do |t|
t.references :organization, null: false, foreign_key: true
t.integer :year, null: false
t.references :auditor, foreign_key: true
t.timestamps
end
end
end
@@ -0,0 +1,12 @@
class CreateFinancialStatements < ActiveRecord::Migration[7.1]
def change
create_table :financial_statements do |t|
t.references :yearly_file, null: false, foreign_key: true
t.references :document_type, null: false, foreign_key: true
t.integer :sheet_number
t.jsonb :data, default: {}
t.timestamps
end
end
end
Generated
+55 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2025_12_09_142642) do ActiveRecord::Schema[7.1].define(version: 2026_03_12_000005) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -42,6 +42,46 @@ ActiveRecord::Schema[7.1].define(version: 2025_12_09_142642) do
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end end
create_table "auditors", force: :cascade do |t|
t.string "name"
t.string "inn"
t.string "ogrn"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["inn"], name: "index_auditors_on_inn"
end
create_table "document_types", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "financial_statements", force: :cascade do |t|
t.bigint "yearly_file_id", null: false
t.bigint "document_type_id", null: false
t.integer "sheet_number"
t.jsonb "data", default: {}
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["document_type_id"], name: "index_financial_statements_on_document_type_id"
t.index ["yearly_file_id"], name: "index_financial_statements_on_yearly_file_id"
end
create_table "organizations", force: :cascade do |t|
t.string "inn", null: false
t.string "full_name"
t.string "kpp"
t.string "okpo"
t.string "okfs"
t.string "okopf"
t.string "okved2"
t.string "address"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["inn"], name: "index_organizations_on_inn"
end
create_table "reports", force: :cascade do |t| create_table "reports", force: :cascade do |t|
t.string "inn" t.string "inn"
t.string "status" t.string "status"
@@ -76,6 +116,20 @@ ActiveRecord::Schema[7.1].define(version: 2025_12_09_142642) do
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
end end
create_table "yearly_files", force: :cascade do |t|
t.bigint "organization_id", null: false
t.integer "year", null: false
t.bigint "auditor_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["auditor_id"], name: "index_yearly_files_on_auditor_id"
t.index ["organization_id"], name: "index_yearly_files_on_organization_id"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "financial_statements", "document_types"
add_foreign_key "financial_statements", "yearly_files"
add_foreign_key "yearly_files", "auditors"
add_foreign_key "yearly_files", "organizations"
end end