luca-jp 0.20.4 → 0.20.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea7c42dee2cf6b8002de5606bf2056714154a68bd4212762580029644f2f6607
4
- data.tar.gz: f759cb53bf48adfe8148eb5f68f6f95a71c8b2decc103d63cfb6e4dce3fac2c9
3
+ metadata.gz: dfd6af4ff40f3d2562f04f71b59e02c944fec0fbc66dc18ae72d868bda2c5b82
4
+ data.tar.gz: 42f779967ee6e22db9a488bebdfa1c123e8c1738532f78e3ed5201916d92ca8a
5
5
  SHA512:
6
- metadata.gz: 3a5e412b5cb291bd2be9e39350385969e7ad6bb9f13b8f67c62b52bdc23a5f8aa226eee59cba6684d9d787bb0e6dff14b6b711113f9e5d2291d2e467419f7c7e
7
- data.tar.gz: 92333c73665f5e7a06a6830a4e79f4cf67ee82b3f04f715efcd1136fe66ebdb3daee355693ceecfec1199b6f8c763ecd95c5af9d2907b0a6aefdfaf2ad10236e
6
+ metadata.gz: 9b7f6ceb960f5dee8eea66435f631567123e7d98757fbb07f607b20fea1725516a87f054f1f48e7173b7fe4d343c644d51f0b5b11904ee681db7cde43e80a9be
7
+ data.tar.gz: 1e4929bef66fc8459ce622c75c590e49d1fdc5a96ac7d4d3e63543ba1971490f69bfd1fc62c469da03111a4973561579a11a76683f055c0b1ef749ce3e4fc9f8
data/exe/luca-jp CHANGED
@@ -4,6 +4,7 @@
4
4
  require 'optparse'
5
5
  require 'luca/jp'
6
6
  require 'luca_cmd'
7
+ require 'luca_salary/jp_adjustment'
7
8
  require 'luca_salary/jp_payreport'
8
9
  require 'luca_support/const'
9
10
  require 'yaml'
@@ -76,6 +77,13 @@ module LucaSalaryCmd
76
77
  puts LucaSalary::JpPayreport.export(args.first)
77
78
  end
78
79
  end
80
+
81
+ class Import
82
+ def self.profiles(args, params = nil)
83
+ path, id = args
84
+ puts LucaSalary::JpAdjustment.import(path, id, params)
85
+ end
86
+ end
79
87
  end
80
88
 
81
89
  #LucaRecord::Base.valid_project?
@@ -135,6 +143,14 @@ when 'k', 'kyuyo', 'salary'
135
143
  LucaSalaryCmd::Export.payreport(args)
136
144
  end
137
145
  end
146
+ when 'n', /nenmats?u/, /nenmats?u-?chou?sei/
147
+ OptionParser.new do |opt|
148
+ opt.banner = 'Usage: luca-jp nenmatsu <zip-file-path> <profile id>'
149
+ args = opt.parse(ARGV)
150
+ LucaCmd.check_dir('profiles', ext_conf: EXT_CONF) do
151
+ LucaSalaryCmd::Import.profiles(args, params)
152
+ end
153
+ end
138
154
  when 'version'
139
155
  puts "luca-jp: version #{Luca::Jp::VERSION}"
140
156
  exit 0
@@ -146,6 +162,7 @@ else
146
162
  puts ' s[youhizei]: 消費税の計算'
147
163
  puts ' c[hihouzei]: 地方税の計算'
148
164
  puts ' k[yuyo]: 給与報告(所得税)の計算'
165
+ puts ' n[enmatsu]: 年末調整アプリのデータインポート'
149
166
  puts ' urikake: 勘定科目内訳明細書の売掛金リスト(CSV)'
150
167
  exit 1
151
168
  end
data/lib/luca/jp/util.rb CHANGED
@@ -21,6 +21,10 @@ module Luca
21
21
  end
22
22
  end
23
23
 
24
+ def alphadigit_ja(str)
25
+ str.tr('A-Z0-9\- ', 'A-Z0-9ー ')
26
+ end
27
+
24
28
  # TODO: customerオプションを適切に扱うには
25
29
  # 納付時にx-customerを付加していないケースの考慮が必要
26
30
  def prepaid_tax(code, customer = nil)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Luca
4
4
  module Jp
5
- VERSION = '0.20.4'
5
+ VERSION = '0.20.6'
6
6
  end
7
7
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaSalaryJp
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
  end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+ require 'luca_salary'
