africompta 1.9.10 → 1.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -1
  3. data/Gemfile.lock +14 -8
  4. data/africompta.gemspec +3 -2
  5. data/bin/afri_compta.rb +119 -0
  6. data/bin/africompta.sh +23 -0
  7. data/config.yaml.default +19 -0
  8. data/lib/africompta.rb +7 -1
  9. data/lib/africompta/acaccess.rb +54 -68
  10. data/lib/africompta/acqooxview.rb +10 -10
  11. data/lib/africompta/entities/account.rb +120 -116
  12. data/lib/africompta/entities/config_base.rb +9 -0
  13. data/lib/africompta/entities/remote.rb +240 -6
  14. data/lib/africompta/entities/report.rb +136 -0
  15. data/lib/africompta/entities/users.rb +18 -6
  16. data/lib/africompta/views/compta/admin.rb +135 -0
  17. data/lib/africompta/views/compta/check.rb +245 -0
  18. data/lib/africompta/views/compta/edit_accounts.rb +138 -0
  19. data/lib/africompta/views/compta/edit_movements.rb +167 -0
  20. data/lib/africompta/views/compta/remotes.rb +23 -0
  21. data/lib/africompta/views/compta/tabs.rb +9 -0
  22. data/lib/africompta/views/compta/users.rb +23 -0
  23. data/lib/africompta/views/report/compta_executive.rb +221 -0
  24. data/lib/africompta/views/report/compta_flat.rb +79 -0
  25. data/lib/africompta/views/report/tabs.rb +7 -2
  26. data/po/afri_compta-ar.po +2356 -0
  27. data/po/afri_compta-en.po +2253 -0
  28. data/po/afri_compta-fr.po +4345 -0
  29. data/test/ac_account.rb +176 -0
  30. data/{Test → test}/ac_africompta.rb +0 -0
  31. data/{Test → test}/ac_big.rb +0 -0
  32. data/test/ac_merge.rb +177 -0
  33. data/{Test → test}/ac_movement.rb +14 -1
  34. data/{Test → test}/ac_sqlite.rb +0 -0
  35. data/{Test → test}/config_test.yaml +1 -6
  36. data/test/other.conf +1 -0
  37. data/test/other.rb +18 -0
  38. data/test/test.conf +1 -0
  39. data/{Test → test}/test.rb +7 -14
  40. metadata +63 -18
  41. data/Test/ac_account.rb +0 -128
  42. data/lib/africompta/africompta.rb +0 -83
  43. data/lib/africompta/views/edit/movement.rb +0 -8
  44. data/lib/africompta/views/edit/tabs.rb +0 -8
  45. data/lib/africompta/views/report/annual.rb +0 -3
