From f973ec78df06608e30360ff1158874517ea4d677 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 12 Mar 2026 13:26:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D0=BB=D0=B5=D1=80=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=B0=20=D0=BF=D0=B0=D1=80=D1=81=D0=B5=D1=80,?= =?UTF-8?q?=20=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BE=20=D0=BA=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 1 + Gemfile.lock | 4 ++ app/controllers/auditors_controller.rb | 22 +++--- app/controllers/welcome_controller.rb | 42 ++++------- app/services/report_parser.rb | 98 ++++++++++++++++++++++++++ app/sidekiq/file_processor_job.rb | 17 ++++- 6 files changed, 142 insertions(+), 42 deletions(-) create mode 100644 app/services/report_parser.rb diff --git a/Gemfile b/Gemfile index 7eb2513..6975eed 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem "rails", "~> 7.1.3", ">= 7.1.3.2" gem "sidekiq" # background jobs processing gem 'httparty' # for http requests 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] gem "sprockets-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 866345b..299c6c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -335,6 +335,9 @@ GEM request_store (1.7.0) rack (>= 1.4) rexml (3.3.9) + roo (2.10.1) + nokogiri (~> 1) + rubyzip (>= 1.3.0, < 3.0.0) rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) @@ -496,6 +499,7 @@ DEPENDENCIES rack-cas! rails (~> 7.1.3, >= 7.1.3.2) redis (>= 4.0.1) + roo (~> 2.10) rspec-rails (~> 6.1.0) rubyzip selenium-webdriver diff --git a/app/controllers/auditors_controller.rb b/app/controllers/auditors_controller.rb index 1c963ee..bb0a4eb 100644 --- a/app/controllers/auditors_controller.rb +++ b/app/controllers/auditors_controller.rb @@ -1,17 +1,13 @@ class AuditorsController < ApplicationController def index - @info_about_auditors = {"A" => {"Наименование аудиторской организации/ФИО индивидуального аудитора" => "ООО \"Инвест-Аудит\"", - "ИНН" => 7606073440, - "ОГРН/ОРГНИП" => 1097606001250 - }, - "B" => {"Наименование аудиторской организации/ФИО индивидуального аудитора" => "ООО \"Инвест-Аудит\"", - "ИНН" => 7606073440, - "ОГРН/ОРГНИП" => 1097606001250 - }, - "C" => {"Наименование аудиторской организации/ФИО индивидуального аудитора" => "ООО \"Инвест-Аудит\"", - "ИНН" => 7606073440, - "ОГРН/ОРГНИП" => 1097606001250 - } - } + auditors = Auditor.all + + @info_about_auditors = auditors.each_with_object({}) do |auditor, hash| + hash[auditor.id] = { + "Наименование аудиторской организации/ФИО индивидуального аудитора" => auditor.name, + "ИНН" => auditor.inn, + "ОГРН/ОРГНИП" => auditor.ogrn + } + end end end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index 2eb0396..83a8c4c 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -1,33 +1,19 @@ class WelcomeController < ApplicationController def index - @info_about_organisations = {"A" => {"ИНН" => 7707602010, - "Полное наименование юридического лица" =>"Публичное акционерное общество \"М.видео\"", - "КПП" => 770101001, - "Код по ОКПО" => 71630621, - "Форма собственности (по ОКФС)" => "34 Совместная частная и иностранная собственность", - "Организационно-правовая форма (по ОКОПФ)" => "12247 Публичные акционерные общества", - "Вид экономической деятельности по ОКВЭД 2" => "70.10.1", - "Местонахождение (адрес)" => "105066, г. Москва, ул. Красносельская Нижн., д. 40/12, к. 20, пом. II" - }, - "B" => {"ИНН" => 7707602010, - "Полное наименование юридического лица" =>"Публичное акционерное общество \"М.видео\"", - "КПП" => 770101001, - "Код по ОКПО" => 71630621, - "Форма собственности (по ОКФС)" => "34 Совместная частная и иностранная собственность", - "Организационно-правовая форма (по ОКОПФ)" => "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" - } - } + organizations = Organization.all + + @info_about_organisations = organizations.each_with_object({}) do |org, hash| + hash[org.id] = { + "ИНН" => org.inn, + "Полное наименование юридического лица" => org.full_name, + "КПП" => org.kpp, + "Код по ОКПО" => org.okpo, + "Форма собственности (по ОКФС)" => org.okfs, + "Организационно-правовая форма (по ОКОПФ)" => org.okopf, + "Вид экономической деятельности по ОКВЭД 2" => org.okved2, + "Местонахождение (адрес)" => org.address + } + end end end diff --git a/app/services/report_parser.rb b/app/services/report_parser.rb new file mode 100644 index 0000000..f277c92 --- /dev/null +++ b/app/services/report_parser.rb @@ -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 diff --git a/app/sidekiq/file_processor_job.rb b/app/sidekiq/file_processor_job.rb index a331ac5..7080ff5 100644 --- a/app/sidekiq/file_processor_job.rb +++ b/app/sidekiq/file_processor_job.rb @@ -15,11 +15,26 @@ class FileProcessorJob 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| + # Прикрепляем файл к Report (обратная совместимость) report.files.attach( io: File.open(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 # Удали временные файлы @@ -69,4 +84,4 @@ class FileProcessorJob end end end -end \ No newline at end of file +end