Compare commits
33
Commits
main
..
e54299c13b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e54299c13b | ||
|
|
bc095da738 | ||
|
|
c750e470d6 | ||
|
|
44cd3993ac | ||
|
|
e9660f016f | ||
|
|
6fae7b50eb | ||
|
|
562fad5b22 | ||
|
|
db08f63ac2 | ||
|
|
3b35a63346 | ||
|
|
a03839a834 | ||
|
|
c74ffc2aea | ||
|
|
4484126e8f | ||
|
|
a246648340 | ||
|
|
1be0c00ba4 | ||
|
|
92f0cdd272 | ||
|
|
7c390e9a69 | ||
|
|
644d7616ae | ||
|
|
9a74c2c598 | ||
|
|
b5ae88f2b8 | ||
|
|
f944c248ea | ||
|
|
b06903f448 | ||
|
|
853b82660f | ||
|
|
df4c307e24 | ||
|
|
85fbac1cfd | ||
|
|
7ee21d5e5d | ||
|
|
207e4b45a7 | ||
|
|
dcba938feb | ||
|
|
70f051da92 | ||
|
|
4e521a5422 | ||
|
|
f973ec78df | ||
|
|
c016d6a319 | ||
|
|
e55ecb7d29 | ||
|
|
0dc0f10da7 |
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M8 15 L1 8 H5 V1 H11 V8 H15 Z"
|
||||||
|
fill="#D9534F"
|
||||||
|
stroke="#C23026"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 209 B |
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M8 1 L15 8 H11 V15 H5 V8 H1 Z"
|
||||||
|
fill="#6A9E8A"
|
||||||
|
stroke="#497C64"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 209 B |
@@ -18,4 +18,9 @@
|
|||||||
@use "jquery-ui/core";
|
@use "jquery-ui/core";
|
||||||
@use "toastr";
|
@use "toastr";
|
||||||
|
|
||||||
@use "base";
|
@use "base";
|
||||||
|
@use "application_settings";
|
||||||
|
@use "main";
|
||||||
|
@use "header";
|
||||||
|
@use "nav";
|
||||||
|
@use "organisation";
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
:root{
|
||||||
|
--bg-color: #F3F3F5;
|
||||||
|
--navbar-border: #d3d3d3;
|
||||||
|
--blue-color: #0078F6;
|
||||||
|
--blue-light-color: #e5f1fe;
|
||||||
|
--text-callout-color: #B6B6B6;
|
||||||
|
--yellow-color: #F9D256;
|
||||||
|
--white-color: #fff;
|
||||||
|
--red-color: #EF3E3B;
|
||||||
|
--footer-text-color: #757575;
|
||||||
|
--purple-color: #4C13A2;
|
||||||
|
--green-color: #4CBFC0;
|
||||||
|
--orange-color: #FE9F40;
|
||||||
|
--grey-color: #C9CBCE;
|
||||||
|
--grey-light-color:#F6F6F6;
|
||||||
|
--grey-dark-color:#a9a9a9;
|
||||||
|
--border-dropdown-color: #e8e8e8;
|
||||||
|
--text-dropdown-color: #0F0F11;
|
||||||
|
--main-header-text: #7C7C7C;
|
||||||
|
|
||||||
|
//TEXT weight
|
||||||
|
--bold: 700;
|
||||||
|
--semi-bold: 600;
|
||||||
|
--medium: 500;
|
||||||
|
--regular: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
width: 90vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.welcome__container__laptop {
|
||||||
|
margin-top: 15%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 36px;
|
||||||
|
font-family: "Ubuntu sans";
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.main-header {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: var(--bold);
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
.add-new {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-block: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.link {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 20px 20px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.link:hover {
|
||||||
|
background-color: black;
|
||||||
|
color: var(--white-color)
|
||||||
|
}
|
||||||
|
|
||||||
|
.header{
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: var(--bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
height: 100vh;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.informations-data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 90%;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.informations-data h2 {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information-data {
|
||||||
|
border: 2px solid black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information-elem {
|
||||||
|
font-weight: var(--semi-bold);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 2.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-new-organisation {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information-button {
|
||||||
|
align-self: center;
|
||||||
|
margin-block: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information-table table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information-table td, th {
|
||||||
|
padding: 5px;
|
||||||
|
border: 3px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information-table tr :is(td, th):first-child {
|
||||||
|
width: 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type_doc_header#tabs {
|
||||||
|
width: 75%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type__doc__list__type__container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
place-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
a.link {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.link:has(.active__button) {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.nav-container {
|
||||||
|
width: 25%;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.up::before,
|
||||||
|
.down::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.up::before {
|
||||||
|
background-image: asset-url('arrow_up.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.down::before {
|
||||||
|
background-image: asset-url('arrow_down.svg');
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class AuditorsController < ApplicationController
|
||||||
|
def index
|
||||||
|
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
|
||||||
@@ -2,11 +2,13 @@ module Authorization
|
|||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :set_paper_trail_whodunnit
|
|
||||||
before_action :load_current_user # load user from database
|
before_action :load_current_user # load user from database
|
||||||
|
before_action :set_paper_trail_whodunnit
|
||||||
before_action :check_authentication # check if user authenticated
|
before_action :check_authentication # check if user authenticated
|
||||||
before_action :check_current_user # check if user authorized
|
before_action :check_current_user # check if user authorized
|
||||||
|
|
||||||
|
helper_method :logged_in?, :current_user
|
||||||
|
|
||||||
rescue_from ActionController::UnknownFormat do
|
rescue_from ActionController::UnknownFormat do
|
||||||
request.format = :html
|
request.format = :html
|
||||||
render_error
|
render_error
|
||||||
@@ -15,6 +17,10 @@ module Authorization
|
|||||||
def current_login
|
def current_login
|
||||||
session['zombie'] || cas_login
|
session['zombie'] || cas_login
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def logged_in?
|
||||||
|
current_login.present?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
def show
|
||||||
|
@type_doc = params[:type_doc].present? ? params[:type_doc].to_i : 0
|
||||||
|
|
||||||
|
statements_by_year_and_form = build_statements_by_year_and_form
|
||||||
|
years = statements_by_year_and_form.keys.sort.reverse
|
||||||
|
grouped_rows = clean_data(build_grouped_rows(statements_by_year_and_form, years))
|
||||||
|
@info_about_organisation = build_organization_payload(years)
|
||||||
|
|
||||||
|
selected_doc = DOC_TYPES.fetch(@type_doc, DOC_TYPES[0])
|
||||||
|
@doc_name = selected_doc[:title]
|
||||||
|
@current_doc_info1 = grouped_rows[@doc_name]
|
||||||
|
if !@current_doc_info1.values.first.nil?
|
||||||
|
@current_doc_info = @info_about_organisation[@doc_name][0..@current_doc_info1.values.first.size + 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
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 clean_data(data)
|
||||||
|
data.transform_values do |doc_type_data|
|
||||||
|
rows = doc_type_data.reject do |_, values|
|
||||||
|
values.all? { |value| value == "-" || value.nil? || value == ""}
|
||||||
|
end
|
||||||
|
|
||||||
|
next {} if rows.empty?
|
||||||
|
|
||||||
|
column_count = rows.values.map(&:size).max
|
||||||
|
columns_to_keep = (0...column_count).select do |i|
|
||||||
|
rows.values.any? { |values| values[i] != "-" && !values[i].nil? && values[i] != ""}
|
||||||
|
end
|
||||||
|
rows.transform_values do |values|
|
||||||
|
columns_to_keep.map { |i| values[i]}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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,4 +1,11 @@
|
|||||||
class WelcomeController < ApplicationController
|
class WelcomeController < ApplicationController
|
||||||
|
def home
|
||||||
def home; end
|
if logged_in?
|
||||||
|
if @current_user.present?
|
||||||
|
redirect_to '/organisations/index'
|
||||||
|
else
|
||||||
|
render_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
module AuditorsHelper
|
||||||
|
end
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
module OrganisationsHelper
|
||||||
|
# Функции для раскраски
|
||||||
|
def group_key(code)
|
||||||
|
return nil if code.nil? || code.size < 2
|
||||||
|
prefix = code[0, 2]
|
||||||
|
case prefix
|
||||||
|
when '21', '22', '23', '24'
|
||||||
|
'21-24'
|
||||||
|
else
|
||||||
|
prefix
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def keep_code?(code, type_data: 'min_max')
|
||||||
|
return false if code.nil? || code.size < 2
|
||||||
|
prefix = code[0, 2]
|
||||||
|
return false unless prefix.match?(/\A\d{2}\z/)
|
||||||
|
if type_data == 'min_max'
|
||||||
|
return false if %w[16 17].include?(prefix)
|
||||||
|
return true if %w[21 22 23 24 25 29].include?(prefix)
|
||||||
|
return !code.end_with?('00')
|
||||||
|
elsif type_data == 'comparsion'
|
||||||
|
return code.end_with?('00')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_number(value)
|
||||||
|
return 0 if value.empty? || ['-', '(-)', 'X'].include?(value)
|
||||||
|
negative = value.start_with?('(') && value.end_with?(')')
|
||||||
|
value = value.tr('()', '') if negative
|
||||||
|
value = value.delete(' ')
|
||||||
|
return 0 unless value.match?(/\A\d+\z/)
|
||||||
|
number = value.to_i
|
||||||
|
negative ? -number : number
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_data(data, type_data: 'min_max')
|
||||||
|
return {} if data.nil?
|
||||||
|
filtered_data = data.reject do |(_, _), values|
|
||||||
|
values.all? { |value| ['-', '(-)', 'X'].include?(value) }
|
||||||
|
end
|
||||||
|
if type_data == 'min_max'
|
||||||
|
prepared_data = filtered_data.each_with_object({}) do |((_, code), values), result|
|
||||||
|
next if code.nil? || !keep_code?(code)
|
||||||
|
group = group_key(code)
|
||||||
|
result[group] ||= Hash.new { |h, k| h[k] = [] }
|
||||||
|
values.each_with_index do |value, index|
|
||||||
|
result[group][index + 1] << parse_number(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
min_max_data = prepared_data.transform_values do |columns|
|
||||||
|
columns.transform_values do |values|
|
||||||
|
sorted = values.sort
|
||||||
|
size = sorted.size
|
||||||
|
p50 = size.odd? ? sorted[size / 2] : (sorted[size / 2 - 1] + sorted[size / 2]) / 2.0
|
||||||
|
{ min: sorted.first, p50: p50, max: sorted.last }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elsif type_data == 'comparsion'
|
||||||
|
prepared_data = filtered_data.each_with_object({}) do |((_, code), values), result|
|
||||||
|
next if code.nil? || !keep_code?(code, type_data: 'comparsion')
|
||||||
|
result[code] ||= []
|
||||||
|
(0...values.size - 1).each do |index|
|
||||||
|
if parse_number(values[index]) == parse_number(values[index + 1])
|
||||||
|
result[code] << nil
|
||||||
|
elsif
|
||||||
|
result[code] << (parse_number(values[index]) > parse_number(values[index + 1]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result[code] << nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_color_for_cell(value, min:, median:, max:)
|
||||||
|
value = parse_number(value)
|
||||||
|
return rgb_to_hex(WHITE) if value.nil? || min.nil? || max.nil? || median.nil?
|
||||||
|
return rgb_to_hex(GREEN) if min == max
|
||||||
|
|
||||||
|
if value <= median
|
||||||
|
ratio = median == min ? 0.0 : (value - min).to_f / (median - min)
|
||||||
|
rgb = interpolate_color(RED, YELLOW, clamp(ratio))
|
||||||
|
else
|
||||||
|
ratio = max == median ? 0.0 : (value - median).to_f / (max - median)
|
||||||
|
rgb = interpolate_color(YELLOW, GREEN, clamp(ratio))
|
||||||
|
end
|
||||||
|
|
||||||
|
rgb_to_hex(rgb)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_arrow_for_cell(value, condition)
|
||||||
|
return content_tag(:td, value) if condition.nil?
|
||||||
|
css_class = condition ? 'up': 'down'
|
||||||
|
content_tag(:td, value, class: css_class)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def interpolate_color(from, to, ratio)
|
||||||
|
from.zip(to).map { |a, b| (a + (b - a) * ratio).round }
|
||||||
|
end
|
||||||
|
|
||||||
|
def rgb_to_hex(rgb)
|
||||||
|
format("#%02X%02X%02X", *rgb)
|
||||||
|
end
|
||||||
|
|
||||||
|
def clamp(value)
|
||||||
|
[[value, 0.0].max, 1.0].min
|
||||||
|
end
|
||||||
|
|
||||||
|
RED = [248, 105, 107].freeze
|
||||||
|
YELLOW = [255, 235, 132].freeze
|
||||||
|
GREEN = [99, 190, 123].freeze
|
||||||
|
WHITE = [255, 255, 255].freeze
|
||||||
|
|
||||||
|
end
|
||||||
@@ -4,4 +4,5 @@ import "controllers"
|
|||||||
import "jquery"
|
import "jquery"
|
||||||
import "jquery_ujs"
|
import "jquery_ujs"
|
||||||
import "toastr"
|
import "toastr"
|
||||||
|
import "tabs_select"
|
||||||
window.toastr = toastr
|
window.toastr = toastr
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
let tabs_select = () => {
|
||||||
|
const tabsContainer = document.getElementById("tabs")
|
||||||
|
if (!tabsContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = tabsContainer.getElementsByClassName("switch__button")
|
||||||
|
for (const button of buttons) {
|
||||||
|
const value = button.getAttribute("data-value");
|
||||||
|
const id = button.getAttribute("id");
|
||||||
|
button.classList.toggle("active__button", value === id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('turbo:load', tabs_select)
|
||||||
|
console.log('Hi')
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class Auditor < ApplicationRecord
|
||||||
|
has_many :yearly_files, dependent: :nullify
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class FinancialStatement < ApplicationRecord
|
||||||
|
belongs_to :yearly_file
|
||||||
|
|
||||||
|
validates :data, presence: true
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
class ReportCode < ApplicationRecord
|
||||||
|
validates :code, presence: true, uniqueness: true
|
||||||
|
validates :name, presence: true
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
def call
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
ReportParsers::OrganizationInfoParser.new(@yearly_file, spreadsheet: spreadsheet).call
|
||||||
|
|
||||||
|
parsed_forms = parse_forms
|
||||||
|
save_aggregated_statements(parsed_forms)
|
||||||
|
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 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 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)
|
||||||
|
previous_data = statement.data.deep_dup if statement.persisted?
|
||||||
|
|
||||||
|
statement.assign_attributes(
|
||||||
|
yearly_file: @yearly_file,
|
||||||
|
sheet_number: year,
|
||||||
|
data: build_payload_for_year(year, parsed_forms, codes, previous_data: previous_data)
|
||||||
|
)
|
||||||
|
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, previous_data: nil)
|
||||||
|
forms_payload = FORM_DEFINITIONS.each_with_object({}) do |(form_code, config), hash|
|
||||||
|
form_present, form_data = parsed_form_for_year(parsed_forms, form_code, year)
|
||||||
|
form_data ||= {}
|
||||||
|
|
||||||
|
unless form_present
|
||||||
|
previous_payload = previous_form_payload(previous_data, form_code)
|
||||||
|
if previous_payload.present?
|
||||||
|
hash[form_code] = previous_payload
|
||||||
|
next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
source_rows = form_data[:rows] || form_data['rows'] || []
|
||||||
|
rows_by_code = index_rows_by_code(source_rows)
|
||||||
|
form_codes = report_codes_for_form(form_code, codes)
|
||||||
|
|
||||||
|
hash[form_code] = {
|
||||||
|
form_code: form_code,
|
||||||
|
sheet_name: form_data[:sheet_name] || form_data['sheet_name'] || config.fetch(:sheet_name),
|
||||||
|
rows: form_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: normalized_statement_value(source_value)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
data_year: year,
|
||||||
|
forms: forms_payload
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def parsed_form_for_year(parsed_forms, form_code, year)
|
||||||
|
form_results = parsed_forms.fetch(form_code, {})
|
||||||
|
return [true, form_results[year]] if form_results.key?(year)
|
||||||
|
return [true, form_results[year.to_s]] if form_results.key?(year.to_s)
|
||||||
|
|
||||||
|
[false, {}]
|
||||||
|
end
|
||||||
|
|
||||||
|
def previous_form_payload(previous_data, form_code)
|
||||||
|
forms = previous_data && (previous_data[:forms] || previous_data['forms'])
|
||||||
|
return unless forms.is_a?(Hash)
|
||||||
|
|
||||||
|
forms[form_code] || forms[form_code.to_sym]
|
||||||
|
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(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(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 normalized_statement_value(value)
|
||||||
|
return DEFAULT_MISSING_VALUE if value.nil?
|
||||||
|
return value if value.is_a?(Numeric)
|
||||||
|
|
||||||
|
text = value.to_s.strip
|
||||||
|
return DEFAULT_MISSING_VALUE if text.blank?
|
||||||
|
|
||||||
|
numeric_text?(text) ? text : DEFAULT_MISSING_VALUE
|
||||||
|
end
|
||||||
|
|
||||||
|
def numeric_text?(text)
|
||||||
|
normalized = text.tr("\u00A0", ' ').squeeze(' ')
|
||||||
|
|
||||||
|
integer_or_decimal = /\A[+-]?\d+(?:[.,]\d+)?\z/
|
||||||
|
integer_or_decimal_grouped = /\A[+-]?\d{1,3}(?: \d{3})+(?:[.,]\d+)?\z/
|
||||||
|
parenthesized_integer_or_decimal = /\A\(\d+(?:[.,]\d+)?\)\z/
|
||||||
|
parenthesized_integer_or_decimal_grouped = /\A\(\d{1,3}(?: \d{3})+(?:[.,]\d+)?\)\z/
|
||||||
|
|
||||||
|
normalized.match?(integer_or_decimal) ||
|
||||||
|
normalized.match?(integer_or_decimal_grouped) ||
|
||||||
|
normalized.match?(parenthesized_integer_or_decimal) ||
|
||||||
|
normalized.match?(parenthesized_integer_or_decimal_grouped)
|
||||||
|
end
|
||||||
|
|
||||||
|
def report_codes
|
||||||
|
@report_codes ||= ReportCode.order(:code).select(:code, :name).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def report_codes_for_form(form_code, codes)
|
||||||
|
codes.select { |report_code| report_code_belongs_to_form?(report_code.code, form_code) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def report_code_belongs_to_form?(code, form_code)
|
||||||
|
normalized_code = normalize_code(code)
|
||||||
|
return false if normalized_code.blank?
|
||||||
|
|
||||||
|
case form_code
|
||||||
|
when '1.1' then normalized_code.start_with?('1')
|
||||||
|
when '1.2' then normalized_code.start_with?('2')
|
||||||
|
when '1.3' then normalized_code.start_with?('3')
|
||||||
|
when '1.4' then normalized_code.start_with?('4')
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
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
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
require 'set'
|
||||||
|
|
||||||
|
module ReportParsers
|
||||||
|
class BaseParser
|
||||||
|
def initialize(yearly_file, spreadsheet:)
|
||||||
|
@yearly_file = yearly_file
|
||||||
|
@spreadsheet = spreadsheet
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :yearly_file, :spreadsheet
|
||||||
|
|
||||||
|
def sheet_exists?(sheet_name)
|
||||||
|
spreadsheet.sheets.include?(sheet_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_cell(sheet, row, col)
|
||||||
|
value = sheet.cell(row, col)
|
||||||
|
|
||||||
|
# Handle merged cells: if value is nil/blank, try reading from nearby cells
|
||||||
|
if value.blank? && sheet.respond_to?(:merged_cells)
|
||||||
|
value = find_merged_cell_value(sheet, row, col)
|
||||||
|
end
|
||||||
|
|
||||||
|
value.is_a?(String) ? value.strip.presence : value
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_merged_cell_value(sheet, row, col)
|
||||||
|
# Try to find value in merged cell range
|
||||||
|
# Check cells in a small radius around the target cell
|
||||||
|
(-1..1).each do |row_offset|
|
||||||
|
(-1..1).each do |col_offset|
|
||||||
|
next if row_offset == 0 && col_offset == 0
|
||||||
|
|
||||||
|
check_row = row + row_offset
|
||||||
|
check_col = col + col_offset
|
||||||
|
|
||||||
|
next if check_row < 1 || check_col < 1
|
||||||
|
next if check_row > sheet.last_row || check_col > sheet.last_column
|
||||||
|
|
||||||
|
value = sheet.cell(check_row, check_col)
|
||||||
|
return value if value.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_value_by_labels(sheet, labels, row_range:, value_offset: 1)
|
||||||
|
position = find_label_position(sheet, labels, row_range: row_range)
|
||||||
|
return if position.nil?
|
||||||
|
|
||||||
|
row, col = position
|
||||||
|
|
||||||
|
max_col = sheet.last_column.to_i
|
||||||
|
(col + value_offset).upto(max_col) do |search_col|
|
||||||
|
value = read_cell(sheet, row, search_col)
|
||||||
|
return value if value.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_label_position(sheet, labels, row_range:)
|
||||||
|
normalized_labels = Array(labels).map { |label| normalize_label(label) }.to_set
|
||||||
|
max_col = sheet.last_column.to_i
|
||||||
|
return if max_col <= 1
|
||||||
|
|
||||||
|
row_range.each do |row|
|
||||||
|
1.upto(max_col - 1) do |col|
|
||||||
|
label_text = normalize_label(sheet.cell(row, col))
|
||||||
|
next if label_text.blank?
|
||||||
|
next unless label_matches?(label_text, normalized_labels)
|
||||||
|
|
||||||
|
return [row, col]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def label_matches?(label_text, normalized_labels)
|
||||||
|
normalized_labels.any? do |label|
|
||||||
|
label_text == label ||
|
||||||
|
label_text.start_with?("#{label} ") ||
|
||||||
|
label_text.start_with?("#{label}:")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_label(value)
|
||||||
|
value.to_s
|
||||||
|
.downcase
|
||||||
|
.tr("\u00A0", ' ')
|
||||||
|
.gsub(/\s+/, ' ')
|
||||||
|
.gsub(/[«»]/, '')
|
||||||
|
.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_line_name(value)
|
||||||
|
text = value.to_s
|
||||||
|
.tr("\u00A0", ' ')
|
||||||
|
.gsub(/[⁰¹²³⁴⁵⁶⁷⁸⁹]/, '')
|
||||||
|
.gsub(/(?<=\p{L})\d{1,2}(?=\s|\z)/u, '')
|
||||||
|
.gsub(/\s+/, ' ')
|
||||||
|
.strip
|
||||||
|
|
||||||
|
text.presence
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
module ReportParsers
|
||||||
|
class Form11Parser < BaseParser
|
||||||
|
SHEET_NAME = 'Бухгалтерский баланс'
|
||||||
|
FORM_CODE = '1.1'
|
||||||
|
IGNORED_KEYS = [2].freeze
|
||||||
|
|
||||||
|
def call
|
||||||
|
return {} unless sheet_exists?(SHEET_NAME)
|
||||||
|
|
||||||
|
sheet = spreadsheet.sheet(SHEET_NAME)
|
||||||
|
extract_year(sheet)
|
||||||
|
build_payloads(sheet)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_payloads(sheet)
|
||||||
|
header_row = find_header_row(sheet)
|
||||||
|
columns = detect_columns(sheet, header_row)
|
||||||
|
|
||||||
|
|
||||||
|
value_headers = columns[:value_columns].each_with_object({}) do |col, hash|
|
||||||
|
header_text = read_cell(sheet, header_row, col).to_s
|
||||||
|
year = extract_year_from_header(header_text)
|
||||||
|
hash[col] = year
|
||||||
|
end
|
||||||
|
|
||||||
|
all_rows = (header_row + 1).upto(sheet.last_row).filter_map do |row|
|
||||||
|
line_name = normalize_line_name(read_cell(sheet, row, columns[:name_column]))
|
||||||
|
line_code = read_cell(sheet, row, columns[:code_column]).to_s.strip.presence
|
||||||
|
next if ignored_row?(line_name, line_code)
|
||||||
|
|
||||||
|
values = columns[:value_columns].each_with_object({}) do |col, hash|
|
||||||
|
value = read_cell(sheet, row, col)
|
||||||
|
year = value_headers[col]
|
||||||
|
hash[year.to_s] = value if value.present? && year.present?
|
||||||
|
end.compact_blank
|
||||||
|
|
||||||
|
next if line_name.blank? && line_code.blank? && values.blank?
|
||||||
|
|
||||||
|
{
|
||||||
|
line_name: line_name,
|
||||||
|
line_code: line_code,
|
||||||
|
values: values
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
payloads_by_year = {}
|
||||||
|
value_headers.each do |col, year|
|
||||||
|
next unless year.present?
|
||||||
|
|
||||||
|
rows_for_year = all_rows.map do |row|
|
||||||
|
value = row[:values][year.to_s]
|
||||||
|
next unless value.present?
|
||||||
|
|
||||||
|
{
|
||||||
|
line_name: row[:line_name],
|
||||||
|
line_code: row[:line_code],
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
end.compact
|
||||||
|
|
||||||
|
payloads_by_year[year] = {
|
||||||
|
form_code: FORM_CODE,
|
||||||
|
sheet_name: SHEET_NAME,
|
||||||
|
header_row: header_row,
|
||||||
|
data_year: year,
|
||||||
|
rows: rows_for_year
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
payloads_by_year
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_year_from_header(header_text)
|
||||||
|
year_match = header_text[/\b(20\d{2})\b/]
|
||||||
|
year_match.to_i if year_match
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_header_row(sheet)
|
||||||
|
1.upto(sheet.last_row) do |row|
|
||||||
|
normalized_cells = 1.upto(sheet.last_column).map { |col| normalize_label(sheet.cell(row, col)) }
|
||||||
|
has_name = normalized_cells.any? { |text| text.include?('наименование показателя') }
|
||||||
|
has_code = normalized_cells.any? { |text| text == 'код' || text.start_with?('код ') || text.start_with?('код:') }
|
||||||
|
return row if has_name && has_code
|
||||||
|
end
|
||||||
|
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def detect_columns(sheet, header_row)
|
||||||
|
columns = 1.upto(sheet.last_column).each_with_object({}) do |col, hash|
|
||||||
|
hash[col] = normalize_label(sheet.cell(header_row, col))
|
||||||
|
end
|
||||||
|
|
||||||
|
name_column = columns.key(columns.values.find { |text| text.include?('наименование показателя') }) || 1
|
||||||
|
|
||||||
|
code_column = columns.key(columns.values.find { |text|
|
||||||
|
text == 'код строки' || text.start_with?('код строки ') ||
|
||||||
|
(text == 'код' && columns[name_column + 1] == text)
|
||||||
|
}) || columns.key(columns.values.find { |text| text == 'код' || text.start_with?('код ') }) || [name_column + 1, sheet.last_column].min
|
||||||
|
|
||||||
|
value_columns = columns.keys.select do |col|
|
||||||
|
text = columns[col]
|
||||||
|
text.include?('на 31') ||
|
||||||
|
text.include?('на ') ||
|
||||||
|
text.match?(/\bна \d+ /) ||
|
||||||
|
text.match?(/\b20\d{2}\b/) ||
|
||||||
|
(col > code_column && text.include?('декабря')) ||
|
||||||
|
(col > code_column && text.match?(/\d+/)) # fallback: numeric values
|
||||||
|
end
|
||||||
|
|
||||||
|
if value_columns.empty?
|
||||||
|
value_columns = columns.keys.select { |col| col > code_column && columns[col].present? }
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
name_column: name_column,
|
||||||
|
code_column: code_column,
|
||||||
|
value_columns: value_columns.sort.first(3)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_year(sheet)
|
||||||
|
period_text = sheet.cell(4, 1).to_s
|
||||||
|
year = period_text[/\d{4}/].to_i
|
||||||
|
yearly_file.update!(year: year) if year.positive?
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignored_row?(line_name, line_code)
|
||||||
|
keys = [line_code, line_name].compact.map { |value| normalize_label(value) }
|
||||||
|
keys.any? { |key| ignored_keys.include?(key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignored_keys
|
||||||
|
@ignored_keys ||= IGNORED_KEYS.map { |value| normalize_label(value) }.to_set
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
module ReportParsers
|
||||||
|
class Form12Parser < BaseParser
|
||||||
|
SHEET_NAME = 'Отчет о финансовых результатах'
|
||||||
|
FORM_CODE = '1.2'
|
||||||
|
# Add line codes or names that should be skipped during parsing.
|
||||||
|
IGNORED_KEYS = [2].freeze
|
||||||
|
|
||||||
|
def call
|
||||||
|
return {} unless sheet_exists?(SHEET_NAME)
|
||||||
|
|
||||||
|
sheet = spreadsheet.sheet(SHEET_NAME)
|
||||||
|
build_payloads(sheet)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_payloads(sheet)
|
||||||
|
header_row = find_header_row(sheet)
|
||||||
|
columns = detect_columns(sheet, header_row)
|
||||||
|
|
||||||
|
value_headers = columns[:value_columns].each_with_object({}) do |col, hash|
|
||||||
|
header_text = read_cell(sheet, header_row, col).to_s
|
||||||
|
year = extract_year_from_header(header_text)
|
||||||
|
hash[col] = year
|
||||||
|
end
|
||||||
|
|
||||||
|
all_rows = (header_row + 1).upto(sheet.last_row).filter_map do |row|
|
||||||
|
line_name = normalize_line_name(read_cell(sheet, row, columns[:name_column]))
|
||||||
|
line_code = read_cell(sheet, row, columns[:code_column]).to_s.strip.presence
|
||||||
|
next if ignored_row?(line_name, line_code)
|
||||||
|
|
||||||
|
values = columns[:value_columns].each_with_object({}) do |col, hash|
|
||||||
|
value = read_cell(sheet, row, col)
|
||||||
|
year = value_headers[col]
|
||||||
|
hash[year.to_s] = value if value.present? && year.present?
|
||||||
|
end.compact_blank
|
||||||
|
|
||||||
|
next if line_name.blank? && line_code.blank? && values.blank?
|
||||||
|
|
||||||
|
{
|
||||||
|
line_name: line_name,
|
||||||
|
line_code: line_code,
|
||||||
|
values: values
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
payloads_by_year = {}
|
||||||
|
value_headers.each do |col, year|
|
||||||
|
next unless year.present?
|
||||||
|
|
||||||
|
rows_for_year = all_rows.map do |row|
|
||||||
|
value = row[:values][year.to_s]
|
||||||
|
next unless value.present?
|
||||||
|
|
||||||
|
{
|
||||||
|
line_name: row[:line_name],
|
||||||
|
line_code: row[:line_code],
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
end.compact
|
||||||
|
|
||||||
|
payloads_by_year[year] = {
|
||||||
|
form_code: FORM_CODE,
|
||||||
|
sheet_name: SHEET_NAME,
|
||||||
|
header_row: header_row,
|
||||||
|
data_year: year,
|
||||||
|
rows: rows_for_year
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
payloads_by_year
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_year_from_header(header_text)
|
||||||
|
year_match = header_text[/\b(20\d{2})\b/]
|
||||||
|
year_match.to_i if year_match
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_header_row(sheet)
|
||||||
|
1.upto(sheet.last_row) do |row|
|
||||||
|
normalized_cells = 1.upto(sheet.last_column).map { |col| normalize_label(sheet.cell(row, col)) }
|
||||||
|
has_name = normalized_cells.any? { |text| text.include?('наименование показателя') }
|
||||||
|
has_code = normalized_cells.any? { |text| text == 'код' || text.start_with?('код ') || text.start_with?('код:') }
|
||||||
|
return row if has_name && has_code
|
||||||
|
end
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def detect_columns(sheet, header_row)
|
||||||
|
columns = 1.upto(sheet.last_column).each_with_object({}) do |col, hash|
|
||||||
|
hash[col] = normalize_label(sheet.cell(header_row, col))
|
||||||
|
end
|
||||||
|
|
||||||
|
name_column = columns.key(columns.values.find { |text| text.include?('наименование показателя') }) || 1
|
||||||
|
|
||||||
|
code_column = columns.key(columns.values.find { |text|
|
||||||
|
text == 'код строки' || text.start_with?('код строки ') ||
|
||||||
|
(text == 'код' && columns[name_column + 1] == text)
|
||||||
|
}) || columns.key(columns.values.find { |text| text == 'код' || text.start_with?('код ') }) || [name_column + 1, sheet.last_column].min
|
||||||
|
|
||||||
|
value_columns = columns.keys.select do |col|
|
||||||
|
text = columns[col]
|
||||||
|
next false if text.include?('поясн')
|
||||||
|
text.match?(/20\d{2}/) || text.include?('за ') || text.include?('на ')
|
||||||
|
end
|
||||||
|
|
||||||
|
value_columns = value_columns.sort.last(2)
|
||||||
|
|
||||||
|
{
|
||||||
|
name_column: name_column,
|
||||||
|
code_column: code_column,
|
||||||
|
value_columns: value_columns
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignored_row?(line_name, line_code)
|
||||||
|
keys = [line_code, line_name].compact.map { |value| normalize_label(value) }
|
||||||
|
keys.any? { |key| ignored_keys.include?(key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignored_keys
|
||||||
|
@ignored_keys ||= IGNORED_KEYS.map { |value| normalize_label(value) }.to_set
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
module ReportParsers
|
||||||
|
class Form14Parser < BaseParser
|
||||||
|
SHEET_NAME = 'Отчет о движении денежных средс' # отличное название листа, мне нравится
|
||||||
|
FORM_CODE = '1.4'
|
||||||
|
# Add line codes or names that should be skipped during parsing.
|
||||||
|
IGNORED_KEYS = [2].freeze
|
||||||
|
|
||||||
|
def call
|
||||||
|
return {} unless sheet_exists?(SHEET_NAME)
|
||||||
|
|
||||||
|
sheet = spreadsheet.sheet(SHEET_NAME)
|
||||||
|
build_payloads(sheet)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_payloads(sheet)
|
||||||
|
header_row = find_header_row(sheet)
|
||||||
|
columns = detect_columns(sheet, header_row)
|
||||||
|
|
||||||
|
value_headers = columns[:value_columns].each_with_object({}) do |col, hash|
|
||||||
|
header_text = read_cell(sheet, header_row, col).to_s
|
||||||
|
year = extract_year_from_header(header_text)
|
||||||
|
hash[col] = year
|
||||||
|
end
|
||||||
|
|
||||||
|
all_rows = (header_row + 1).upto(sheet.last_row).filter_map do |row|
|
||||||
|
line_name = normalize_line_name(read_cell(sheet, row, columns[:name_column]))
|
||||||
|
line_code = read_cell(sheet, row, columns[:code_column]).to_s.strip.presence
|
||||||
|
next if ignored_row?(line_name, line_code)
|
||||||
|
|
||||||
|
values = columns[:value_columns].each_with_object({}) do |col, hash|
|
||||||
|
value = read_cell(sheet, row, col)
|
||||||
|
year = value_headers[col]
|
||||||
|
hash[year.to_s] = value if value.present? && year.present?
|
||||||
|
end.compact_blank
|
||||||
|
|
||||||
|
next if line_name.blank? && line_code.blank? && values.blank?
|
||||||
|
|
||||||
|
{
|
||||||
|
line_name: line_name,
|
||||||
|
line_code: line_code,
|
||||||
|
values: values
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
payloads_by_year = {}
|
||||||
|
value_headers.each do |_, year|
|
||||||
|
next unless year.present?
|
||||||
|
|
||||||
|
rows_for_year = all_rows.map do |row|
|
||||||
|
value = row[:values][year.to_s]
|
||||||
|
next unless value.present?
|
||||||
|
|
||||||
|
{
|
||||||
|
line_name: row[:line_name],
|
||||||
|
line_code: row[:line_code],
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
end.compact
|
||||||
|
|
||||||
|
payloads_by_year[year] = {
|
||||||
|
form_code: FORM_CODE,
|
||||||
|
rows: rows_for_year
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
payloads_by_year
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_year_from_header(header_text)
|
||||||
|
year_match = header_text[/\b(20\d{2})\b/]
|
||||||
|
year_match.to_i if year_match
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_header_row(sheet)
|
||||||
|
1.upto(sheet.last_row) do |row|
|
||||||
|
normalized_cells = 1.upto(sheet.last_column).map { |col| normalize_label(sheet.cell(row, col)) }
|
||||||
|
has_name = normalized_cells.any? { |text| text.include?('наименование показателя') }
|
||||||
|
has_code = normalized_cells.any? { |text| text == 'код' || text.start_with?('код ') || text.start_with?('код:') }
|
||||||
|
return row if has_name && has_code
|
||||||
|
end
|
||||||
|
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def detect_columns(sheet, header_row)
|
||||||
|
columns = 1.upto(sheet.last_column).each_with_object({}) do |col, hash|
|
||||||
|
hash[col] = normalize_label(sheet.cell(header_row, col))
|
||||||
|
end
|
||||||
|
|
||||||
|
name_column = columns.key(columns.values.find { |text| text.include?('наименование показателя') }) || 1
|
||||||
|
|
||||||
|
code_column = columns.key(columns.values.find { |text|
|
||||||
|
text == 'код строки' || text.start_with?('код строки ') ||
|
||||||
|
(text == 'код' && columns[name_column + 1] == text)
|
||||||
|
}) || columns.key(columns.values.find { |text| text == 'код' || text.start_with?('код ') }) || [name_column + 1, sheet.last_column].min
|
||||||
|
|
||||||
|
value_columns = columns.keys.select do |col|
|
||||||
|
text = columns[col]
|
||||||
|
next false if text.include?('поясн')
|
||||||
|
|
||||||
|
text.match?(/20\d{2}/) || text.include?('за ') || text.include?('на ')
|
||||||
|
end
|
||||||
|
value_columns = value_columns.sort.last(2)
|
||||||
|
|
||||||
|
{
|
||||||
|
name_column: name_column,
|
||||||
|
code_column: code_column,
|
||||||
|
value_columns: value_columns
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignored_row?(line_name, line_code)
|
||||||
|
keys = [line_code, line_name].compact.map { |value| normalize_label(value) }
|
||||||
|
keys.any? { |key| ignored_keys.include?(key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignored_keys
|
||||||
|
@ignored_keys ||= IGNORED_KEYS.map { |value| normalize_label(value) }.to_set
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
module ReportParsers
|
||||||
|
class OrganizationInfoParser < BaseParser
|
||||||
|
SHEET_NAME = 'Сведения об организации'
|
||||||
|
|
||||||
|
ORGANIZATION_LABELS = {
|
||||||
|
full_name: ['Полное наименование юридического лица'],
|
||||||
|
inn: ['ИНН'],
|
||||||
|
kpp: ['КПП'],
|
||||||
|
okpo: ['Код по ОКПО'],
|
||||||
|
okfs: ['Форма собственности (по ОКФС)'],
|
||||||
|
okopf: ['Организационно-правовая форма (по ОКОПФ)'],
|
||||||
|
okved2: ['Вид экономической деятельности по ОКВЭД 2'],
|
||||||
|
address: ['Местонахождение (адрес)']
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
AUDITOR_LABELS = {
|
||||||
|
name: ['Наименование аудиторской организации/ФИО индивидуального аудитора'],
|
||||||
|
inn: ['ИНН'],
|
||||||
|
ogrn: ['ОГРН/ОГРНИП']
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def call
|
||||||
|
return unless sheet_exists?(SHEET_NAME)
|
||||||
|
|
||||||
|
sheet = spreadsheet.sheet(SHEET_NAME)
|
||||||
|
parse_organization(sheet)
|
||||||
|
parse_auditor(sheet)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_organization(sheet)
|
||||||
|
end_row = auditor_section_start_row(sheet) ? auditor_section_start_row(sheet) - 1 : sheet.last_row
|
||||||
|
org_data = ORGANIZATION_LABELS.each_with_object({}) do |(attr, labels), hash|
|
||||||
|
hash[attr] = find_value_by_labels(sheet, labels, row_range: 1..end_row)
|
||||||
|
end
|
||||||
|
|
||||||
|
yearly_file.organization.update!(org_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_auditor(sheet)
|
||||||
|
start_row = auditor_section_start_row(sheet) || 1
|
||||||
|
auditor_data = AUDITOR_LABELS.each_with_object({}) do |(attr, labels), hash|
|
||||||
|
hash[attr] = find_value_by_labels(sheet, labels, row_range: start_row..sheet.last_row)
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
def auditor_section_start_row(sheet)
|
||||||
|
@auditor_section_start_row ||= begin
|
||||||
|
labels = AUDITOR_LABELS.fetch(:name)
|
||||||
|
find_label_position(sheet, labels, row_range: 1..sheet.last_row)&.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
|
|
||||||
# Удали временные файлы
|
# Удали временные файлы
|
||||||
@@ -69,4 +84,4 @@ class FileProcessorJob
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.main-header
|
||||||
|
= 'Отчетность за 2026 год'
|
||||||
|
.exit
|
||||||
|
- if logged_in?
|
||||||
|
a.link[href="/logout"]
|
||||||
|
span.text-large
|
||||||
|
= 'Выйти'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- if @current_user.present?
|
||||||
|
.nav-container
|
||||||
|
a.link[href="/"]
|
||||||
|
= 'Организации'
|
||||||
|
a.link[href="/auditors/index"]
|
||||||
|
= 'Аудиторы'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.header
|
||||||
|
.header-title
|
||||||
|
= "Аудиторы"
|
||||||
|
.main-container
|
||||||
|
- unless @info_about_auditors.any?
|
||||||
|
= "Аудиторы не найдены"
|
||||||
|
- else
|
||||||
|
.informations-data
|
||||||
|
- @info_about_auditors.each do |auditor, params|
|
||||||
|
.information-data
|
||||||
|
- params.keys.each do |key|
|
||||||
|
.information-elem
|
||||||
|
= "#{key}: #{params[key]}"
|
||||||
@@ -2,7 +2,7 @@ doctype html
|
|||||||
html
|
html
|
||||||
head
|
head
|
||||||
title
|
title
|
||||||
=t('app_name')
|
= "Отчетность 2026"
|
||||||
meta[name="viewport" content="width=device-width,initial-scale=1"]
|
meta[name="viewport" content="width=device-width,initial-scale=1"]
|
||||||
meta name="turbo-refresh-method" content="morph"
|
meta name="turbo-refresh-method" content="morph"
|
||||||
= csrf_meta_tags
|
= csrf_meta_tags
|
||||||
@@ -16,4 +16,6 @@ html
|
|||||||
= javascript_importmap_tags
|
= javascript_importmap_tags
|
||||||
= hotwire_livereload_tags if Rails.env.development?
|
= hotwire_livereload_tags if Rails.env.development?
|
||||||
body
|
body
|
||||||
|
= render 'header'
|
||||||
|
= render 'navbar'
|
||||||
= yield
|
= yield
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.header
|
||||||
|
.header-title
|
||||||
|
= "Организации"
|
||||||
|
.main-container
|
||||||
|
.add-new
|
||||||
|
a.link[href="/reports"]
|
||||||
|
= "Добавить новую организацию"
|
||||||
|
- unless @info_about_organisations.any?
|
||||||
|
= 'Организации не найдены'
|
||||||
|
- else
|
||||||
|
.informations-data
|
||||||
|
- @info_about_organisations.each do |organisation, params|
|
||||||
|
.information-data
|
||||||
|
- params.keys.each do |key|
|
||||||
|
.information-elem
|
||||||
|
= "#{key}: #{params[key]}"
|
||||||
|
.information-button
|
||||||
|
a.link[href="/organisations/#{organisation}"]
|
||||||
|
= "Подробнее"
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.header
|
||||||
|
.header-title
|
||||||
|
= "Организация"
|
||||||
|
.main-container
|
||||||
|
- unless @info_about_organisation.any?
|
||||||
|
= 'У данной организации пока не загружена отчетность'
|
||||||
|
- else
|
||||||
|
- min_max_data = prepare_data(@current_doc_info1)
|
||||||
|
- comparsion_data = prepare_data(@current_doc_info1, type_data: 'comparsion')
|
||||||
|
.informations-data
|
||||||
|
h2
|
||||||
|
= "Сведения об организации"
|
||||||
|
.information-data
|
||||||
|
- @info_about_organisation["Сведения об организации"].each do |key, value|
|
||||||
|
.information-elem
|
||||||
|
= "#{key}: #{value}"
|
||||||
|
= render 'organisations/templates/type_doc', target: "type_doc_#{@type_doc}", org_id: "#{@org.id}"
|
||||||
|
h2
|
||||||
|
= "#{@doc_name}"
|
||||||
|
- if @current_doc_info.present?
|
||||||
|
.information-table
|
||||||
|
table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
- @current_doc_info.each do |header|
|
||||||
|
th = header
|
||||||
|
tbody
|
||||||
|
- @current_doc_info1.keys.each do |key|
|
||||||
|
tr
|
||||||
|
td = key[0]
|
||||||
|
td = key[1]
|
||||||
|
- @current_doc_info1[key].each_with_index do |value, index|
|
||||||
|
- key_group = key[1].nil? ? nil : group_key(key[1])
|
||||||
|
- if min_max_data.has_key?(key_group) && keep_code?(key[1])
|
||||||
|
- min_value = min_max_data[key_group][index + 1][:min]
|
||||||
|
- max_value = min_max_data[key_group][index + 1][:max]
|
||||||
|
- p50 = min_max_data[key_group][index + 1][:p50]
|
||||||
|
- color = get_color_for_cell(value, min: min_value, median: p50, max: max_value)
|
||||||
|
td style="background-color: #{color}" = value
|
||||||
|
- elsif comparsion_data.has_key?(key[1]) && keep_code?(key[1], type_data: 'comparsion')
|
||||||
|
- condition = comparsion_data[key[1]][index]
|
||||||
|
= get_arrow_for_cell(value, condition)
|
||||||
|
- else
|
||||||
|
td = value
|
||||||
|
- else
|
||||||
|
h3 = "Нет данных"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.main__container
|
||||||
|
.type_doc_header#tabs
|
||||||
|
.type__doc__list__type__container
|
||||||
|
a.link[href="/organisations/#{org_id}?type_doc=0"]
|
||||||
|
.switch__button.text-large#type_doc_0 data-value=target
|
||||||
|
= 'Бухгалтерский баланс'
|
||||||
|
a.link[href="/organisations/#{org_id}?type_doc=1"]
|
||||||
|
.switch__button.text-large#type_doc_1 data-value=target
|
||||||
|
= 'Отчёт о финансовых результатах'
|
||||||
|
/ - if not_simplified
|
||||||
|
a.link[href="/organisations/#{org_id}?type_doc=2"]
|
||||||
|
.switch__button.text-large#type_doc_2 data-value=target
|
||||||
|
= 'Отчёт об изменении капитала'
|
||||||
|
a.link[href="/organisations/#{org_id}?type_doc=3"]
|
||||||
|
.switch__button.text-large#type_doc_3 data-value=target
|
||||||
|
= 'Отчёт о движении денежных средств'
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<!-- app/views/reports/index.html.erb -->
|
<!-- app/views/reports/index.html.erb -->
|
||||||
<h1>Загрузка отчётов по ИНН</h1>
|
<h1>Загрузка отчётов по ИНН</h1>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Тестовые ИНН: <br>Полные отчеты: 7707602010, 9705114405<br>Упрощенка: 7724298987
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% if notice %>
|
<% if notice %>
|
||||||
<p style="color: green;"><%= notice %></p>
|
<p style="color: green;"><%= notice %></p>
|
||||||
@@ -22,4 +27,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.welcome__container__laptop
|
||||||
|
div
|
||||||
|
= 'Информационная система'
|
||||||
|
div
|
||||||
|
= '«Электронный дневник»'
|
||||||
|
|
||||||
|
- unless logged_in?
|
||||||
|
a.link[href="/auth/login"]
|
||||||
|
.welcome-login__container
|
||||||
|
span
|
||||||
|
= image_tag('auth.svg', class: 'icon')
|
||||||
|
span.text-login
|
||||||
|
= 'Войти'
|
||||||
+2
-1
@@ -11,4 +11,5 @@ pin "jquery", to: "jquery.min.js", preload: true
|
|||||||
pin "jquery_ujs", to: "jquery_ujs.js", preload: true
|
pin "jquery_ujs", to: "jquery_ujs.js", preload: true
|
||||||
pin "jquery-ui", to: "jquery-ui.js", preload: true
|
pin "jquery-ui", to: "jquery-ui.js", preload: true
|
||||||
pin "inputmask", to: "jquery.inputmask.bundle.min.js", preload: true
|
pin "inputmask", to: "jquery.inputmask.bundle.min.js", preload: true
|
||||||
pin 'slimselect', to: 'slimselect.min.js', preload: true
|
pin 'slimselect', to: 'slimselect.min.js', preload: true
|
||||||
|
pin "tabs_select", to: 'tabs_select.js', preload: true
|
||||||
+6
-2
@@ -1,10 +1,14 @@
|
|||||||
# require 'sidekiq/web'
|
# require 'sidekiq/web'
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
get 'reports/create'
|
|
||||||
|
get '/auth/login', to: 'auth#login', as: :login
|
||||||
|
get '/organisations/index'
|
||||||
|
get '/organisations/:id', to: 'organisations#show'
|
||||||
|
get '/auditors/index'
|
||||||
|
get '/reports/create'
|
||||||
get '/reports', to: 'reports#index'
|
get '/reports', to: 'reports#index'
|
||||||
post '/reports', to: 'reports#create'
|
post '/reports', to: 'reports#create'
|
||||||
|
|
||||||
# mount Sidekiq::Web => '/sidekiq' # Для проверки работы Sidekiq через веб-интерфейс
|
# mount Sidekiq::Web => '/sidekiq' # Для проверки работы Sidekiq через веб-интерфейс
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class AddUniqueIndexToFinancialStatements < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_index :financial_statements,
|
||||||
|
[:yearly_file_id, :document_type_id, :sheet_number],
|
||||||
|
name: 'index_fs_on_yearly_file_doc_type_sheet',
|
||||||
|
unique: true,
|
||||||
|
if_not_exists: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
class SeedDocumentTypes < ActiveRecord::Migration[7.0]
|
||||||
|
def up
|
||||||
|
execute <<-SQL
|
||||||
|
INSERT INTO document_types (id, name, created_at, updated_at) VALUES
|
||||||
|
(1, 'Бухгалтерский баланс', NOW(), NOW()),
|
||||||
|
(2, 'Отчет о финансовых результатах', NOW(), NOW()),
|
||||||
|
(3, 'Отчет об изменениях капитала', NOW(), NOW()),
|
||||||
|
(4, 'Отчет о движении денежных средств', NOW(), NOW())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute <<-SQL
|
||||||
|
DELETE FROM document_types WHERE id IN (1,2,3,4);
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class CreateReportCodes < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
create_table :report_codes do |t|
|
||||||
|
t.string :code, null: false
|
||||||
|
t.string :name, null: false
|
||||||
|
t.text :description, null: false, default: ''
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :report_codes, :code, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
class SeedReportCodes < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
rows = [
|
||||||
|
{ code: "1100", name: "Итого внеоборотных активов", description: "Сумма строк 1110, 1120, 1130, 1140, 1150" },
|
||||||
|
{ code: "1105", name: "Гудвил", description: "-" },
|
||||||
|
{ code: "1110", name: "Нематериальные активы", description: "-" },
|
||||||
|
{ code: "1130", name: "Нематериальные поисковые активы", description: "-" },
|
||||||
|
{ code: "1140", name: "Материальные поисковые активы", description: "-" },
|
||||||
|
{ code: "1150", name: "Основные средства", description: "-" },
|
||||||
|
{ code: "1160", name: "Инвестиционная недвижимость", description: "-" },
|
||||||
|
{ code: "1170", name: "Финансовые вложения", description: "-" },
|
||||||
|
{ code: "1180", name: "Отложенные налоговые активы", description: "-" },
|
||||||
|
{ code: "1190", name: "Прочие внеоборотные активы", description: "-" },
|
||||||
|
{ code: "1200", name: "Итого оборотных активов", description: "-" },
|
||||||
|
{ code: "1210", name: "Запасы", description: "-" },
|
||||||
|
{ code: "1215", name: "Долгосрочные активы к продаже", description: "-" },
|
||||||
|
{ code: "1220", name: "Налог на добавленную стоимость по приобретенным ценностям", description: "-" },
|
||||||
|
{ code: "1230", name: "Дебиторская задолженность", description: "-" },
|
||||||
|
{ code: "1240", name: "Финансовые вложения (за исключением денежных эквивалентов)", description: "-" },
|
||||||
|
{ code: "1250", name: "Денежные средства и денежные эквиваленты", description: "-" },
|
||||||
|
{ code: "1260", name: "Прочие оборотные активы", description: "-" },
|
||||||
|
{ code: "1600", name: "БАЛАНС (актив)", description: "-" },
|
||||||
|
{ code: "1300", name: "Итого капитал/Итого целевое финансирование (в некоммерческой организации)", description: "-" },
|
||||||
|
{ code: "1310", name: "Уставный капитал/Паевой фонд (в некоммерческой организации)", description: "-" },
|
||||||
|
{ code: "1320", name: "Собственные акции, принадлежащие обществу, задолженность акционеров по оплате акций/Целевой капитал (в некоммерческой организации)", description: "-" },
|
||||||
|
{ code: "1330", name: "Целевые средства (в некоммерческой организации)", description: "-" },
|
||||||
|
{ code: "1340", name: "Накопленная дооценка внеоборотных активов", description: "-" },
|
||||||
|
{ code: "1350", name: "Добавочный капитал (без накопленной дооценки)", description: "-" },
|
||||||
|
{ code: "1360", name: "Резервный капитал/Фонд недвижимого и особо ценного движимого имущества (в некоммерческой организации)", description: "-" },
|
||||||
|
{ code: "1370", name: "Нераспределенная прибыль (непокрытый убыток)/Резервный и иные целевые фонды (в некоммерческой организации)", description: "-" },
|
||||||
|
{ code: "1400", name: "Итого долгосрочных обязательств", description: "-" },
|
||||||
|
{ code: "1410", name: "Долгосрочные заемные средства", description: "-" },
|
||||||
|
{ code: "1420", name: "Отложенные налоговые обязательства", description: "-" },
|
||||||
|
{ code: "1430", name: "Долгосрочные оценочные обязательства", description: "-" },
|
||||||
|
{ code: "1450", name: "Прочие долгосрочные обязательства", description: "-" },
|
||||||
|
{ code: "1500", name: "Итого краткосрочных обязательств", description: "-" },
|
||||||
|
{ code: "1510", name: "Краткосрочные заемные средства", description: "-" },
|
||||||
|
{ code: "1520", name: "Краткосрочная кредиторская задолженность", description: "-" },
|
||||||
|
{ code: "1530", name: "Доходы будущих периодов", description: "-" },
|
||||||
|
{ code: "1540", name: "Краткосрочные оценочные обязательства", description: "-" },
|
||||||
|
{ code: "1550", name: "Прочие краткосрочные обязательства", description: "-" },
|
||||||
|
{ code: "2110", name: "Выручка", description: "-" },
|
||||||
|
{ code: "2120", name: "Себестоимость продаж", description: "-" },
|
||||||
|
{ code: "2100", name: "Валовая прибыль (убыток)", description: "-" },
|
||||||
|
{ code: "2210", name: "Коммерческие расходы", description: "-" },
|
||||||
|
{ code: "2220", name: "Управленческие расходы", description: "-" },
|
||||||
|
{ code: "2200", name: "Прибыль (убыток) от продаж", description: "-" },
|
||||||
|
{ code: "2310", name: "Доходы от участия в других организациях", description: "-" },
|
||||||
|
{ code: "2320", name: "Проценты к получению", description: "-" },
|
||||||
|
{ code: "2330", name: "Проценты к уплате", description: "-" },
|
||||||
|
{ code: "2340", name: "Прочие доходы", description: "-" },
|
||||||
|
{ code: "2350", name: "Прочие расходы", description: "-" },
|
||||||
|
{ code: "2300", name: "Прибыль (убыток) от продолжающейся деятельности до налогообложения", description: "-" },
|
||||||
|
{ code: "2410", name: "Налог на прибыль организаций", description: "-" },
|
||||||
|
{ code: "2411", name: "текущий налог на прибыль организаций", description: "в том числе" },
|
||||||
|
{ code: "2412", name: "отложенный налог на прибыль организаций", description: "в том числе" },
|
||||||
|
{ code: "2420", name: "Прибыль (убыток) от прекращаемой деятельности (за вычетом относящегося к ней налога на прибыль организаций)", description: "-" },
|
||||||
|
{ code: "2460", name: "Прочее", description: "-" },
|
||||||
|
{ code: "2400", name: "Чистая прибыль (убыток)", description: "-" },
|
||||||
|
{ code: "2510", name: "Результат от переоценки внеоборотных активов, не включаемый в чистую прибыль (убыток)", description: "-" },
|
||||||
|
{ code: "2520", name: "Результат от прочих операций, не включаемый в чистую прибыль (убыток)", description: "-" },
|
||||||
|
{ code: "2530", name: "Налог на прибыль организаций, относящийся к результатам переоценки внеоборотных активов и прочих операций, не включаемых в чистую прибыль (убыток)", description: "-" },
|
||||||
|
{ code: "2500", name: "Совокупный финансовый результат", description: "-" },
|
||||||
|
{ code: "2900", name: "Базовая прибыль (убыток) на акцию", description: "-" },
|
||||||
|
{ code: "2910", name: "Разводненная прибыль (убыток) на акцию", description: "-" },
|
||||||
|
{ code: "3100", name: "Величина капитала на 31 декабря года, предшествующего предыдущему", description: "-" },
|
||||||
|
{ code: "3110", name: "Корректировка в связи с изменением учетной политики", description: "-" },
|
||||||
|
{ code: "3120", name: "Корректировка в связи с исправлением ошибок", description: "-" },
|
||||||
|
{ code: "3101", name: "Величина капитала на 31 декабря года, предшествующего предыдущему, после корректировки", description: "-" },
|
||||||
|
{ code: "3211", name: "Чистая прибыль (убыток)", description: "-" },
|
||||||
|
{ code: "3212", name: "Переоценка внеоборотных активов", description: "-" },
|
||||||
|
{ code: "3227", name: "Дивиденды", description: "-" },
|
||||||
|
{ code: "3230", name: "Иные изменения за счет операций с собственниками (за исключением дивидендов) - всего", description: "-" },
|
||||||
|
{ code: "3216", name: "Реорганизация юридического лица", description: "-" },
|
||||||
|
{ code: "3240", name: "Иные изменения - всего", description: "-" },
|
||||||
|
{ code: "3250", name: "Величина капитала на дату окончания периода предыдущего года, аналогичного отчетному периоду", description: "-" },
|
||||||
|
{ code: "3200", name: "Величина капитала на 31 декабря предыдущего года", description: "-" },
|
||||||
|
{ code: "3210", name: "Корректировка в связи с изменением учетной политики", description: "-" },
|
||||||
|
{ code: "3220", name: "Корректировка в связи с исправлением ошибок", description: "-" },
|
||||||
|
{ code: "3201", name: "Величина капитала на 31 декабря предыдущего года после корректировки", description: "-" },
|
||||||
|
{ code: "3311", name: "Чистая прибыль (убыток)", description: "-" },
|
||||||
|
{ code: "3312", name: "Переоценка внеоборотных активов", description: "-" },
|
||||||
|
{ code: "3327", name: "Дивиденды", description: "-" },
|
||||||
|
{ code: "3330", name: "Иные изменения за счет операций с собственниками (за исключением дивидендов) - всего", description: "-" },
|
||||||
|
{ code: "3316", name: "Реорганизация юридического лица", description: "-" },
|
||||||
|
{ code: "3340", name: "Иные изменения - всего", description: "-" },
|
||||||
|
{ code: "3300", name: "Величина капитала на отчетную дату", description: "-" },
|
||||||
|
{ code: "4110", name: "Поступления - всего", description: "-" },
|
||||||
|
{ code: "4111", name: "От продажи продукции, товаров, выполнения работ, оказания услуг", description: "-" },
|
||||||
|
{ code: "4112", name: "Арендных платежей, роялти, комиссионных и иных аналогичных платежей", description: "в-" },
|
||||||
|
{ code: "4113", name: "От перепродажи финансовых вложений", description: "-" },
|
||||||
|
{ code: "4114", name: "Процентов по дебиторской задолженности покупателей", description: "-" },
|
||||||
|
{ code: "4119", name: "Прочие поступления", description: "-" },
|
||||||
|
{ code: "4120", name: "Платежи - всего", description: "-" },
|
||||||
|
{ code: "4121", name: "Поставщикам (подрядчикам) за сырье, материалы, выполненные работы, оказанные услуги", description: "в-" },
|
||||||
|
{ code: "4122", name: "В связи с оплатой труда работников", description: "-" },
|
||||||
|
{ code: "4123", name: "Процентов по долговым обязательствам", description: "-" },
|
||||||
|
{ code: "4124", name: "Налога на прибыль организаций", description: "-" },
|
||||||
|
{ code: "4129", name: "Прочие платежи", description: "-" },
|
||||||
|
{ code: "4100", name: "Сальдо денежных потоков от текущих операций", description: "-" },
|
||||||
|
{ code: "4210", name: "Поступления - всего", description: "-" },
|
||||||
|
{ code: "4211", name: "От продажи внеоборотных активов (кроме финансовых вложений)", description: "-" },
|
||||||
|
{ code: "4212", name: "От продажи акций других организаций (долей участия)", description: "-" },
|
||||||
|
{ code: "4213", name: "От возврата предоставленных займов, от продажи долговых ценных бумаг (прав требования денежных средств к другим лицам)", description: "-" },
|
||||||
|
{ code: "4214", name: "Дивидендов, процентов по долговым финансовым вложениям и аналогичных поступлений от долевого участия в других организациях", description: "-" },
|
||||||
|
{ code: "4219", name: "Прочие поступления", description: "-" },
|
||||||
|
{ code: "4220", name: "Платежи - всего", description: "-" },
|
||||||
|
{ code: "4221", name: "В связи с приобретением, созданием, модернизацией, реконструкцией и подготовкой к использованию внеоборотных активов", description: "-" },
|
||||||
|
{ code: "4222", name: "В связи с приобретением акций других организаций (долей участия)", description: "-" },
|
||||||
|
{ code: "4223", name: "В связи с приобретением долговых ценных бумаг (прав требования денежных средств к другим лицам), предоставление займов другим лицам", description: "-" },
|
||||||
|
{ code: "4224", name: "Процентов по долговым обязательствам, включаемым в стоимость инвестиционного актива", description: "-" },
|
||||||
|
{ code: "4229", name: "Прочие платежи", description: "-" },
|
||||||
|
{ code: "4200", name: "Сальдо денежных потоков от инвестиционных операций", description: "-" },
|
||||||
|
{ code: "4310", name: "Поступления - всего", description: "-" },
|
||||||
|
{ code: "4311", name: "Получение кредитов и займов", description: "-" },
|
||||||
|
{ code: "4312", name: "Денежных вкладов собственников (участников)", description: "-" },
|
||||||
|
{ code: "4313", name: "От выпуска акций, увеличения долей участия", description: "-" },
|
||||||
|
{ code: "4314", name: "От выпуска облигаций, векселей и других долговых ценных бумаг", description: "-" },
|
||||||
|
{ code: "4319", name: "Прочие поступления", description: "-" },
|
||||||
|
{ code: "4320", name: "Платежи - всего", description: "-" },
|
||||||
|
{ code: "4321", name: "Собственникам (участникам) в связи с выкупом у них акций (долей участия) организации или их выходом из состава участников", description: "-" },
|
||||||
|
{ code: "4322", name: "На уплату дивидендов и иных платежей по распределению прибыли в пользу собственников (участников)", description: "-" },
|
||||||
|
{ code: "4323", name: "В связи с погашением (выкупом) векселей и других долговых ценных бумаг, возврат кредитов и займов", description: "-" },
|
||||||
|
{ code: "4329", name: "Прочие платежи", description: "-" },
|
||||||
|
{ code: "4300", name: "Сальдо денежных потоков от финансовых операций", description: "-" },
|
||||||
|
{ code: "4400", name: "Сальдо денежных потоков за период", description: "-" },
|
||||||
|
{ code: "4450", name: "Остаток денежных средств и денежных эквивалентов на начало периода", description: "-" },
|
||||||
|
{ code: "4500", name: "Остаток денежных средств и денежных эквивалентов на конец периода", description: "-" },
|
||||||
|
{ code: "4490", name: "Величина влияния изменения курса иностранной валюты по отношению к рублю", description: "-" }
|
||||||
|
]
|
||||||
|
|
||||||
|
return if rows.empty?
|
||||||
|
|
||||||
|
rows.each do |row|
|
||||||
|
execute <<~SQL
|
||||||
|
INSERT INTO report_codes (code, name, description, created_at, updated_at)
|
||||||
|
VALUES (
|
||||||
|
#{connection.quote(row.fetch(:code))},
|
||||||
|
#{connection.quote(row.fetch(:name))},
|
||||||
|
#{connection.quote(row.fetch(:description, ''))},
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (code)
|
||||||
|
DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
updated_at = NOW();
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
codes = [
|
||||||
|
"1100",
|
||||||
|
"1105",
|
||||||
|
"1110",
|
||||||
|
"1130",
|
||||||
|
"1140",
|
||||||
|
"1150",
|
||||||
|
"1160",
|
||||||
|
"1170",
|
||||||
|
"1180",
|
||||||
|
"1190",
|
||||||
|
"1200",
|
||||||
|
"1210",
|
||||||
|
"1215",
|
||||||
|
"1220",
|
||||||
|
"1230",
|
||||||
|
"1240",
|
||||||
|
"1250",
|
||||||
|
"1260",
|
||||||
|
"1600",
|
||||||
|
"1300",
|
||||||
|
"1310",
|
||||||
|
"1320",
|
||||||
|
"1330",
|
||||||
|
"1340",
|
||||||
|
"1350",
|
||||||
|
"1360",
|
||||||
|
"1370",
|
||||||
|
"1400",
|
||||||
|
"1410",
|
||||||
|
"1420",
|
||||||
|
"1430",
|
||||||
|
"1450",
|
||||||
|
"1500",
|
||||||
|
"1510",
|
||||||
|
"1520",
|
||||||
|
"1530",
|
||||||
|
"1540",
|
||||||
|
"1550",
|
||||||
|
"2110",
|
||||||
|
"2120",
|
||||||
|
"2100",
|
||||||
|
"2210",
|
||||||
|
"2220",
|
||||||
|
"2200",
|
||||||
|
"2310",
|
||||||
|
"2320",
|
||||||
|
"2330",
|
||||||
|
"2340",
|
||||||
|
"2350",
|
||||||
|
"2300",
|
||||||
|
"2410",
|
||||||
|
"2411",
|
||||||
|
"2412",
|
||||||
|
"2420",
|
||||||
|
"2460",
|
||||||
|
"2400",
|
||||||
|
"2510",
|
||||||
|
"2520",
|
||||||
|
"2530",
|
||||||
|
"2500",
|
||||||
|
"2900",
|
||||||
|
"2910",
|
||||||
|
"3100",
|
||||||
|
"3110",
|
||||||
|
"3120",
|
||||||
|
"3101",
|
||||||
|
"3211",
|
||||||
|
"3212",
|
||||||
|
"3227",
|
||||||
|
"3230",
|
||||||
|
"3216",
|
||||||
|
"3240",
|
||||||
|
"3250",
|
||||||
|
"3200",
|
||||||
|
"3210",
|
||||||
|
"3220",
|
||||||
|
"3201",
|
||||||
|
"3311",
|
||||||
|
"3312",
|
||||||
|
"3327",
|
||||||
|
"3330",
|
||||||
|
"3316",
|
||||||
|
"3340",
|
||||||
|
"3300",
|
||||||
|
"4110",
|
||||||
|
"4111",
|
||||||
|
"4112",
|
||||||
|
"4113",
|
||||||
|
"4114",
|
||||||
|
"4119",
|
||||||
|
"4120",
|
||||||
|
"4121",
|
||||||
|
"4122",
|
||||||
|
"4123",
|
||||||
|
"4124",
|
||||||
|
"4129",
|
||||||
|
"4100",
|
||||||
|
"4210",
|
||||||
|
"4211",
|
||||||
|
"4212",
|
||||||
|
"4213",
|
||||||
|
"4214",
|
||||||
|
"4219",
|
||||||
|
"4220",
|
||||||
|
"4221",
|
||||||
|
"4222",
|
||||||
|
"4223",
|
||||||
|
"4224",
|
||||||
|
"4229",
|
||||||
|
"4200",
|
||||||
|
"4310",
|
||||||
|
"4311",
|
||||||
|
"4312",
|
||||||
|
"4313",
|
||||||
|
"4314",
|
||||||
|
"4319",
|
||||||
|
"4320",
|
||||||
|
"4321",
|
||||||
|
"4322",
|
||||||
|
"4323",
|
||||||
|
"4329",
|
||||||
|
"4300",
|
||||||
|
"4400",
|
||||||
|
"4450",
|
||||||
|
"4500",
|
||||||
|
"4490"
|
||||||
|
]
|
||||||
|
|
||||||
|
return if codes.empty?
|
||||||
|
|
||||||
|
quoted_codes = codes.map { |code| connection.quote(code) }.join(', ')
|
||||||
|
execute "DELETE FROM report_codes WHERE code IN (#{quoted_codes});"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
class RemoveDocumentTypesFromFinancialStatements < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
remove_index :financial_statements, name: 'index_fs_on_yearly_file_doc_type_sheet', if_exists: true
|
||||||
|
remove_index :financial_statements, :document_type_id, if_exists: true
|
||||||
|
|
||||||
|
remove_foreign_key :financial_statements, :document_types if foreign_key_exists?(:financial_statements, :document_types)
|
||||||
|
remove_column :financial_statements, :document_type_id if column_exists?(:financial_statements, :document_type_id)
|
||||||
|
|
||||||
|
add_index :financial_statements,
|
||||||
|
[:yearly_file_id, :sheet_number],
|
||||||
|
name: 'index_fs_on_yearly_file_sheet',
|
||||||
|
unique: true,
|
||||||
|
if_not_exists: true
|
||||||
|
|
||||||
|
drop_table :document_types, if_exists: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
create_table :document_types do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end unless table_exists?(:document_types)
|
||||||
|
|
||||||
|
add_column :financial_statements, :document_type_id, :bigint unless column_exists?(:financial_statements, :document_type_id)
|
||||||
|
|
||||||
|
add_foreign_key :financial_statements, :document_types unless foreign_key_exists?(:financial_statements, :document_types)
|
||||||
|
|
||||||
|
add_index :financial_statements, :document_type_id, if_not_exists: true
|
||||||
|
|
||||||
|
remove_index :financial_statements, name: 'index_fs_on_yearly_file_sheet', if_exists: true
|
||||||
|
|
||||||
|
add_index :financial_statements,
|
||||||
|
[:yearly_file_id, :document_type_id, :sheet_number],
|
||||||
|
name: 'index_fs_on_yearly_file_doc_type_sheet',
|
||||||
|
unique: true,
|
||||||
|
if_not_exists: true
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+56
-1
@@ -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_04_18_113000) 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,48 @@ 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 "financial_statements", force: :cascade do |t|
|
||||||
|
t.bigint "yearly_file_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 ["yearly_file_id", "sheet_number"], name: "index_fs_on_yearly_file_sheet", unique: true
|
||||||
|
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 "report_codes", force: :cascade do |t|
|
||||||
|
t.string "code", null: false
|
||||||
|
t.string "name", null: false
|
||||||
|
t.text "description", default: "", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["code"], name: "index_report_codes_on_code", unique: true
|
||||||
|
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 +118,19 @@ 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", "yearly_files"
|
||||||
|
add_foreign_key "yearly_files", "auditors"
|
||||||
|
add_foreign_key "yearly_files", "organizations"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
# Specs in this file have access to a helper object that includes
|
||||||
|
# the AuditorsHelper. For example:
|
||||||
|
#
|
||||||
|
# describe AuditorsHelper do
|
||||||
|
# describe "string concat" do
|
||||||
|
# it "concats two strings with spaces" do
|
||||||
|
# expect(helper.concat_strings("this","that")).to eq("this that")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
RSpec.describe AuditorsHelper, type: :helper do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
# Specs in this file have access to a helper object that includes
|
||||||
|
# the OrganisationsHelper. For example:
|
||||||
|
#
|
||||||
|
# describe OrganisationsHelper do
|
||||||
|
# describe "string concat" do
|
||||||
|
# it "concats two strings with spaces" do
|
||||||
|
# expect(helper.concat_strings("this","that")).to eq("this that")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
RSpec.describe OrganisationsHelper, type: :helper do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "Auditors", type: :request do
|
||||||
|
describe "GET /index" do
|
||||||
|
it "returns http success" do
|
||||||
|
get "/auditors/index"
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "Organisations", type: :request do
|
||||||
|
describe "GET /index" do
|
||||||
|
it "returns http success" do
|
||||||
|
get "/organisations/index"
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /show" do
|
||||||
|
it "returns http success" do
|
||||||
|
get "/organisations/show"
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "auditors/index.html.tailwindcss", type: :view do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "organisations/index.html.tailwindcss", type: :view do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "organisations/show.html.tailwindcss", type: :view do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user