@@ -0,0 +1,136 @@
1
+ class ReportAccounts < Entities
2
+ def setup_data
3
+ value_entity_account :root, :drop, :path
4
+ value_entity_account :account, :drop, :path
5
+ value_int :level
6
+ end
7
+ end
8
+
9
+
10
+ class Reports < Entities
11
+ def setup_data
12
+ value_str :name
13
+ value_list_entity_reportAccounts :accounts
14
+ end
15
+ end
16
+
17
+ class Report < Entity
18
+ attr_accessor :print_accounts, :print_movements, :print_account
19
+
20
+ def print_account_monthly(acc, start, months, counter)
21
+ stop = (start >> months) - 1
22
+
23
+ line = []
24
+ zeros = false
25
+ @print_account = ''
26
+ @print_accounts = counter.to_f / accounts.count
27
+ p_a = 0
28
+ acc.account.get_tree(acc.level.to_i) { |acc_sub, depth|
29
+ acc_sub.get_tree(depth > 0 ? 0 : -1) { p_a += accounts.count }
30
+ }
31
+ acc.account.get_tree(acc.level.to_i) { |acc_sub, depth|
32
+ dputs(2) { "Doing #{acc_sub.path} - #{depth.inspect}. Done: #{@print_accounts}" }
33
+ sum = Array.new(months) { 0 }
34
+ acc_sub.get_tree(depth > 0 ? 0 : -1) { |acc_sum|
35
+ @print_account = acc_sum.get_path
36
+ @print_accounts += 1.0 / p_a
37
+ p_m = 1.0 / acc_sum.movements.count
38
+ @print_movements = 0
39
+ acc_sum.movements.each { |m|
40
+ @print_movements += p_m
41
+ if (start..stop).include? m.date
42
+ sum[m.date.month - start.month] += m.get_value(acc_sum)
43
+ end
44
+ }
45
+ }
46
+ if sum.inject(false) { |m, o| m |= o != 0 } or line.size == 0
47
+ if line.size == 0
48
+ zeros = true
49
+ elsif zeros
50
+ line = []
51
+ zeros = false
52
+ end
53
+ line.push [acc_sub.path, sum + [sum.inject(:+)]]
54
+ end
55
+ }
56
+ line
57
+ end
58
+
59
+ def print_heading_monthly(start = Date.today, months = 12)
60
+ ['Period', (0...months).collect { |m|
61
+ (start >> m).strftime('%Y/%m')
62
+ } + ['Sum']]
63
+ end
64
+
65
+ def print_list_monthly(start = Date.today, months = 12)
66
+ stop = start >> (months - 1)
67
+ acc_counter = -1
68
+ list = accounts.collect { |acc|
69
+ line = print_account_monthly(acc, start, months, acc_counter += 1)
70
+ if line.size > 1
71
+ line.push ['Sum', line.reduce(Array.new(months+1, 0)) { |memo, obj|
72
+ dputs(2) { "#{memo}, #{obj.inspect}" }
73
+ memo = memo.zip(obj[1]).map { |a, b| a + b }
74
+ }]
75
+ end
76
+ line
77
+ }
78
+ if list.size > 1
79
+ list + [[['Total', running = list.reduce(Array.new(months+1, 0)) { |memo, obj|
80
+ dputs(2) { "#{memo}, #{obj.inspect}" }
81
+ memo = memo.zip(obj.last.last).map { |a, b| a + b }
82
+ }],
83
+ ['Running', running[0..-2].reduce([]) { |m, o| m + [(m.last || 0) + o] }]]]
84
+ else
85
+ list
86
+ end
87
+ end
88
+
89
+ def print(start = Date.today, months = 12)
90
+
91
+ end
92
+
93
+ def listp_accounts
94
+ accounts.collect { |a|
95
+ [a.id, "#{a.level}: #{a.account.path}"]
96
+ }
97
+ end
98
+
99
+ def print_pdf_monthly(start = Date.today, months = 12)
100
+ stop = start >> (months - 1)
101
+ file = "/tmp/report_#{name}.pdf"
102
+ Prawn::Document.generate(file,
103
+ :page_size => 'A4',
104
+ :page_layout => :landscape,
105
+ :bottom_margin => 2.cm) do |pdf|
106
+
107
+ pdf.text "Report for #{name}",
108
+ :align => :center, :size => 20
109
+ pdf.font_size 7
110
+ pdf.text "From #{start.strftime('%Y/%m')} to #{stop.strftime('%Y/%m')}"
111
+ pdf.move_down 1.cm
112
+
113
+ pdf.table([print_heading_monthly(start, months).flatten.collect { |ch|
114
+ {:content => ch, :align => :center} }] +
115
+ print_list_monthly(start, months).collect { |acc|
116
+ acc.collect { |a, values|
117
+ [a] + values.collect { |v| Account.total_form(v) }
118
+ }
119
+ }.flatten(1).collect { |line|
120
+ a, s = (line[0] =~ /::/ ? [:left, :normal] : [:right, :bold])
121
+ [{:content => line.shift, :align => a, :font_style => s}] +
122
+ line.collect { |v|
123
+ {:content => v, :align => :right, :font_style => s} }
124
+ },
125
+ :header => true)
126
+ pdf.move_down(2.cm)
127
+
128
+ pdf.repeat(:all, :dynamic => true) do
129
+ pdf.draw_text "#{Date.today} - #{name}",
130
+ :at => [0, -20], :size => 10
131
+ pdf.draw_text pdf.page_number, :at => [18.cm, -20]
132
+ end
133
+ end
134
+ file
135
+ end
136
+ end
@@ -14,18 +14,25 @@ class Users < Entities
14
14
  value_int :movement_index
15
15
  end
16
16
 
