afd_parser 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/afd_parser.rb +257 -0
- data/lib/afd_parser/clock_in_out.rb +71 -0
- data/lib/afd_parser/header.rb +144 -0
- data/lib/afd_parser/record_parser.rb +65 -0
- data/lib/afd_parser/set_employee.rb +98 -0
- data/lib/afd_parser/set_employer.rb +109 -0
- data/lib/afd_parser/set_time.rb +77 -0
- data/lib/afd_parser/trailer.rb +83 -0
- data/lib/afd_parser/version.rb +3 -0
- data/test/afd_file +11 -0
- data/test/afd_invalid_utf-8_chars +2 -0
- data/test/afd_parser_test.rb +633 -0
- data/test/clock_in_out_test.rb +32 -0
- data/test/header_test.rb +52 -0
- data/test/set_employee_test.rb +67 -0
- data/test/set_employer_test.rb +29 -0
- data/test/set_time_test.rb +23 -0
- data/test/test_helper.rb +25 -0
- data/test/trailer_test.rb +26 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 23281239730f2fc9a56515c9cc307eca390d957a
|
4
|
+
data.tar.gz: 4919382b66f53e7796168b3cc3bdf8f71aeac03d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4498b18497c30a5c580090b3f751bf0f450148a775ef0dd82d152e1e0a84488c96cf7bddb9615093333ab28c032b69da1ec1280ad7ad6d8b400edd87349f895a
|
7
|
+
data.tar.gz: '038906e8404160933b602f429259a72e83c98bb08573ac494721b7e4c283ade240fc767c4a0149113073c5838cf0fa73831fca62fd703a5977816b074393fbd6'
|
data/lib/afd_parser.rb
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# Controle de Horas - Sistema para gestão de horas trabalhadas
|
3
|
+
# Copyright (C) 2009 O.S. Systems Softwares Ltda.
|
4
|
+
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU Affero General Public License as
|
7
|
+
# published by the Free Software Foundation, either version 3 of the
|
8
|
+
# License, or (at your option) any later version.
|
9
|
+
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU Affero General Public License for more details.
|
14
|
+
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
# Rua Clóvis Gularte Candiota 132, Pelotas-RS, Brasil.
|
19
|
+
# e-mail: contato@ossystems.com.br
|
20
|
+
|
21
|
+
require 'date'
|
22
|
+
|
23
|
+
# Parser para a PORTARIA No 1.510, DE 21 DE AGOSTO DE 2009, do
|
24
|
+
# Ministério do Trabalho;
|
25
|
+
|
26
|
+
class AfdParser
|
27
|
+
require 'afd_parser/clock_in_out'
|
28
|
+
require 'afd_parser/header'
|
29
|
+
require 'afd_parser/set_employee'
|
30
|
+
require 'afd_parser/set_employer'
|
31
|
+
require 'afd_parser/set_time'
|
32
|
+
require 'afd_parser/trailer'
|
33
|
+
|
34
|
+
class AfdParserException < Exception; end
|
35
|
+
attr_reader :records, :header, :trailer
|
36
|
+
|
37
|
+
def initialize(*args)
|
38
|
+
if args.size == 1
|
39
|
+
initialize_variables(args[0])
|
40
|
+
elsif args.size == 2
|
41
|
+
initialize_variables(args[1])
|
42
|
+
File.open(args[0], "r") do |file|
|
43
|
+
@raw_data = file.readlines
|
44
|
+
end
|
45
|
+
else
|
46
|
+
raise AfdParser::AfdParserException.new("wrong number of arguments, should be 1 or 2")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse
|
51
|
+
@raw_data.each_with_index do |line, index|
|
52
|
+
parse_line(line, index)
|
53
|
+
end
|
54
|
+
|
55
|
+
if @validate_structure and not trailer_found?
|
56
|
+
raise AfdParser::AfdParserException.new("AFD ended without a trailer record")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_line(line, index)
|
61
|
+
line_id, record_type_id = line.unpack("A9A").collect{|id| id.to_i}
|
62
|
+
record_type = get_record_type(line_id, record_type_id, line)
|
63
|
+
|
64
|
+
@previous_line_id = @current_line_id unless @current_line_id == 0
|
65
|
+
@current_line_id = line_id
|
66
|
+
|
67
|
+
if @validate_structure
|
68
|
+
validate_afd(line, line_id, @previous_line_id, index, record_type)
|
69
|
+
end
|
70
|
+
|
71
|
+
case record_type
|
72
|
+
when :header
|
73
|
+
@header = Header.new(line)
|
74
|
+
return @header
|
75
|
+
when :set_employer
|
76
|
+
@records << SetEmployer.new(line)
|
77
|
+
when :clock_in_out
|
78
|
+
@records << ClockInOut.new(line)
|
79
|
+
when :set_time
|
80
|
+
@records << SetTime.new(line)
|
81
|
+
when :set_employee
|
82
|
+
@records << SetEmployee.new(line)
|
83
|
+
when :trailer
|
84
|
+
@trailer = Trailer.new(line, count_records)
|
85
|
+
return @trailer
|
86
|
+
else
|
87
|
+
if @validate_structure
|
88
|
+
raise AfdParser::AfdParserException.new("Unknown record type found in AFD file, line #{index.to_s}: '#{line}'")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
return @records.last
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_header(employer_type, employer_document, employer_cei, employer_name, rep_serial_number, afd_start_date,afd_end_date, afd_creation_time)
|
96
|
+
if header_found?
|
97
|
+
raise AfdParser::AfdParserException.new("Cannot add a second AFD header")
|
98
|
+
else
|
99
|
+
@header = Header.new(employer_type, employer_document, employer_cei, employer_name, rep_serial_number, afd_start_date,afd_end_date, afd_creation_time)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def create_trailer
|
104
|
+
if trailer_found?
|
105
|
+
raise AfdParser::AfdParserException.new("Cannot add a second AFD trailer")
|
106
|
+
else
|
107
|
+
@trailer = Trailer.new(count_records)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def export
|
112
|
+
exported_data = ""
|
113
|
+
(exported_data += @header.export + "\r\n") if @header
|
114
|
+
@records.each do |record|
|
115
|
+
exported_data += record.export + "\r\n"
|
116
|
+
end
|
117
|
+
(exported_data += @trailer.export + "\r\n") if @trailer
|
118
|
+
|
119
|
+
exported_data
|
120
|
+
end
|
121
|
+
|
122
|
+
def first_creation_date
|
123
|
+
record = @records[0]
|
124
|
+
if record
|
125
|
+
time = record.creation_time
|
126
|
+
return Date.civil(time.year, time.month, time.day)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def last_creation_date
|
131
|
+
record = @records[-1]
|
132
|
+
if record
|
133
|
+
time = record.creation_time
|
134
|
+
return Date.civil(time.year, time.month, time.day)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def ==(other)
|
139
|
+
return self.class == other.class && @records == other.records
|
140
|
+
end
|
141
|
+
|
142
|
+
# get the first id after the AFD header
|
143
|
+
def first_id
|
144
|
+
first_record = @records[0]
|
145
|
+
first_record ? first_record.line_id : nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# get the last id before the AFD trailer
|
149
|
+
def last_id
|
150
|
+
last_record = @records[-1]
|
151
|
+
last_record ? last_record.line_id : nil
|
152
|
+
end
|
153
|
+
|
154
|
+
def merge(other)
|
155
|
+
other_first_id, other_last_id = other.first_id, other.last_id
|
156
|
+
if other_first_id.nil? || other_last_id.nil?
|
157
|
+
raise AfdParser::AfdParserException.new("Cannot merge with a empty parser")
|
158
|
+
end
|
159
|
+
|
160
|
+
@header = other.header if other.header
|
161
|
+
|
162
|
+
# merge is done by grouping all the records by line id, and replacing
|
163
|
+
# the ones in "self" by duplicates of the ones in "other".
|
164
|
+
this_records_by_line_id = @records.group_by{|record| record.line_id}
|
165
|
+
other_records_by_line_id = other.records.group_by{|record| record.line_id}
|
166
|
+
other_records_by_line_id.keys.each do |key|
|
167
|
+
this_records_by_line_id[key] = other_records_by_line_id[key].dup
|
168
|
+
end
|
169
|
+
@records = this_records_by_line_id.keys.sort.collect{|key| this_records_by_line_id[key]}.flatten
|
170
|
+
|
171
|
+
@trailer = nil
|
172
|
+
create_trailer
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
def initialize_variables(options)
|
177
|
+
@records = []
|
178
|
+
@current_line_id = nil
|
179
|
+
@previous_line_id = nil
|
180
|
+
if options.is_a?(Hash)
|
181
|
+
@validate_structure = options[:validate_structure]
|
182
|
+
@validate_expected_afd_records_ids = options[:validate_expected_afd_records_ids]
|
183
|
+
else
|
184
|
+
@validate_expected_afd_records_ids = @validate_structure = !!options
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def get_record_type(line_id, record_type_id, line)
|
189
|
+
if record_type_id == 1
|
190
|
+
return :header
|
191
|
+
elsif line_id == 999999999 and line.unpack("x45A").first.to_i == 9
|
192
|
+
return :trailer
|
193
|
+
elsif line_id != 0
|
194
|
+
case record_type_id
|
195
|
+
when 2
|
196
|
+
return :set_employer
|
197
|
+
when 3
|
198
|
+
return :clock_in_out
|
199
|
+
when 4
|
200
|
+
return :set_time
|
201
|
+
when 5
|
202
|
+
return :set_employee
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
return nil
|
207
|
+
end
|
208
|
+
|
209
|
+
def validate_afd(line, line_id, previous_line_id, index, record_type)
|
210
|
+
raise AfdParser::AfdParserException.new("Line #{index.to_s} is blank") if line.nil? || line.empty?
|
211
|
+
|
212
|
+
if trailer_found?
|
213
|
+
raise AfdParser::AfdParserException.new("Unexpected AFD record found after trailer, line #{index.to_s}: '#{line}'")
|
214
|
+
end
|
215
|
+
|
216
|
+
if not header_found? and record_type != :header
|
217
|
+
raise AfdParser::AfdParserException.new("Unexpected AFD record found before header, line #{index.to_s}: '#{line}'")
|
218
|
+
end
|
219
|
+
|
220
|
+
if header_found? and record_type == :header
|
221
|
+
raise AfdParser::AfdParserException.new("Unexpected second AFD header found, line #{index.to_s}: '#{line}'")
|
222
|
+
end
|
223
|
+
|
224
|
+
if @validate_expected_afd_records_ids and previous_line_id.nil? and index == 1 and line_id != index
|
225
|
+
raise AfdParser::AfdParserException.new("AFD records starts at an unexpected line ID; expected '1', got '#{line_id.to_s}'.")
|
226
|
+
end
|
227
|
+
|
228
|
+
if not previous_line_id.nil? and line_id != previous_line_id + 1 and not (line_id == 999999999 and not trailer_found?)
|
229
|
+
raise AfdParser::AfdParserException.new("Out-of-order line id on line 1; expected '#{index.to_s}', got '#{line_id.to_s}'")
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def header_found?
|
234
|
+
!@header.nil?
|
235
|
+
end
|
236
|
+
|
237
|
+
def trailer_found?
|
238
|
+
!@trailer.nil?
|
239
|
+
end
|
240
|
+
|
241
|
+
def count_records
|
242
|
+
counter = {:set_employer => 0, :clock_in_out => 0, :set_time => 0, :set_employee => 0}
|
243
|
+
@records.each do |record|
|
244
|
+
case record
|
245
|
+
when *SetEmployer
|
246
|
+
counter[:set_employer] += 1
|
247
|
+
when *ClockInOut
|
248
|
+
counter[:clock_in_out] += 1
|
249
|
+
when *SetTime
|
250
|
+
counter[:set_time] += 1
|
251
|
+
when *SetEmployee
|
252
|
+
counter[:set_employee] += 1
|
253
|
+
end
|
254
|
+
end
|
255
|
+
return counter
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# Controle de Horas - Sistema para gestão de horas trabalhadas
|
3
|
+
# Copyright (C) 2009 O.S. Systems Softwares Ltda.
|
4
|
+
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU Affero General Public License as
|
7
|
+
# published by the Free Software Foundation, either version 3 of the
|
8
|
+
# License, or (at your option) any later version.
|
9
|
+
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU Affero General Public License for more details.
|
14
|
+
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
# Rua Clóvis Gularte Candiota 132, Pelotas-RS, Brasil.
|
19
|
+
# e-mail: contato@ossystems.com.br
|
20
|
+
|
21
|
+
require 'afd_parser/record_parser'
|
22
|
+
|
23
|
+
class AfdParser::ClockInOut < AfdParser::RecordParser
|
24
|
+
attr_reader :line_id, :record_type_id, :creation_time, :pis
|
25
|
+
|
26
|
+
def initialize(line)
|
27
|
+
self.line_id, self.record_type_id, self.creation_time, self.pis =
|
28
|
+
line.unpack("A9AA12A12").collect{|str| _clean!(str)}
|
29
|
+
end
|
30
|
+
|
31
|
+
def export
|
32
|
+
line_export = ""
|
33
|
+
line_export += line_id.to_s.rjust(9,"0")
|
34
|
+
line_export += record_type_id.to_s
|
35
|
+
line_export += format_time(creation_time)
|
36
|
+
line_export += pis.to_s.rjust(11,"0")
|
37
|
+
line_export
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.size
|
41
|
+
34
|
42
|
+
end
|
43
|
+
|
44
|
+
def ==(other)
|
45
|
+
return self.class == other.class && [:line_id, :record_type_id, :creation_time, :pis].all? do |reader|
|
46
|
+
self.send(reader) == other.send(reader)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def line_id=(data)
|
52
|
+
@line_id = well_formed_number_string?(data) ? data.to_i : data
|
53
|
+
end
|
54
|
+
|
55
|
+
def record_type_id=(data)
|
56
|
+
@record_type_id = well_formed_number_string?(data) ? data.to_i : data
|
57
|
+
end
|
58
|
+
|
59
|
+
def pis=(data)
|
60
|
+
@pis = well_formed_number_string?(data.strip) ? data.to_i : data
|
61
|
+
end
|
62
|
+
|
63
|
+
def creation_time=(raw_time)
|
64
|
+
begin
|
65
|
+
parsed_time = parse_time(raw_time)
|
66
|
+
@creation_time = parsed_time
|
67
|
+
rescue
|
68
|
+
@creation_time = ""
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# Controle de Horas - Sistema para gestão de horas trabalhadas
|
3
|
+
# Copyright (C) 2009 O.S. Systems Softwares Ltda.
|
4
|
+
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU Affero General Public License as
|
7
|
+
# published by the Free Software Foundation, either version 3 of the
|
8
|
+
# License, or (at your option) any later version.
|
9
|
+
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU Affero General Public License for more details.
|
14
|
+
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
# Rua Clóvis Gularte Candiota 132, Pelotas-RS, Brasil.
|
19
|
+
# e-mail: contato@ossystems.com.br
|
20
|
+
|
21
|
+
require 'afd_parser/record_parser'
|
22
|
+
|
23
|
+
class AfdParser::Header < AfdParser::RecordParser
|
24
|
+
attr_reader :line_id, :record_type_id, :employer_type, :employer_document, :employer_cei, :employer_name, :rep_serial_number, :afd_start_date, :afd_end_date, :afd_creation_time
|
25
|
+
|
26
|
+
EMPLOYER_TYPES = {1 => :cnpj, 2 => :cpf}
|
27
|
+
|
28
|
+
def initialize(*args)
|
29
|
+
if args.size == 1
|
30
|
+
line = args[0]
|
31
|
+
|
32
|
+
self.line_id, self.record_type_id, self.employer_type, self.employer_document,
|
33
|
+
self.employer_cei, self.employer_name, self.rep_serial_number, self.afd_start_date,
|
34
|
+
self.afd_end_date, self.afd_creation_time =
|
35
|
+
line.unpack("A9AAA14A12A150A17A8A8A12").collect{|str| _clean!(str)}
|
36
|
+
elsif args.size == 8
|
37
|
+
@line_id = 0
|
38
|
+
@record_type_id = 1
|
39
|
+
@employer_type = args[0]
|
40
|
+
@employer_document = args[1]
|
41
|
+
@employer_cei = args[2]
|
42
|
+
@employer_name = args[3]
|
43
|
+
@rep_serial_number = args[4]
|
44
|
+
@afd_start_date = args[5]
|
45
|
+
@afd_end_date = args[6]
|
46
|
+
@afd_creation_time = args[7]
|
47
|
+
else
|
48
|
+
raise AfdParser::AfdParserException.new("wrong number of arguments for header object, should be 1 or 8")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def export
|
53
|
+
line_export = ""
|
54
|
+
line_export += @line_id.to_s.rjust(9,"0")
|
55
|
+
line_export += @record_type_id.to_s
|
56
|
+
line_export += get_employer_type_number(@employer_type).to_s
|
57
|
+
line_export += @employer_document.to_s.rjust(14, "0")
|
58
|
+
line_export += @employer_cei.to_s.rjust(12, "0")
|
59
|
+
line_export += @employer_name.to_s.ljust(150, " ")
|
60
|
+
line_export += @rep_serial_number
|
61
|
+
line_export += format_date(@afd_start_date)
|
62
|
+
line_export += format_date(@afd_end_date)
|
63
|
+
line_export += format_time(@afd_creation_time)
|
64
|
+
line_export
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.size
|
68
|
+
232
|
69
|
+
end
|
70
|
+
|
71
|
+
def ==(other)
|
72
|
+
return self.class == other.class && [:line_id, :record_type_id, :employer_type, :employer_document, :employer_cei, :employer_name, :rep_serial_number, :afd_start_date, :afd_end_date, :afd_creation_time].all? do |reader|
|
73
|
+
self.send(reader) == other.send(reader)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def line_id=(data)
|
79
|
+
@line_id = well_formed_number_string?(data) ? data.to_i : data
|
80
|
+
end
|
81
|
+
|
82
|
+
def record_type_id=(data)
|
83
|
+
@record_type_id = well_formed_number_string?(data) ? data.to_i : data
|
84
|
+
end
|
85
|
+
|
86
|
+
def employer_type=(data)
|
87
|
+
@employer_type = get_employer_type(data.to_i)
|
88
|
+
end
|
89
|
+
|
90
|
+
def employer_document=(data)
|
91
|
+
@employer_document = well_formed_number_string?(data) ? data.to_i : data
|
92
|
+
end
|
93
|
+
|
94
|
+
def employer_cei=(data)
|
95
|
+
@employer_cei = well_formed_number_string?(data) ? data.to_i : data
|
96
|
+
end
|
97
|
+
|
98
|
+
def employer_name=(data)
|
99
|
+
@employer_name = data.rstrip
|
100
|
+
end
|
101
|
+
|
102
|
+
def rep_serial_number=(data)
|
103
|
+
@rep_serial_number = data
|
104
|
+
end
|
105
|
+
|
106
|
+
def afd_start_date=(raw_date)
|
107
|
+
begin
|
108
|
+
parsed_date = parse_date(raw_date)
|
109
|
+
@afd_start_date = parsed_date
|
110
|
+
rescue
|
111
|
+
@afd_start_date = raw_date
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def afd_end_date=(raw_date)
|
116
|
+
begin
|
117
|
+
parsed_date = parse_date(raw_date)
|
118
|
+
@afd_end_date = parsed_date
|
119
|
+
rescue
|
120
|
+
@afd_end_date = raw_date
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def afd_creation_time=(raw_time)
|
125
|
+
begin
|
126
|
+
parsed_time = parse_time(raw_time)
|
127
|
+
@afd_creation_time = parsed_time
|
128
|
+
rescue
|
129
|
+
@afd_creation_time = raw_time
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def get_employer_type(employer_type_id)
|
134
|
+
type = EMPLOYER_TYPES[employer_type_id]
|
135
|
+
if type.nil?
|
136
|
+
raise AfdParser::AfdParserException.new("Unknown employer type id '#{employer_type_id.to_s}' found in AFD header")
|
137
|
+
end
|
138
|
+
type
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_employer_type_number(employer_type)
|
142
|
+
EMPLOYER_TYPES.invert[employer_type]
|
143
|
+
end
|
144
|
+
end
|