unipump 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/README.md +31 -0
- data/Rakefile +8 -0
- data/TODO +5 -0
- data/exe/unipump +103 -0
- data/lib/unipump/credentials.rb +20 -0
- data/lib/unipump/mssql.rb +58 -0
- data/lib/unipump/version.rb +5 -0
- data/lib/unipump.rb +435 -0
- data/note.txt +28 -0
- data/produktions-overf/303/270rsel-20240220/uniconta.json +3766 -0
- data/produktions-overf/303/270rsel-20240220/unipump.log +1278 -0
- data/produktions-overf/303/270rsel-20240220/unipump.transaction.error-20240320 +18 -0
- data/sig/timepump.rbs +4 -0
- data/v_fakturalinie.sql +155 -0
- metadata +101 -0
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
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
data/TODO
ADDED
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
|
+
|
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
|
+
|