3
+ require 'luca_support/const'
4
+ require 'luca/jp/util'
5
+ require 'zip'
6
+
7
+ require 'rexml/document'
8
+
9
+ module LucaSalary
10
+ class JpAdjustment < LucaSalary::Profile
11
+
12
+ def self.import(path, id = nil, params = nil)
13
+ s_profile = profiles(id)
14
+ bulk_load(path).each do |o|
15
+ s_profile['spouse']['name'] ||= o['spouse']['name']
16
+ s_profile['spouse']['katakana'] ||= o['spouse']['kana']
17
+ s_profile['spouse']['income'].merge!(o['spouse']['income'])
18
+ if ! s_profile['family'].empty? || ! o['family'].empty?
19
+ s_profile['family'] = merge_family(s_profile['family'].concat(o['family']))
20
+ end
21
+ end
22
+ save(s_profile, 's_profiles')
23
+ end
24
+
25
+ def self.profiles(id = nil)
26
+ if id
27
+ list = id_completion(id, basedir: 's_profiles')
28
+ id = if list.length > 1
29
+ raise "#{list.length} entries found for ID: #{id}. abort..."
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')
35
+ end
36
+ end
37
+
38
+ # XMLタグ定義: https://www.nta.go.jp/users/gensen/oshirase/0019004-159.htm
39
+ #
40
+ TAGS = {
41
+ 'NTAAPP001' => {
42
+ year: 'xml001_B00020',
43
+ family: {
44
+ root: 'xml001_D00000', # 扶養親族情報繰り返し
45
+ kana: 'xml001_D00020', # フリガナ
46
+ name: 'xml001_D00030', # 氏名
47
+ birth_date: {
48
+ root: 'xml001_D00080', # 生年月日
49
+ year: 'xml001_D00090', # 西暦
50
+ month: 'xml001_D00120', # 月
51
+ day: 'xml001_D00130' # 日
52
+ },
53
+ income: 'xml001_D00160', # 本年中の所得の見積額
54
+ elderly: 'xml001_D00140', # 老人扶養親族
55
+ tokutei: 'xml001_D00150', # 特定扶養親族/特定親族
56
+ nonresident: 'xml001_D00170', # 非居住者である親族/控除対象外国外扶養親族
57
+ handicapped: {
58
+ root: 'xml001_D00290', # 障害者である事実
59
+ type: 'xml001_D00300' # 障害者区分
60
+ }
61
+ }
62
+ },
63
+ 'NTAAPP004' => {
64
+ year: 'xml004_B00020',
65
+ spouse: {
66
+ root: 'xml004_D00000',
67
+ kana: 'xml004_D00010', # フリガナ
68
+ name: 'xml004_D00020', # 氏名
69
+ income: 'xml004_D00260', # 配偶者の本年中の合計所得金額の見積額
70
+ elderly: 'xml004_D00160', # 老人控除対象配偶者 (1: 該当)
71
+ nonresident: 'xml004_D00170' # 非居住者である配偶者
72
+ }
73
+ }
74
+ }
75
+
76
+ def self.parse_xml(xml_set)
77
+ h = { 'spouse' => { 'income' => {} }, 'family' => [] }
78
+ xml_set.each do |xml|
79
+ # ルート要素の属性から様式IDを取得
80
+ form_id = xml.root.name
81
+ tags = TAGS[form_id]
82
+ next unless tags
83
+
84
+ year_node = xml.elements["//#{tags[:year]}"]
85
+ year = year_node&.text&.to_i
86
+
87
+ # NTAAPP001: 扶養控除等申告書
88
+ if form_id == 'NTAAPP001'
89
+ xml.elements.each("//#{tags[:family][:root]}") do |dep|
90
+ f = {}
91
+ f['name'] = dep.elements[tags[:family][:name]]&.text
92
+ f['kana'] = dep.elements[tags[:family][:kana]]&.text
93
+
94
+ bd_tags = tags[:family][:birth_date]
95
+ bd_node = dep.elements[bd_tags[:root]]
96
+ if bd_node
97
+ y = bd_node.elements[bd_tags[:year]]&.text
98
+ m = bd_node.elements[bd_tags[:month]]&.text
99
+ d = bd_node.elements[bd_tags[:day]]&.text
100
+ if y && m && d
101
+ f['birth_date'] = Date.new(y.to_i, m.to_i, d.to_i)
102
+ end
103
+ end
104
+
105
+ if year
106
+ dep_income = dep.elements[tags[:family][:income]]&.text
107
+ if dep_income && !dep_income.empty?
108
+ f['income'] = { year => dep_income.to_i }
109
+ end
110
+
111
+ elderly = dep.elements[tags[:family][:elderly]]&.text
112
+ if elderly && (elderly == '1' || elderly == '2')
113
+ f['elderly'] = { year => elderly.to_i }
114
+ end
115
+
116
+ tokutei = dep.elements[tags[:family][:tokutei]]&.text
117
+ if tokutei && (tokutei == '1' || tokutei == '2')
118
+ f['tokutei'] = { year => tokutei.to_i }
119
+ end
120
+
121
+ nonresident = dep.elements[tags[:family][:nonresident]]&.text
122
+ f['nonresident'] = { year => nonresident.to_i } if nonresident == '1'
123
+
124
+ hc_tags = tags[:family][:handicapped]
125
+ hc_node = dep.elements[hc_tags[:root]]
126
+ if hc_node
127
+ hc_type = hc_node.elements[hc_tags[:type]]&.text
128
+ if hc_type && !hc_type.empty? && hc_type != '0'
129
+ f['handicapped'] = { year => hc_type.to_i }
130
+ end
131
+ end
132
+ end
133
+
134
+ h['family'] << f
135
+ end
136
+ end
137
+
138
+ # NTAAPP004: 配偶者控除等申告書
139
+ if form_id == 'NTAAPP004'
140
+ if year
141
+ spouse = xml.elements["//#{tags[:spouse][:root]}"]
142
+ if spouse
143
+ h['spouse']['name'] = spouse.elements[tags[:spouse][:name]]&.text
144
+ h['spouse']['kana'] = spouse.elements[tags[:spouse][:kana]]&.text
145
+
146
+ nonresident = spouse.elements[tags[:spouse][:nonresident]]&.text
147
+ if nonresident == '1'
148
+ h['spouse']['nonresident'] = {} unless h['spouse']['nonresident']
149
+ h['spouse']['nonresident'][year] = nonresident.to_i
150
+ end
151
+
152
+ income = spouse.elements[tags[:spouse][:income]]&.text
153
+ h['spouse']['income'][year] = income.to_i if income && !income.empty?
154
+
155
+ elderly = spouse.elements[tags[:spouse][:elderly]]&.text
156
+ h['spouse']['elderly'] = '1' if elderly == '1'
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ h['family'] = merge_family(h['family']) # 翌年の扶養控除等申告書との重複
163
+ h
164
+ end
165
+
166
+ def self.merge_family(family_list)
167
+ merged = {}
168
+ family_list.each do |f|
169
+ key = [f['name'].gsub(" ", "").strip, f['birth_date']]
170
+ if merged.key?(key)
171
+ target = merged[key]
172
+
173
+ ['income', 'elderly', 'tokutei', 'nonresident', 'handicapped'].each do |field|
174
+ if f[field]
175
+ target[field] ||= {}
176
+ target[field].merge!(f[field])
177
+ end
178
+ end
179
+ else
180
+ merged[key] = f
181
+ end
182
+ end
183
+ merged.values
184
+ end
185
+
186
+ def self.bulk_load(path)
187
+ return enum_for(:bulk_load, path) unless block_given?
188
+
189
+ has_many = if File.directory?(path)
190
+ children = Dir.children(path).map { |c| File.join(path, c) }
191
+ has_subdir = children.any? { |c| File.directory?(c) }
192
+ has_zip = children.any? { |c| File.file?(c) && File.extname(c).downcase == '.zip' }
193
+
194
+ has_subdir || has_zip
195
+ elsif File.file?(path)
196
+ if File.extname(path).downcase == '.zip'
197
+ false
198
+ else
199
+ raise "Unsupported file type: #{path}"
200
+ end
201
+ else
202
+ raise "Path not found: #{path}"
203
+ end
204
+
205
+ if has_many
206
+ # NOTE implement search by content logic
207
+ raise "Multiple import is not supported yet."
208
+
209
+ Dir.children(path).sort.map do |child|
210
+ full_path = File.join(path, child)
211
+ if File.directory?(full_path) || (File.file?(full_path) && File.extname(full_path).downcase == '.zip')
212
+ data = load_xml_export(full_path)
213
+ yield parse_xml(data) unless data.empty?
214
+ else
215
+ nil
216
+ end
217
+ end.compact
218
+ else
219
+ data = load_xml_export(path)
220
+ yield parse_xml(data) unless data.empty?
221
+ end
222
+ end
223
+
224
+ # 年末調整アプリのエクスポートデータ読み込み(非暗号zipまたはzipを解凍したディレクトリをサポート)
225
+ def self.load_xml_export(path)
226
+ docs = []
227
+ if File.directory?(path)
228
+ Dir.children(path).sort.each do |child|
229
+ full_path = File.join(path, child)
230
+ next if File.directory?(full_path)
231
+
232
+ ext = File.extname(child).downcase
233
+ if ext == '.xml'
234
+ begin
235
+ docs << REXML::Document.new(File.read(full_path))
236
+ rescue StandardError => e
237
+ STDERR.puts "#{full_path}: #{e.message}"
238
+ end
239
+ elsif ext == '.zip'
240
+ # skip without warning
241
+ else
242
+ STDERR.puts full_path
243
+ end
244
+ end
245
+ return docs
246
+ end
247
+
248
+ if File.file?(path) && File.extname(path).downcase == '.zip'
249
+ Zip::File.open(path) do |zip|
250
+ zip.each do |entry|
251
+ next if entry.directory?
252
+
253
+ ext = File.extname(entry.name).downcase
254
+ if ext == '.xml'
255
+ begin
256
+ docs << REXML::Document.new(entry.get_input_stream.read)
257
+ rescue StandardError => e
258
+ STDERR.puts "#{entry.name}: #{e.message}"
259
+ end
260
+ elsif ext == '.zip'
261
+ # skip without warning
262
+ else
263
+ STDERR.puts entry.name
264
+ end
265
+ end
266
+ end
267
+ end
268
+ docs
269
+ end
270
+ end
271
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  require 'luca_salary'
3
3
  require 'luca_support/const'
