unipump 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 47b3ded4adb8542b7c97b5163ad60d83b2415ded901121e205451977d2269fcf
4
+ data.tar.gz: 62853432f82b5cd7a64ec0ad16fb24efe8e71a2ab59e7aab7e0971dff6f09065
5
+ SHA512:
6
+ metadata.gz: 00c5a85ec55dfb5e29fd2a01d7255a2fd08b421686bb2d23f5aa1f8c7ca3a455061f23e7940d9b0efc170ca337fcd02a3048f7cea526e98e2a6e690d39e8095a
7
+ data.tar.gz: 3d2350a002c0e4f4b569ee7fb39868d036cd896e04954143b9a774d766af48c36b398deb69c738efd4136745931a9865b107a97bd23648045a0d0d6c16706b0a
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.1.2
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Timepump
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/unipump`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/unipump.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/TODO ADDED
@@ -0,0 +1,5 @@
1
+ o Hvad med engelsk/dansk tekst?
2
+ o Diskuter record-by-record update af TIMEREG med Hans: Kan det ske, at der er
3
+ records, som ikke kommer med i v_fakturalinie?
4
+
5
+ + Log
data/exe/unipump ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'shellopts'
5
+ require 'unipump.rb'
6
+
7
+ SPEC = %(
8
+ @ Upload invoice lines to Uniconta
9
+
10
+ -- END-DATE [ACCOUNT...]
11
+
12
+ Uploads outstanding invoice lines to Uniconta and updates the
13
+ TIDSREG.FAKTURADATO field in the database. Note that END-DATE is inclusive
14
+
15
+ unipump will process the given orders or all available orders if ACCOUNTs are
16
+ absent
17
+
18
+ -e,environment=ENV
19
+ Set the run-time environment to 'production' or 'test'. In the test
20
+ environment, data will be uploaded to the Uniconta test system and
21
+ TIMEREG_TEST is updated in the database instead of the TIMEREG table. The
22
+ default is 'test'
23
+
24
+ FIXME: Remove this. Only used to test sagsys update
25
+
26
+ -U,username=USERNAME
27
+ Uniconta username. Note that this is insecure. Use a credentials file for
28
+ better security
29
+
30
+ --password=PASSWORD
31
+ Uniconta password. Note that this is insecure. Use a credentials file for
32
+ better security
33
+
34
+ -c,credentials=FILE
35
+ Credentials file. Default is ./.unipump.environment
36
+
37
+ -l,log=FILE
38
+ Log file. The file is only appended to
39
+
40
+ -x,xlog=FILE
41
+ Transaction log file. It is used to save the affected timereg records so
42
+ that we can recover if the database crashes after orders have been
43
+ uploaded. It is a manual operation to fix the database, though. Note that
44
+ it is an error if the file exists because it indicates that the last run
45
+ failed. The default is 'unipump.transaction'
46
+
47
+ -j,jlog=FILE
48
+ Write generated JSON data to FILE
49
+
50
+ -q,quiet
51
+ Be quiet. Messages will still be written to the log file
52
+
53
+ -d,dry-run
54
+ Don't do any changes
55
+ )
56
+
57
+ opts, args = ShellOpts.process(SPEC, ARGV)
58
+
59
+ # Get end date and optinally account numbers
60
+ end_date, *accounts = args.extract(1..)
61
+ end_date =~ /^\d\d\d\d-\d\d-\d\d$/ or ShellOpts.error "Illegal date: '#{end_date}'. Use YYYY-MM-DD"
62
+
63
+ # Get username/password
64
+ if opts.username
65
+ if !opts.password
66
+ ShellOpts.error "Password is required"
67
+ else
68
+ username = opts.username
69
+ password = opts.password
70
+ end
71
+ elsif opts.password
72
+ ShellOpts.error "Username is required"
73
+ else
74
+ credentials_file = opts.credentials || Pump::DEFAULT_CREDENTIALS_FILE
75
+ if !File.exist?(credentials_file)
76
+ ShellOpts.error "Can't find credentials file '#{credentials_file}'"
77
+ else
78
+ yaml = YAML.load(IO.read credentials_file)
79
+ username = yaml['username']
80
+ password = yaml['password']
81
+ username or ShellOpts.error "Can't find username in '#{credentials_file}'"
82
+ password or ShellOpts.error "Can't find password in '#{credentials_file}'"
83
+ end
84
+ end
85
+
86
+ # Get environment
87
+ environment = opts.environment || Pump::DEFAULT_ENVIRONMENT
88
+ %w(test prod).include?(environment) or ShellOpts.error "Illegal environment '#{environment}'"
89
+
90
+ # Pump object
91
+ pump = Pump.new(
92
+ username,
93
+ password,
94
+ environment,
95
+ log: opts.log,
96
+ xlog: opts.xlog,
97
+ jlog: opts.jlog,
98
+ dry_run: opts.dry_run?,
99
+ quiet: opts.quiet?)
100
+
101
+ # Pumping
102
+ pump.upload(end_date, accounts)
103
+
@@ -0,0 +1,20 @@
1
+ module PloneLogin
2
+ module Credentials
3
+ def self.load
4
+ credentials = {
5
+ host: ENV['PLONE_LOGIN_HOST'],
6
+ port: ENV['PLONE_LOGIN_PORT'],
7
+ database: ENV['PLONE_LOGIN_DATABASE'],
8
+ username: ENV['PLONE_LOGIN_USERNAME'],
9
+ password: ENV['PLONE_LOGIN_PASSWORD']
10
+ }
11
+
12
+ credentials.each { |k,v|
13
+ next if [:port, :password].include?(k)
14
+ !v.nil? or raise "Undefined connection parameter: #{k}. Please define PLONE_LOGIN_#{k.to_s.upcase}"
15
+ }
16
+
17
+ credentials
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,58 @@
1
+ require "tiny_tds"
2
+
3
+ module UniPump
4
+ module MsSql
5
+ CREDENTIALS = {
6
+ host: "mary.danak.lan",
7
+ port: 1435,
8
+ database: "sagsys",
9
+ username: "sa",
10
+ password: "JHe1Mff!"
11
+ }
12
+
13
+ def self.connect(credentials = CREDENTIALS) TinyTds::Client.new(credentials) end
14
+ def self.active?(conn) conn.active? end
15
+ def self.close(conn) conn.close end
16
+ def self.exec(conn, sql) conn.execute(sql) end
17
+ def self.size(result) result.each.size end
18
+ def self.each(result, &block) result.each(&block) end
19
+ end
20
+
21
+ class Connection
22
+ def initialize(credentials = MsSql::CREDENTIALS)
23
+ @connection = MsSql.connect(credentials)
24
+ ObjectSpace.define_finalizer(self, self.class.finalize(@connection))
25
+ end
26
+
27
+ def connected?() return MsSql.active?(@connection) rescue MsSql::Error; false end
28
+ def close() MsSql.close(@connection) if connected? end
29
+ def exec(sql) do_exec(sql).each end
30
+ def structs(sql) do_exec(sql).each.map { |row| OpenStruct.new(row) } end
31
+ def records(sql) do_exec(sql).each.map end
32
+
33
+ private
34
+ def self.finalize(connection) proc { do_finalize(connection) } end
35
+ # def self.do_finalize(connection) MsSql.close(connection) end
36
+ def self.do_finalize(connection) MsSql.close(connection) if MsSql.active?(connection) end
37
+
38
+ def do_exec(sql) Result.new(MsSql.exec(@connection, sql)) end
39
+ end
40
+
41
+ class Result
42
+ attr_reader :result
43
+ def size() MsSql.size(@result) end
44
+ def each(&block) MsSql.each(@result, &block) end
45
+ private
46
+ def initialize(result) @result = result end
47
+ end
48
+
49
+ def self.connect(credentials, &block)
50
+ conn = Connection.new(credentials)
51
+ begin
52
+ yield(conn)
53
+ ensure
54
+ conn.close
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UniPump
4
+ VERSION = "0.2.0"
5
+ end
data/lib/unipump.rb ADDED
@@ -0,0 +1,435 @@
1
+ # frozen_string_literal: true
2
+
3
+ # NOTE
4
+ # Test cases
5
+ # 100149 100780 100502 100707 100080 100082 101948 101864 100094
6
+ #
7
+ # TODO
8
+ # o Use v_fakturalinie_test
9
+ # o More filtering in v_fakturalinie
10
+ # o Hardcoded 24 mill.
11
+ # o Add a --reset-test option/command
12
+ # o Fixe typer på ordrenummer, kontonummer, (pris), timer. Både i
13
+ # v_fakturalinie og sag
14
+ # o Fixe curr_comma hack
15
+ # o Check for Net::ReadTimeout
16
+ #
17
+ # + .unipump.environment
18
+ # + Move #emit out of update methods
19
+
20
+ require 'json'
21
+ require 'net/http'
22
+ require 'uri'
23
+ require 'base64'
24
+ require 'fileutils'
25
+ require 'indented_io'
26
+
27
+ require_relative "unipump/version"
28
+ require_relative "unipump/mssql"
29
+
30
+ module UniPump; end
31
+
32
+ class Pump
33
+ class Error < StandardError; end
34
+
35
+ DEFAULT_CREDENTIALS_FILE = ".unipump.environment"
36
+ DEFAULT_ENVIRONMENT = "test"
37
+
38
+ # Environment-dependent constants
39
+ TEST_SYSTEM = "81470" # Uniconta test system ID
40
+ PROD_SYSTEM = nil
41
+ TEST_TIMEREG_TABLE = "tidsreg_test"
42
+ PROD_TIMEREG_TABLE = "tidsreg"
43
+ TEST_FAKTURALINIE_VIEW = "v_fakturalinie_test"
44
+ PROD_FAKTURALINIE_VIEW = "v_fakturalinie"
45
+
46
+ # Transaction file. This contains IDs of the most recently uploaded TIDSREG
47
+ # records. It is cleared when the corresponding records in the database are
48
+ # updated
49
+ TRANSACTION_FILE = "unipump.transaction"
50
+
51
+ attr_reader :username
52
+ attr_reader :password
53
+ attr_reader :environment # Run-time environment. "prod" or "test"
54
+ attr_reader :log # Path to log file
55
+ attr_reader :xlog # Path to transaction log file
56
+ attr_reader :jlog # Path to JSON log file or nil
57
+ attr_reader :quiet, :dry_run # Standard options
58
+
59
+ attr_reader :log_file # Log file object
60
+ attr_reader :conn # Database connection
61
+
62
+ attr_reader :curr_comma # Hack FIXME
63
+
64
+ # Environment-dependent resources
65
+ def system = (environment == "prod" ? PROD_SYSTEM : TEST_SYSTEM)
66
+ def tidsreg_table = (environment == "prod" ? PROD_TIMEREG_TABLE : TEST_TIMEREG_TABLE)
67
+ def fakturalinie_view = (environment == "prod" ? PROD_FAKTURALINIE_VIEW : TEST_FAKTURALINIE_VIEW)
68
+ def root_url = "https://odata.uniconta.com/api/Entities/#{system}"
69
+ def order_list_url = "#{root_url}/DebtorOrderClient"
70
+ def insert_order_list_url = "#{root_url}/InsertList/DebtorOrderClient"
71
+ def insert_order_line_list_url = "#{root_url}/InsertList/DebtorOrderLineClient"
72
+
73
+ def initialize(
74
+ username, password, environment,
75
+ log: nil, xlog: nil, jlog: nil,
76
+ dry_run: false, quiet: false)
77
+ @environment = environment
78
+ @username = username
79
+ @password = password
80
+ @log = log || '/dev/null'
81
+ @xlog = xlog || TRANSACTION_FILE
82
+ @jlog = jlog
83
+ @dry_run = dry_run
84
+ @quiet = quiet
85
+ @log_file = File.open(@log, "a")
86
+ @conn = UniPump::Connection.new
87
+ @curr_comma = ""
88
+ !File.exist?(@xlog) or ShellOpts::error "Found transaction file: #{@xlog}. Please fix this"
89
+ FileUtils.rm_f(@jlog) if @jlog
90
+ end
91
+
92
+ def upload(end_date, accounts)
93
+ t0 = Time.now
94
+
95
+ emit("#{ShellOpts.instance.name} @ #{Time.now.strftime("%F %T")}")
96
+
97
+ # Select specific accounts. Mostly for debug
98
+ konto_constraint = (accounts.empty? ? "and 1 = 1" : "and kontonummer in (#{accounts.join(', ')})")
99
+
100
+ # Fetch existing Uniconta orders
101
+ uniconta_order_numbers =
102
+ emit_exec(" Fetch orders") { get_uniconta_order_numbers(order_list_url) }
103
+ emit " Found #{uniconta_order_numbers.size} orders in Uniconta"
104
+
105
+ # Compute orders that are does not exist in Uniconta
106
+ uniconta_order_numbers.reject! { |order_number|
107
+ if order_number.to_s.size < 8
108
+ emit " Illegal order number: '#{order_number}', ignored"
109
+ elsif order_number < 24000000
110
+ emit " Outdated order number: '#{order_number}', ignored"
111
+ end
112
+ }
113
+
114
+ # Find order with lines that have not been pumped yet (lines with
115
+ # fakturadato equal to null)
116
+ orders = conn.structs %(
117
+ select distinct
118
+ sagsnummer,
119
+ ansvarlig,
120
+ kontonummer,
121
+ ordrenummer
122
+ from #{fakturalinie_view}
123
+ where dato < '#{end_date}'
124
+ and fakturadato is null
125
+ #{konto_constraint}
126
+ order by ansvarlig, sagsnummer
127
+ )
128
+ emit " Found #{orders.size} orders in Sagsys"
129
+
130
+ # Exclude orders with kontonummer equal to nil
131
+ excluded = []
132
+ orders.delete_if { |order|
133
+ if order.kontonummer.nil?
134
+ excluded << order
135
+ end
136
+ }
137
+ emit " #{excluded.size} with absent kontonummer - ignored" if !excluded.empty?
138
+
139
+ # Compute order numbers that should be created. This is computed as a Set
140
+ # for fast lookup
141
+ missing_order_numbers = Set.new(orders.map(&:ordrenummer).map(&:to_i) - uniconta_order_numbers)
142
+
143
+ # create order in Uniconta
144
+ emit " Create #{missing_order_numbers.size} orders"
145
+ for order in orders
146
+ if missing_order_numbers.include?(order.ordrenummer.to_i)
147
+ json = format_order(order)
148
+ emit_exec(" #{order.ordrenummer}") { upload_json(insert_order_list_url, json) }
149
+ end
150
+ end
151
+
152
+ # Process each order
153
+ emit " Upload orders"
154
+ for order in orders
155
+ # We want to save a list of uploaded tidsreg records in case the
156
+ # database update fails after the invoice lines have been uploaded to
157
+ # Uniconta, but we also want the database to aggregate the tidsreg
158
+ # records for us so we end up with the two queries below. It is
159
+ # important the the two queries selects the same records so we use the
160
+ # common method #line_constraint to supply the where constraint
161
+ #
162
+ # The list is written to a FILE that is removed after the records have
163
+ # been updated sucessfully. It is an error if it exists already
164
+ #
165
+ line_constraint = %(
166
+ dato <= '#{end_date}'
167
+ and ordrenummer = '#{order.ordrenummer}'
168
+ and fakturadato is null
169
+ #{konto_constraint}
170
+ )
171
+
172
+ # Begin transaction. We hope that the default isolation level is
173
+ # resonable in MsSql
174
+ conn.exec("begin transaction")
175
+
176
+ # Find keys for affected TIMEREG records. These are saved to disk so
177
+ # that we may recover from a database failure after the order lines
178
+ # have been uploaded
179
+ tidsreg_keys = conn.records %(
180
+ select
181
+ meid,
182
+ said,
183
+ acid,
184
+ dato
185
+ from #{fakturalinie_view}
186
+ where #{line_constraint}
187
+ order by sagsnummer
188
+ )
189
+
190
+ # Save tidsreg keys to disk
191
+ File.open(xlog, "w") { |xfile|
192
+ xfile.puts "# Ordrenummer: #{order.ordrenummer} (#{order.sagsnummer})"
193
+ xfile.puts tidsreg_keys.to_a
194
+ }
195
+
196
+ # Find the order lines. These are aggregated tidsreg records and could
197
+ # be coalesced into the previous select but then we would have to do
198
+ # grouping and ordering in ruby. Note that the select clause in the pre
199
+ order_lines = conn.structs %(
200
+ select
201
+ ordrenummer,
202
+ medarbejderogrolle as "rolle",
203
+ maaned,
204
+ tekst + ': ' + md + ' ' + cast(aar as nvarchar) as "tekst",
205
+ ydelse,
206
+ sum(timer) as antal,
207
+ pris
208
+ from #{fakturalinie_view} f
209
+ where #{line_constraint}
210
+ group by ordrenummer, medarbejderogrolle, acid, tekst, md, maaned, aar, ydelse, pris
211
+ order by ordrenummer, maaned, medarbejderogrolle, acid
212
+ )
213
+ emit " #{order.ordrenummer} (#{order.sagsnummer}) #{order_lines.size} items"
214
+
215
+ # Create json object for HTTP
216
+ json = format_order_line(order, order_lines)
217
+
218
+ # Transfer JSON document. Note that HTTP redirects are not supported
219
+ # (yet)
220
+ emit_exec(" Upload") { upload_json(insert_order_line_list_url, json) }
221
+
222
+ # Update Sagsys database
223
+ emit_exec(" Update") { update_tidsreg(tidsreg_keys, end_date) }
224
+
225
+ # The whole operation completed successfully so we remove the transaction file
226
+ FileUtils.rm_f(xlog)
227
+ end
228
+
229
+ emit "Done @ #{Time.now.strftime("%F %T")}"
230
+ emit ""
231
+ end
232
+
233
+ private
234
+ def get_uniconta_order_numbers(url) # ChatGPT
235
+ # Prepare HTTP request
236
+ uri = URI(url)
237
+ http = Net::HTTP.new(uri.host, uri.port)
238
+ http.use_ssl = true if uri.scheme == 'https'
239
+ request = Net::HTTP::Get.new(uri, 'Content-Type' => 'application/json')
240
+
241
+ # Add authorization
242
+ credentials = Base64.strict_encode64("#{username}:#{password}")
243
+ request['Authorization'] = "Basic #{credentials}"
244
+
245
+ response = nil
246
+ begin
247
+ response = http.request(request)
248
+ case response
249
+ when Net::HTTPSuccess
250
+ ; # nop
251
+ when Net::HTTPRedirection
252
+ raise StandardError, "Redirects are not supported (yet)"
253
+ else
254
+ raise StandardError, "#{response.code} - #{response.message}"
255
+ end
256
+
257
+ JSON.load(response.body).map { |r| r["OrderNumber"] }
258
+
259
+ rescue StandardError => ex
260
+ msg = ex.message
261
+ log_file.puts "ERROR: #{msg}"
262
+ ShellOpts.failure "Get order list failed: #{msg}"
263
+ end
264
+ end
265
+
266
+ def upload_json(url, json) # ChatGPT
267
+ return
268
+ # Write to json log
269
+ File.open(jlog, "a") { |f| f.puts json } if jlog
270
+
271
+ return if dry_run
272
+
273
+ # Prepare HTTP request
274
+ uri = URI(url)
275
+ http = Net::HTTP.new(uri.host, uri.port)
276
+ http.use_ssl = true if uri.scheme == 'https'
277
+ request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
278
+
279
+ # Add authorization
280
+ credentials = Base64.strict_encode64("#{username}:#{password}")
281
+ request['Authorization'] = "Basic #{credentials}"
282
+
283
+ # Add data
284
+ request.body = json
285
+ response = nil
286
+
287
+ # Transfer JSON document. Note that HTTP redirects are not supported
288
+ # (yet)
289
+ begin
290
+ response = http.request(request)
291
+ case response
292
+ when Net::HTTPSuccess
293
+ ; # nop
294
+ when Net::HTTPRedirection
295
+ raise StandardError, "Redirects are not supported (yet)"
296
+ else
297
+ raise StandardError, "#{response.code} - #{response.message}"
298
+ end
299
+
300
+ rescue StandardError => ex
301
+ FileUtils.rm_f(xlog) # Nothing happened so we delete the transaction
302
+ log_file.puts "ERROR: #{ex.message}"
303
+ ShellOpts.failure "Upload failed: #{ex.message}"
304
+ end
305
+ end
306
+
307
+ def update_tidsreg(keys, end_date)
308
+ return if dry_run
309
+
310
+ # Update Sagsys TIDSREG.FAKTURADATO
311
+ begin
312
+ keys.each { |key|
313
+ conn.exec %(
314
+ update #{tidsreg_table}
315
+ set fakturadato = '#{end_date}'
316
+ where meid = '#{key["meid"]}'
317
+ and said = '#{key["said"]}'
318
+ and acid = '#{key["acid"]}'
319
+ and dato = '#{key["dato"].strftime("%F")}'
320
+ )
321
+ }
322
+
323
+ # Commit this order
324
+ conn.exec("commit")
325
+
326
+ rescue StandardError => ex
327
+ msg = "Update failed: #{ex.message}\nPlease save the transaction file '#{xlog}'"
328
+ log_file.puts "ERROR: #{msg}"
329
+ ShellOpts.failure msg
330
+ end
331
+ end
332
+
333
+ def emit(msg)
334
+ puts msg if !quiet
335
+ log_file.puts msg
336
+ true
337
+ end
338
+
339
+ def emit_n(msg)
340
+ print msg if !quiet
341
+ log_file.print msg
342
+ true
343
+ end
344
+
345
+ def emit_exec(text, &block)
346
+ begin
347
+ emit_n text
348
+ result = yield
349
+ emit ", ok"
350
+ rescue
351
+ emit ", failed"
352
+ raise
353
+ end
354
+ result
355
+ end
356
+
357
+ def format_fields(out, fields)
358
+ out.puts "{ " +
359
+ fields.map { |field, value|
360
+ case value
361
+ when Integer
362
+ if field == "OrderNumber"
363
+ "\"#{field}\":#{value}"
364
+ else
365
+ "\"#{field}\":\"#{value}\""
366
+ end
367
+ when Float, BigDecimal;
368
+ "\"#{field}\":#{'%.2f' % value}"
369
+ when String; "\"#{field}\":\"#{value}\""
370
+ else
371
+ raise ArgumentError, "Illegal type: #{value.class}"
372
+ end
373
+ }.join(", ") +
374
+ " }" + curr_comma
375
+ end
376
+
377
+ def format_blank_line(out, ordrenummer)
378
+ format_fields(
379
+ out,
380
+ "OrderNumber" => ordrenummer,
381
+ "Text" => ""
382
+ )
383
+ end
384
+
385
+ def format_order(order)
386
+ @curr_comma = ""
387
+ out = StringIO.new
388
+ out.puts "["
389
+ format_fields(out,
390
+ "Employee" => order.ansvarlig,
391
+ "Account" => order.kontonummer,
392
+ "OrderNumber" => order.ordrenummer.to_i,
393
+ "Dimension1" => order.sagsnummer,
394
+ "Remark" => order.sagsnummer
395
+ )
396
+ out.puts "]"
397
+ out.string
398
+ end
399
+
400
+ def format_order_line(order, order_lines)
401
+ @curr_comma = ","
402
+ out = StringIO.new
403
+ out.puts "["
404
+ out.indent { |out|
405
+ prev_rolle = nil
406
+ prev_maaned = nil
407
+ for order_line in order_lines
408
+ if prev_rolle != order_line.rolle || prev_maaned != order_line.maaned
409
+ format_blank_line(out, order_line.ordrenummer.to_i) if !prev_rolle.nil? && !prev_maaned.nil?
410
+ format_fields(out,
411
+ "OrderNumber" => order_line.ordrenummer.to_i,
412
+ "Text" => order_line.rolle)
413
+ prev_rolle = order_line.rolle
414
+ prev_maaned = order_line.maaned
415
+ end
416
+ format_fields(out,
417
+ "OrderNumber" => order_line.ordrenummer.to_i,
418
+ "Text" => " #{order_line.tekst}",
419
+ "Item" => order_line.ydelse,
420
+ "Qty" => order_line.antal,
421
+ "Price" => order_line.pris)
422
+ end
423
+ @curr_comma = ""
424
+ format_blank_line(out, order_line.ordrenummer.to_i)
425
+ }
426
+ out.puts "]"
427
+ out.string
428
+ end
429
+ end
430
+
431
+
432
+
433
+
434
+
435
+