17
+ def init
18
+ user = Users.create('local', Digest::MD5.hexdigest((rand 2**128).to_s).to_s,
19
+ rand(2 ** 128).to_s)
20
+ user.account_index, user.movement_index = 0, 0
21
+ dputs(1) { "Created local user #{user}" }
22
+ user
23
+ end
24
+
17
25
  def load
18
26
  super
19
27
  if Users.search_by_name('local').count == 0
20
- user = Users.create('local', Digest::MD5.hexdigest((rand 2**128).to_s).to_s,
21
- rand(2 ** 128).to_s)
22
- dputs(1) { "Created local user #{user}" }
28
+ dputs(0) { 'User init not here' }
29
+ init
23
30
  end
24
31
  end
25
32
 
26
33
  def migration_1(u)
27
- u.account_index ||= 0
28
- u.movement_index ||= 0
34
+ u.account_index ||= -1
35
+ u.movement_index ||= -1
29
36
  end
30
37
 
31
38
  def create(name, full = nil, pass = nil)
@@ -34,7 +41,7 @@ class Users < Entities
34
41
  name, full, pass = name[:name], name[:full], name[:pass]
35
42
  end
36
43
  new_user = super(:name => name, :full => full, :pass => pass)
37
- new_user.account_index, new_user.movement_index = 0, 0
44
+ new_user.account_index, new_user.movement_index = -1, -1
38
45
  new_user
39
46
  end
40
47
  end
@@ -52,4 +59,9 @@ class User < Entity
52
59
  update_movement_index
53
60
  update_account_index
54
61
  end
62
+
63
+ def reset_id
64
+ self.full = Digest::MD5.hexdigest((rand 2**128).to_s).to_s
65
+ end
66
+
55
67
  end
