From 7ee21d5e5d9dabef1c4eaee454284faf42582d38 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 25 Mar 2026 09:48:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BF=D0=B0=D1=80=D1=81=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=2014?= =?UTF-8?q?=20=D0=B8=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BD=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BE=D1=82=D1=87=D0=B5=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D0=B1=D0=B0=D0=B7=D1=83=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85;=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D1=82=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/report_parser.rb | 43 +++---- app/services/report_parsers/form12_parser.rb | 13 +- app/services/report_parsers/form14_parser.rb | 112 ++++++++++++++++++ .../20260325063000_seed_document_types.rb | 18 +++ db/schema.rb | 2 +- 5 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 app/services/report_parsers/form14_parser.rb create mode 100644 db/migrate/20260325063000_seed_document_types.rb diff --git a/app/services/report_parser.rb b/app/services/report_parser.rb index c1dc75e..ccda01a 100644 --- a/app/services/report_parser.rb +++ b/app/services/report_parser.rb @@ -11,8 +11,11 @@ class ReportParser form_11_data = ReportParsers::Form11Parser.new(@yearly_file, spreadsheet: spreadsheet).call form_12_data = ReportParsers::Form12Parser.new(@yearly_file, spreadsheet: spreadsheet).call + form_14_data = ReportParsers::Form14Parser.new(@yearly_file, spreadsheet: spreadsheet).call - merge_financial_statements(form_11_data, form_12_data) + save_form_statements(form_11_data, 1) # 1 - Бух. баланс + save_form_statements(form_12_data, 2) # 2 - Отчет о фин. результатах + save_form_statements(form_14_data, 4) # 4 - Отчет о движении денежных средств end end @@ -28,35 +31,21 @@ class ReportParser end end - def merge_financial_statements(form_11_data, form_12_data) - document_type = DocumentType.find_or_create_by!(name: 'Финансовые данные') + def save_form_statements(form_data, form_id) + document_type = DocumentType.find_by(id: form_id) + raise "DocumentType with id=#{form_id} not found!" unless document_type - all_years = (form_11_data.keys + form_12_data.keys).uniq + form_data.each do |year, data| + rows = data[:rows] || data['rows'] || [] + next if rows.blank? - all_years.each do |year| statement = find_or_build_statement(document_type, year) - - all_rows = [] - - if form_11_data[year].present? - form_11_rows = form_11_data[year][:rows] || form_11_data[year]['rows'] || [] - all_rows.concat(form_11_rows) - end - - if form_12_data[year].present? - form_12_rows = form_12_data[year][:rows] || form_12_data[year]['rows'] || [] - all_rows.concat(form_12_rows) - end - - if all_rows.present? - payload = { - data_year: year, - rows: all_rows - } - - statement.assign_attributes(data: payload) - statement.save! - end + payload = { + data_year: year, + rows: rows + } + statement.assign_attributes(data: payload) + statement.save! end end diff --git a/app/services/report_parsers/form12_parser.rb b/app/services/report_parsers/form12_parser.rb index eda69e0..90f366b 100644 --- a/app/services/report_parsers/form12_parser.rb +++ b/app/services/report_parsers/form12_parser.rb @@ -98,21 +98,16 @@ module ReportParsers value_columns = columns.keys.select do |col| text = columns[col] - text.include?('за ') || - text.include?('на ') || - text.match?(/\b20\d{2}\b/) || - (col > code_column && text.include?('года')) || - (col > code_column && text.match?(/\d+/)) + next false if text.include?('поясн') + text.match?(/20\d{2}/) || text.include?('за ') || text.include?('на ') end - if value_columns.empty? - value_columns = columns.keys.select { |col| col > code_column && columns[col].present? } - end + value_columns = value_columns.sort.last(2) { name_column: name_column, code_column: code_column, - value_columns: value_columns.sort.first(3) + value_columns: value_columns } end end diff --git a/app/services/report_parsers/form14_parser.rb b/app/services/report_parsers/form14_parser.rb new file mode 100644 index 0000000..0c935bf --- /dev/null +++ b/app/services/report_parsers/form14_parser.rb @@ -0,0 +1,112 @@ + 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 normalize_label(text) + text.to_s.downcase.gsub(/[[:space:]]+/, ' ').strip + end +module ReportParsers + class Form14Parser < BaseParser + SHEET_NAME = 'Отчет о движении денежных средс' + FORM_CODE = '1.4' + + 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 = read_cell(sheet, row, columns[:name_column]) + line_code = read_cell(sheet, row, columns[:code_column]).to_s.strip.presence + + 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, + rows: rows_for_year + } + end + + payloads_by_year + end + end +end diff --git a/db/migrate/20260325063000_seed_document_types.rb b/db/migrate/20260325063000_seed_document_types.rb new file mode 100644 index 0000000..3f8724a --- /dev/null +++ b/db/migrate/20260325063000_seed_document_types.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 7a18b93..72e9e6b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2026_03_21_000006) do +ActiveRecord::Schema[7.1].define(version: 2026_03_25_063000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql"