README neu gegliedert, Beispiel für einen produktiven Lauf, Fehlersuche und Umstiegshinweise

This commit is contained in:
wkh
2026-10-09 21:55:16 +02:00
parent baf09d3947
commit 0b72635e08
6 changed files with 1070 additions and 536 deletions
+59
View File
@@ -0,0 +1,59 @@
#
# Produktiver Lauf: Buchungen prüfen, Summe und Saldo abgleichen, wiederholbar in die Buchhaltungs-Datenbank
# und als CSV in Windows-1252 für das Zielsystem schreiben; mit Sperre, Laufhistorie und Slack bei Problemen
#
# bin/etl run examples/buchungen_export.rb [--out=tmp] [--dry-run]
#
require 'fileutils'
require 'datapipeline'
DATA = File.expand_path('data', __dir__)
out = DataPipeline.params.fetch(:out, 'tmp')
FileUtils.mkdir_p(out)
# Zugangsdaten stehen nicht im Skript, sondern in der Verbindungsdatei (Default: config/connections.yml)
DataPipeline.connections_file = File.expand_path('config/connections.yml', __dir__)
# Slack nur, wenn ein Token gesetzt ist; so fällt ein fehlender Token schon vor dem Lauf auf
slack = DataPipeline::Slack.new(channel: '#etl') if ENV['SLACK_TOKEN']
DataPipeline.new do
name 'buchungen_export'
lock "#{out}/.buchungen.lock" # läuft schon ein Export, bricht dieser ab (Exit-Code 3)
history "#{out}/runs.ndjson" # eine JSON-Zeile je Lauf, auch nach Abbruch
read container(:csv, file: "#{DATA}/buchungen.csv", csv: { col_sep: ';' })
# 1. prüfen: ohne on_error bricht ein Fehler den Lauf ab, es entsteht keine halbe Buchung
validate 'belnr' => { required: true, format: /\A\d{10}\z/ },
'konto' => { required: true, format: /\A\d+\z/ },
'betrag' => { required: true, type: :decimal }
# 2. umbauen mit Kurzschritten und einem map
default 'text' => 'ohne Text'
rename 'text' => 'buchungstext'
cast 'buzei' => :integer, 'datum' => :date, 'betrag' => :decimal
map do |row|
row.merge('shkzg' => row['betrag'].negative? ? 'H' : 'S', 'dmbtr' => format('%.2f', row['betrag'].abs))
end
select %w[belnr buzei datum konto shkzg dmbtr betrag buchungstext]
# 3. abgleichen, bevor etwas geschrieben wird
totals 'betrag'
reconcile do |stats|
quelle, ergebnis = stats.total('betrag', :read), stats.total('betrag')
next "Summe geändert: Quelle #{DataPipeline.decimal(quelle)}, Ergebnis #{DataPipeline.decimal(ergebnis)}" if quelle != ergebnis
"Soll und Haben ungleich, Saldo #{DataPipeline.decimal(ergebnis)}" unless ergebnis.zero?
end
# 4. schreiben: Upsert macht den Lauf wiederholbar; die CSV-Datei im Zeichensatz des Zielsystems
write connection(:buchhaltung)
write container(:csv, file: "#{out}/buchungen_sap.csv", encoding: 'Windows-1252', csv: { col_sep: ';' },
columns: %w[belnr buzei datum konto shkzg dmbtr buchungstext])
on_finish do |stats|
puts "#{stats.name}: #{stats}"
slack&.send_message("*#{stats.name}*: #{stats}") unless stats.ok?
end
end.run
+8
View File
@@ -0,0 +1,8 @@
# Verbindungen der Beispiele. Die Datei wird mit ERB gelesen: Zugangsdaten und Pfade kommen aus der Umgebung,
# bei Postgres z. B. password: <%= ENV['PDS_PASSWORD'] %>
buchhaltung:
type: sqlite
database: <%= ENV.fetch('BUCHHALTUNG_DB', 'tmp/buchhaltung.sqlite3') %>
table: buchungen
create: true
upsert: [belnr, buzei]
+8
View File
@@ -0,0 +1,8 @@
belnr;buzei;datum;konto;betrag;text
5100000001;1;2026-10-01;1020;1250.00;Zahlung Müller GmbH
5100000001;2;2026-10-01;1100;-1250.00;Zahlung Müller GmbH
5100000002;1;2026-10-02;6500;380.50;Büromaterial
5100000002;2;2026-10-02;1170;29.30;Vorsteuer
5100000002;3;2026-10-02;2000;-409.80;Papeterie Zürcher AG
5100000003;1;2026-10-03;3200;99.95;
5100000003;2;2026-10-03;1020;-99.95;Gutschrift
1 belnr buzei datum konto betrag text
2 5100000001 1 2026-10-01 1020 1250.00 Zahlung Müller GmbH
3 5100000001 2 2026-10-01 1100 -1250.00 Zahlung Müller GmbH
4 5100000002 1 2026-10-02 6500 380.50 Büromaterial
5 5100000002 2 2026-10-02 1170 29.30 Vorsteuer
6 5100000002 3 2026-10-02 2000 -409.80 Papeterie Zürcher AG
7 5100000003 1 2026-10-03 3200 99.95
8 5100000003 2 2026-10-03 1020 -99.95 Gutschrift