@@ -0,0 +1,135 @@
1
+ class ComptaAdmin < View
2
+ def layout
3
+ @order = 300
4
+ @rsync_log = '/tmp/update_africompta'
5
+
6
+ gui_vbox do
7
+ gui_hbox do
8
+ show_button :merge
9
+ end
10
+ gui_hbox do
11
+ show_button :update_program
12
+ end
13
+ gui_hbox do
14
+ show_button :archive, :clean_up, :connect_server
15
+ end
16
+ gui_window :result do
17
+ show_html :txt
18
+ show_button :close
19
+ end
20
+
21
+ gui_window :get_server do
22
+ show_str :url, width: 200
23
+ show_str :user
24
+ show_str :pass
25
+ show_button :copy_from_server
26
+ end
27
+ end
28
+ end
29
+
30
+ def rpc_update_view(session)
31
+ super(session) +
32
+ reply_visible(ConfigBase.has_function?(:accounting_standalone),
33
+ [:connect_server, :update_program])
34
+ end
35
+
36
+ def rpc_button_archive(session, data)
37
+ Accounts.archive
38
+ end
39
+
40
+ def rpc_button_clean_up(session, data)
41
+ count_mov, bad_mov,
42
+ count_acc, bad_acc = AccountRoot.clean
43
+ reply(:window_show, :result) +
44
+ reply(:update, :txt => "Movements total / bad: #{count_mov}/#{bad_mov}<br>" +
45
+ "Accounts total / bad: #{count_acc}/#{bad_acc}")
46
+ end
47
+
48
+ def rpc_button_connect_server(session, data)
49
+ reply(:window_show, :get_server) +
50
+ reply(:update, url: 'http://localhost:3303/acaccess',
51
+ user: 'other', pass: 'other')
52
+ end
53
+
54
+ def rpc_button_copy_from_server(session, data)
55
+ remote = Remotes.get_db(data._url, data._user, data._pass)
56
+ reply(:window_hide) +
57
+ reply(:window_show, :result) +
58
+ if remote.class == Remote
59
+ reply(:update, txt: 'Copying was successful.')
60
+ else
61
+ reply(:update, txt: "Error while copying:\n#{remote}")
62
+ end
63
+ end
64
+
65
+ def rpc_button_close(session, data)
66
+ reply(:window_hide)
67
+ end
68
+
69
+ def rpc_button_merge(session, data)
70
+ @merge = Thread.new do
71
+ System.rescue_all do
72
+ (@remote = Remotes.search_all_.first).do_merge
73
+ end
74
+ end
75
+ session.s_data._compta_admin = :merge
76
+ reply(:window_show, :result) +
77
+ reply(:update, txt: 'Starting merge') +
78
+ reply(:auto_update, -1)
79
+ end
80
+
81
+ def rpc_update_with_values(session, data)
82
+ case session.s_data._compta_admin
83
+ when :merge
84
+ stat_str = %w(got_accounts put_accounts got_movements put_movements
85
+ put_movements_changes).collect { |v| "#{v}: #{@remote.send(v)}" }.
86
+ join('<br>')
87
+ ret = @remote.step =~ /^done/ ? reply(:auto_update, 0) : []
88
+ ret + reply(:update, txt: "Merge-step: #{@remote.step}<br>" + stat_str)
89
+ when :update_program
90
+ stat = IO.read(@rsync_log).split("\r")
91
+ if Process.waitpid(session.s_data._update_program, Process::WNOHANG)
92
+ session.s_data._update_program = nil
93
+ reply(:update, txt: 'Update finished<br>eventual non-100% total is normal<br>' +
94
+ stat.last(2).join('<br>')) +
95
+ reply(:auto_update, 0)
96
+ else
97
+ reply(:update, txt: 'Updating<br>' + stat.last.to_s)
98
+ end
99
+ else
100
+ dputs(0) { "Updating with #{data.inspect} and #{session.inspect}" }
101
+ end
102
+ end
103
+
104
+ def rpc_button_close(session, data)
105
+ if session.s_data._compta_admin == :update_program
106
+ if (pid = session.s_data._update_program) &&
107
+ !Process.waitpid(pid, Process::WNOHANG)
108
+ log_msg :update_program, 'Killing rsync'
109
+ Process.kill('KILL', pid)
110
+ end
111
+ end
112
+ reply(:window_hide) +
113
+ reply(:auto_update, 0)
114
+ end
115
+
116
+ def rpc_button_update_program(session, data)
117
+ session.s_data._compta_admin = :update_program
118
+ remote_path = 'profeda.org::africompta-mac'
119
+ if System.run_str('pwd') =~ /AfriCompta\.app/
120
+ log_msg :update_africompta, 'Updating real app'
121
+ bwlimit = ''
122
+ local_path = '../../../..'
123
+ else
124
+ log_msg :update_africompta, 'Simulating update in /tmp'
125
+ bwlimit = '--bwlimit=10k'
126
+ local_path = '/tmp'
127
+ end
128
+ ac_cmd = "rsync -az --delete --info=progress2 #{bwlimit} #{remote_path} #{local_path}"
129
+ session.s_data._update_program = spawn(ac_cmd, :out => @rsync_log)
130
+
131
+ reply(:window_show, :result) +
132
+ reply(:update, txt: 'Starting update') +
133
+ reply(:auto_update, -1)
134
+ end
135
+ end
@@ -0,0 +1,245 @@
1
+ class ComptaCheck < View
2
+ def layout
3
+ @order = 400
4
+ gui_vbox do
5
+ gui_hbox :nogroup do
6
+ gui_vbox :nogroup do
7
+ show_int_ro :accounts_only_db
8
+ show_int_ro :accounts_only_server
9
+ show_int_ro :accounts_mixed
10
+ end
11
+ gui_vboxg :nogroup do
12
+ show_table :accounts, :headings => [:Check, :Name, :Total],
13
+ :widths => [100, 300, 75], :height => 400,
14
+ :columns => [0, 0, :align_right]
15
+ show_button :accounts_delete, :accounts_copy
16
+ end
17
+ end
18
+
19
+ gui_hbox :nogroup do
20
+ gui_vbox :nogroup do
21
+ show_int_ro :movements_only_db
22
+ show_int_ro :movements_only_server
23
+ show_int_ro :movements_mixed
24
+
25
+ end
26
+ gui_vboxg :nogroup do
27
+ show_table :movements, :headings => [:Check, :Date, :Desc, :Value, :Src, :Dst],
28
+ :widths => [100, 100, 300, 100, 100, 100], :height => 400,
29
+ :columns => [0, 0, 0, :align_right, 0, 0], :flexwidth => 1
30
+ show_button :movements_delete, :movements_copy
31
+ end
32
+ end
33
+
34
+ gui_hbox :nogroup do
35
+ show_upload :upload_db, :callback => true
36
+ end
37
+
38
+ gui_window :progress do
39
+ show_html :progress_txt
40
+ show_button :close, :continue
41
+ end
42
+ end
43
+ end
44
+
45
+ def check_split(arr)
46
+ [arr[0].collect { |a| a.split(/[\r\t]/) },
47
+ arr[1].collect { |a, b|
48
+ [a.split(/[\r\t]/),
49
+ b.split(/[\r\t]/)]
50
+ },
51
+ arr[2].collect { |a| a.split(/[\r\t]/) }]
52
+ end
53
+
54
+ def start_thread(session, mod)
55
+ session.s_data._check_module = mod
56
+ Thread.start {
57
+ System.rescue_all do
58
+ case mod
59
+ when /Accounts/
60
+ dputs(2) { 'Starting check_accounts' }
61
+ session.s_data._check_accounts =
62
+ check_split(orig = Accounts.check_against_db(session._s_data._filename))
63
+ session.s_data._check_accounts_orig = orig
64
+ when /Movements/
65
+ dputs(2) { 'Starting check_movements' }
66
+ session.s_data._check_movements =
67
+ check_split(orig = Movements.check_against_db(session._s_data._filename))
68
+ session.s_data._check_movements_orig = orig
69
+ end
70
+ end
71
+ }
72
+ sleep 1
73
+ end
74
+
75
+ def get_accounts(accounts, row)
76
+ accounts.each { |a|
77
+ row[a] = (acc = Accounts.match_by_global_id(row[a])) ? acc.name : 'nil'
78
+ }
79
+ row
80
+ end
81
+
82
+ def collect_table(id, check, rows, cols, accounts = [])
83
+ level = check.class == String ? 0 : 1
84
+ counter = 0
85
+ rows.collect { |r|
86
+ counter += 1
87
+ if check.class == String
88
+ c = get_accounts(accounts, r)
89
+ ["#{id},#{counter}", cols.collect { |col_nbr| c[col_nbr] }.unshift(check)]
90
+ else
91
+ c1 = get_accounts(accounts, r[0])
92
+ c2 = get_accounts(accounts, r[1])
93
+ [["#{id},#{counter}", cols.collect { |col_nbr| c1[col_nbr] }.unshift(check[0])],
94
+ ["#{id},#{counter}", cols.collect { |col_nbr| c2[col_nbr] }.unshift(check[1])]]
95
+ end
96
+ }.flatten(level)
97
+ end
98
+
99
+ def update_counters_tables(session, data)
100
+ accs, movs = session.s_data._check_accounts, session.s_data._check_movements
101
+ dputs(2) { "Done. accs, movs = #{accs.collect { |a| a.size }} - "+
102
+ "#{movs.collect { |m| m.size }}" }
103
+ reply(:window_hide) +
104
+ reply(:auto_update, 0) +
105
+ reply(:update, accounts_only_db: accs[0].size, accounts_only_server: accs[2].size,
106
+ accounts_mixed: accs[1].size,
107
+ movements_only_db: movs[0].size, movements_only_server: movs[2].size,
108
+ movements_mixed: movs[1].size) +
109
+ reply(:update, accounts: collect_table(0, 'File only', accs[0], [3, 2]) +
110
+ collect_table(2, 'Server only', accs[2], [3, 2]) +
111
+ collect_table(1, ['Mix - File', 'Mix - Server'], accs[1], [3, 2]),
112
+ movements: collect_table(0, 'File only', movs[0], [3, 0, 2, 4, 5], [4, 5]) +
113
+ collect_table(2, 'Server only', movs[2], [3, 0, 2, 4, 5], [4, 5]) +
114
+ collect_table(1, ['Mix - File', 'Mix - Server'], movs[1], [3, 0, 2, 4, 5], [4, 5]))
115
+ end
116
+
117
+ def rpc_update_with_values(session, data)
118
+ state, progress = case mod = session.s_data._check_module
119
+ when /Accounts/
120
+ [Accounts.check_state, Accounts.check_progress]
121
+ when /Movements/
122
+ [Movements.check_state, Movements.check_progress]
123
+ else
124
+ ['Error', 0]
125
+ end
126
+ dputs(3) { "Found module #{mod} with state #{state}: #{progress}" }
127
+ ret = reply(:update, :progress_txt => "#{mod}: #{state} - #{(progress * 100).floor}%")
128
+ if state == 'Done'
129
+ if mod == :Accounts
130
+ dputs(2) { 'Module Accounts done, starting Movements' }
131
+ start_thread(session, :Movements)
132
+ else
133
+ ret += update_counters_tables(session, data)
134
+ end
135
+ end
136
+ ret
137
+ end
138
+
139
+ def rpc_button_upload_db(session, data)
140
+ session._s_data._filename = "/tmp/#{UploadFiles.escape_chars(data._filename)}"
141
+ reply(:window_show, :progress) +
142
+ reply(:update, progress_txt: "Uploaded file #{session._s_data._filename}") +
143
+ reply(:unhide, :continue) +
144
+ rpc_button_continue(session, data)
145
+ end
146
+
147
+ def rpc_button_continue(session, data)
148
+ start_thread(session, :Accounts)
149
+ reply(:auto_update, -1) +
150
+ reply(:update, progress_txt: 'Starting checking') +
151
+ reply(:hide, :continue)
152
+ end
153
+
154
+ def get_entities(ent, rows, table, gid)
155
+ table.collect { |t|
156
+ r, ind = t.split(',').collect { |i| i.to_i }
157
+ [r, ind-1, if r == 1
158
+ ent.match_by_global_id(rows[r][ind-1][1][gid])
159
+ else
160
+ ent.match_by_global_id(rows[r][ind-1][gid])
161
+ end]
162
+ }
163
+ end
164
+
165
+ def action(session, ent, table, what)
166
+ case ent.to_s
167
+ when /Movements/
168
+ rows = session.s_data._check_movements
169
+ rows_orig = session.s_data._check_movements_orig
170
+ when /Accounts/
171
+ rows = session.s_data._check_accounts
172
+ rows_orig = session.s_data._check_accounts_orig
173
+ end
174
+ gid = ent == Movements ? 1 : 1
175
+ msgs = []
176
+ get_entities(ent, rows, table, gid).
177
+ each { |source, ind, e|
178
+ case ent.to_s
179
+ when /Movements/
180
+ case what
181
+ when :copy
182
+ case source
183
+ when 0
184
+ mov = Movements.from_s(rows_orig[0][ind])
185
+ msgs.push "Created movement #{mov.desc}"
186
+ when 1
187
+ mov = Movements.from_s(rows_orig[1][ind][0])
188
+ msgs.push "Created movement #{mov.desc}"
189
+ when 2
190
+ e.new_index
191
+ msgs.push "Updated index of #{e.desc}"
192
+ end
193
+ when :delete
194
+ if source > 0
195
+ e.delete
196
+ msgs.push 'Deleted movement'
197
+ else
198
+ msgs.push "Didn't delete movement from file"
199
+ end
200
+ end
201
+ when /Accounts/
202
+ case what
203
+ when :copy
204
+ if source == 0
205
+ acc = Accounts.from_s(rows_orig[0][ind])
206
+ msgs.push "Copied account #{acc.name} from file"
207
+ else
208
+ msgs.push "Didn't copy account from server to file or clean-up mixed"
209
+ end
210
+ when :delete
211
+ if source > 0
212
+ if e.is_empty
213
+ e.delete
214
+ msgs.push "Deleted account #{e.get_path}"
215
+ else
216
+ msgs.push "Account #{e.get_path} is not empty"
217
+ end
218
+ else
219
+ msgs.push "Can't delete account in file"
220
+ end
221
+ end
222
+ end
223
+ }
224
+ reply(:window_show, :progress) +
225
+ reply(:update, progress_txt: "Done. Msgs:<br><pre>#{msgs.join("\n")}</pre>") +
226
+ reply(:hide, :continue)
227
+ end
228
+
229
+ def rpc_button_movements_copy(session, data)
230
+ action(session, Movements, data._movements, :copy)
231
+ end
232
+
233
+ def rpc_button_movements_delete(session, data)
234
+ action(session, Movements, data._movements, :delete)
235
+ end
236
+
237
+ def rpc_button_accounts_copy(session, data)
238
+ action(session, Accounts, data._accounts, :copy)
239
+ end
240
+
241
+ def rpc_button_accounts_delete(session, data)
242
+ action(session, Accounts, data._accounts, :delete)
243
+ end
244
+
245
+ end