From 44cd3993ac94e887d193a796e82036df93ad93fa Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 20 Apr 2026 18:45:52 +0000 Subject: [PATCH 01/13] =?UTF-8?q?SU2-33=20|=20=D0=A4=D0=B8=D0=BA=D1=81=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=80=D1=81=D0=B5=D1=80=D0=BE=D0=BC=20=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=91=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/report_parser.rb | 33 ++++++++++++++++++-- app/services/report_parsers/form12_parser.rb | 1 - 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/services/report_parser.rb b/app/services/report_parser.rb index 62f48a1..fe233f2 100644 --- a/app/services/report_parser.rb +++ b/app/services/report_parser.rb @@ -64,10 +64,12 @@ class ReportParser 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) + data: build_payload_for_year(year, parsed_forms, codes, previous_data: previous_data) ) statement.save! end @@ -82,9 +84,19 @@ class ReportParser years.sort.reverse end - def build_payload_for_year(year, parsed_forms, codes) + 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_data = parsed_forms.fetch(form_code, {})[year] || {} + 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) @@ -111,6 +123,21 @@ class ReportParser } 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']) diff --git a/app/services/report_parsers/form12_parser.rb b/app/services/report_parsers/form12_parser.rb index 12df2d8..80ac29b 100644 --- a/app/services/report_parsers/form12_parser.rb +++ b/app/services/report_parsers/form12_parser.rb @@ -83,7 +83,6 @@ module ReportParsers has_code = normalized_cells.any? { |text| text == 'код' || text.start_with?('код ') || text.start_with?('код:') } return row if has_name && has_code end - 1 end From c750e470d6bccf203929b96d6ce77905ea19d076 Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Mon, 20 Apr 2026 23:40:48 +0300 Subject: [PATCH 02/13] =?UTF-8?q?SU2-33=20|=20=D0=9E=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BE=D1=82=20=D0=BF=D1=83=D1=81=D1=82=D1=8B=D1=85=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=20=D0=B8=20=D1=81=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D0=B1=D1=86=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/organisations_controller.rb | 25 ++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 2f2aa53..f489d6d 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -30,14 +30,15 @@ class OrganisationsController < ApplicationController statements_by_year_and_form = build_statements_by_year_and_form years = statements_by_year_and_form.keys.sort.reverse - grouped_rows = build_grouped_rows(statements_by_year_and_form, years) - + 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_info = @info_about_organisation[@doc_name] @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 @@ -95,6 +96,24 @@ class OrganisationsController < ApplicationController 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] = {} } From bc095da738391514be73b84600bdd7632e4bff1d Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Tue, 21 Apr 2026 00:29:20 +0300 Subject: [PATCH 03/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D1=80=D0=B5=D0=BB=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=20=D1=81=D1=82=D0=B8=D0=BB=D1=8C=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B8=D1=85=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/images/arrow_down.svg | 8 ++++++++ app/assets/images/arrow_up.svg | 8 ++++++++ app/assets/stylesheets/organisation.scss | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100755 app/assets/images/arrow_down.svg create mode 100755 app/assets/images/arrow_up.svg create mode 100644 app/assets/stylesheets/organisation.scss diff --git a/app/assets/images/arrow_down.svg b/app/assets/images/arrow_down.svg new file mode 100755 index 0000000..0afa0a4 --- /dev/null +++ b/app/assets/images/arrow_down.svg @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/assets/images/arrow_up.svg b/app/assets/images/arrow_up.svg new file mode 100755 index 0000000..4b8db4d --- /dev/null +++ b/app/assets/images/arrow_up.svg @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/assets/stylesheets/organisation.scss b/app/assets/stylesheets/organisation.scss new file mode 100644 index 0000000..2f27bfa --- /dev/null +++ b/app/assets/stylesheets/organisation.scss @@ -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'); +} From e54299c13bc86cd9eb1d4675624342523eb51404 Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Tue, 21 Apr 2026 00:30:22 +0300 Subject: [PATCH 04/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D1=80=D0=B5=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D1=80=D0=B0=D1=81=D0=BA=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=20|=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=88=D0=B8=D1=80=D0=B8=D0=BD=D0=B0=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=BE=D0=B3=D0=BE=20=D1=81=D1=82=D0=BE=D0=BB=D0=B1?= =?UTF-8?q?=D1=86=D0=B0=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.scss | 3 +- app/assets/stylesheets/main.scss | 4 + app/helpers/organisations_helper.rb | 115 ++++++++++++++++++++++++ app/views/organisations/show.html.slim | 22 +++-- 4 files changed, 137 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 3e3a59d..127e4d5 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -22,4 +22,5 @@ @use "application_settings"; @use "main"; @use "header"; - @use "nav"; \ No newline at end of file + @use "nav"; + @use "organisation"; \ No newline at end of file diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index b70cad6..01357e5 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -81,6 +81,10 @@ a.link:hover { border: 3px solid black; } +.information-table tr :is(td, th):first-child { + width: 35%; +} + .type_doc_header#tabs { width: 75%; margin: 0 auto; diff --git a/app/helpers/organisations_helper.rb b/app/helpers/organisations_helper.rb index 1b84554..d07afde 100644 --- a/app/helpers/organisations_helper.rb +++ b/app/helpers/organisations_helper.rb @@ -1,2 +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 diff --git a/app/views/organisations/show.html.slim b/app/views/organisations/show.html.slim index 7a0fd0c..dd06b90 100644 --- a/app/views/organisations/show.html.slim +++ b/app/views/organisations/show.html.slim @@ -5,6 +5,8 @@ - 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 = "Сведения об организации" @@ -27,10 +29,18 @@ tr td = key[0] td = key[1] - - @current_doc_info1[key].each do |value| - td = value - + - @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 - = "Нет данных" - \ No newline at end of file + h3 = "Нет данных" \ No newline at end of file From a1cbddf4e2fd9481b01adfb1b9392ad7da6cd75f Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Tue, 21 Apr 2026 09:22:51 +0300 Subject: [PATCH 05/13] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=BE=D0=B2=20=D0=B8=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BA=D0=BE=D0=B4=D0=BE=D0=B2=20=D0=B2=20=D0=91=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/report_parser.rb | 2 +- db/migrate/20260414100500_seed_report_codes.rb | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/services/report_parser.rb b/app/services/report_parser.rb index fe233f2..9819598 100644 --- a/app/services/report_parser.rb +++ b/app/services/report_parser.rb @@ -202,7 +202,7 @@ class ReportParser end def report_codes - @report_codes ||= ReportCode.order(:code).select(:code, :name).to_a + @report_codes ||= ReportCode.select(:code, :name).to_a end def report_codes_for_form(form_code, codes) diff --git a/db/migrate/20260414100500_seed_report_codes.rb b/db/migrate/20260414100500_seed_report_codes.rb index d762fb1..91f06f1 100644 --- a/db/migrate/20260414100500_seed_report_codes.rb +++ b/db/migrate/20260414100500_seed_report_codes.rb @@ -1,7 +1,6 @@ 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: "-" }, @@ -11,7 +10,7 @@ class SeedReportCodes < ActiveRecord::Migration[7.1] { code: "1170", name: "Финансовые вложения", description: "-" }, { code: "1180", name: "Отложенные налоговые активы", description: "-" }, { code: "1190", name: "Прочие внеоборотные активы", description: "-" }, - { code: "1200", name: "Итого оборотных активов", description: "-" }, + { code: "1100", name: "Итого внеоборотных активов", description: "Сумма строк 1110, 1120, 1130, 1140, 1150" }, { code: "1210", name: "Запасы", description: "-" }, { code: "1215", name: "Долгосрочные активы к продаже", description: "-" }, { code: "1220", name: "Налог на добавленную стоимость по приобретенным ценностям", description: "-" }, @@ -19,8 +18,8 @@ class SeedReportCodes < ActiveRecord::Migration[7.1] { code: "1240", name: "Финансовые вложения (за исключением денежных эквивалентов)", description: "-" }, { code: "1250", name: "Денежные средства и денежные эквиваленты", description: "-" }, { code: "1260", name: "Прочие оборотные активы", description: "-" }, + { code: "1200", name: "Итого оборотных активов", description: "-" }, { code: "1600", name: "БАЛАНС (актив)", description: "-" }, - { code: "1300", name: "Итого капитал/Итого целевое финансирование (в некоммерческой организации)", description: "-" }, { code: "1310", name: "Уставный капитал/Паевой фонд (в некоммерческой организации)", description: "-" }, { code: "1320", name: "Собственные акции, принадлежащие обществу, задолженность акционеров по оплате акций/Целевой капитал (в некоммерческой организации)", description: "-" }, { code: "1330", name: "Целевые средства (в некоммерческой организации)", description: "-" }, @@ -28,17 +27,19 @@ class SeedReportCodes < ActiveRecord::Migration[7.1] { code: "1350", name: "Добавочный капитал (без накопленной дооценки)", description: "-" }, { code: "1360", name: "Резервный капитал/Фонд недвижимого и особо ценного движимого имущества (в некоммерческой организации)", description: "-" }, { code: "1370", name: "Нераспределенная прибыль (непокрытый убыток)/Резервный и иные целевые фонды (в некоммерческой организации)", description: "-" }, - { code: "1400", name: "Итого долгосрочных обязательств", description: "-" }, + { code: "1300", name: "Итого капитал/Итого целевое финансирование (в некоммерческой организации)", description: "-" }, { code: "1410", name: "Долгосрочные заемные средства", description: "-" }, { code: "1420", name: "Отложенные налоговые обязательства", description: "-" }, { code: "1430", name: "Долгосрочные оценочные обязательства", description: "-" }, { code: "1450", name: "Прочие долгосрочные обязательства", description: "-" }, - { code: "1500", name: "Итого краткосрочных обязательств", description: "-" }, + { code: "1400", name: "Итого долгосрочных обязательств", description: "-" }, { code: "1510", name: "Краткосрочные заемные средства", description: "-" }, { code: "1520", name: "Краткосрочная кредиторская задолженность", description: "-" }, { code: "1530", name: "Доходы будущих периодов", description: "-" }, { code: "1540", name: "Краткосрочные оценочные обязательства", description: "-" }, { code: "1550", name: "Прочие краткосрочные обязательства", description: "-" }, + { code: "1500", name: "Итого краткосрочных обязательств", description: "-" }, + { code: "1700", name: "БАЛАНС (пассив)", description: "-" }, { code: "2110", name: "Выручка", description: "-" }, { code: "2120", name: "Себестоимость продаж", description: "-" }, { code: "2100", name: "Валовая прибыль (убыток)", description: "-" }, From 136de5f2382ef15eca85ebafb1d0ac109bad51df Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 21 Apr 2026 07:54:59 +0000 Subject: [PATCH 06/13] =?UTF-8?q?SU2-33=20|=20=D0=A4=D0=B8=D0=BA=D1=81=20?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D1=85=20=D0=B8=20=D1=81=D0=B5=D1=80=D1=8C=D1=91=D0=B7=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA,=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5/=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=80=D0=B3=D0=B0=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8,?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=D1=86=D0=B8=D1=8F,=20fa?= =?UTF-8?q?vicon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/images/favicon.svg | 10 ++++ app/assets/stylesheets/main.scss | 52 +++++++++++++++++++++ app/controllers/concerns/Authorization.rb | 7 +-- app/controllers/organisations_controller.rb | 15 +++++- app/controllers/reports_controller.rb | 8 +++- app/helpers/organisations_helper.rb | 2 +- app/services/report_parser.rb | 16 ++++--- app/sidekiq/file_processor_job.rb | 27 ++++++++--- app/views/layouts/application.html.slim | 4 +- app/views/organisations/index.html.slim | 1 + app/views/organisations/show.html.slim | 11 +++++ config/routes.rb | 2 + 12 files changed, 127 insertions(+), 28 deletions(-) create mode 100644 app/assets/images/favicon.svg 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' From aea8b7975e5bb9a4d023c5564c8ba4dde9ff8b99 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 21 Apr 2026 08:33:24 +0000 Subject: [PATCH 07/13] =?UTF-8?q?SU2-33=20|=20=D0=9F=D0=B0=D1=80=D1=81?= =?UTF-8?q?=D0=B5=D1=80=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=201.3,=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=20=D1=81=D0=BC=D0=B5=D1=89=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA=20=D0=B2=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=BE=D1=89=D1=91=D0=BD=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/report_parser.rb | 2 +- app/services/report_parsers/form11_parser.rb | 29 ++++- app/services/report_parsers/form13_parser.rb | 115 +++++++++++++++++++ 3 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 app/services/report_parsers/form13_parser.rb diff --git a/app/services/report_parser.rb b/app/services/report_parser.rb index 1c5e5c6..7754bdd 100644 --- a/app/services/report_parser.rb +++ b/app/services/report_parser.rb @@ -11,7 +11,7 @@ class ReportParser sheet_name: 'Отчет о финансовых результатах' }, '1.3' => { - parser: nil, + parser: ReportParsers::Form13Parser, sheet_name: 'Отчет об изменениях капитала' }, '1.4' => { diff --git a/app/services/report_parsers/form11_parser.rb b/app/services/report_parsers/form11_parser.rb index 1e86f87..bd66e0d 100644 --- a/app/services/report_parsers/form11_parser.rb +++ b/app/services/report_parsers/form11_parser.rb @@ -19,10 +19,11 @@ module ReportParsers 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 читается по оригинальному col заголовка, данные — по откалиброванному + value_headers = columns[:value_columns].zip(columns[:data_value_columns]).each_with_object({}) do |(header_col, data_col), hash| + header_text = read_cell(sheet, header_row, header_col).to_s year = extract_year_from_header(header_text) - hash[col] = year + hash[data_col] = year end all_rows = (header_row + 1).upto(sheet.last_row).filter_map do |row| @@ -30,7 +31,7 @@ module ReportParsers 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| + values = columns[:data_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? @@ -46,7 +47,7 @@ module ReportParsers end payloads_by_year = {} - value_headers.each do |col, year| + value_headers.each do |_data_col, year| next unless year.present? rows_for_year = all_rows.map do |row| @@ -114,13 +115,29 @@ module ReportParsers value_columns = columns.keys.select { |col| col > code_column && columns[col].present? } end + original_cols = value_columns.sort.first(3) { name_column: name_column, code_column: code_column, - value_columns: value_columns.sort.first(3) + value_columns: original_cols, + data_value_columns: calibrate_value_columns(sheet, header_row, original_cols, code_column) } end + def calibrate_value_columns(sheet, header_row, value_columns, code_column) + data_row = (header_row + 2).upto(sheet.last_row).find do |row| + sheet.cell(row, code_column).to_s.strip.match?(/\A\d{4}\z/) + end + return value_columns unless data_row + + value_columns.map do |col| + next col unless sheet.cell(data_row, col).nil? || sheet.cell(data_row, col).to_s.strip.empty? + + shifted = col + 1 + shifted <= sheet.last_column && !sheet.cell(data_row, shifted).to_s.strip.empty? ? shifted : col + end + end + def extract_year(sheet) period_text = sheet.cell(4, 1).to_s year = period_text[/\d{4}/].to_i diff --git a/app/services/report_parsers/form13_parser.rb b/app/services/report_parsers/form13_parser.rb new file mode 100644 index 0000000..0480126 --- /dev/null +++ b/app/services/report_parsers/form13_parser.rb @@ -0,0 +1,115 @@ +module ReportParsers + class Form13Parser < BaseParser + SHEET_NAME = 'Отчет об изменениях капитала' + FORM_CODE = '1.3' + + 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_section1_header_row(sheet) + return {} unless header_row + + report_year = extract_report_year(sheet) + return {} unless report_year + + name_col = find_name_column(sheet, header_row) + code_col = find_code_column(sheet, header_row) + total_col = find_total_column(sheet, header_row) + return {} unless code_col && total_col + + end_row = section1_end_row(sheet, header_row) + payloads_by_year = {} + + (header_row + 2).upto(end_row) do |row| + line_code = read_cell(sheet, row, code_col).to_s.strip.presence + next unless line_code&.match?(/\A3[123]\d{2}\z/) + + row_year = year_from_code(line_code, report_year) + next unless row_year + + name_cell = read_cell(sheet, row, name_col).to_s.strip + name_cell = read_cell(sheet, row, 1).to_s.strip if name_cell.blank? + value = read_cell(sheet, row, total_col) + + payloads_by_year[row_year] ||= { form_code: FORM_CODE, sheet_name: SHEET_NAME, rows: [] } + payloads_by_year[row_year][:rows] << { + line_name: normalize_line_name(name_cell), + line_code: line_code, + value: value + } + end + + payloads_by_year + end + + def year_from_code(code, report_year) + case code[1] + when '1' then report_year - 2 + when '2' then report_year - 1 + when '3' then report_year + end + end + + def extract_report_year(sheet) + 1.upto([sheet.last_row, 10].min) do |row| + 1.upto(sheet.last_column) do |col| + m = sheet.cell(row, col).to_s.match(/\bза\s+(20\d{2})\s*г/i) + return m[1].to_i if m + end + end + year = yearly_file&.year.to_i + year.positive? ? year : nil + end + + def find_section1_header_row(sheet) + 1.upto(sheet.last_row) do |row| + cells = 1.upto(sheet.last_column).map { |col| normalize_label(sheet.cell(row, col)) } + next unless cells.any? { |c| c.include?('наименование показателя') } + next unless cells.any? { |c| c.include?('код строки') || c == 'код' } + + return row + end + nil + end + + def find_name_column(sheet, header_row) + 1.upto(sheet.last_column) do |col| + return col if normalize_label(sheet.cell(header_row, col)).include?('наименование показателя') + end + 1 + end + + def find_code_column(sheet, header_row) + 1.upto(sheet.last_column) do |col| + text = normalize_label(sheet.cell(header_row, col)) + return col if text.include?('код строки') || text == 'код' + end + nil + end + + def find_total_column(sheet, header_row) + result = nil + [header_row, header_row + 1].each do |row| + 1.upto(sheet.last_column) do |col| + result = col if normalize_label(sheet.cell(row, col)) == 'итого' + end + return result if result + end + nil + end + + def section1_end_row(sheet, from_row) + (from_row + 1).upto(sheet.last_row) do |row| + return row - 1 if normalize_label(sheet.cell(row, 1)).match?(/\A2\.\s*корректировки/i) + end + sheet.last_row + end + end +end From 1134732a326a31d19cb9cd9f033cbbeec70def6a Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Tue, 21 Apr 2026 13:06:37 +0300 Subject: [PATCH 08/13] =?UTF-8?q?SU2-33=20|=20=D0=9F=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D1=82=D1=8B=D1=85=20=D1=81=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BE=D0=B1=20=D0=BE=D1=80=D0=B3?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/organisations_controller.rb | 27 ++++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index b38582c..a6b00d4 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -60,19 +60,22 @@ class OrganisationsController < ApplicationController 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 - } + russian_names = { + 'inn' => 'ИНН', + 'full_name' => 'Полное наименование юридического лица', + 'kpp' => 'КПП', + 'okpo' => 'Код по ОКПО', + 'okfs' => 'Форма собственности (по ОКФС)', + 'okopf' => 'Организационно-правовая форма (по ОКОПФ)', + 'okved2' => 'Вид экономической деятельности по ОКВЭД 2', + 'address' => 'Местонахождение (адрес)' } - + + payload = { + 'Сведения об организации' => @org.attributes.filter_map { |key, value| [russian_names[key], value] \ + if !['id', 'created_at', 'updated_at'].include?(key) && !value.nil? }.to_h + } + DOC_TYPES.each_value do |doc_type| payload[doc_type[:title]] = ['Наименование показателя', 'Код'] + years.map do |year| column_header_for(doc_type[:form_code], year) From 3f889b5b9a4f4572bbb4ec0996ba5cfe45955aeb Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Mon, 27 Apr 2026 13:01:50 +0300 Subject: [PATCH 09/13] =?UTF-8?q?SU2-33=20|=20=D0=9F=D0=BE=D1=8F=D1=81?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D1=85=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260414100500_seed_report_codes.rb | 117 +++++++++--------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/db/migrate/20260414100500_seed_report_codes.rb b/db/migrate/20260414100500_seed_report_codes.rb index 91f06f1..a4b82da 100644 --- a/db/migrate/20260414100500_seed_report_codes.rb +++ b/db/migrate/20260414100500_seed_report_codes.rb @@ -2,68 +2,69 @@ class SeedReportCodes < ActiveRecord::Migration[7.1] def up rows = [ { 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: "1100", name: "Итого внеоборотных активов", description: "Сумма строк 1110, 1120, 1130, 1140, 1150" }, - { code: "1210", name: "Запасы", description: "-" }, + { code: "1110", name: "Нематериальные активы", description: "Ценности, которые не имеют вещественной формы (товарные знаки, патенты, программы, авторские права)" }, + { code: "1120", 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: "Долгосрочное имущество, не включенное в другие строки раздела I" }, + { code: "1100", name: "Итого внеоборотных активов", description: "Сумма всего долгосрочного имущества (> 12 мес.)" }, + { 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: "1200", name: "Итого оборотных активов", description: "-" }, - { code: "1600", name: "БАЛАНС (актив)", description: "-" }, - { code: "1310", name: "Уставный капитал/Паевой фонд (в некоммерческой организации)", description: "-" }, - { code: "1320", name: "Собственные акции, принадлежащие обществу, задолженность акционеров по оплате акций/Целевой капитал (в некоммерческой организации)", description: "-" }, - { code: "1330", name: "Целевые средства (в некоммерческой организации)", description: "-" }, + { code: "1220", name: "Налог на добавленную стоимость по приобретенным ценностям", description: "Сумма входящего НДС, не принятая на отчетную дату к вычету" }, + { code: "1230", name: "Дебиторская задолженность", description: "Неисполненные обязательства контрагентов перед организацией" }, + { code: "1240", name: "Финансовые вложения (за исключением денежных эквивалентов)", description: "Краткосрочные доходные инвестиции (ценные бумаги, вклады)" }, + { code: "1250", name: "Денежные средства и денежные эквиваленты", description: "Деньги и вклады до востребования" }, + { code: "1260", name: "Прочие оборотные активы", description: "Краткосрочное имущество, не включенное в другие строки раздела I" }, + { code: "1200", name: "Итого оборотных активов", description: "Сумма всего краткосрочного имущества (< 12 мес.)" }, + { code: "1600", 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: "1300", name: "Итого капитал/Итого целевое финансирование (в некоммерческой организации)", description: "-" }, - { code: "1410", name: "Долгосрочные заемные средства", description: "-" }, - { code: "1420", name: "Отложенные налоговые обязательства", description: "-" }, - { code: "1430", name: "Долгосрочные оценочные обязательства", description: "-" }, - { code: "1450", name: "Прочие долгосрочные обязательства", description: "-" }, - { code: "1400", name: "Итого долгосрочных обязательств", description: "-" }, - { code: "1510", name: "Краткосрочные заемные средства", description: "-" }, - { code: "1520", name: "Краткосрочная кредиторская задолженность", description: "-" }, + { code: "1350", name: "Добавочный капитал (без накопленной дооценки)", description: "Дополнительные взносы учредителей (участников), эмиссионный доход" }, + { code: "1360", name: "Резервный капитал/Фонд недвижимого и особо ценного движимого имущества (в некоммерческой организации)", description: "Часть чистой прибыли, используемая для покрытия возможных убытков" }, + { code: "1370", name: "Нераспределенная прибыль (непокрытый убыток)/Резервный и иные целевые фонды (в некоммерческой организации)", description: "Чистая прибыль, которая не была распределена между участниками/акционерами" }, + { code: "1300", name: "Итого капитал/Итого целевое финансирование (в некоммерческой организации)", description: "Сумма собственного капитала" }, + { code: "1410", name: "Долгосрочные заемные средства", description: "Кредиты и займы, привлеченные организацией на срок > 12 мес." }, + { code: "1420", name: "Отложенные налоговые обязательства", description: "Сумма налога на прибыль, которую компания заплатит в будущем" }, + { code: "1430", name: "Долгосрочные оценочные обязательства", description: "Обязательства с неопределенной суммой или сроком исполнения (> 12 мес.)" }, + { code: "1450", name: "Прочие долгосрочные обязательства", description: "Долгосрочные обязательства, не включенные в другие строки раздела IV" }, + { code: "1400", name: "Итого долгосрочных обязательств", description: "Сумма всех долгосрочных обязательств" }, + { code: "1510", name: "Краткосрочные заемные средства", description: "Кредиты и займы, привлеченные организацией на срок < 12 мес." }, + { code: "1520", name: "Краткосрочная кредиторская задолженность", description: "Неисполненные обязательства организации перед контрагентами" }, { code: "1530", name: "Доходы будущих периодов", description: "-" }, - { code: "1540", name: "Краткосрочные оценочные обязательства", description: "-" }, - { code: "1550", name: "Прочие краткосрочные обязательства", description: "-" }, - { code: "1500", name: "Итого краткосрочных обязательств", description: "-" }, - { code: "1700", 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: "1540", name: "Краткосрочные оценочные обязательства", description: "Обязательства с неопределенной суммой или сроком исполнения (< 12 мес.)" }, + { code: "1550", name: "Прочие краткосрочные обязательства", description: "Краткосрочные обязательства, не включенные в другие строки раздела V" }, + { code: "1500", name: "Итого краткосрочных обязательств", description: "Сумма всех краткосрочных обязательств" }, + { code: "1700", 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: "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: "-" }, From 37e7d2597614d515e57a5fdd52159333238aa142 Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Mon, 27 Apr 2026 13:02:28 +0300 Subject: [PATCH 10/13] =?UTF-8?q?SU2-33=20|=20=D0=98=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=80=D0=B0=D1=81=D0=BA=D1=80=D0=B0=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/organisations_helper.rb | 26 +++++++++++--------------- app/views/organisations/show.html.slim | 6 +++--- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app/helpers/organisations_helper.rb b/app/helpers/organisations_helper.rb index 3ff6666..9050002 100644 --- a/app/helpers/organisations_helper.rb +++ b/app/helpers/organisations_helper.rb @@ -4,8 +4,8 @@ module OrganisationsHelper return nil if code.nil? || code.size < 2 prefix = code[0, 2] case prefix - when '21', '22', '23', '24' - '21-24' + when '21', '22', '23', '24', '25' + '21-25' else prefix end @@ -16,8 +16,8 @@ module OrganisationsHelper 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 false if %w[16 17 29].include?(prefix) || prefix.start_with?("3") + return true if %w[21 22 23 24 25].include?(prefix) return !code.end_with?('00') elsif type_data == 'comparsion' return code.end_with?('00') @@ -43,18 +43,14 @@ module OrganisationsHelper 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 + result[group] ||= [] + result[group] += values.map { |value| parse_number(value)} 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 + min_max_data = prepared_data.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 elsif type_data == 'comparsion' prepared_data = filtered_data.each_with_object({}) do |((_, code), values), result| diff --git a/app/views/organisations/show.html.slim b/app/views/organisations/show.html.slim index 149efd3..ad5e286 100644 --- a/app/views/organisations/show.html.slim +++ b/app/views/organisations/show.html.slim @@ -43,9 +43,9 @@ - @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] + - min_value = min_max_data[key_group][:min] + - max_value = min_max_data[key_group][:max] + - p50 = min_max_data[key_group][: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') From e682897dcef6c33b0a81ebab4ea381618efe09f6 Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Mon, 27 Apr 2026 13:43:54 +0300 Subject: [PATCH 11/13] =?UTF-8?q?SU2-33=20|=20=D0=9E=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=8F?= =?UTF-8?q?=D1=81=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/main.scss | 8 ++++++-- app/controllers/organisations_controller.rb | 14 +++++++++++--- app/views/organisations/show.html.slim | 2 ++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 5fcbdcc..a6a9903 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -34,9 +34,9 @@ a.link:hover { .informations-data { display: flex; flex-direction: column; - width: 90%; + width: 95%; min-width: 300px; - max-width: 1400px; + max-width: 1500px; margin: 0 auto; padding-bottom: inherit; } @@ -85,6 +85,10 @@ a.link:hover { width: 35%; } +.description { + width: 15%; +} + .type_doc_header#tabs { width: 75%; margin: 0 auto; diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index a6b00d4..a4e1160 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, :destroy, :reload] + before_action :set_organisation, :set_report_codes, only: [:show, :destroy, :reload] DOC_TYPES = { 0 => { title: 'Бухгалтерский баланс', form_code: '1.1' }, @@ -37,7 +37,9 @@ class OrganisationsController < ApplicationController @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] + number_of_columns = @current_doc_info1.values.first.size + 1 + number_of_columns += 1 if @type_doc < 2 + @current_doc_info = @info_about_organisation[@doc_name][0..number_of_columns] end end @@ -59,6 +61,10 @@ class OrganisationsController < ApplicationController @org = Organization.find(params[:id]) end + def set_report_codes + @report_codes = ReportCode.all.filter_map { |report_code| [report_code.code, report_code.description] if report_code.code.match?(/\A[12]/) }.to_h + end + def build_organization_payload(years) russian_names = { 'inn' => 'ИНН', @@ -77,7 +83,9 @@ class OrganisationsController < ApplicationController } DOC_TYPES.each_value do |doc_type| - payload[doc_type[:title]] = ['Наименование показателя', 'Код'] + years.map do |year| + first_headers = ['Наименование показателя', 'Код'] + first_headers << 'Пояснения' if ['Бухгалтерский баланс', 'Отчёт о финансовых результатах'].include?(doc_type[:title]) + payload[doc_type[:title]] = first_headers + years.map do |year| column_header_for(doc_type[:form_code], year) end end diff --git a/app/views/organisations/show.html.slim b/app/views/organisations/show.html.slim index ad5e286..d7bf58d 100644 --- a/app/views/organisations/show.html.slim +++ b/app/views/organisations/show.html.slim @@ -40,6 +40,8 @@ tr td = key[0] td = key[1] + -if @report_codes.has_key?(key[1]) + td.description = @report_codes[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]) From e47ec8afccb6908236de65e91557fd76eea0c9d8 Mon Sep 17 00:00:00 2001 From: ARLakhin Date: Mon, 27 Apr 2026 13:46:02 +0300 Subject: [PATCH 12/13] =?UTF-8?q?SU2-33=20|=20=D0=98=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/main.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index a6a9903..06e3dff 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -82,7 +82,7 @@ a.link:hover { } .information-table tr :is(td, th):first-child { - width: 35%; + width: 20%; } .description { From fabc2df38c311abeb29047b172ac1fd35290191f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 27 Apr 2026 18:55:57 +0300 Subject: [PATCH 13/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=B8=D0=B3=D0=BD=D0=BE=D1=80=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 99db197..627bc60 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ /node_modules .idea/ +TZ.xlsx