afd_parser 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|