SU2-33 | Фикс критических и серьёзных ошибок, удаление/перезагрузка организации, пагинация, favicon

This commit is contained in:
Dmitry
2026-04-21 07:54:59 +00:00
parent a1cbddf4e2
commit 136de5f238
12 changed files with 127 additions and 28 deletions
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#1a1a1a"/>
<rect x="5" y="22" width="5" height="6" rx="1" fill="white"/>
<rect x="13" y="16" width="5" height="12" rx="1" fill="white"/>
<rect x="21" y="10" width="5" height="18" rx="1" fill="white"/>
<polyline points="7.5,18 15.5,12 23.5,6" stroke="#4ade80" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="7.5" cy="18" r="1.5" fill="#4ade80"/>
<circle cx="15.5" cy="12" r="1.5" fill="#4ade80"/>
<circle cx="23.5" cy="6" r="1.5" fill="#4ade80"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

+52
View File
@@ -104,3 +104,55 @@ a.link:hover {
a.link:has(.active__button) { a.link:has(.active__button) {
background-color: gray; background-color: gray;
} }
nav.pagy {
display: flex;
justify-content: center;
gap: 8px;
margin-block: 20px;
font-size: 1.2rem;
a {
padding: 4px 10px;
border: 1px solid black;
text-decoration: none;
color: inherit;
&.current { font-weight: 700; }
&[aria-disabled="true"] { opacity: 0.4; }
&:not([aria-disabled="true"]):hover { background-color: black; color: white; }
}
}
.organisation-actions {
display: flex;
gap: 12px;
margin-block: 16px;
}
a.link-action {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid black;
border-radius: 20px;
padding: 8px 16px;
font-size: 1.2rem;
text-decoration: none;
color: inherit;
&:hover {
background-color: black;
color: var(--white-color);
}
&.link-action--danger {
border-color: #c0392b;
color: #c0392b;
&:hover {
background-color: #c0392b;
color: var(--white-color);
}
}
}
+1 -6
View File
@@ -28,18 +28,13 @@ module Authorization
session['cas'] && session['cas']['user'] session['cas'] && session['cas']['user']
end end
def set_current_user(options = {})
return nil if current_login.blank?
load_current_user
end
def load_current_user def load_current_user
@current_user ||= User.where('lower(login) = lower(?)', current_login).first @current_user ||= User.where('lower(login) = lower(?)', current_login).first
end end
def check_authentication def check_authentication
if session.blank? || session['cas'].blank? || session['cas']['user'].blank? || if session.blank? || session['cas'].blank? || session['cas']['user'].blank? ||
(request.get? && !request.xhr? && request.xhr? != 0 && (session['cas']['last_validated_at'].blank? || session['cas']['last_validated_at'] < 15.minutes.ago)) (request.get? && !request.xhr? && (session['cas']['last_validated_at'].blank? || session['cas']['last_validated_at'] < 15.minutes.ago))
render plain: 'Требуется авторизация', status: 401 render plain: 'Требуется авторизация', status: 401
end end
end end
+13 -2
View File
@@ -1,5 +1,5 @@
class OrganisationsController < ApplicationController class OrganisationsController < ApplicationController
before_action :set_organisation, only: [:show] before_action :set_organisation, only: [:show, :destroy, :reload]
DOC_TYPES = { DOC_TYPES = {
0 => { title: 'Бухгалтерский баланс', form_code: '1.1' }, 0 => { title: 'Бухгалтерский баланс', form_code: '1.1' },
@@ -9,7 +9,7 @@ class OrganisationsController < ApplicationController
}.freeze }.freeze
def index def index
organizations = Organization.all @pagy, organizations = pagy(Organization.order(:created_at))
@info_about_organisations = organizations.each_with_object({}) do |org, hash| @info_about_organisations = organizations.each_with_object({}) do |org, hash|
hash[org.id] = { hash[org.id] = {
@@ -42,6 +42,17 @@ class OrganisationsController < ApplicationController
end end
def destroy
@org.destroy!
redirect_to '/organisations/index', notice: "Организация #{@org.inn} удалена"
end
def reload
report = Report.create!(inn: @org.inn, status: 'processing')
FileProcessorJob.perform_async(@org.inn, report.id)
redirect_to "/organisations/#{@org.id}", notice: 'Данные обновляются в фоне'
end
private private
def set_organisation def set_organisation
+6 -2
View File
@@ -4,9 +4,13 @@ class ReportsController < ApplicationController
end end
def create def create
inn = params[:inn] inn = params[:inn].to_s.strip
report = Report.create!(inn: inn, status: 'processing') unless inn.match?(/\A\d{10}(\d{2})?\z/)
redirect_to reports_path, alert: 'Некорректный ИНН (должен содержать 10 или 12 цифр)'
return
end
report = Report.create!(inn: inn, status: 'processing')
FileProcessorJob.perform_async(inn, report.id) FileProcessorJob.perform_async(inn, report.id)
redirect_to reports_path, notice: 'Отчёт загружается в фоне' redirect_to reports_path, notice: 'Отчёт загружается в фоне'
+1 -1
View File
@@ -25,7 +25,7 @@ module OrganisationsHelper
end end
def parse_number(value) def parse_number(value)
return 0 if value.empty? || ['-', '(-)', 'X'].include?(value) return 0 if value.nil? || value.empty? || ['-', '(-)', 'X'].include?(value)
negative = value.start_with?('(') && value.end_with?(')') negative = value.start_with?('(') && value.end_with?(')')
value = value.tr('()', '') if negative value = value.tr('()', '') if negative
value = value.delete(' ') value = value.delete(' ')
+9 -7
View File
@@ -33,17 +33,19 @@ class ReportParser
parsed_forms = parse_forms parsed_forms = parse_forms
save_aggregated_statements(parsed_forms) save_aggregated_statements(parsed_forms)
end end
ensure
@tempfile&.close!
end end
private private
def spreadsheet def spreadsheet
@spreadsheet ||= begin @spreadsheet ||= begin
tempfile = Tempfile.new(['report', '.xlsx']) @tempfile = Tempfile.new(['report', '.xlsx'])
tempfile.binmode @tempfile.binmode
@yearly_file.file.download { |chunk| tempfile.write(chunk) } @yearly_file.file.download { |chunk| @tempfile.write(chunk) }
tempfile.rewind @tempfile.rewind
Roo::Excelx.new(tempfile.path) Roo::Excelx.new(@tempfile.path)
end end
end end
@@ -202,7 +204,7 @@ class ReportParser
end end
def report_codes def report_codes
@report_codes ||= ReportCode.select(:code, :name).to_a @report_codes ||= ReportCode.select(:code, :name).order(:id).to_a
end end
def report_codes_for_form(form_code, codes) def report_codes_for_form(form_code, codes)
@@ -229,7 +231,7 @@ class ReportParser
.where(sheet_number: year) .where(sheet_number: year)
.where(yearly_files: { organization_id: @yearly_file.organization_id }) .where(yearly_files: { organization_id: @yearly_file.organization_id })
.where.not(id: keeper_id) .where.not(id: keeper_id)
.delete_all .destroy_all
end end
end end
+20 -7
View File
@@ -6,11 +6,12 @@ class FileProcessorJob
include Sidekiq::Job include Sidekiq::Job
def perform(inn, report_id) def perform(inn, report_id)
raise ArgumentError, "Некорректный ИНН: #{inn}" unless inn.to_s.match?(/\A\d{10}(\d{2})?\z/)
report = Report.find(report_id) report = Report.find(report_id)
Rails.logger.info "=== Начало загрузки для ИНН: #{inn} ===" Rails.logger.info "=== Начало загрузки для ИНН: #{inn} ==="
# Временная папка temp_dir = Rails.root.join('tmp', 'reports', inn.to_s.gsub(/[^0-9]/, ''))
temp_dir = Rails.root.join('tmp', 'reports', inn)
FileUtils.mkdir_p(temp_dir) FileUtils.mkdir_p(temp_dir)
download_inn(inn, temp_dir.to_s) download_inn(inn, temp_dir.to_s)
@@ -26,7 +27,9 @@ class FileProcessorJob
) )
# Создаём YearlyFile и прикрепляем xlsx # Создаём YearlyFile и прикрепляем xlsx
yearly_file = organization.yearly_files.build(year: 0) year_from_filename = File.basename(file_path, '.*').split('_')[2].to_i
year_from_filename = 0 unless (2000..2099).cover?(year_from_filename)
yearly_file = organization.yearly_files.build(year: year_from_filename)
yearly_file.file.attach( yearly_file.file.attach(
io: File.open(file_path), io: File.open(file_path),
filename: File.basename(file_path) filename: File.basename(file_path)
@@ -57,18 +60,28 @@ class FileProcessorJob
'Accept-Language' => 'en-US,en;q=0.9', 'Accept-Language' => 'en-US,en;q=0.9',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
} }
content = HTTParty.get(url, headers: headers).body response = HTTParty.get(url, headers: headers, timeout: 30)
json_need ? JSON.parse(content) : content raise "HTTP #{response.code} при запросе #{url}" unless response.success?
json_need ? JSON.parse(response.body) : response.body
end end
def download_inn(inn, output) def download_inn(inn, output)
url = "https://bo.nalog.gov.ru/advanced-search/organizations/search?query=#{inn}&page=0&size=20" url = "https://bo.nalog.gov.ru/advanced-search/organizations/search?query=#{inn}&page=0&size=20"
content = get_content(url, true) content = get_content(url, true)
id = content['content'][0]['id'] org_entry = content.dig('content', 0) or raise "Организация с ИНН #{inn} не найдена"
id = org_entry['id']
url = "https://bo.nalog.gov.ru/nbo/organizations/#{id}/bfo/" url = "https://bo.nalog.gov.ru/nbo/organizations/#{id}/bfo/"
content = get_content(url, true) content = get_content(url, true)
periods_and_details_ids = content.map { |item| [item['period'], item['typeCorrections'][0]['correction']['id']] } raise "Нет отчётности для организации #{id}" if content.blank?
periods_and_details_ids = content.filter_map do |item|
details_id = item.dig('typeCorrections', 0, 'correction', 'id')
next if details_id.nil?
[item['period'], details_id]
end
periods_and_details_ids.each do |period, details_id| periods_and_details_ids.each do |period, details_id|
url = "https://bo.nalog.gov.ru/download/bfo/#{id}?auditReport=false&balance=true"\ url = "https://bo.nalog.gov.ru/download/bfo/#{id}?auditReport=false&balance=true"\
+1 -3
View File
@@ -7,9 +7,7 @@ html
meta name="turbo-refresh-method" content="morph" meta name="turbo-refresh-method" content="morph"
= csrf_meta_tags = csrf_meta_tags
= csp_meta_tag = csp_meta_tag
/ = favicon_link_tag 'favicon.ico' = favicon_link_tag 'favicon.svg', type: 'image/svg+xml'
/ = favicon_link_tag 'favicon-32x32.png', sizes: '32x32'
/ = favicon_link_tag 'apple-icon.png', rel: 'apple-touch-icon'
= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" = stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload"
= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" = stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload"
script[async src="https://ga.jspm.io/npm:es-module-shims@1.8.2/dist/es-module-shims.js" data-turbo-track="reload"] script[async src="https://ga.jspm.io/npm:es-module-shims@1.8.2/dist/es-module-shims.js" data-turbo-track="reload"]
+1
View File
@@ -17,3 +17,4 @@
.information-button .information-button
a.link[href="/organisations/#{organisation}"] a.link[href="/organisations/#{organisation}"]
= "Подробнее" = "Подробнее"
== pagy_nav(@pagy)
+11
View File
@@ -14,6 +14,17 @@
- @info_about_organisation["Сведения об организации"].each do |key, value| - @info_about_organisation["Сведения об организации"].each do |key, value|
.information-elem .information-elem
= "#{key}: #{value}" = "#{key}: #{value}"
.organisation-actions
= link_to "/organisations/#{@org.id}/reload",
class: 'link-action',
data: { turbo_method: :post, turbo_confirm: 'Перекачать данные с nalog.gov.ru?' } do
i.bi.bi-arrow-clockwise
| &nbsp;Обновить данные
= link_to "/organisations/#{@org.id}",
class: 'link-action link-action--danger',
data: { turbo_method: :delete, turbo_confirm: 'Удалить организацию и все её данные? Это действие необратимо.' } do
i.bi.bi-trash
| &nbsp;Удалить
= render 'organisations/templates/type_doc', target: "type_doc_#{@type_doc}", org_id: "#{@org.id}" = render 'organisations/templates/type_doc', target: "type_doc_#{@type_doc}", org_id: "#{@org.id}"
h2 h2
= "#{@doc_name}" = "#{@doc_name}"
+2
View File
@@ -5,6 +5,8 @@ Rails.application.routes.draw do
get '/auth/login', to: 'auth#login', as: :login get '/auth/login', to: 'auth#login', as: :login
get '/organisations/index' get '/organisations/index'
get '/organisations/:id', to: 'organisations#show' get '/organisations/:id', to: 'organisations#show'
delete '/organisations/:id', to: 'organisations#destroy'
post '/organisations/:id/reload', to: 'organisations#reload'
get '/auditors/index' get '/auditors/index'
get '/reports/create' get '/reports/create'
get '/reports', to: 'reports#index' get '/reports', to: 'reports#index'