# # Rechnungen aus verschachteltem JSON in eine Datei mit Satzarten (Datei-, Kopf-, Positions-, Schlusssatz) # # bin/etl run examples/rechnungen_satzarten.rb [--out=tmp] # require 'fileutils' require 'datapipeline' DATA = File.expand_path('data', __dir__) out = DataPipeline.params.fetch(:out, 'tmp') FileUtils.mkdir_p(out) # Feld: [Name, Typ, Länge, Pflicht]; Typ AN Text, N Zahl (11.2 = 9 Vor-, 2 Nachkommastellen), DAT YYYYMMDD LAYOUT = DataPipeline::Layout.new do record 'DATEI', [['SATYP', 'AN', 5, true], ['ERSTELLT', 'DAT', 8, true], ['ANZAHL', 'N', 6, true]] record 'KOPF', [['SATYP', 'AN', 5, true], ['LFDNR', 'N', 6, true], ['BELNR', 'AN', 10, true], ['DATUM', 'DAT', 8, true], ['KUNNR', 'AN', 10, true], ['SUMME', 'N', '11.2', true]] record 'POS', [['SATYP', 'AN', 5, true], ['LFDNR', 'N', 6, true], ['BELNR', 'AN', 10, true], ['POSNR', 'N', 3, true], ['TEXT', 'AN', 30], ['BETRAG', 'N', '11.2', true]] record 'ENDE', [['SATYP', 'AN', 5, true], ['ANZAHL', 'N', 6, true]] end DataPipeline.new do read container(:json, file: "#{DATA}/rechnungen.json") # 1. eine Rechnung -> Kopfsatz + n Positionssätze map do |rechnung| positionen = rechnung['positionen'].each_with_index.map do |pos, i| { 'satyp' => 'POS', 'belnr' => rechnung['nr'], 'posnr' => format('%03d', i + 1), 'text' => pos['text'], 'betrag' => format('%.2f', pos['menge'] * pos['preis']) } end kopf = { 'satyp' => 'KOPF', 'belnr' => rechnung['nr'], 'datum' => rechnung['datum'].delete('-'), 'kunnr' => rechnung['kunde'], 'summe' => format('%.2f', positionen.sum { |p| p['betrag'].to_f }) } [kopf, *positionen] end # 2. laufende Nummer über alle Kopf- und Positionssätze map { |row, ctx| row.merge('lfdnr' => format('%06d', ctx.index + 1)) } # 3. gegen das Layout prüfen: zu langer Text wird gemeldet, die Zeile bleibt validate LAYOUT, warn: true # Datei- und Schlusssatz kennen die Anzahl der Datensätze header { |ctx| { 'satyp' => 'DATEI', 'erstellt' => Date.today.strftime('%Y%m%d'), 'anzahl' => format('%06d', ctx.written) } } trailer { |ctx| { 'satyp' => 'ENDE', 'anzahl' => format('%06d', ctx.written) } } # Felder in der Reihenfolge des Layouts; Positionen zusätzlich als NDJSON write container(:csv, file: "#{out}/rechnungen.txt", layout: LAYOUT, csv: { headers: false, col_sep: '|' }) write container(:json, file: "#{out}/positionen.ndjson"), only: ->(row) { row['satyp'] == 'POS' } end.run