diff --git a/app/assets/images/favicon.svg b/app/assets/images/favicon.svg new file mode 100644 index 0000000..b790ba6 --- /dev/null +++ b/app/assets/images/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 01357e5..5fcbdcc 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -104,3 +104,55 @@ a.link:hover { a.link:has(.active__button) { 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); + } + } +} diff --git a/app/controllers/concerns/Authorization.rb b/app/controllers/concerns/Authorization.rb index 0b692d4..2b06bd8 100644 --- a/app/controllers/concerns/Authorization.rb +++ b/app/controllers/concerns/Authorization.rb @@ -28,18 +28,13 @@ module Authorization session['cas'] && session['cas']['user'] end - def set_current_user(options = {}) - return nil if current_login.blank? - load_current_user - end - def load_current_user @current_user ||= User.where('lower(login) = lower(?)', current_login).first end def check_authentication 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 end end diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index f489d6d..b38582c 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -1,5 +1,5 @@ class OrganisationsController < ApplicationController - before_action :set_organisation, only: [:show] + before_action :set_organisation, only: [:show, :destroy, :reload] DOC_TYPES = { 0 => { title: 'Бухгалтерский баланс', form_code: '1.1' }, @@ -9,7 +9,7 @@ class OrganisationsController < ApplicationController }.freeze def index - organizations = Organization.all + @pagy, organizations = pagy(Organization.order(:created_at)) @info_about_organisations = organizations.each_with_object({}) do |org, hash| hash[org.id] = { @@ -42,6 +42,17 @@ class OrganisationsController < ApplicationController 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 def set_organisation diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index accc7a7..dbd227d 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -4,9 +4,13 @@ class ReportsController < ApplicationController end def create - inn = params[:inn] - report = Report.create!(inn: inn, status: 'processing') + inn = params[:inn].to_s.strip + 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) redirect_to reports_path, notice: 'Отчёт загружается в фоне' diff --git a/app/helpers/organisations_helper.rb b/app/helpers/organisations_helper.rb index d07afde..3ff6666 100644 --- a/app/helpers/organisations_helper.rb +++ b/app/helpers/organisations_helper.rb @@ -25,7 +25,7 @@ module OrganisationsHelper end 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?(')') value = value.tr('()', '') if negative value = value.delete(' ') diff --git a/app/services/report_parser.rb b/app/services/report_parser.rb index 9819598..1c5e5c6 100644 --- a/app/services/report_parser.rb +++ b/app/services/report_parser.rb @@ -33,17 +33,19 @@ class ReportParser parsed_forms = parse_forms save_aggregated_statements(parsed_forms) end + ensure + @tempfile&.close! 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) + @tempfile = Tempfile.new(['report', '.xlsx']) + @tempfile.binmode + @yearly_file.file.download { |chunk| @tempfile.write(chunk) } + @tempfile.rewind + Roo::Excelx.new(@tempfile.path) end end @@ -202,7 +204,7 @@ class ReportParser end def report_codes - @report_codes ||= ReportCode.select(:code, :name).to_a + @report_codes ||= ReportCode.select(:code, :name).order(:id).to_a end def report_codes_for_form(form_code, codes) @@ -229,7 +231,7 @@ class ReportParser .where(sheet_number: year) .where(yearly_files: { organization_id: @yearly_file.organization_id }) .where.not(id: keeper_id) - .delete_all + .destroy_all end end diff --git a/app/sidekiq/file_processor_job.rb b/app/sidekiq/file_processor_job.rb index 7080ff5..59b04aa 100644 --- a/app/sidekiq/file_processor_job.rb +++ b/app/sidekiq/file_processor_job.rb @@ -6,11 +6,12 @@ class FileProcessorJob include Sidekiq::Job def perform(inn, report_id) + raise ArgumentError, "Некорректный ИНН: #{inn}" unless inn.to_s.match?(/\A\d{10}(\d{2})?\z/) + report = Report.find(report_id) Rails.logger.info "=== Начало загрузки для ИНН: #{inn} ===" - # Временная папка - temp_dir = Rails.root.join('tmp', 'reports', inn) + temp_dir = Rails.root.join('tmp', 'reports', inn.to_s.gsub(/[^0-9]/, '')) FileUtils.mkdir_p(temp_dir) download_inn(inn, temp_dir.to_s) @@ -26,7 +27,9 @@ class FileProcessorJob ) # Создаём 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( io: File.open(file_path), filename: File.basename(file_path) @@ -57,18 +60,28 @@ class FileProcessorJob 'Accept-Language' => 'en-US,en;q=0.9', 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' } - content = HTTParty.get(url, headers: headers).body - json_need ? JSON.parse(content) : content + response = HTTParty.get(url, headers: headers, timeout: 30) + raise "HTTP #{response.code} при запросе #{url}" unless response.success? + + json_need ? JSON.parse(response.body) : response.body end def download_inn(inn, output) url = "https://bo.nalog.gov.ru/advanced-search/organizations/search?query=#{inn}&page=0&size=20" 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/" 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| url = "https://bo.nalog.gov.ru/download/bfo/#{id}?auditReport=false&balance=true"\ diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 03be6ca..7fedada 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -7,9 +7,7 @@ html meta name="turbo-refresh-method" content="morph" = csrf_meta_tags = csp_meta_tag - / = favicon_link_tag 'favicon.ico' - / = favicon_link_tag 'favicon-32x32.png', sizes: '32x32' - / = favicon_link_tag 'apple-icon.png', rel: 'apple-touch-icon' + = favicon_link_tag 'favicon.svg', type: 'image/svg+xml' = stylesheet_link_tag "tailwind", "inter-font", "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"] diff --git a/app/views/organisations/index.html.slim b/app/views/organisations/index.html.slim index 3d14171..77f515c 100644 --- a/app/views/organisations/index.html.slim +++ b/app/views/organisations/index.html.slim @@ -17,3 +17,4 @@ .information-button a.link[href="/organisations/#{organisation}"] = "Подробнее" + == pagy_nav(@pagy) diff --git a/app/views/organisations/show.html.slim b/app/views/organisations/show.html.slim index dd06b90..149efd3 100644 --- a/app/views/organisations/show.html.slim +++ b/app/views/organisations/show.html.slim @@ -14,6 +14,17 @@ - @info_about_organisation["Сведения об организации"].each do |key, value| .information-elem = "#{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 + |  Обновить данные + = link_to "/organisations/#{@org.id}", + class: 'link-action link-action--danger', + data: { turbo_method: :delete, turbo_confirm: 'Удалить организацию и все её данные? Это действие необратимо.' } do + i.bi.bi-trash + |  Удалить = render 'organisations/templates/type_doc', target: "type_doc_#{@type_doc}", org_id: "#{@org.id}" h2 = "#{@doc_name}" diff --git a/config/routes.rb b/config/routes.rb index 3793173..9b6d1fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,8 @@ Rails.application.routes.draw do get '/auth/login', to: 'auth#login', as: :login get '/organisations/index' get '/organisations/:id', to: 'organisations#show' + delete '/organisations/:id', to: 'organisations#destroy' + post '/organisations/:id/reload', to: 'organisations#reload' get '/auditors/index' get '/reports/create' get '/reports', to: 'reports#index'