4
+ require 'luca/jp/util'
5
+
4
6
  require 'csv'
5
7
  require 'open3'
6
8
 
@@ -39,7 +41,7 @@ end
39
41
  private
40
42
 
41
43
  def 給与支払報告明細行(slip, company, year)
42
- [
44
+ record = [
43
45
  315, # 法定資料の種類
44
46
  提出義務者(company),
45
47
  0, # 提出区分(新規0, 追加1, 訂正2, 取消3)
@@ -49,18 +51,20 @@ def 給与支払報告明細行(slip, company, year)
49
51
  支払を受ける者の詳細(slip, year),
50
52
  company['tax_id'], # 法人番号
51
53
  支払を受ける者の扶養情報(slip['profile'], year),
52
- slip['911'] == 480_000 ? nil : slip['911'], # 基礎控除の額、48万の場合記載しない
54
+ (year <= 2024 && slip['911'] == 480_000) ? nil : slip['911'], # 基礎控除の額
53
55
  nil, # 所得金額調整控除額 TODO: 未実装 措法41の3の3
54
56
  nil, # ひとり親
55
- 提出先判定(slip), # 必須:作成区分(国税のみ0, 地方のみ1, 両方2)
56
- ].flatten
57
+ ]
58
+ record << [nil, nil, nil] if year >= 2025 # 特定親族控除 TODO: 未実装
59
+ record << 提出先判定(slip) # 必須:作成区分(国税のみ0, 地方のみ1, 両方2)
60
+ record.flatten
57
61
  end
