luca-jp 0.20.6 → 0.21.0
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 +4 -4
- data/exe/luca-jp +1 -1
- data/lib/luca/jp/version.rb +1 -1
- data/lib/luca_salary/jp_adjustment.rb +209 -87
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b7d7394cf2864fb65e6b575cb233dbac9939ccaff36fcf27c78bc9feb1e6da7
|
|
4
|
+
data.tar.gz: 67663785b91e181476b8fa12209ba8bdd0cddae11916e58b7695729cf05c3ecd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eccbca739cb53247f7e984ea6a84a87ec74cbf8796c0347dd763c8e61a83bd12bc3290968fae0d45a9e8633301e9f54057308dd0e1877c2190e467db11b64f47
|
|
7
|
+
data.tar.gz: 18e49a30a3a67b9fb2896a13d3f62fa75320c6f64a12654894f6f22ead17a13d1c9338c0902c92bd7370e2b648ed364292e28e220fe571c54058a47131dc4c26
|
data/exe/luca-jp
CHANGED
|
@@ -145,7 +145,7 @@ when 'k', 'kyuyo', 'salary'
|
|
|
145
145
|
end
|
|
146
146
|
when 'n', /nenmats?u/, /nenmats?u-?chou?sei/
|
|
147
147
|
OptionParser.new do |opt|
|
|
148
|
-
opt.banner = 'Usage: luca-jp nenmatsu <
|
|
148
|
+
opt.banner = 'Usage: luca-jp nenmatsu <import-path> [profile id]'
|
|
149
149
|
args = opt.parse(ARGV)
|
|
150
150
|
LucaCmd.check_dir('profiles', ext_conf: EXT_CONF) do
|
|
151
151
|
LucaSalaryCmd::Import.profiles(args, params)
|
data/lib/luca/jp/version.rb
CHANGED
|
@@ -1,38 +1,98 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'fileutils'
|
|
2
3
|
require 'luca_salary'
|
|
3
4
|
require 'luca_support/const'
|
|
4
5
|
require 'luca/jp/util'
|
|
5
|
-
require 'zip'
|
|
6
|
-
|
|
7
6
|
require 'rexml/document'
|
|
7
|
+
require 'zip'
|
|
8
8
|
|
|
9
9
|
module LucaSalary
|
|
10
10
|
class JpAdjustment < LucaSalary::Profile
|
|
11
11
|
|
|
12
12
|
def self.import(path, id = nil, params = nil)
|
|
13
|
-
|
|
14
|
-
bulk_load(path).each do |o|
|
|
15
|
-
s_profile
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
tax_ids, names, s_profiles = create_index
|
|
14
|
+
bulk_load(path).each do |o, path|
|
|
15
|
+
s_profile = if id
|
|
16
|
+
find_profile(id).tap do |p|
|
|
17
|
+
if p.nil?
|
|
18
|
+
FileUtils.mkdir_p(path.parent / 'rejected')
|
|
19
|
+
FileUtils.move(path, path.parent / 'rejected')
|
|
20
|
+
raise "No entries found for ID: #{id}. abort..."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
id = search_id(o, tax_ids, names)
|
|
25
|
+
if id.nil?
|
|
26
|
+
STDERR.puts "#{o['name']} record not found. skip..."
|
|
27
|
+
FileUtils.mkdir_p(path.parent / 'rejected')
|
|
28
|
+
FileUtils.move(path, path.parent / 'rejected')
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
s_profiles[id]
|
|
32
|
+
end
|
|
33
|
+
s_profile = update_profile(s_profile, o)
|
|
34
|
+
save(s_profile, 's_profiles')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.create_index
|
|
39
|
+
tax_ids = {}
|
|
40
|
+
names = {}
|
|
41
|
+
profiles = {}
|
|
42
|
+
all('profiles').each do |p|
|
|
43
|
+
profile = find_secure(p['id'], 'profiles')
|
|
44
|
+
if profile['tax_id']
|
|
45
|
+
tax_ids[profile['tax_id'].to_s] = p['id']
|
|
20
46
|
end
|
|
47
|
+
key = [profile['name'].gsub(" ", "").strip, profile['birth_date'].to_s]
|
|
48
|
+
names[key] = p['id']
|
|
49
|
+
s_profile = find(p['id'], 's_profiles')
|
|
50
|
+
profiles[profile['id']] = s_profile
|
|
21
51
|
end
|
|
22
|
-
|
|
52
|
+
[tax_ids, names, profiles]
|
|
23
53
|
end
|
|
24
54
|
|
|
25
|
-
def self.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
else
|
|
31
|
-
list.first
|
|
32
|
-
end
|
|
33
|
-
#merged = find_secure(id, 'profiles') # NOTE for content match by name, birth_date
|
|
34
|
-
find(id, 's_profiles')
|
|
55
|
+
def self.find_profile(id_fragment)
|
|
56
|
+
list = id_completion(id_fragment, basedir: 's_profiles')
|
|
57
|
+
if list.length > 1
|
|
58
|
+
STDERR.puts "#{list.length} entries found for ID: #{id_fragment}. abort..."
|
|
59
|
+
return nil
|
|
35
60
|
end
|
|
61
|
+
|
|
62
|
+
id = list.first
|
|
63
|
+
find(id, 's_profiles')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.update_profile(previous, imported)
|
|
67
|
+
previous['tax_id'] ||= imported['tax_id']
|
|
68
|
+
previous['name'] ||= imported['name']
|
|
69
|
+
previous['katakana'] ||= imported['kana']
|
|
70
|
+
previous['birth_date'] ||= imported['birth_date']
|
|
71
|
+
|
|
72
|
+
if imported['spouse'] && !imported['spouse'].empty?
|
|
73
|
+
if same_person?(previous['spouse'], imported['spouse'])
|
|
74
|
+
# NOTE imported['spouse']をベースにし、incomeだけマージ
|
|
75
|
+
income = (previous['spouse']['income'] || {}).merge(imported['spouse']['income'])
|
|
76
|
+
previous['spouse'] = imported['spouse']
|
|
77
|
+
previous['spouse']['income'] = income
|
|
78
|
+
else
|
|
79
|
+
previous['spouse'] = imported['spouse']
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if !imported['family'].empty?
|
|
84
|
+
previous['family'] = imported['family'].map do |latest|
|
|
85
|
+
registered = previous['family'].find { |m| same_person?(m, latest) }
|
|
86
|
+
if registered
|
|
87
|
+
['income', 'elderly', 'tokutei', 'nonresident', 'handicapped'].each do |k|
|
|
88
|
+
latest[k] = registered[k].merge(new_member[k]) if registered[k] && new_member[k]
|
|
89
|
+
latest[k] ||= registered[k]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
latest
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
previous
|
|
36
96
|
end
|
|
37
97
|
|
|
38
98
|
# XMLタグ定義: https://www.nta.go.jp/users/gensen/oshirase/0019004-159.htm
|
|
@@ -40,8 +100,18 @@ module LucaSalary
|
|
|
40
100
|
TAGS = {
|
|
41
101
|
'NTAAPP001' => {
|
|
42
102
|
year: 'xml001_B00020',
|
|
103
|
+
id: 'xml001_B00170', # マイナンバー
|
|
104
|
+
kana: 'xml001_B00150', # フリガナ
|
|
105
|
+
name: 'xml001_B00160', # 氏名
|
|
106
|
+
birth_date: {
|
|
107
|
+
root: 'xml001_B00230', # 生年月日
|
|
108
|
+
year: 'xml001_B00240', # 西暦
|
|
109
|
+
month: 'xml001_B00270', # 月
|
|
110
|
+
day: 'xml001_B00280' # 日
|
|
111
|
+
},
|
|
43
112
|
family: {
|
|
44
113
|
root: 'xml001_D00000', # 扶養親族情報繰り返し
|
|
114
|
+
id: 'xml001_D00040', # マイナンバー
|
|
45
115
|
kana: 'xml001_D00020', # フリガナ
|
|
46
116
|
name: 'xml001_D00030', # 氏名
|
|
47
117
|
birth_date: {
|
|
@@ -64,6 +134,7 @@ module LucaSalary
|
|
|
64
134
|
year: 'xml004_B00020',
|
|
65
135
|
spouse: {
|
|
66
136
|
root: 'xml004_D00000',
|
|
137
|
+
id: 'xml004_D00030', # マイナンバー
|
|
67
138
|
kana: 'xml004_D00010', # フリガナ
|
|
68
139
|
name: 'xml004_D00020', # 氏名
|
|
69
140
|
income: 'xml004_D00260', # 配偶者の本年中の合計所得金額の見積額
|
|
@@ -74,7 +145,7 @@ module LucaSalary
|
|
|
74
145
|
}
|
|
75
146
|
|
|
76
147
|
def self.parse_xml(xml_set)
|
|
77
|
-
h = { 'spouse' => {
|
|
148
|
+
h = { 'spouse' => {}, 'family' => [] }
|
|
78
149
|
xml_set.each do |xml|
|
|
79
150
|
# ルート要素の属性から様式IDを取得
|
|
80
151
|
form_id = xml.root.name
|
|
@@ -84,89 +155,143 @@ module LucaSalary
|
|
|
84
155
|
year_node = xml.elements["//#{tags[:year]}"]
|
|
85
156
|
year = year_node&.text&.to_i
|
|
86
157
|
|
|
87
|
-
# NTAAPP001: 扶養控除等申告書
|
|
88
158
|
if form_id == 'NTAAPP001'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
f['birth_date'] = Date.new(y.to_i, m.to_i, d.to_i)
|
|
102
|
-
end
|
|
159
|
+
tags = TAGS[form_id]
|
|
160
|
+
h['tax_id'] ||= xml.elements["//#{tags[:id]}"]&.text
|
|
161
|
+
h['name'] ||= xml.elements["//#{tags[:name]}"]&.text
|
|
162
|
+
h['kana'] ||= xml.elements["//#{tags[:kana]}"]&.text
|
|
163
|
+
bd_tags = tags[:birth_date]
|
|
164
|
+
bd_node = xml.elements["//#{bd_tags[:root]}"]
|
|
165
|
+
if bd_node
|
|
166
|
+
y = bd_node.elements[bd_tags[:year]]&.text
|
|
167
|
+
m = bd_node.elements[bd_tags[:month]]&.text
|
|
168
|
+
d = bd_node.elements[bd_tags[:day]]&.text
|
|
169
|
+
if y && m && d
|
|
170
|
+
h['birth_date'] = Date.new(y.to_i, m.to_i, d.to_i)
|
|
103
171
|
end
|
|
172
|
+
end
|
|
173
|
+
h['family'] << parse_family(xml, year)
|
|
174
|
+
end
|
|
104
175
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
end
|
|
176
|
+
if form_id == 'NTAAPP004' && year
|
|
177
|
+
h['spouse'] = parse_spouse(xml, year)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
110
180
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
181
|
+
h['family'] = merge_family(h['family']) # 翌年の扶養控除等申告書との重複
|
|
182
|
+
h
|
|
183
|
+
end
|
|
115
184
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
end
|
|
185
|
+
def self.search_id(query, tax_ids, names)
|
|
186
|
+
id = tax_ids[query['tax_id']] if query['tax_id']
|
|
187
|
+
return id if id
|
|
120
188
|
|
|
121
|
-
|
|
122
|
-
|
|
189
|
+
key = [query['name'].gsub(" ", "").strip, query['birth_date'].to_s]
|
|
190
|
+
names[key]
|
|
191
|
+
end
|
|
123
192
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
193
|
+
def self.same_person?(p1, p2)
|
|
194
|
+
return false if p1.nil? || p2.nil? || p1.empty? || p2.empty?
|
|
195
|
+
|
|
196
|
+
if p1['tax_id'] && p2['tax_id']
|
|
197
|
+
return p1['tax_id'].to_s == p2['tax_id'].to_s
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
n1 = p1['name'].to_s.gsub(' ', '').strip
|
|
201
|
+
n2 = p2['name'].to_s.gsub(' ', '').strip
|
|
202
|
+
bd1 = p1['birth_date'].to_s
|
|
203
|
+
bd2 = p2['birth_date'].to_s
|
|
204
|
+
|
|
205
|
+
n1 == n2 && bd1 == bd2
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# NTAAPP004: 配偶者控除等申告書
|
|
209
|
+
def self.parse_spouse(xml, year)
|
|
210
|
+
tags = TAGS['NTAAPP004']
|
|
211
|
+
spouse = xml.elements["//#{tags[:spouse][:root]}"]
|
|
212
|
+
return {} if ! spouse
|
|
213
|
+
|
|
214
|
+
{ 'income' => {} }.tap do |h|
|
|
215
|
+
h['tax_id'] = spouse.elements[tags[:spouse][:id]]&.text
|
|
216
|
+
h['name'] = spouse.elements[tags[:spouse][:name]]&.text
|
|
217
|
+
h['kana'] = spouse.elements[tags[:spouse][:kana]]&.text
|
|
218
|
+
|
|
219
|
+
nonresident = spouse.elements[tags[:spouse][:nonresident]]&.text
|
|
220
|
+
if nonresident == '1'
|
|
221
|
+
h['nonresident'] = {} unless h['nonresident']
|
|
222
|
+
h['nonresident'][year] = nonresident.to_i
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
income = spouse.elements[tags[:spouse][:income]]&.text
|
|
226
|
+
h['income'][year] = income.to_i if income && !income.empty?
|
|
133
227
|
|
|
134
|
-
|
|
228
|
+
elderly = spouse.elements[tags[:spouse][:elderly]]&.text
|
|
229
|
+
h['elderly'] = '1' if elderly == '1'
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# NTAAPP001: 扶養控除等申告書
|
|
234
|
+
def self.parse_family(xml, year)
|
|
235
|
+
tags = TAGS['NTAAPP001']
|
|
236
|
+
h = []
|
|
237
|
+
xml.elements.each("//#{tags[:family][:root]}") do |dep|
|
|
238
|
+
f = {}
|
|
239
|
+
f['tax_id'] = dep.elements[tags[:family][:id]]&.text
|
|
240
|
+
f['name'] = dep.elements[tags[:family][:name]]&.text
|
|
241
|
+
f['kana'] = dep.elements[tags[:family][:kana]]&.text
|
|
242
|
+
|
|
243
|
+
bd_tags = tags[:family][:birth_date]
|
|
244
|
+
bd_node = dep.elements[bd_tags[:root]]
|
|
245
|
+
if bd_node
|
|
246
|
+
y = bd_node.elements[bd_tags[:year]]&.text
|
|
247
|
+
m = bd_node.elements[bd_tags[:month]]&.text
|
|
248
|
+
d = bd_node.elements[bd_tags[:day]]&.text
|
|
249
|
+
if y && m && d
|
|
250
|
+
f['birth_date'] = Date.new(y.to_i, m.to_i, d.to_i)
|
|
135
251
|
end
|
|
136
252
|
end
|
|
137
253
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
254
|
+
if year
|
|
255
|
+
dep_income = dep.elements[tags[:family][:income]]&.text
|
|
256
|
+
if dep_income && !dep_income.empty?
|
|
257
|
+
f['income'] = { year => dep_income.to_i }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
elderly = dep.elements[tags[:family][:elderly]]&.text
|
|
261
|
+
if elderly && (elderly == '1' || elderly == '2')
|
|
262
|
+
f['elderly'] = { year => elderly.to_i }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
tokutei = dep.elements[tags[:family][:tokutei]]&.text
|
|
266
|
+
if tokutei && (tokutei == '1' || tokutei == '2')
|
|
267
|
+
f['tokutei'] = { year => tokutei.to_i }
|
|
268
|
+
end
|
|
151
269
|
|
|
152
|
-
|
|
153
|
-
|
|
270
|
+
nonresident = dep.elements[tags[:family][:nonresident]]&.text
|
|
271
|
+
f['nonresident'] = { year => nonresident.to_i } if nonresident == '1'
|
|
154
272
|
|
|
155
|
-
|
|
156
|
-
|
|
273
|
+
hc_tags = tags[:family][:handicapped]
|
|
274
|
+
hc_node = dep.elements[hc_tags[:root]]
|
|
275
|
+
if hc_node
|
|
276
|
+
hc_type = hc_node.elements[hc_tags[:type]]&.text
|
|
277
|
+
if hc_type && !hc_type.empty? && hc_type != '0'
|
|
278
|
+
f['handicapped'] = { year => hc_type.to_i }
|
|
157
279
|
end
|
|
158
280
|
end
|
|
159
281
|
end
|
|
282
|
+
h << f
|
|
160
283
|
end
|
|
161
|
-
|
|
162
|
-
h['family'] = merge_family(h['family']) # 翌年の扶養控除等申告書との重複
|
|
163
284
|
h
|
|
164
285
|
end
|
|
165
286
|
|
|
166
287
|
def self.merge_family(family_list)
|
|
167
288
|
merged = {}
|
|
168
|
-
family_list.each do |f|
|
|
169
|
-
key =
|
|
289
|
+
family_list.flatten.each do |f|
|
|
290
|
+
key = if f['tax_id']
|
|
291
|
+
f['tax_id']
|
|
292
|
+
else
|
|
293
|
+
[f['name'].gsub(" ", "").strip, f['birth_date'].to_s]
|
|
294
|
+
end
|
|
170
295
|
if merged.key?(key)
|
|
171
296
|
target = merged[key]
|
|
172
297
|
|
|
@@ -203,21 +328,18 @@ module LucaSalary
|
|
|
203
328
|
end
|
|
204
329
|
|
|
205
330
|
if has_many
|
|
206
|
-
# NOTE implement search by content logic
|
|
207
|
-
raise "Multiple import is not supported yet."
|
|
208
|
-
|
|
209
331
|
Dir.children(path).sort.map do |child|
|
|
210
332
|
full_path = File.join(path, child)
|
|
211
333
|
if File.directory?(full_path) || (File.file?(full_path) && File.extname(full_path).downcase == '.zip')
|
|
212
334
|
data = load_xml_export(full_path)
|
|
213
|
-
yield
|
|
335
|
+
yield(parse_xml(data), Pathname(full_path)) unless data.empty?
|
|
214
336
|
else
|
|
215
337
|
nil
|
|
216
338
|
end
|
|
217
339
|
end.compact
|
|
218
340
|
else
|
|
219
341
|
data = load_xml_export(path)
|
|
220
|
-
yield
|
|
342
|
+
yield(parse_xml(data), Pathname(path)) unless data.empty?
|
|
221
343
|
end
|
|
222
344
|
end
|
|
223
345
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: luca-jp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuma Takahiro
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lucabook
|