├── .github └── workflows │ └── push.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── datev.gemspec ├── examples ├── EXTF_Buchungsstapel.csv ├── EXTF_Kontenbeschriftungen.csv └── EXTF_Stammdaten.csv ├── lib ├── datev.rb └── datev │ ├── base.rb │ ├── base │ ├── account.rb │ ├── booking.rb │ ├── contact.rb │ ├── header.rb │ └── header │ │ ├── account_header.rb │ │ ├── booking_header.rb │ │ └── contact_header.rb │ ├── export.rb │ ├── export │ ├── account_export.rb │ ├── booking_export.rb │ └── contact_export.rb │ ├── field.rb │ ├── field │ ├── boolean_field.rb │ ├── date_field.rb │ ├── decimal_field.rb │ ├── integer_field.rb │ └── string_field.rb │ └── version.rb └── spec ├── datev ├── base │ ├── account_spec.rb │ ├── booking_spec.rb │ ├── contact_spec.rb │ └── header_spec.rb ├── base_spec.rb ├── export │ ├── account_export_spec.rb │ ├── booking_export_spec.rb │ └── contact_export_spec.rb ├── field │ ├── boolean_field_spec.rb │ ├── date_field_spec.rb │ ├── decimal_field_spec.rb │ ├── integer_field_spec.rb │ └── string_field_spec.rb └── field_spec.rb ├── datev_spec.rb └── spec_helper.rb /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | ruby: ["3.2", "3.3", "3.4"] 11 | 12 | runs-on: ubuntu-latest 13 | name: Test against Ruby ${{ matrix.ruby }} 14 | 15 | steps: 16 | - uses: actions/checkout@v5 17 | 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | 23 | - name: Install gems 24 | run: | 25 | gem install bundler 26 | bundle install --jobs 4 --retry 3 27 | 28 | - name: Run tests 29 | env: 30 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 31 | run: bundle exec rspec 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Please see the [GitHub "Releases" page](https://github.com/ledermann/datev/releases). 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at georg@ledermann.dev. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in datev.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 Georg Ledermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datev 2 | 3 | Ruby gem to export bookings and more to DATEV format as CSV file 4 | 5 | Supported DATEV format: v7.0 6 | 7 | [![Build Status](https://github.com/ledermann/datev/workflows/CI/badge.svg)](https://github.com/ledermann/datev/actions) 8 | [![Code Climate](https://codeclimate.com/github/ledermann/datev/badges/gpa.svg)](https://codeclimate.com/github/ledermann/datev) 9 | [![Coverage Status](https://coveralls.io/repos/github/ledermann/datev/badge.svg?branch=master)](https://coveralls.io/github/ledermann/datev?branch=master) 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'datev' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install datev 26 | 27 | ## Usage 28 | 29 | To export bookings, you need an BookingExport instance with an array of records. Example: 30 | 31 | ```ruby 32 | export = Datev::BookingExport.new( 33 | 'Herkunft' => 'XY', 34 | 'Exportiert von' => 'Chief Accounting Officer', 35 | 'Berater' => 1001, 36 | 'Mandant' => 456, 37 | 'WJ-Beginn' => Date.new(2018,1,1), 38 | 'Datum vom' => Date.new(2018,2,1), 39 | 'Datum bis' => Date.new(2018,2,28), 40 | 'Bezeichnung' => 'Beispiel-Buchungen' 41 | ) # For available hash keys see /lib/datev/base/header.rb 42 | 43 | export << { 44 | 'Belegdatum' => Date.new(2018,2,21), 45 | 'Buchungstext' => 'Fachbuch: Controlling für Dummies', 46 | 'Umsatz (ohne Soll/Haben-Kz)' => 24.95, 47 | 'Soll/Haben-Kennzeichen' => 'H', 48 | 'Konto' => 1200, 49 | 'Gegenkonto (ohne BU-Schlüssel)' => 4940, 50 | 'BU-Schlüssel' => '8' 51 | } # For available hash keys see /lib/datev/base/booking.rb 52 | 53 | export << { 54 | 'Belegdatum' => Date.new(2018,2,22), 55 | 'Buchungstext' => 'Honorar FiBu-Seminar', 56 | 'Umsatz (ohne Soll/Haben-Kz)' => 5950.00, 57 | 'Soll/Haben-Kennzeichen' => 'S', 58 | 'Konto' => 10000, 59 | 'Gegenkonto (ohne BU-Schlüssel)' => 8400, 60 | 'Belegfeld 1' => 'RE201802-135' 61 | } 62 | 63 | export.to_file('EXTF_Buchungsstapel.csv') 64 | ``` 65 | 66 | Result: [CSV file](examples/EXTF_Buchungsstapel.csv) 67 | 68 | All records are validated against the defined schema. 69 | 70 | Beside bookings, some other exports are available, too: 71 | 72 | * `AccountExport` ("Kontenbeschriftungen") 73 | * `ContactExport` ("Stammdaten") 74 | 75 | 76 | ## Development 77 | 78 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 79 | 80 | 81 | ## Contributing 82 | 83 | Bug reports and pull requests are welcome on GitHub at https://github.com/ledermann/datev. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 84 | 85 | 86 | ## License 87 | 88 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 89 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "datev" 5 | require "date" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /datev.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'datev/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "datev" 8 | spec.version = Datev::VERSION 9 | spec.authors = ["Georg Ledermann"] 10 | spec.email = ["georg@ledermann.dev"] 11 | 12 | spec.summary = %q{Export booking data in DATEV format} 13 | spec.description = %q{Provides an easy way to create CSV files which can be imported into accounting applications} 14 | spec.homepage = "https://github.com/ledermann/datev" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | spec.required_ruby_version = '>= 3.2' 22 | 23 | spec.add_development_dependency "bundler" 24 | spec.add_development_dependency "csv" 25 | spec.add_development_dependency "rake" 26 | spec.add_development_dependency "rspec" 27 | spec.add_development_dependency 'simplecov' 28 | spec.add_development_dependency 'coveralls_reborn' 29 | end 30 | -------------------------------------------------------------------------------- /examples/EXTF_Buchungsstapel.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledermann/datev/6a1af35c21e83d46dd784245e0d1d004d1d2a5f2/examples/EXTF_Buchungsstapel.csv -------------------------------------------------------------------------------- /examples/EXTF_Kontenbeschriftungen.csv: -------------------------------------------------------------------------------- 1 | "EXTF";700;20;"Kontenbeschriftungen";2;20180223152500000;;"XY";"Chief Accounting Officer";;1001;456;20180101;4;;;"Beispiel-Konten";;;;;;;;;;;;;; 2 | Konto;Kontenbeschriftung;Sprach-ID 3 | 400;"Betriebsausstattung";"de-DE" 4 | 1000;"Kasse";"de-DE" 5 | 1200;"Girokonto";"de-DE" 6 | -------------------------------------------------------------------------------- /examples/EXTF_Stammdaten.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledermann/datev/6a1af35c21e83d46dd784245e0d1d004d1d2a5f2/examples/EXTF_Stammdaten.csv -------------------------------------------------------------------------------- /lib/datev.rb: -------------------------------------------------------------------------------- 1 | require 'datev/version' 2 | 3 | require 'datev/field' 4 | require 'datev/field/string_field' 5 | require 'datev/field/integer_field' 6 | require 'datev/field/boolean_field' 7 | require 'datev/field/decimal_field' 8 | require 'datev/field/date_field' 9 | 10 | require 'datev/base' 11 | require 'datev/base/header' 12 | require 'datev/base/header/account_header' 13 | require 'datev/base/header/contact_header' 14 | require 'datev/base/header/booking_header' 15 | require 'datev/base/account' 16 | require 'datev/base/contact' 17 | require 'datev/base/booking' 18 | 19 | require 'datev/export' 20 | require 'datev/export/account_export' 21 | require 'datev/export/contact_export' 22 | require 'datev/export/booking_export' 23 | -------------------------------------------------------------------------------- /lib/datev/base.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class Base 3 | class << self 4 | attr_accessor :fields 5 | attr_writer :default_attributes 6 | 7 | def default_attributes 8 | if @default_attributes.is_a?(Proc) 9 | @default_attributes.call 10 | else 11 | @default_attributes 12 | end 13 | end 14 | 15 | def inherited(subclass) 16 | subclass.fields = self.fields 17 | subclass.default_attributes = self.default_attributes 18 | end 19 | end 20 | 21 | def self.field(name, type, options={}, &block) 22 | self.fields ||= [] 23 | 24 | # Check if there is already a field with the same name 25 | if field_by_name(name) 26 | raise ArgumentError.new("Field '#{name}' already exists") 27 | end 28 | 29 | field_class = const_get("Datev::#{type.to_s.capitalize}Field") 30 | self.fields << field_class.new(name, options, &block) 31 | end 32 | 33 | def self.field_by_name(name) 34 | self.fields.find { |f| f.name == name } 35 | end 36 | 37 | def initialize(attributes) 38 | raise ArgumentError.new('Hash required') unless attributes.is_a?(Hash) 39 | 40 | @attributes = self.class.default_attributes || {} 41 | 42 | # Check existing names and set value (if valid) 43 | attributes.each_pair do |name,value| 44 | unless field = self.class.field_by_name(name) 45 | raise ArgumentError.new("Field '#{name}' not found") 46 | end 47 | 48 | field.validate!(value) 49 | @attributes[name] = value 50 | end 51 | 52 | # Check for missing values 53 | self.class.fields.select(&:required?).each do |field| 54 | if @attributes[field.name].nil? 55 | raise ArgumentError.new("Value for field '#{field.name}' is required but missing") 56 | end 57 | end 58 | end 59 | 60 | def [](name) 61 | field = self.class.field_by_name(name) 62 | raise ArgumentError.new("Field '#{name}' not found") unless field 63 | 64 | @attributes[field.name] 65 | end 66 | 67 | def output(context=nil) 68 | self.class.fields.map do |field| 69 | value = @attributes[field.name] 70 | field.output(value, context) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/datev/base/account.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class Account < Base 3 | # http://www.datev.de/dnlexom/client/app/index.html#/document/1036228/D103622800011 4 | 5 | # 1 6 | field 'Konto', :integer, limit: 8, required: true 7 | # Sachkontennummer (max. 8-stellig). 8 | 9 | # 2 10 | field 'Kontenbeschriftung', :string, limit: 40 11 | # Beschriftung des Sachkontos 12 | 13 | # 3 14 | field 'Sprach-ID', :string, limit: 5 15 | # Sprach-ID der Kontenbeschriftung 16 | # de-DE = Deutsch 17 | # en-GB = Englisch 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/datev/base/booking.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class Booking < Base 3 | # http://www.datev.de/dnlexom/client/app/index.html#/document/1036228/D103622800010 4 | 5 | # 1 6 | field 'Umsatz (ohne Soll/Haben-Kz)', :decimal, precision: 12, scale: 2, required: true 7 | # Beispiel: 1234567890,12 8 | # Muss immer ein positiver Wert sein 9 | 10 | # 2 11 | field 'Soll/Haben-Kennzeichen', :string, limit: 1, required: true 12 | # Die Soll-/Haben-Kennzeichnung des Umsatzes bezieht sich auf das Konto, das im Feld Konto angegeben wird: 13 | # S = Soll 14 | # H = Haben 15 | 16 | # 3 17 | field 'WKZ Umsatz', :string, limit: 3 18 | # Dreistelliger ISO-Code der Währung (Dok.-Nr. 1080170); gibt an, welche Währung dem Betrag zugrunde liegt. 19 | # Wenn kein Wert angegeben ist, wird das WKZ aus dem Header übernommen. 20 | 21 | # 4 22 | field 'Kurs', :decimal, precision: 10, scale: 6 23 | # Der Fremdwährungskurs bestimmt, wie der angegebene Umsatz, der in Fremdwährung übergeben wird, in die Basiswährung umzurechnen ist, wenn es sich um ein Nicht-EWU-Land handelt. 24 | # Beispiel: 1123,123456 25 | # Achtung: Der Wert 0 ist unzulässig. 26 | 27 | # 5 28 | field 'Basisumsatz', :decimal, precision: 12, scale: 2 29 | # Wenn das Feld Basisumsatz verwendet wird, muss auch das Feld WKZ Basisumsatz gefüllt werden. 30 | # Beispiel: 1123123123,12 31 | 32 | # 6 33 | field 'WKZ Basisumsatz', :string, limit: 3 34 | # Währungskennzeichen der hinterlegten Basiswährung. Wenn das Feld WKZ Basisumsatz verwendet wird, muss auch das Feld Basisumsatz verwendet werden. 35 | # ISO-Code beachten (siehe Dok.-Nr.1080170) 36 | 37 | # 7 38 | field 'Konto', :integer, limit: 9, required: true do 39 | # Sach- oder Personen-Kontonummer 40 | # Darf max. 8- bzw. max. 9-stellig sein (abhängig von der Information im Header) 41 | # Die Personenkontenlänge darf nur 1 Stelle länger sein als die definierte Sachkontennummernlänge. 42 | 43 | def output(value, context) 44 | length = context['Sachkontenlänge'] 45 | value.to_s.rjust(length, '0') 46 | end 47 | end 48 | 49 | # 8 50 | field 'Gegenkonto (ohne BU-Schlüssel)', :integer, limit: 9, required: true do 51 | # Sach- oder Personen-Kontonummer 52 | # Darf max. 8- bzw. max. 9-stellig sein (abhängig von der Information im Header) 53 | # Die Personenkontenlänge darf nur 1 Stelle länger sein als die definierte Sachkontennummernlänge. 54 | 55 | def output(value, context) 56 | length = context['Sachkontenlänge'] 57 | value.to_s.rjust(length, '0') 58 | end 59 | end 60 | 61 | # 9 62 | field 'BU-Schlüssel', :string, limit: 4 63 | # Steuerschlüssel und/oder Berichtigungsschlüssel 64 | 65 | # 10 66 | field 'Belegdatum', :date, format: '%d%m', required: true 67 | # Achtung: Auch bei individueller Feldformatierung mit vierstelliger Jahreszahl wird immer in das aktuelle Wirtschaftsjahr importiert, wenn Tag und Monat des Datums im bebuchbaren Zeitraum liegen, da die Jahreszahl nicht berücksichtigt wird. 68 | 69 | # 11 70 | field 'Belegfeld 1', :string, limit: 36, regex: %r{\A[a-zA-Z0-9\$\&\%\*\+\-\/]*\z} 71 | # Rechnungs-/Belegnummer 72 | # Das Belegfeld 1 ist der "Schlüssel" für die Verwaltung von Offenen Posten. 73 | # Bei einer Zahlung oder Gutschrift erfolgt nur dann ein OP-Ausgleich, wenn die Belegnummer mit dem Belegfeld 1 identisch ist. 74 | 75 | # 12 76 | field 'Belegfeld 2', :string, limit: 12, regex: %r{\A[a-zA-Z0-9\$\&\%\*\+\-\/]*\z} 77 | # Belegnummer oder OPOS-Verarbeitungsinformationen 78 | 79 | # 13 80 | field 'Skonto', :decimal, precision: 10, scale: 2 81 | # Skonto-Betrag/-Abzug 82 | # Nur bei Zahlungen zulässig. 83 | # Beispiel 12123123,12 84 | # Achtung: Der Wert 0 ist unzulässig. 85 | 86 | # 14 87 | field 'Buchungstext', :string, limit: 60 88 | 89 | # 15 90 | field 'Postensperre', :boolean 91 | # Mahn-/Zahl-Sperre 92 | # Die Rechnung kann aus dem Mahnwesen / Zahlungsvorschlag ausgeschlossen werden. 93 | # true = Postensperre 94 | # false/keine Angabe = keine Sperre 95 | # Nur in Verbindung mit einer Rechnungsbuchung und Personenkonto (OPOS) relevant. 96 | 97 | # 16 98 | field 'Diverse Adressnummer', :string, limit: 9 99 | # Adressnummer einer diversen Adresse 100 | # Nur in Verbindung mit OPOS relevant. 101 | 102 | # 17 103 | field 'Geschäftspartnerbank', :integer, limit: 3 104 | # Wenn für eine Lastschrift oder Überweisung eine bestimmte Bank des Geschäftspartners genutzt werden soll. 105 | # Nur in Verbindung mit OPOS relevant. 106 | 107 | # 18 108 | field 'Sachverhalt', :integer, limit: 2 109 | # Der Sachverhalt wird in Rechnungswesen pro verwendet, um Buchungen/Posten als Mahnzins/Mahngebühr zu identifizieren. 110 | # Für diese Posten werden z. B. beim Erstellen von Mahnungen keine Mahnzinsen berechnet. 111 | # 31 = Mahnzins 112 | # 40 = Mahngebühr 113 | # Nur in Verbindung mit OPOS relevant. 114 | 115 | # 19 116 | field 'Zinssperre', :boolean 117 | # Hier kann eine Zinssperre übergeben werden; dadurch werden für diesen Posten bei Erstellung einer Mahnung keine Mahnzinsen ermittelt. 118 | # Nur in Verbindung mit OPOS relevant. 119 | # keine Angabe und 0 = keine Sperre 120 | # 1 = Zinssperre 121 | 122 | # 20 123 | field 'Beleglink', :string, limit: 210 124 | # Link auf den Buchungsbeleg, der digital in einem Dokumenten-Management-System (z. B. DATEV Dokumentenablage, DATEV DMS classic) abgelegt wurde. 125 | # Der Beleglink hat folgenden Aufbau: 126 | # 4-stelliges Kürzel für Dokumentenmanagementsystem (siehe unten) 127 | # Leerzeichen 128 | # Anführungszeichen 129 | # Beleglink (GUID, Dateiname des Belegs), max. 36 Zeichen 130 | # Schlusszeichen 131 | # Beispiel für einen Beleglink aus Belege online: 132 | # BEDI “CB6A8F8F-099A-B3A9-2BAA-0CB64E299BA” 133 | # Das Kürzel bezeichnet das Quellsystem (Dokumentenmanagement), indem die digitalen Belege abgelegt sind. 134 | # DATEV verwendet für seine Dokumentenmanagement-Systeme folgende Kürzel: 135 | # Belegverwaltung online → BEDI 136 | # DATEV DMS → DDMS 137 | # Dokumentenablage → DDMS (vormals DORG) 138 | 139 | # 21 bis 36 140 | (1..8).each do |number| 141 | field "Beleginfo – Art #{number}", :string, limit: 20 142 | field "Beleginfo – Inhalt #{number}", :string, limit: 210 143 | end 144 | # Bei einem ASCII-Format, das aus einem DATEV pro-Rechnungswesen-Programm erstellt wurde, können diese Felder Informationen aus einem Beleg (z. B. einem elektronischen Kontoumsatz) enthalten. 145 | # Wenn die Feldlänge eines Beleginfo-Inhalts-Felds überschritten wird, wird die Information im nächsten Beleginfo-Feld weitergeführt. 146 | # Wichtiger Hinweis 147 | # Eine Beleginfo besteht immer aus den Bestandteilen Beleginfo-Art und Beleginfo-Inhalt. Wenn Sie die Beleginfo nutzen möchten, befüllen Sie immer beide Felder. 148 | # Beispiel: 149 | # Beleginfo-Art: 150 | # Kontoumsätze der jeweiligen Bank 151 | # Beleginfo-Inhalt: 152 | # Buchungsspezifische Inhalte zu den oben genannten Informationsarten 153 | 154 | # 37 155 | field 'KOST1 – Kostenstelle', :string, limit: 8 156 | # Über KOST1 erfolgt die Zuordnung des Geschäftsvorfalls für die anschließende Kostenrechnung. 157 | 158 | # 38 159 | field 'KOST2 – Kostenstelle', :string, limit: 8 160 | # Über KOST2 erfolgt die Zuordnung des Geschäftsvorfalls für die anschließende Kostenrechnung. 161 | 162 | # 39 163 | field 'Kost Menge', :decimal, precision: 11, scale: 2 164 | # Im KOST-Mengenfeld wird die Wertgabe zu einer bestimmten Bezugsgröße für eine Kostenstelle erfasst. Diese Bezugsgröße kann z. B. kg, g, cm, m, % sein. Die Bezugsgröße ist definiert in den Kostenrechnungs-Stammdaten. 165 | # Beispiel: 123123123,12 166 | 167 | # 40 168 | field 'EU-Land u. USt-IdNr.', :string, limit: 15 169 | # Die USt-IdNr. besteht aus: 170 | # 2-stelligen Länderkürzel (siehe Dok.-Nr. 1080169; Ausnahme Griechenland: Das Länderkürzel lautet EL) 171 | # 13-stelliger USt-IdNr. 172 | 173 | # 41 174 | field 'EU-Steuersatz', :decimal, precision: 4, scale: 2 175 | # Nur für entsprechende EU-Buchungen: 176 | # Der im EU-Bestimmungsland gültige Steuersatz. 177 | # Beispiel: 12,12 178 | 179 | # 42 180 | field 'Abw. Versteuerungsart', :string, limit: 1 181 | # Für Buchungen, die in einer von der Mandantenstammdaten-Schlüsselung abweichenden Umsatzsteuerart verarbeitet werden sollen, kann die abweichende Versteuerungsart im Buchungssatz übergeben werden: 182 | # I = Ist-Versteuerung 183 | # K = keine Umsatzsteuerrechnung 184 | # P = Pauschalierung (z. B. für Land- und Forstwirtschaft) 185 | # S = Soll-Versteuerung 186 | 187 | # 43 188 | field 'Sachverhalt L+L', :integer, limit: 3 189 | # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.ff UStG 190 | # Achtung: Der Wert 0 ist unzulässig. 191 | # (siehe Dok.-Nr. 1034915) 192 | 193 | # 44 194 | field 'Funktionsergänzung L+L', :integer, limit: 3 195 | # Steuersatz/Funktion zum L+L-Sachverhalt 196 | # Achtung: Der Wert 0 ist unzulässig. 197 | # (siehe Dok.-Nr. 1034915) 198 | 199 | # 45 200 | field 'BU 49 Hauptfunktionstyp', :integer, limit: 1 201 | # Bei Verwendung des BU-Schlüssels 49 für "andere Steuersätze" muss der steuerliche Sachverhalt mitgegeben werden. 202 | 203 | # 46 204 | field 'BU 49 Hauptfunktionsnummer', :integer, limit: 2 205 | 206 | # 47 207 | field 'BU 49 Funktionsergänzung', :integer, limit: 3 208 | 209 | # 48 bis 87 210 | (1..20).each do |number| 211 | field "Zusatzinformation – Art #{number}", :string, limit: 20 212 | field "Zusatzinformation – Inhalt #{number}", :string, limit: 210 213 | end 214 | # Zusatzinformationen, die zu Buchungssätzen erfasst werden können. 215 | # Diese Zusatzinformationen besitzen den Charakter eines Notizzettels und können frei erfasst werden. 216 | # Wichtiger Hinweis 217 | # Eine Zusatzinformation besteht immer aus den Bestandteilen Informationsart und Informationsinhalt. Wenn Sie die Zusatzinformation nutzen möchten, füllen Sie immer beide Felder. 218 | # Beispiel: 219 | # Informationsart, z. B. Filiale oder Mengengrößen (qm) 220 | # Informationsinhalt: Buchungsspezifische Inhalte zu den oben genannten Informationsarten. 221 | 222 | # 88 223 | field 'Stück', :integer, limit: 8 224 | # Wirkt sich nur bei Sachverhalt mit SKR14 Land- und Forstwirtschaft aus, für andere SKR werden die Felder beim Import/Export überlesen bzw. leer exportiert. 225 | 226 | # 89 227 | field 'Gewicht', :decimal, limit: 10, scale: 2 228 | 229 | # 90 230 | field 'Zahlweise', :integer, limit: 2 231 | # OPOS-Informationen kommunal 232 | # 1 = Lastschrift 233 | # 2 = Mahnung 234 | # 3 = Zahlung 235 | 236 | # 91 237 | field 'Forderungsart', :string, limit: 10 238 | # OPOS-Informationen kommunal 239 | 240 | # 92 241 | field 'Veranlagungsjahr', :date, format: '%Y' 242 | # OPOS-Informationen kommunal 243 | 244 | # 93 245 | field 'Zugeordnete Fälligkeit', :date, format: '%d%m%Y' 246 | # OPOS-Informationen kommunal 247 | 248 | # 94 249 | field 'Skontotyp', :integer, limit: 1 250 | # 1 = Einkauf von Waren 251 | # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen 252 | 253 | # 95 254 | field 'Auftragsnummer', :string, limit: 30 255 | # Allgemeine Bezeichnung, des Auftrags/Projekts 256 | 257 | # 96 258 | field 'Buchungstyp', :string, limit: 2 259 | # AA = Angeforderte Anzahlung/Abschlagsrechnung 260 | # AG = Erhaltene Anzahlung (Geldeingang) 261 | # AV = Erhaltene Anzahlung (Verbindlichkeit) 262 | # SR = Schlussrechnung 263 | # SU = Schlussrechnung (Umbuchung) 264 | # SG = Schlussrechnung (Geldeingang) 265 | # SO = Sonstige 266 | 267 | # 97 268 | field 'USt-Schlüssel (Anzahlungen)', :integer, limit: 2 269 | # USt-Schlüssel der späteren Schlussrechnung 270 | 271 | # 98 272 | field 'EU-Mitgliedstaat (Anzahlungen)', :string, limit: 2 273 | # EU-Mitgliedstaat der späteren Schlussrechnung 274 | # (siehe Dok.-Nr. 1080169) 275 | 276 | # 99 277 | field 'Sachverhalt L+L (Anzahlungen)', :integer, limit: 3 278 | # L+L-Sachverhalt der späteren Schlussrechnung 279 | # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG 280 | # Achtung: Der Wert 0 ist unzulässig. 281 | 282 | # 100 283 | field 'EU-Steuersatz (Anzahlungen)', :decimal, precision: 4, scale: 2 284 | # EU-Steuersatz der späteren Schlussrechnung 285 | # Nur für entsprechende EU-Buchungen: Der im EU-Bestimmungsland gültige Steuersatz. 286 | # Beispiel: 12,12 287 | 288 | # 101 289 | field 'Erlöskonto (Anzahlungen)', :integer, limit: 9 290 | # Erlöskonto der späteren Schlussrechnung 291 | 292 | # 102 293 | field 'Herkunft-Kz', :string, limit: 2 294 | # Wird beim Import durch SV (Stapelverarbeitung) ersetzt. 295 | 296 | # 103 297 | field 'Leerfeld', :string, limit: 36 298 | # Wird von DATEV verwendet. 299 | 300 | # 104 301 | field 'KOST-Datum', :date, format: '%d%m%Y' 302 | # Format TTMMJJJJ 303 | 304 | # 105 305 | field 'SEPA-Mandatsreferenz', :string, limit: 35 306 | # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats (z. B. Rechnungs- oder Kundennummer). 307 | 308 | # 106 309 | field 'Skontosperre', :boolean 310 | # 0 = keine Skontosperre 311 | # 1 = Skontosperre 312 | 313 | # 107 314 | field 'Gesellschaftername', :string, limit: 76 315 | 316 | # 108 317 | field 'Beteiligtennummer', :integer, limit: 4 318 | 319 | # 109 320 | field 'Identifikationsnummer', :string, limit: 11 321 | 322 | # 110 323 | field 'Zeichnernummer', :string, limit: 20 324 | 325 | # 111 326 | field 'Postensperre bis', :date, format: '%d%m%Y' 327 | 328 | # 112 329 | field 'Bezeichnung', :string, limit: 30 330 | # SoBil-Sachverhalt 331 | 332 | # 113 333 | field 'Kennzeichen', :integer, limit: 2 334 | # SoBil-Buchung 335 | 336 | # 114 337 | field 'Festschreibung', :boolean 338 | # leer = nicht definiert; wird ab Jahreswechselversion 2016/2017 automatisch festgeschrieben 339 | # 0 = keine Festschreibung 340 | # 1 = Festschreibung 341 | # Hat ein Buchungssatz in diesem Feld den Inhalt 1, so wird der gesamte Stapel nach dem Import festgeschrieben. 342 | # Ab Jahreswechselversion 2016/2017 gilt das auch bei Inhalt = leer. 343 | 344 | # 115 345 | field 'Leistungsdatum', :date, format: '%d%m%Y' 346 | 347 | # 116 348 | field 'Datum Zuord.', :date, format: '%d%m%Y' 349 | # Steuerperiode 350 | 351 | # Neue Felder ab Programmversion 7.1 352 | 353 | # 117 354 | field 'Fälligkeit', :date, format: '%d%m%Y' 355 | # OPOS Informationen 356 | 357 | # 118 358 | field 'Generalumkehr', :string, limit: 1 359 | # 1 = Generalumkehr 360 | # 0 = keine Generalumkehr 361 | 362 | # 119 363 | field 'Steuersatz', :decimal, precision: 4, scale: 2 364 | 365 | # 120 366 | field 'Land', :string, limit: 2 367 | # Beispiel: DE für Deutschland 368 | 369 | # 121 370 | field 'Abrechnungsreferent', :string, limit: 50 371 | # Die Abrechnungsreferenz stellt eine Klammer über alle Transaktionen 372 | # des Zahlungsdienstleisters und die dazu gehörige Auszahlung dar. 373 | # Sie wird über den Zahlungsdatenservice bereitgestellt und bei der 374 | # Erzeugung von Buchungsvorschläge berücksichtigt. 375 | 376 | # 122 377 | field 'BVV-Position', :integer, limit: 1 378 | # Betriebsvermögensvergleich 379 | # 1 Kapitalanpassung 380 | # 2 Entnahme / Ausschüttung lfd. WJ 381 | # 3 Einlage / Kapitalzuführung lfd. WJ 382 | # 4 Übertragung § 6b Rücklage 383 | # 5 Umbuchung (keine Zuordnung) 384 | 385 | # 123 386 | field 'EU-Mitgliedstaat u. UStID (Ursprung)', :string, limit: 15 387 | # Die USt-IdNr. besteht aus 388 | # - 2-stelligen Länderkürzel (siehe Dok.-Nr. 1080169) 389 | # Ausnahme Griechenland: Das Länderkürzel lautet EL) 390 | # - 13-stelliger USt-IdNr. 391 | # - Beispiel: DE133546770. Die USt-IdNr kann auch Buchstaben haben, z.B.: bei Österreich 392 | # Detaillierte Informationen zur Erfassung von EU-Informationen im Buchungssatz: Dok.-Nr: 9211462 393 | 394 | # 124 395 | field 'EU-Steuersatz (Ursprung)', :decimal, precision: 4, scale: 2 396 | # Nur für entsprechende EU-Buchungen: 397 | # Der im EU-Ursprungsland gültige Steuersatz. 398 | # Beispiel: 12,12 399 | 400 | # 125 401 | field 'Abw. Skontokonto', :integer, limit: 8 402 | # Zulässig sind hier, bei Zahlungsbuchungen mit Skontoabzug, Konten mit dem Kontenzweck „sonstige betriebliche Aufwendungen“. 403 | # Eine Eingabe in diesem Feld bedeutet, dass der Skontobetrag auf dieses Aufwandskonto gebucht wird. 404 | # Wenn in der Importdatei keine Angaben zum Skontokonto enthalten sind, wird der Skontobetrag auf das entsprechende Skontosammelkonto gebucht. 405 | 406 | 407 | end 408 | end 409 | -------------------------------------------------------------------------------- /lib/datev/base/contact.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class Contact < Base 3 | def self.bank_account(number) 4 | field "Bankleitzahl #{number}", :string, limit: 8 5 | 6 | field "Bankbezeichnung #{number}", :string, limit: 30 7 | 8 | field "Bankkonto-Nummer #{number}", :string, limit: 10 9 | 10 | field "Länderkennzeichen #{number}", :string, limit: 2 11 | # ISO-Code beachten (siehe Dok.-Nr. 1080169) 12 | 13 | field "IBAN #{number}", :string, limit: 34 14 | 15 | field "Leerfeld #{number}", :string, limit: 1 16 | 17 | field "SWIFT-Code #{number}", :string, limit: 11 18 | # Beachten Sie, dass für Zahlung und Lastschriften bis zur Installation der Programm-DVD DATEV pro 8.3 (Januar 2015) BLZ und/oder BIC noch erforderlich sind. 19 | 20 | field "Abw. Kontoinhaber #{number}", :string, limit: 70 21 | 22 | field "Kennz. Haupt-Bankverb. #{number}", :boolean 23 | # Kennzeichnung als Haupt-Bankverbindung 24 | # 1 = Ja 25 | # 0 = Nein 26 | # Nur eine Bankverbindung eines Debitoren oder Kreditoren kann als Haupt-Bankverbindung gekennzeichnet werden. 27 | 28 | field "Bankverb. #{number} Gültig von", :date, format: '%d%m%Y' 29 | field "Bankverb. #{number} Gültig bis", :date, format: '%d%m%Y' 30 | end 31 | 32 | # http://www.datev.de/dnlexom/client/app/index.html#/document/1036228/D103622800012 33 | 34 | # 1 35 | field 'Konto', :integer, limit: 9, required: true 36 | # Personen-Kontonummer (abhängig von der Information im Header) 37 | # Sachkontennummernlänge + 1 = Personenkontenlänge 38 | 39 | # 2 40 | field 'Name (Adressatentyp Unternehmen)', :string, limit: 50 41 | # Beim Import werden die Felder in der Datenbank gefüllt, auch wenn sie nicht dem Adressatentyp aus Feld 7 entsprechen. Das kann zu ungewollten Effekten im Programm führen. Bitte übergeben Sie nur die zum Adressatentyp passenden Felder. 42 | 43 | # 3 44 | field 'Unternehmensgegenstand', :string, limit: 50 45 | 46 | # 4 47 | field 'Name (Adressatentyp natürl. Person)', :string, limit: 30 48 | 49 | # 5 50 | field 'Vorname (Adressatentyp natürl. Person)', :string, limit: 30 51 | 52 | # 6 53 | field 'Name (Adressatentyp keine Angabe)', :string, limit: 50 54 | 55 | # 7 56 | field 'Adressatentyp', :string, limit: 1 57 | # 0 = keine Angabe 58 | # 1 = natürliche Person 59 | # 2 = Unternehmen 60 | # Standardwert = Unternehmen 61 | 62 | # 8 63 | field 'Kurzbezeichnung', :string, limit: 15 64 | 65 | # 9 66 | field 'EU-Land', :string, limit: 2 67 | # Die USt-IdNr. besteht aus 68 | # 2-stelligen Länderkürzel 69 | # (siehe Dok.-Nr. 1080169; Ausnahme Griechenland: Das Länderkürzel lautet EL) 70 | # 13-stelliger USt-IdNr. 71 | # Beachten Sie bitte, dass kein Leerzeichen zwischen diesen beiden Eingabewerten sein darf. 72 | 73 | # 10 74 | field 'EU-USt-IdNr.', :string, limit: 13 75 | 76 | # 11 77 | field 'Anrede', :string, limit: 30 78 | 79 | # 12 80 | field 'Titel/Akad. Grad', :string, limit: 25 81 | # Nur bei Adressatentyp "natürliche Person" relevant. 82 | # Wird der Titel/Akad.Grad bei einem Adressatentyp "Unternehmen" übergeben, wird der Wert in den Datenbestand übernommen, ist aber an der Oberfläche nicht sichtbar. 83 | 84 | # 13 85 | field 'Adelstitel', :string, limit: 15 86 | # Nur bei Adressatentyp "natürliche Person" relevant. 87 | # Wird der Adelstitel bei einem Adressatentyp "Unternehmen" übergeben, wird der Wert in den Datenbestand übernommen, ist aber an der Oberfläche nicht sichtbar. 88 | 89 | # 14 90 | field 'Namensvorsatz', :string, limit: 14 91 | # Nur bei Adressatentyp "natürliche Person" relevant. 92 | # Wird der Namensvorsatz bei einem Adressatentyp "Unternehmen" übergeben, wird der Wert in den Datenbestand übernommen, ist aber an der Oberfläche nicht sichtbar. 93 | 94 | # 15 95 | field 'Adressart', :string, limit: 3 96 | # STR = Straße 97 | # PF = Postfach 98 | # GK = Großkunde 99 | # Wird die Adressart nicht übergeben, wird sie automatisch in Abhängigkeit zu den übergebenen Feldern (Straße oder Postfach) gesetzt. 100 | 101 | # 16 102 | field 'Straße', :string, limit: 36 103 | # Wird sowohl eine Straße als auch ein Postfach übergeben, werden beide Werte in den Datenbestand übernommen; auf der Visitenkarte in den Debitoren-/Kreditoren-Stammdaten wird die Postfachadresse angezeigt. 104 | 105 | #17 106 | field 'Postfach', :string, limit: 10 107 | 108 | # 18 109 | field 'Postleitzahl', :string, limit: 10 110 | 111 | # 19 112 | field 'Ort', :string, limit: 30 113 | 114 | # 20 115 | field 'Land', :string, limit: 2 116 | # ISO-Code beachten! (Dok.-Nr. 1080169) 117 | 118 | # 21 119 | field 'Versandzusatz', :string, limit: 50 120 | 121 | # 22 122 | field 'Adresszusatz', :string, limit: 36 123 | # Beispiel: z. Hd. Herrn Mustermann 124 | 125 | # 23 126 | field 'Abweichende Anrede', :string, limit: 30 127 | # Es kann ein beliebiger individueller Text verwendet werden. 128 | 129 | # 24 130 | field 'Abw. Zustellbezeichnung 1', :string, limit: 50 131 | 132 | # 25 133 | field 'Abw. Zustellbezeichnung 2', :string, limit: 36 134 | 135 | # 26 136 | field 'Kennz. Korrespondenzadresse', :boolean 137 | # 1= Kennzeichnung Korrespondenzadresse 138 | 139 | # 27 140 | field 'Adresse Gültig von', :date, format: '%d%m%Y' 141 | 142 | # 28 143 | field 'Adresse Gültig bis', :date, format: '%d%m%Y' 144 | 145 | # 29 146 | field 'Telefon', :string, limit: 60 147 | # Standard-Telefonnummer 148 | 149 | # 30 150 | field 'Bemerkung (Telefon)', :string, limit: 40 151 | 152 | # 31 153 | field 'Telefon Geschäftsleitung', :string, limit: 60 154 | # Geschäftsleitungs-Telefonnummer 155 | 156 | # 32 157 | field 'Bemerkung (Telefon GL)', :string, limit: 40 158 | 159 | # 33 160 | field 'E-Mail', :string, limit: 60 161 | 162 | # 34 163 | field 'Bemerkung (E-Mail)', :string, limit: 40 164 | 165 | # 35 166 | field 'Internet', :string, limit: 60 167 | 168 | # 36 169 | field 'Bemerkung (Internet)', :string, limit: 40 170 | 171 | # 37 172 | field 'Fax', :string, limit: 60 173 | 174 | # 38 175 | field 'Bemerkung (Fax)', :string, limit: 40 176 | 177 | # 39 178 | field 'Sonstige', :string, limit: 60 179 | 180 | # 40 181 | field 'Bemerkung (Sonstige)', :string, limit: 40 182 | 183 | # 41 bis 95 184 | (1..5).each do |number| 185 | self.bank_account(number) 186 | end 187 | 188 | # 96 189 | field 'Leerfeld 11', :integer, limit: 3 190 | 191 | # 97 192 | field 'Briefanrede', :string, limit: 100 193 | 194 | # 98 195 | field 'Grußformel', :string, limit: 50 196 | 197 | # 99 198 | field 'Kundennummer', :string, limit: 15 199 | # Kann nicht geändert werden, wenn zentralisierte Geschäftspartner verwendet werden. 200 | 201 | # 100 202 | field 'Steuernummer', :string, limit: 20 203 | 204 | # 101 205 | field 'Sprache', :integer, limit: 2 206 | # 1 = Deutsch 207 | # 4 = Französisch 208 | # 5 = Englisch 209 | # 10 = Spanisch 210 | # 19 = Italienisch 211 | 212 | # 102 213 | field 'Ansprechpartner', :string, limit: 40 214 | 215 | # 103 216 | field 'Vertreter', :string, limit: 40 217 | 218 | # 104 219 | field 'Sachbearbeiter', :string, limit: 40 220 | 221 | # 105 222 | field 'Diverse-Konto', :boolean 223 | # 0 = Nein 224 | # 1 = Ja 225 | 226 | # 106 227 | field 'Ausgabeziel', :integer, limit: 1 228 | # 1 = Druck  229 | # 2 = Telefax 230 | # 3 = E-Mail 231 | 232 | # 107 233 | field 'Währungssteuerung', :integer, limit: 1 234 | # 0 = Zahlungen in Eingabewährung 235 | # 2 = Ausgabe in EUR 236 | 237 | # 108 238 | field 'Kreditlimit (Debitor)', :integer, limit: 10 239 | # Nur für Debitoren gültig 240 | # Beispiel: 1.123.123.123 241 | 242 | # 109 243 | field 'Zahlungsbedingung', :integer, limit: 3 244 | # Eine gespeicherte Zahlungsbedingung kann hier einem Geschäftspartner zugeordnet werden. 245 | 246 | # 110 247 | field 'Fälligkeit in Tagen (Debitor)', :integer, limit: 3 248 | # Nur für Debitoren gültig 249 | 250 | # 111 251 | field 'Skonto in Prozent (Debitor)', :decimal, precision: 4, scale: 2 252 | # Nur für Debitoren gültig 253 | # Beispiel: 12,12 254 | 255 | # 112 bis 120 256 | (1..5).each do |number| 257 | if 3 == number 258 | field "Kreditoren-Ziel #{number} Brutto (Tage)", :integer, limit: 3 259 | else 260 | field "Kreditoren-Ziel #{number} (Tage)", :integer, limit: 2 261 | # Nur für Kreditoren gültig 262 | 263 | field "Kreditoren-Skonto #{number} (%)", :decimal, precision: 4, scale: 2 264 | # Nur für Kreditoren gültig 265 | # Beispiel: 12,12 266 | end 267 | end 268 | 269 | # 121 270 | field 'Mahnung', :integer, limit: 1 271 | # 0 = Keine Angaben 272 | # 1 = 1. Mahnung 273 | # 2 = 2. Mahnung 274 | # 3 = 1. + 2. Mahnung 275 | # 4 = 3. Mahnung 276 | # 5 = (nicht vergeben) 277 | # 6 = 2. + 3. Mahnung 278 | # 7 = 1., 2. + 3. Mahnung 279 | # 9 = keine Mahnung 280 | 281 | # 122 282 | field 'Kontoauszug', :integer, limit: 1 283 | # 1 = Kontoauszug für alle Posten 284 | # 2 = Auszug nur dann, wenn ein Posten mahnfähig ist 285 | # 3 = Auszug für alle mahnfälligen Posten 286 | # 9 = kein Kontoauszug 287 | 288 | field 'Mahntext', :integer, limit: 1 289 | # Leer = keinen Mahntext ausgewählt 290 | # 1 = Textgruppe 1 291 | # ... 292 | # 9 = Textgruppe 9 293 | 294 | # 124 295 | field 'Mahntext 2', :integer, limit: 1 296 | # Leer = keinen Mahntext ausgewählt 297 | # 1 = Textgruppe 1 298 | # ... 299 | # 9 = Textgruppe 9 300 | 301 | # 125 302 | field 'Mahntext 3', :integer, limit: 1 303 | # Leer = keinen Mahntext ausgewählt 304 | # 1 = Textgruppe 1 305 | # ... 306 | # 9 = Textgruppe 9 307 | 308 | # 126 309 | field 'Kontoauszugstext', :integer, limit: 1 310 | # Leer = kein Kontoauszugstext ausgewählt 311 | # 1 = Kontoauszugstext 1 312 | # ... 313 | # 8 = Kontoauszugstext 8 314 | # 9 = Kein Kontoauszugstext 315 | 316 | # 127 317 | field 'Mahnlimit Betrag', :decimal, precision: 7, scale: 2 318 | # Beispiel: 12.123,12 319 | 320 | # 128 321 | field 'Mahnlimit %', :decimal, precision: 4, scale: 2 322 | # Beispiel: 12,12 323 | 324 | # 129 325 | field 'Zinsberechnung', :integer, limit: 1 326 | # 0 = MPD-Schlüsselung gilt 327 | # 1 = Fester Zinssatz 328 | # 2 = Zinssatz über Staffel 329 | # 9 = Keine Berechnung für diesen Debitor 330 | 331 | # 130 - 132 332 | (1..3).each do |number| 333 | field "Mahnzinssatz #{number}", :decimal, precision: 4, scale: 2 334 | # Beispiel: 12,12 335 | end 336 | 337 | # 133 338 | field 'Lastschrift', :string, limit: 1 339 | # Leer bzw. 0 = keine Angaben, es gilt die MPD-Schlüsselung 340 | # 1 = Einzellastschrift mit einer Rechnung 341 | # 2 = Einzellastschrift mit mehreren Rechnungen 342 | # 3 = Sammellastschrift mit einer Rechnung 343 | # 4 = Sammellastschrift mit mehreren Rechnungen 344 | # 5 = Datenträgeraustausch mit einer Rechnung 345 | # 6 = Datenträgeraustausch mit mehreren Rechnungen 346 | # 7 = SEPA-Lastschrift mit einer Rechnung 347 | # 8 = SEPA-Lastschrift mit mehreren Rechnungen 348 | # 9 = kein Lastschriftverfahren bei diesem Debitor 349 | 350 | # 134 351 | field 'Verfahren', :string, limit: 1 352 | # 0 = Einzugsermächtigung 353 | # 1 = Abbuchungsverfahren 354 | 355 | # 135 356 | field 'Mandantenbank', :integer, limit: 4 357 | # Zuordnung der gespeicherten Mandantenbank, die für das Lastschriftverfahren verwendet werden soll. 358 | 359 | # 136 360 | field 'Zahlungsträger', :string, limit: 1 361 | # Leer bzw. 0 = keine Angaben, es gilt die MPD-Schlüsselung 362 | # 1 = Einzelüberweisung mit einer Rechnung 363 | # 2 = Einzelüberweisung mit mehreren Rechnungen 364 | # 3 = Sammelüberweisung mit einer Rechnung 365 | # 4 = Sammelüberweisung mit mehreren Rechnungen 366 | # 5 = Einzelscheck 367 | # 6 = Sammelscheck 368 | # 7 = SEPA-Überweisung mit einer Rechnung 369 | # 8 = SEPA-Überweisung mit mehreren Rechnungen 370 | # 9 = keine Überweisungen, Schecks 371 | 372 | # 137 bis 151 373 | (1..15).each do |number| 374 | field "Indiv. Feld #{number}", :string, limit: 40 375 | 376 | # 11 bis 15 wird derzeit nicht übernommen 377 | end 378 | 379 | # 152 380 | field 'Abweichende Anrede (Rechnungsadresse)', :string, limit: 30 381 | # Es kann ein beliebiger individueller Text verwendet werden. 382 | 383 | # 153 384 | field 'Adressart (Rechnungsadresse)', :string, limit: 3 385 | # STR = Straße  386 | # PF = Postfach 387 | # GK = Großkunde 388 | # Wird die Adressart nicht übergeben, wird sie automatisch in Abhängigkeit zu den übergebenen Feldern (Straße oder Postfach) gesetzt. 389 | 390 | # 154 391 | field 'Straße (Rechnungsadresse)', :string, limit: 36 392 | # Wird sowohl eine Straße als auch ein Postfach übergeben, werden beide Werte in den Datenbestand übernommen; auf der Visitenkarte in den Debitoren-/Kreditoren-Stammdaten wird die Postfachadresse angezeigt. 393 | 394 | # 155 395 | field 'Postfach (Rechnungsadresse)', :string, limit: 10 396 | 397 | # 156 398 | field 'Postleitzahl (Rechnungsadresse)', :string, limit: 10 399 | 400 | # 157 401 | field 'Ort (Rechnungsadresse)', :string, limit: 30 402 | 403 | # 158 404 | field 'Land (Rechnungsadresse)', :string, limit: 2 405 | # ISO-Code beachten (siehe Dok.-Nr. 1080169) 406 | 407 | # 159 408 | field 'Versandzusatz (Rechnungsadresse)', :string, limit: 50 409 | 410 | # 160 411 | field 'Adresszusatz (Rechnungsadresse)', :string, limit: 36 412 | # Beispiel: z. Hd. Herrn Mustermann 413 | 414 | # 161 415 | field 'Abw. Zustellbezeichnung 1 (Rechnungsadresse)', :string, limit: 50 416 | 417 | # 162 418 | field 'Abw. Zustellbezeichnung 2 (Rechnungsadresse)', :string, limit: 36 419 | 420 | # 163 421 | field 'Adresse Gültig von (Rechnungsadresse)', :date, format: '%d%m%Y' 422 | 423 | # 164 424 | field 'Adresse Gültig bis (Rechnungsadresse)', :date, format: '%d%m%Y' 425 | 426 | # 165 bis 219 427 | (6..10).each do |number| 428 | bank_account(number) 429 | end 430 | 431 | # 220 432 | field 'Nummer Fremdsystem', :string, limit: 15 433 | # Achtung: Wird bei Verwendung zentralisierter Geschäftspartner von DATEV überschrieben. 434 | 435 | # 221 436 | field 'Insolvent', :boolean 437 | # 0 = Nein 438 | # 1 = Ja 439 | 440 | # 222 bis 231 441 | (1..10).each do |number| 442 | field "SEPA-Mandatsreferenz #{number}", :string, limit: 35 443 | # Sie können im Feld Mandatsreferenz dem Geschäftspartner je Bank eine Mandatsreferenz eintragen. Für eine korrekte Verwendung muss in der SEPA-Mandatsverwaltung die Mandatsreferenz für den Lastschriftteilnehmer vorhanden sein. 444 | end 445 | 446 | # 232 447 | field 'Verknüpftes OPOS-Konto', :integer, limit: 9 448 | # Sie können für den Geschäftspartner das korrespondierende Konto (im Kreditorenbereich) erfassen, wenn es sich bei dem Geschäftspartner sowohl um einen Kunden als auch um einen Lieferanten handelt. 449 | 450 | # --- Erweiterungen zur Jahreswechselversion 2015/2016 451 | 452 | # 233 453 | field 'Mahnsperre bis', :date, format: '%d%m%Y' 454 | 455 | # 234 456 | field 'Lastschriftsperre bis', :date, format: '%d%m%Y' 457 | 458 | # 235 459 | field 'Zahlungssperre bis', :date, format: '%d%m%Y' 460 | 461 | # 236 462 | field 'Gebührenberechnung', :integer, limit: 1 463 | # 0 = MPD-Schlüsselung gilt 464 | # 1 = Mahngebühr berechnen 465 | # 9 = Keine Berechnung für diesen Debitor 466 | 467 | # 237 bis 239 468 | (1..3).each do |number| 469 | field "Mahngebühr #{number}", :decimal, precision: 4, scale: 2 470 | # Beispiel: 12,12 471 | end 472 | 473 | # 240 474 | field 'Pauschalberechnung', :integer, limit: 1 475 | # 0 = MPD-Schlüsselung gilt 476 | # 1 = Verzugspauschale berechnen 477 | # 9 = Keine Berechnung für diesen Debitor 478 | 479 | # 241 bis 243 480 | (1..3).each do |number| 481 | field "Verzugspauschale #{number}", :decimal, precision: 5, scale: 2 482 | # Beispiel: 12,12 483 | end 484 | 485 | # 244 486 | field 'Alternativer Suchname', :string, limit: 50 487 | 488 | # 245 489 | field 'Status', :integer, limit: 1 490 | 491 | # 246 492 | field 'Anschrift manuell geändert (Korrespondenzadresse)', :integer, limit: 1 493 | 494 | # 247 495 | field 'Anschrift individuell (Korrespondenzadresse)', :string, limit: 306 496 | 497 | # 248 498 | field 'Anschrift manuell geändert (Rechnungsadresse)', :integer, limit: 1 499 | 500 | # 249 501 | field 'Anschrift individuell (Rechnungsadresse)', :string, limit: 306 502 | 503 | # 250 504 | field 'Fristberechnung bei Debitor', :integer, limit: 1 505 | # 0 = nein 506 | # 1 = ja 507 | 508 | # 251 bis 253 509 | (1..3).each do |number| 510 | field "Mahnfrist #{number}", :integer, limit: 3 511 | # Mahnfristen in Tagen 512 | end 513 | 514 | # 254 515 | field 'Letzte Frist', :integer, limit: 3 516 | # Mahnfristen in Tagen 517 | end 518 | end 519 | -------------------------------------------------------------------------------- /lib/datev/base/header.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class Header < Base 3 | # 1 4 | field 'DATEV-Format-KZ', :string, limit: 4 5 | # vom Datev angegeben 6 | # EXTF = für Datei-Formate, die von externen Programmen erstellt wurden 7 | 8 | # 2 9 | field 'Versionsnummer', :integer, limit: 3 10 | # entspricht der zugrundeliegenden Versionsnummer des Schnittstellen-Entwicklungsleitfadens 11 | 12 | # 3 13 | field 'Datenkategorie', :integer, limit: 2 14 | # vom Datev angegeben 15 | 16 | # 4 17 | field 'Formatname', :string 18 | # vom Datev angegeben 19 | 20 | # 5 21 | field 'Formatversion', :integer, limit: 3 22 | # vom Datev angegeben 23 | 24 | # 6 25 | field 'Erzeugt am', :date, format: '%Y%m%d%H%M%S%L' 26 | 27 | # 7 28 | field 'Importiert', :date 29 | # Darf nicht gefüllt werden, durch Import gesetzt. 30 | 31 | # 8 32 | field 'Herkunft', :string, limit: 2 33 | # Herkunfts-Kennzeichen 34 | # Beim Import wird das Herkunfts-Kennzeichen durch „SV“ (= Stapelverarbeitung) ersetzt. 35 | 36 | # 9 37 | field 'Exportiert von', :string, limit: 25 38 | # Benutzername 39 | 40 | # 10 41 | field 'Importiert von', :string, limit: 10 42 | # Darf nicht gefüllt werden, durch Import gesetzt. 43 | 44 | # 11 45 | field 'Berater', :integer, limit: 7, minimum: 1001 46 | # Beraternummer 47 | 48 | # 12 49 | field 'Mandant', :integer, limit: 5 50 | # Mandantennummer 51 | 52 | # 13 53 | field 'WJ-Beginn', :date, format: '%Y%m%d' 54 | # Wirtschaftsjahresbeginn 55 | 56 | # 14 57 | field 'Sachkontenlänge', :integer, limit: 1 58 | # Kleinste Sachkontenlänge = 4, Grösste Sachkontenlänge = 8 59 | 60 | # 15 61 | field 'Datum vom', :date, format: '%Y%m%d' 62 | 63 | # 16 64 | field 'Datum bis', :date, format: '%Y%m%d' 65 | 66 | # 17 67 | field 'Bezeichnung', :string, limit: 30 68 | # Bezeichnung des Buchungsstapels 69 | 70 | # 18 71 | field 'Diktatkürzel', :string, limit: 2 72 | # Diktatkürzel Beispiel MM = Max Mustermann 73 | 74 | # 19 75 | field 'Buchungstyp', :integer, limit: 1 76 | # 1 = Finanzbuchhaltung, 2 = Jahresabschluss 77 | 78 | # 20 79 | field 'Rechnungslegungszweck' , :integer, limit: 2 80 | # 0 oder leer = Rechnungslegungszweckunabhängig 81 | 82 | # 21 83 | field 'Festschreibung', :boolean 84 | # leer = nicht definiert; wird ab Jahreswechselversion 2016/2017 automatisch festgeschrieben 85 | # 0 = keine Festschreibung 86 | # 1 = Festschreibung 87 | 88 | # 22 89 | field 'WKZ', :string, limit: 3 90 | # Währungskennzeichen 91 | 92 | # 23 93 | field 'reserviert', :string 94 | 95 | # 24 96 | field 'Derivatskennzeichen', :string 97 | 98 | # 25 99 | field 'reserviert 2', :string 100 | 101 | # 26 102 | field 'reserviert 3', :string 103 | 104 | # 27 105 | field 'SKR', :string 106 | 107 | # 28 108 | field 'Branchenlösung-Id', :integer 109 | 110 | # 29 111 | field 'reserviert 4', :integer 112 | 113 | # 30 114 | field 'reserviert 5', :string 115 | 116 | # 31 117 | field 'Anwendungsinformation', :string, limit: 16 118 | # Verarbeitungskennzeichen der abgebenden Anwendung => Bsp. '9/2016' 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/datev/base/header/account_header.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class AccountHeader < Header 3 | self.default_attributes = proc do 4 | { 5 | 'DATEV-Format-KZ' => 'EXTF', 6 | 'Versionsnummer' => 700, 7 | 'Datenkategorie' => 20, 8 | 'Formatname' => 'Kontenbeschriftungen', 9 | 'Formatversion' => 2, 10 | 'Erzeugt am' => Time.now.utc, 11 | 'Sachkontenlänge' => 4, 12 | 'Bezeichnung' => 'Kontenbeschriftungen' 13 | } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/datev/base/header/booking_header.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class BookingHeader < Header 3 | self.default_attributes = proc do 4 | { 5 | 'DATEV-Format-KZ' => 'EXTF', 6 | 'Versionsnummer' => 700, 7 | 'Datenkategorie' => 21, 8 | 'Formatname' => 'Buchungsstapel', 9 | 'Formatversion' => 13, 10 | 'Erzeugt am' => Time.now.utc, 11 | 'Sachkontenlänge' => 4, 12 | 'Bezeichnung' => 'Buchungen', 13 | 'Buchungstyp' => 1, 14 | 'WKZ' => 'EUR' 15 | } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/datev/base/header/contact_header.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class ContactHeader < Header 3 | self.default_attributes = proc do 4 | { 5 | 'DATEV-Format-KZ' => 'EXTF', 6 | 'Versionsnummer' => 700, 7 | 'Datenkategorie' => 16, 8 | 'Formatname' => 'Debitoren/Kreditoren', 9 | 'Formatversion' => 5, 10 | 'Erzeugt am' => Time.now.utc, 11 | 'Sachkontenlänge' => 4 12 | } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/datev/export.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class Export 3 | CSV_OPTIONS = { col_sep: ';', encoding: 'windows-1252' } 4 | 5 | class << self 6 | attr_accessor :header_class, :row_class 7 | end 8 | 9 | def initialize(header_attributes) 10 | @header = self.class.header_class.new(header_attributes) 11 | @rows = [] 12 | end 13 | 14 | def <<(attributes) 15 | @rows << self.class.row_class.new(attributes) 16 | end 17 | 18 | def to_s 19 | string = to_csv_line(@header.output) << 20 | to_csv_line(self.class.row_class.fields.map(&:name)) 21 | 22 | @rows.each do |row| 23 | string << to_csv_line(row.output(@header)) 24 | end 25 | 26 | string.encode(CSV_OPTIONS[:encoding], invalid: :replace, undef: :replace, replace: ' ') 27 | end 28 | 29 | def to_file(filename) 30 | File.open(filename, 'wb') do |file| 31 | file.write(to_s) 32 | end 33 | end 34 | 35 | private 36 | 37 | def to_csv_line(data) 38 | data.join(CSV_OPTIONS[:col_sep]) + "\r\n" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/datev/export/account_export.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class AccountExport < Export 3 | self.header_class = AccountHeader 4 | self.row_class = Account 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/datev/export/booking_export.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class BookingExport < Export 3 | self.header_class = BookingHeader 4 | self.row_class = Booking 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/datev/export/contact_export.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class ContactExport < Export 3 | self.header_class = ContactHeader 4 | self.row_class = Contact 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/datev/field.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class Field 3 | attr_accessor :name, :options, :block 4 | 5 | def initialize(name, options={}, &block) 6 | unless name.is_a?(String) 7 | raise ArgumentError.new("Argument 'name' has to be a String") 8 | end 9 | 10 | unless options.is_a?(Hash) 11 | raise ArgumentError.new("Argument 'options' has to be a Hash") 12 | end 13 | 14 | self.name = name 15 | self.options = options 16 | 17 | if block_given? 18 | self.instance_eval(&block) 19 | end 20 | end 21 | 22 | def required? 23 | options[:required] == true 24 | end 25 | 26 | def validate!(value) 27 | if value.nil? 28 | raise ArgumentError.new("Value for field '#{name}' is required") if required? 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/datev/field/boolean_field.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class BooleanField < Field 3 | def validate!(value) 4 | super 5 | 6 | unless value.nil? 7 | raise ArgumentError.new("Value given for field '#{name}' is not a Boolean") unless [true, false].include?(value) 8 | end 9 | end 10 | 11 | def output(value, _context=nil) 12 | value ? 1 : 0 unless value.nil? 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/datev/field/date_field.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class DateField < Field 3 | def format 4 | options[:format] 5 | end 6 | 7 | def validate!(value) 8 | super 9 | 10 | if value 11 | raise ArgumentError.new("Value given for field '#{name}' is not a Date or Time") unless value.is_a?(Time) || value.is_a?(Date) 12 | end 13 | end 14 | 15 | def output(value, _context=nil) 16 | value.strftime(format) if value 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/datev/field/decimal_field.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class DecimalField < Field 3 | def precision 4 | options[:precision] 5 | end 6 | 7 | def scale 8 | options[:scale] 9 | end 10 | 11 | def validate!(value) 12 | super 13 | 14 | if value 15 | raise ArgumentError.new("Value given for field '#{name}' is not a Decimal") unless value.is_a?(Numeric) 16 | raise ArgumentError.new("Value '#{value}' for field '#{name}' is too long") if precision && value.to_s.length > precision+1 17 | end 18 | end 19 | 20 | def output(value, _context=nil) 21 | ("%.#{scale}f" % value).sub('.',',') if value 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/datev/field/integer_field.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class IntegerField < Field 3 | def limit 4 | options[:limit] 5 | end 6 | 7 | def maximum 8 | options[:maximum] 9 | end 10 | 11 | def minimum 12 | options[:minimum] 13 | end 14 | 15 | def validate!(value) 16 | super 17 | 18 | if value 19 | raise ArgumentError.new("Value given for field '#{name}' is not an Integer") unless value.is_a?(Integer) 20 | raise ArgumentError.new("Value '#{value}' for field '#{name}' is too long") if limit && value.to_s.length > limit 21 | raise ArgumentError.new("Value '#{value}' for field '#{name}' is too large") if maximum && value > maximum 22 | raise ArgumentError.new("Value '#{value}' for field '#{name}' is too small") if minimum && value < minimum 23 | end 24 | end 25 | 26 | def output(value, _context=nil) 27 | # Return empty string for nil values (required for DATEV reserved fields) 28 | return '' if value.nil? 29 | 30 | # Return bare integer value without quotes (DATEV CSV format requirement) 31 | value.to_s 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/datev/field/string_field.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | class StringField < Field 3 | def limit 4 | options[:limit] 5 | end 6 | 7 | def regex 8 | options[:regex] 9 | end 10 | 11 | def validate!(value) 12 | super 13 | 14 | if value 15 | raise ArgumentError.new("Value given for field '#{name}' is not a String") unless value.is_a?(String) 16 | raise ArgumentError.new("Value '#{value}' for field '#{name}' is too long") if limit && value.length > limit 17 | raise ArgumentError.new("Value '#{value}' for field '#{name}' does not match regex") if regex && value !~ regex 18 | end 19 | end 20 | 21 | def output(value, _context=nil) 22 | # If value is nil or empty, return empty string (no quotes) 23 | # This is required for DATEV's reserved fields which MUST be completely empty 24 | return '' if value.nil? || (value.respond_to?(:empty?) && value.empty?) 25 | 26 | # Original behavior for non-empty values 27 | value = value.slice(0, limit || 255) if value 28 | 29 | quote(value) 30 | end 31 | 32 | private 33 | 34 | def quote(value) 35 | # Existing quotes have to be doubled 36 | value = value.gsub('"','""') if value 37 | 38 | "\"#{value}\"" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/datev/version.rb: -------------------------------------------------------------------------------- 1 | module Datev 2 | VERSION = "0.12.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/datev/base/account_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::Account do 4 | describe :initialize do 5 | it "should be allowed with all required fields" do 6 | expect { 7 | Datev::Account.new( 8 | 'Konto' => 1000 9 | ) 10 | }.to_not raise_error 11 | end 12 | 13 | it "should not be allowed if fields are missing" do 14 | expect { 15 | Datev::Account.new 'Kontenbeschriftung' => 'Kasse' 16 | }.to raise_error(ArgumentError, "Value for field 'Konto' is required but missing") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/datev/base/booking_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::Booking do 4 | describe :initialize do 5 | it "should be allowed with all required fields" do 6 | expect { 7 | Datev::Booking.new( 8 | 'Umsatz (ohne Soll/Haben-Kz)' => 100.12, 9 | 'Soll/Haben-Kennzeichen' => 'S', 10 | 'Konto' => 1000, 11 | 'Gegenkonto (ohne BU-Schlüssel)' => 1200, 12 | 'Belegdatum' => Date.new(2016,6,20), 13 | 'Buchungstext' => 'ATM' 14 | ) 15 | }.to_not raise_error 16 | end 17 | 18 | it "should not be allowed if fields are missing" do 19 | expect { 20 | Datev::Booking.new 'Umsatz (ohne Soll/Haben-Kz)' => 100.12 21 | }.to raise_error(ArgumentError, "Value for field 'Soll/Haben-Kennzeichen' is required but missing") 22 | end 23 | end 24 | 25 | describe :output, 'account numbers' do 26 | let(:booking) { 27 | Datev::Booking.new( 28 | 'Umsatz (ohne Soll/Haben-Kz)' => 100, 29 | 'Soll/Haben-Kennzeichen' => 'S', 30 | 'Konto' => 400, 31 | 'Gegenkonto (ohne BU-Schlüssel)' => 70000, 32 | 'Belegdatum' => Date.today 33 | ) 34 | } 35 | 36 | it "should pad strings with zeros" do 37 | header = Datev::Header.new 'Sachkontenlänge' => 6 38 | output = booking.output(header) 39 | 40 | expect(output[6]).to eq('000400') 41 | expect(output[7]).to eq('070000') 42 | end 43 | 44 | it "should allow longer strings" do 45 | header = Datev::Header.new 'Sachkontenlänge' => 4 46 | output = booking.output(header) 47 | 48 | expect(output[6]).to eq('0400') 49 | expect(output[7]).to eq('70000') 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/datev/base/contact_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::Contact do 4 | describe :initialize do 5 | it "should be allowed with all required fields" do 6 | expect { 7 | Datev::Contact.new( 8 | 'Konto' => 10000 9 | ) 10 | }.to_not raise_error 11 | end 12 | 13 | it "should not be allowed if fields are missing" do 14 | expect { 15 | Datev::Contact.new 'Name (Adressatentyp Unternehmen)' => 'Meyer GmbH' 16 | }.to raise_error(ArgumentError, "Value for field 'Konto' is required but missing") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/datev/base/header_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::Header do 4 | describe :initialize do 5 | it "should be allowed with defined fields" do 6 | expect { 7 | Datev::Header.new( 8 | 'Berater' => 1001, 9 | 'Mandant' => 456 10 | ) 11 | }.to_not raise_error 12 | end 13 | 14 | it "should not be allowed with unknown field names" do 15 | expect { 16 | Datev::Header.new( 17 | 'Foo' => 100.12 18 | ) 19 | }.to raise_error(ArgumentError, "Field 'Foo' not found") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/datev/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class Person < Datev::Base 4 | field 'name', :string, limit: 30, required: true 5 | field 'favorite number', :integer, limit: 10 6 | field 'weight', :decimal, precision: 4, scale: 1 7 | field 'birthday', :date, format: '%d%m%Y' 8 | field 'alive', :boolean do 9 | def output(value, context) 10 | value ? 'yes' : 'no' 11 | end 12 | end 13 | end 14 | 15 | describe Datev::Base do 16 | let(:person) { 17 | Person.new( 18 | 'name' => 'John', 19 | 'favorite number' => 666, 20 | 'weight' => 83.6, 21 | 'birthday' => Date.new(1975,4,30), 22 | 'alive' => true 23 | ) 24 | } 25 | 26 | describe '.field' do 27 | it "should build definition" do 28 | expect(Person.fields.count).to eq(5) 29 | end 30 | 31 | it "should not allow duplicate field names" do 32 | expect { 33 | Person.field 'name', :string 34 | }.to raise_error(ArgumentError, "Field 'name' already exists") 35 | end 36 | 37 | it "should not allow unknown field type" do 38 | expect { 39 | Person.field 'xy', :foo 40 | }.to raise_error(NameError) 41 | end 42 | end 43 | 44 | describe :initialize do 45 | it "should accept valid attributes" do 46 | expect { 47 | Person.new 'name' => 'John' 48 | }.to_not raise_error 49 | end 50 | 51 | it "should not accept missing required key" do 52 | expect { 53 | Person.new 'alive' => true 54 | }.to raise_error(ArgumentError, "Value for field 'name' is required but missing") 55 | end 56 | 57 | it "should not accept wrong type" do 58 | expect { 59 | Person.new 'name' => 42 60 | }.to raise_error(ArgumentError, "Value given for field 'name' is not a String") 61 | end 62 | 63 | it "should not accept unknown key" do 64 | expect { 65 | Person.new 'locale' => 'de' 66 | }.to raise_error(ArgumentError, "Field 'locale' not found") 67 | end 68 | 69 | it "should not accept something other than Hash" do 70 | expect { 71 | Person.new 42 72 | }.to raise_error(ArgumentError, "Hash required") 73 | end 74 | end 75 | 76 | describe :[] do 77 | it "should return raw value" do 78 | expect(person['name']).to eq 'John' 79 | expect(person['favorite number']).to eq 666 80 | expect(person['weight']).to eq 83.6 81 | expect(person['birthday']).to eq Date.new(1975,4,30) 82 | expect(person['alive']).to eq true 83 | end 84 | 85 | it "should fail for unknown field name" do 86 | expect { 87 | person['foobar'] 88 | }.to raise_error(ArgumentError, "Field 'foobar' not found") 89 | end 90 | end 91 | 92 | describe :output do 93 | it "should return array with formatted values" do 94 | expect(person.output).to eq([ '"John"', '666', '83,6', '30041975', 'yes' ]) 95 | end 96 | 97 | it "should pass context to field's output method" do 98 | expect(Person.fields[0]).to receive(:output).with('John', 42) 99 | person.output(42) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/datev/export/account_export_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::AccountExport do 4 | let(:account1) { 5 | { 6 | 'Konto' => 400, 7 | 'Kontenbeschriftung' => 'Betriebsausstattung', 8 | 'Sprach-ID' => 'de-DE' 9 | } 10 | } 11 | 12 | let(:account2) { 13 | { 14 | 'Konto' => 1000, 15 | 'Kontenbeschriftung' => 'Kasse', 16 | 'Sprach-ID' => 'de-DE' 17 | } 18 | } 19 | 20 | let(:account3) { 21 | { 22 | 'Konto' => 1200, 23 | 'Kontenbeschriftung' => 'Girokonto', 24 | 'Sprach-ID' => 'de-DE' 25 | } 26 | } 27 | 28 | let(:export) do 29 | export = Datev::AccountExport.new( 30 | 'Herkunft' => 'XY', 31 | 'Exportiert von' => 'Chief Accounting Officer', 32 | 'Erzeugt am' => Time.new(2018,2,23,15,25,0, '+02:00'), 33 | 'Berater' => 1001, 34 | 'Mandant' => 456, 35 | 'WJ-Beginn' => Date.new(2018,1,1), 36 | 'Bezeichnung' => 'Beispiel-Konten' 37 | ) 38 | 39 | export << account1 40 | export << account2 41 | export << account3 42 | export 43 | end 44 | 45 | describe :to_s do 46 | subject { export.to_s } 47 | 48 | it 'should export as string' do 49 | expect(subject).to be_a(String) 50 | expect(subject.lines.length).to eq(5) 51 | end 52 | 53 | it "should encode in Windows-1252" do 54 | expect(subject.encoding).to eq(Encoding::WINDOWS_1252) 55 | end 56 | 57 | it "should contain header" do 58 | expect(subject.lines[0]).to include('"EXTF";700') 59 | end 60 | 61 | it "should contain field names" do 62 | expect(subject.lines[1]).to include('Konto;Kontenbeschriftung') 63 | end 64 | 65 | it "should contain accounts" do 66 | expect(subject.lines[2]).to include('400;"Betriebsausstattung"') 67 | expect(subject.lines[3]).to include('1000;"Kasse"') 68 | expect(subject.lines[4]).to include('1200;"Girokonto"') 69 | end 70 | end 71 | 72 | describe :to_file do 73 | it 'should export a valid CSV file' do 74 | Dir.mktmpdir do |dir| 75 | filename = "#{dir}/EXTF_Kontenbeschriftungen.csv" 76 | export.to_file(filename) 77 | 78 | expect { 79 | CSV.read(filename, **Datev::Export::CSV_OPTIONS) 80 | }.to_not raise_error 81 | end 82 | end 83 | 84 | it 'should export a file identically to the given example' do 85 | Dir.mktmpdir do |dir| 86 | filename = "#{dir}/EXTF_Kontenbeschriftungen.csv" 87 | export.to_file(filename) 88 | if ENV['CREATE_EXAMPLES'] 89 | export.to_file('examples/EXTF_Kontenbeschriftungen.csv') 90 | end 91 | 92 | expect(IO.read(filename)).to eq(IO.read('examples/EXTF_Kontenbeschriftungen.csv')) 93 | end 94 | end 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /spec/datev/export/booking_export_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'spec_helper' 3 | 4 | describe Datev::BookingExport do 5 | let(:booking1) { 6 | { 7 | 'Belegdatum' => Date.new(2018,2,21), 8 | 'Buchungstext' => 'Fachbuch: Controlling für Dummies', 9 | 'Umsatz (ohne Soll/Haben-Kz)' => 24.95, 10 | 'Soll/Haben-Kennzeichen' => 'H', 11 | 'Konto' => 1200, 12 | 'Gegenkonto (ohne BU-Schlüssel)' => 4940, 13 | 'BU-Schlüssel' => '8' 14 | } 15 | } 16 | 17 | let(:booking2) { 18 | { 19 | 'Belegdatum' => Date.new(2018,2,22), 20 | 'Buchungstext' => 'Honorar FiBu-Seminar', 21 | 'Umsatz (ohne Soll/Haben-Kz)' => 5950.00, 22 | 'Soll/Haben-Kennzeichen' => 'S', 23 | 'Konto' => 10000, 24 | 'Gegenkonto (ohne BU-Schlüssel)' => 8400, 25 | 'Belegfeld 1' => 'RE201802-135' 26 | } 27 | } 28 | 29 | let(:export) do 30 | export = Datev::BookingExport.new( 31 | 'Herkunft' => 'XY', 32 | 'Exportiert von' => 'Chief Accounting Officer', 33 | 'Erzeugt am' => Time.new(2018,3,6,10,25,0, '+02:00'), 34 | 'Berater' => 1001, 35 | 'Mandant' => 456, 36 | 'WJ-Beginn' => Date.new(2018,1,1), 37 | 'Datum vom' => Date.new(2018,2,1), 38 | 'Datum bis' => Date.new(2018,2,28), 39 | 'Bezeichnung' => 'Beispiel-Buchungen', 40 | 'Festschreibung' => false 41 | ) 42 | 43 | export << booking1 44 | export << booking2 45 | export 46 | end 47 | 48 | describe :initialize do 49 | it "should accept Hash with valid keys" do 50 | expect { 51 | Datev::BookingExport.new( 52 | 'Berater' => 1001, 53 | 'Mandant' => 456 54 | ) 55 | }.to_not raise_error 56 | end 57 | 58 | it "should accept blank Hash" do 59 | expect { 60 | Datev::BookingExport.new({}) 61 | }.to_not raise_error 62 | end 63 | 64 | it "should not accept Hash with invalid keys" do 65 | expect { 66 | Datev::BookingExport.new( 67 | 'foo' => 'bar' 68 | ) 69 | }.to raise_error(ArgumentError, "Field 'foo' not found") 70 | end 71 | 72 | it "should not accept other types" do 73 | expect { 74 | Datev::BookingExport.new(42) 75 | }.to raise_error(ArgumentError, "Hash required") 76 | end 77 | end 78 | 79 | describe :<< do 80 | it "should accept Hash with valid keys" do 81 | expect { 82 | export << { 83 | 'Belegdatum' => Date.today, 84 | 'Umsatz (ohne Soll/Haben-Kz)' => 24.95, 85 | 'Soll/Haben-Kennzeichen' => 'H', 86 | 'Konto' => 1200, 87 | 'Gegenkonto (ohne BU-Schlüssel)' => 4940 88 | } 89 | }.to_not raise_error 90 | end 91 | 92 | it "should not accept hash with missing keys" do 93 | expect { 94 | export << {} 95 | }.to raise_error(ArgumentError, "Value for field 'Umsatz (ohne Soll/Haben-Kz)' is required but missing") 96 | end 97 | 98 | it "should not accept other types" do 99 | expect { 100 | export << 42 101 | }.to raise_error(ArgumentError, "Hash required") 102 | end 103 | end 104 | 105 | describe :to_s do 106 | subject { export.to_s } 107 | 108 | it 'should export as string' do 109 | expect(subject).to be_a(String) 110 | expect(subject.lines.length).to eq(4) 111 | end 112 | 113 | it "should encode in Windows-1252" do 114 | expect(subject.encoding).to eq(Encoding::WINDOWS_1252) 115 | end 116 | 117 | it "should contain header" do 118 | expect(subject.lines[0]).to include('"EXTF";700') 119 | end 120 | 121 | it "should contain field names" do 122 | expect(subject.lines[1]).to include('Umsatz (ohne Soll/Haben-Kz);Soll/Haben-Kennzeichen') 123 | end 124 | 125 | it "should contain bookings" do 126 | expect(subject.lines[2]).to include('4940') 127 | expect(subject.lines[2].encode(Encoding::UTF_8)).to include('Controlling für Dummies') 128 | 129 | expect(subject.lines[3]).to include('8400') 130 | expect(subject.lines[3].encode(Encoding::UTF_8)).to include('Honorar FiBu-Seminar') 131 | end 132 | end 133 | 134 | describe :to_file do 135 | it 'should export a valid CSV file' do 136 | Dir.mktmpdir do |dir| 137 | filename = "#{dir}/EXTF_Buchungsstapel.csv" 138 | export.to_file(filename) 139 | 140 | expect { 141 | CSV.read(filename, **Datev::Export::CSV_OPTIONS) 142 | }.to_not raise_error 143 | end 144 | end 145 | 146 | it 'should export a file identically to the given example' do 147 | Dir.mktmpdir do |dir| 148 | filename = "#{dir}/EXTF_Buchungsstapel.csv" 149 | export.to_file(filename) 150 | if ENV['CREATE_EXAMPLES'] 151 | export.to_file('examples/EXTF_Buchungsstapel.csv') 152 | end 153 | 154 | expect(IO.read(filename)).to eq(IO.read('examples/EXTF_Buchungsstapel.csv')) 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /spec/datev/export/contact_export_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::ContactExport do 4 | let(:contact1) { 5 | { 6 | 'Konto' => 10000, 7 | 'Vorname (Adressatentyp natürl. Person)' => 'Sabine', 8 | 'Name (Adressatentyp natürl. Person)' => 'Mustermann', 9 | 'Adressatentyp' => '1', 10 | 'Anrede' => 'Frau', 11 | 'Titel/Akad. Grad' => 'Dr.', 12 | 'Adressart' => 'STR', 13 | 'Straße' => 'Am Haagelspfädchen 14', 14 | 'Postleitzahl' => '50999', 15 | 'Ort' => 'Köln', 16 | 'Land' => 'DE', 17 | 'Telefon' => '0221/1234567', 18 | 'E-Mail' => 'sabine@mustermann.de', 19 | 'Internet' => 'www.mustermann.de', 20 | 'Fax' => '0221/1234568', 21 | 'IBAN 1' => 'DE12500105170648489890', 22 | 'Briefanrede' => 'Sehr geehrte Frau Dr. Mustermann', 23 | 'Sprache' => 1 24 | } 25 | } 26 | 27 | let(:contact2) { 28 | { 29 | 'Konto' => 70001, 30 | 'Name (Adressatentyp Unternehmen)' => 'Meyer GmbH', 31 | 'Adressatentyp' => '2' 32 | } 33 | } 34 | 35 | let(:contact3) { 36 | { 37 | 'Konto' => 70002, 38 | 'Name (Adressatentyp Unternehmen)' => 'Schulze GmbH', 39 | 'Adressatentyp' => '2' 40 | } 41 | } 42 | 43 | let(:contact4) { 44 | { 45 | 'Konto' => 70003, 46 | 'Name (Adressatentyp Unternehmen)' => 'Scary Kitten🙀 AG', 47 | 'Adressatentyp' => '2' 48 | } 49 | } 50 | 51 | let(:export) do 52 | export = Datev::ContactExport.new( 53 | 'Herkunft' => 'XY', 54 | 'Exportiert von' => 'Chief Accounting Officer', 55 | 'Erzeugt am' => Time.new(2018,3,6,10,25,0, '+02:00'), 56 | 'Berater' => 1001, 57 | 'Mandant' => 456, 58 | 'WJ-Beginn' => Date.new(2018,1,1), 59 | 'Bezeichnung' => 'Kunden und Lieferanten' 60 | ) 61 | 62 | export << contact1 63 | export << contact2 64 | export << contact3 65 | export << contact4 66 | export 67 | end 68 | 69 | describe :to_s do 70 | subject { export.to_s } 71 | 72 | it 'should export as string' do 73 | expect(subject).to be_a(String) 74 | expect(subject.lines.length).to eq(6) 75 | end 76 | 77 | it "should encode in Windows-1252" do 78 | expect(subject.encoding).to eq(Encoding::WINDOWS_1252) 79 | end 80 | 81 | it "should contain header" do 82 | expect(subject.lines[0]).to include('"EXTF";700') 83 | end 84 | 85 | it "should contain field names" do 86 | expect(subject.lines[1]).to include('Konto;Name (Adressatentyp Unternehmen)') 87 | end 88 | 89 | it "should contain accounts" do 90 | expect(subject.lines[2]).to include('10000;;;"Mustermann"') 91 | expect(subject.lines[3]).to include('70001;"Meyer GmbH"') 92 | expect(subject.lines[4]).to include('70002;"Schulze GmbH"') 93 | end 94 | 95 | it "should replace non-convertible characters by spaces" do 96 | expect(subject.lines[5]).to include('70003;"Scary Kitten AG"') 97 | end 98 | end 99 | 100 | describe :to_file do 101 | it 'should export a valid CSV file' do 102 | Dir.mktmpdir do |dir| 103 | filename = "#{dir}/EXTF_Stammdaten.csv" 104 | export.to_file(filename) 105 | 106 | expect { 107 | CSV.read(filename, **Datev::Export::CSV_OPTIONS) 108 | }.to_not raise_error 109 | end 110 | end 111 | 112 | it 'should export a file identically to the given example' do 113 | Dir.mktmpdir do |dir| 114 | filename = "#{dir}/EXTF_Stammdaten.csv" 115 | export.to_file(filename) 116 | if ENV['CREATE_EXAMPLES'] 117 | export.to_file('examples/EXTF_Stammdaten.csv') 118 | end 119 | 120 | expect(IO.read(filename)).to eq(IO.read('examples/EXTF_Stammdaten.csv')) 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/datev/field/boolean_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::BooleanField do 4 | subject { Datev::BooleanField.new 'foo', required: true } 5 | 6 | describe :validate! do 7 | it 'should accept valid value' do 8 | expect { subject.validate!(true) }.to_not raise_error 9 | expect { subject.validate!(false) }.to_not raise_error 10 | end 11 | 12 | it 'should fail for invalid values' do 13 | [ 1000, '123', 100.5 ].each do |value| 14 | expect { subject.validate!(value) }.to raise_error(ArgumentError, "Value given for field 'foo' is not a Boolean") 15 | end 16 | expect { subject.validate!(nil) }.to raise_error(ArgumentError, "Value for field 'foo' is required") 17 | end 18 | end 19 | 20 | describe :output do 21 | it 'should return 0 or 1' do 22 | expect(subject.output(true)).to eq(1) 23 | expect(subject.output(false)).to eq(0) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/datev/field/date_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::DateField do 4 | subject { Datev::DateField.new 'foo', format: "%m%Y", required: true } 5 | 6 | describe :validate! do 7 | it "should accept valid value" do 8 | expect { subject.validate!(Date.today) }.to_not raise_error 9 | expect { subject.validate!(Time.now) }.to_not raise_error 10 | end 11 | 12 | it "should fail for invalid values" do 13 | [ 1000, '123', 100.5 ].each do |value| 14 | expect { subject.validate!(value) }.to raise_error(ArgumentError, "Value given for field 'foo' is not a Date or Time") 15 | end 16 | expect { subject.validate!(nil) }.to raise_error(ArgumentError, "Value for field 'foo' is required") 17 | end 18 | end 19 | 20 | describe :output do 21 | it "should format using option" do 22 | expect(subject.output(Date.new(2016,3,28))).to eq('032016') 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/datev/field/decimal_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::DecimalField do 4 | subject { Datev::DecimalField.new 'foo', precision: 6, scale: 2, required: true } 5 | 6 | describe :validate! do 7 | it "should accept valid value" do 8 | expect { subject.validate!(12.23) }.to_not raise_error 9 | end 10 | 11 | it "should fail for invalid values" do 12 | [ '123', true, Date.new(2016,1,1) ].each do |value| 13 | expect { subject.validate!(value) }.to raise_error(ArgumentError, "Value given for field 'foo' is not a Decimal") 14 | end 15 | expect { subject.validate!(100000.5)}.to raise_error(ArgumentError, "Value '100000.5' for field 'foo' is too long") 16 | expect { subject.validate!(nil) }.to raise_error(ArgumentError, "Value for field 'foo' is required") 17 | end 18 | end 19 | 20 | describe :output do 21 | it "should format" do 22 | expect(subject.output(1)).to eq('1,00') 23 | expect(subject.output(10)).to eq('10,00') 24 | expect(subject.output(1.2)).to eq('1,20') 25 | expect(subject.output(10.21)).to eq('10,21') 26 | expect(subject.output(1.238)).to eq('1,24') 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/datev/field/integer_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::IntegerField do 4 | subject { Datev::IntegerField.new 'foo', limit: 3, minimum: 5, maximum: 500, required: true } 5 | 6 | describe :validate! do 7 | it "should accept valid value" do 8 | expect { subject.validate!(42) }.to_not raise_error 9 | end 10 | 11 | it "should fail for invalid values" do 12 | [ '123', 100.5, true ].each do |value| 13 | expect { subject.validate!(value) }.to raise_error(ArgumentError, "Value given for field 'foo' is not an Integer") 14 | end 15 | expect { subject.validate!(nil) }.to raise_error(ArgumentError, "Value for field 'foo' is required") 16 | end 17 | 18 | it "should fail for too long value" do 19 | expect { subject.validate!(1000) }.to raise_error(ArgumentError, "Value '1000' for field 'foo' is too long") 20 | end 21 | 22 | it "should fail for too small value" do 23 | expect { subject.validate!(4) }.to raise_error(ArgumentError, "Value '4' for field 'foo' is too small") 24 | end 25 | 26 | it "should fail for too large value" do 27 | expect { subject.validate!(501) }.to raise_error(ArgumentError, "Value '501' for field 'foo' is too large") 28 | end 29 | end 30 | 31 | describe :output do 32 | it "should return value as unquoted string (DATEV CSV format requirement)" do 33 | expect(subject.output(1)).to eq('1') 34 | expect(subject.output(700)).to eq('700') 35 | expect(subject.output(42)).to eq('42') 36 | end 37 | 38 | it "should return empty string for nil value (DATEV reserved fields requirement)" do 39 | expect(subject.output(nil)).to eq('') 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/datev/field/string_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::StringField do 4 | subject { Datev::StringField.new 'foo', limit: 9, required: true, regex: %r{\A[a-zA-Z0-9\$\&\%\*\+\-\/]*\z} } 5 | 6 | describe :validate! do 7 | it "should accept valid value" do 8 | expect { subject.validate!('Bar') }.to_not raise_error 9 | end 10 | 11 | it "should fail for invalid values" do 12 | [ 123, 123.45, true ].each do |value| 13 | expect { subject.validate!(value) }.to raise_error(ArgumentError, "Value given for field 'foo' is not a String") 14 | end 15 | expect { subject.validate!('MuchTooLong') }.to raise_error(ArgumentError, "Value 'MuchTooLong' for field 'foo' is too long") 16 | expect { subject.validate!(nil) }.to raise_error(ArgumentError, "Value for field 'foo' is required") 17 | 18 | expect { subject.validate!('Ömläüte') }.to raise_error(ArgumentError, "Value 'Ömläüte' for field 'foo' does not match regex") 19 | expect { subject.validate!('RE.1234') }.to raise_error(ArgumentError, "Value 'RE.1234' for field 'foo' does not match regex") 20 | expect { subject.validate!('RE_1234') }.to raise_error(ArgumentError, "Value 'RE_1234' for field 'foo' does not match regex") 21 | end 22 | end 23 | 24 | describe :output do 25 | describe "quoting" do 26 | it "should add quotes" do 27 | expect(subject.output('foo')).to eq('"foo"') 28 | end 29 | 30 | it "should return empty string for nil value (DATEV reserved fields requirement)" do 31 | expect(subject.output(nil)).to eq('') 32 | end 33 | 34 | it "should return empty string for empty string value" do 35 | expect(subject.output('')).to eq('') 36 | end 37 | 38 | it "should handle single quotes" do 39 | expect(subject.output("Kaiser's")).to eq('"Kaiser\'s"') 40 | end 41 | 42 | it "should duplicate existing double quotes" do 43 | expect(subject.output('"ZEIT"ung')).to eq('"""ZEIT""ung"') 44 | end 45 | end 46 | 47 | it "should truncate string to limit" do 48 | expect(subject.output('1234567890')).to eq('"123456789"') 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/datev/field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev::Field do 4 | describe :initialize do 5 | it 'should not allow invalid options' do 6 | expect { 7 | Datev::Field.new 'foo', 42 8 | }.to raise_error(ArgumentError, "Argument 'options' has to be a Hash") 9 | end 10 | 11 | it 'should not allow invalid name' do 12 | expect { 13 | Datev::Field.new :foo, limit: 3 14 | }.to raise_error(ArgumentError, "Argument 'name' has to be a String") 15 | end 16 | end 17 | 18 | describe :validate! do 19 | context 'for required field' do 20 | subject { Datev::Field.new 'foo', required: true } 21 | 22 | it 'should accept non-nil value' do 23 | expect { subject.validate!('bar') }.to_not raise_error 24 | end 25 | 26 | it 'should fail for nil value' do 27 | expect { subject.validate!(nil) }.to raise_error(ArgumentError, "Value for field 'foo' is required") 28 | end 29 | end 30 | 31 | context 'for not-required field' do 32 | subject { Datev::Field.new 'foo' } 33 | 34 | it 'should accept non-nil value' do 35 | expect { subject.validate!('bar') }.to_not raise_error 36 | end 37 | 38 | it 'should accept nil value' do 39 | expect { subject.validate!(nil) }.to_not raise_error 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/datev_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Datev do 4 | it 'has a version number' do 5 | expect(Datev::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | 4 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 5 | SimpleCov::Formatter::HTMLFormatter, 6 | Coveralls::SimpleCov::Formatter 7 | ]) 8 | SimpleCov.start do 9 | add_filter '/spec/' 10 | end 11 | 12 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 13 | require 'datev' 14 | require 'tmpdir' 15 | require 'csv' 16 | --------------------------------------------------------------------------------