58
62
 
59
63
  def 提出義務者(company)
60
64
  [
61
65
  nil, # 整理番号1
62
66
  nil, # 本支店等区分番号
63
- ['address', 'address2'].map { |attr| company[attr] }
67
+ ['address', 'address2'].map { |attr| Luca::Jp::Util.alphadigit_ja(company[attr]) }
64
68
  .compact.join(' '), # 必須:住所又は所在地
65
69
  company['name'], # 必須:氏名又は名称
66
70
  company['tel'], # 電話番号
@@ -72,7 +76,7 @@ end
72
76
 
73
77
  def 支払を受ける者(profile)
74
78
  [
75
- ['address', 'address2'].map { |attr| profile[attr] }
79
+ ['address', 'address2'].map { |attr| Luca::Jp::Util.alphadigit_ja(profile[attr]) }
76
80
  .compact.join(' '), # 必須:住所又は居所
77
81
  nil, # 国外住所表示(国内は"0"、国外は"1")
78
82
  profile['name'], # 必須:氏名
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.20.4
4
+ version: 0.20.6
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-09 00:00:00.000000000 Z
11
+ date: 2026-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lucabook
@@ -80,6 +80,34 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.3.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: rexml
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubyzip
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: bundler
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -185,6 +213,7 @@ files:
185
213
  - lib/luca_salary/jp.rb
186
214
  - lib/luca_salary/jp/insurance.rb
187
215
  - lib/luca_salary/jp/version.rb
216
+ - lib/luca_salary/jp_adjustment.rb
188
217
  - lib/luca_salary/jp_payreport.rb
189
218
  homepage: https://github.com/chumaltd/luca-jp
190
219
  licenses: