Class: Pump

Inherits:
Object
  • Object
show all
Defined in:
lib/unipump.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

DEFAULT_CREDENTIALS_FILE =
".unipump.environment"
DEFAULT_ENVIRONMENT =
"test"
TEST_SYSTEM =

Environment-dependent constants

"81470"
PROD_SYSTEM =

Uniconta test system ID

"86837"
TEST_TIMEREG_TABLE =
"tidsreg_test"
PROD_TIMEREG_TABLE =
"tidsreg"
TEST_FAKTURALINIE_VIEW =
"v_fakturalinie_test"
PROD_FAKTURALINIE_VIEW =
"v_fakturalinie"
TRANSACTION_FILE =

Transaction file. This contains IDs of the most recently uploaded TIDSREG records. It is cleared when the corresponding records in the database are updated

"unipump.transaction"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(username, password, environment, log: nil, xlog: nil, jlog: nil, dry_run: false, quiet: false) ⇒ Pump

Returns a new instance of Pump.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/unipump.rb', line 73

def initialize(
    username, password, environment,
    log: nil, xlog: nil, jlog: nil,
    dry_run: false, quiet: false)
  @environment = environment
  @username = username
  @password = password
  @log = log || '/dev/null'
  @xlog = xlog || TRANSACTION_FILE
  @jlog = jlog
  @dry_run = dry_run
  @quiet = quiet
  @log_file = File.open(@log, "a")
  @conn = UniPump::Connection.new
  @curr_comma = ""
  !File.exist?(@xlog) or ShellOpts::error "Found transaction file: #{@xlog}. Please fix this"
  FileUtils.rm_f(@jlog) if @jlog
end

Instance Attribute Details

#connObject (readonly)

Database connection



60
61
62
# File 'lib/unipump.rb', line 60

def conn
  @conn
end

#curr_commaObject (readonly)

Hack FIXME



62
63
64
# File 'lib/unipump.rb', line 62

def curr_comma
  @curr_comma
end

#dry_runObject (readonly)

Standard options



57
58
59
# File 'lib/unipump.rb', line 57

def dry_run
  @dry_run
end

#environmentObject (readonly)

Run-time environment. “prod” or “test”



53
54
55
# File 'lib/unipump.rb', line 53

def environment
  @environment
end

#jlogObject (readonly)

Path to JSON log file or nil



56
57
58
# File 'lib/unipump.rb', line 56

def jlog
  @jlog
end

#logObject (readonly)

Path to log file



54
55
56
# File 'lib/unipump.rb', line 54

def log
  @log
end

#log_fileObject (readonly)

Log file object



59
60
61
# File 'lib/unipump.rb', line 59

def log_file
  @log_file
end

#passwordObject (readonly)

Returns the value of attribute password.



52
53
54
# File 'lib/unipump.rb', line 52

def password
  @password
end

#quietObject (readonly)

Standard options



57
58
59
# File 'lib/unipump.rb', line 57

def quiet
  @quiet
end

#usernameObject (readonly)

Returns the value of attribute username.



51
52
53
# File 'lib/unipump.rb', line 51

def username
  @username
end

#xlogObject (readonly)

Path to transaction log file



55
56
57
# File 'lib/unipump.rb', line 55

def xlog
  @xlog
end

Instance Method Details

#fakturalinie_viewObject



67
# File 'lib/unipump.rb', line 67

def fakturalinie_view = (environment == "prod" ? PROD_FAKTURALINIE_VIEW : TEST_FAKTURALINIE_VIEW)

#insert_order_line_list_urlObject



71
# File 'lib/unipump.rb', line 71

def insert_order_line_list_url = "#{root_url}/InsertList/DebtorOrderLineClientUser"

#insert_order_list_urlObject



70
# File 'lib/unipump.rb', line 70

def insert_order_list_url = "#{root_url}/InsertList/DebtorOrderClientUser"

#order_list_urlObject



69
# File 'lib/unipump.rb', line 69

def order_list_url = "#{root_url}/DebtorOrderClientUser"

#root_urlObject



68
# File 'lib/unipump.rb', line 68

def root_url = "https://odata.uniconta.com/api/Entities/#{system}"

#systemObject

Environment-dependent resources



65
# File 'lib/unipump.rb', line 65

def system = (environment == "prod" ? PROD_SYSTEM : TEST_SYSTEM)

#tidsreg_tableObject



66
# File 'lib/unipump.rb', line 66

def tidsreg_table = (environment == "prod" ? PROD_TIMEREG_TABLE : TEST_TIMEREG_TABLE)

#upload(end_date, accounts) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/unipump.rb', line 92

def upload(end_date, accounts)
  t0 = Time.now

  emit("#{ShellOpts.instance.name} @ #{Time.now.strftime("%F %T")}")

  # Select specific accounts. Mostly for debug
  konto_constraint = (accounts.empty? ? "and 1 = 1" : "and kontonummer in (#{accounts.join(', ')})")

  # Fetch existing Uniconta orders
  uniconta_order_numbers = 
      emit_exec("  Fetch orders") { get_uniconta_order_numbers(order_list_url) }
  emit "  Found #{uniconta_order_numbers.size} orders in Uniconta"

  # Compute orders that are does not exist in Uniconta
  uniconta_order_numbers.reject! { |order_number| 
    if order_number.to_s.size < 8
      emit "    Illegal order number: '#{order_number}', ignored"
    elsif order_number < 24000000
      emit "    Outdated order number: '#{order_number}', ignored"
    end
  }

  # Find order with lines that have not been pumped yet (lines with
  # fakturadato equal to null)
  orders = conn.structs %(
    select distinct
      sagsnummer,
      ansvarlig, 
      kontonummer,
      ordrenummer
    from #{fakturalinie_view}
    where dato < '#{end_date}'
    and fakturadato is null
    #{konto_constraint}
    order by ansvarlig, sagsnummer
  )
  emit "  Found #{orders.size} orders in Sagsys"

  # Exclude orders with kontonummer equal to nil
  excluded = []
  orders.delete_if { |order|
    if order.kontonummer.nil?
      excluded << order
    end
  }
  emit "  #{excluded.size} with absent kontonummer - ignored" if !excluded.empty?

  # Compute order numbers that should be created. This is computed as a Set
  # for fast lookup
  missing_order_numbers = Set.new(orders.map(&:ordrenummer).map(&:to_i) - uniconta_order_numbers)

  # create order in Uniconta
  emit "  Create #{missing_order_numbers.size} orders"
  for order in orders
    if missing_order_numbers.include?(order.ordrenummer.to_i)
      json = format_order(order)
      emit_exec("    #{order.ordrenummer}") { upload_json(insert_order_list_url, json) }
    end
  end

  # Process each order
  emit "  Upload orders"
  for order in orders
    # We want to save a list of uploaded tidsreg records in case the
    # database update fails after the invoice lines have been uploaded to
    # Uniconta, but we also want the database to aggregate the tidsreg
    # records for us so we end up with the two queries below. It is
    # important the the two queries selects the same records so we use the
    # common method #line_constraint to supply the where constraint
    #
    # The list is written to a FILE that is removed after the records have
    # been updated sucessfully. It is an error if it exists already
    #
    line_constraint = %(
      dato <= '#{end_date}'
      and ordrenummer = '#{order.ordrenummer}'
      and fakturadato is null
      #{konto_constraint}
    )
    
    # Begin transaction. We hope that the default isolation level is
    # resonable in MsSql
    conn.exec("begin transaction")

    # Find keys for affected TIMEREG records. These are saved to disk so
    # that we may recover from a database failure after the order lines
    # have been uploaded
    tidsreg_keys = conn.records %(
      select distinct
        meid,
        said,
        acid,
        dato
      from #{fakturalinie_view}
      where #{line_constraint}
    )

    # Save tidsreg keys to disk
    File.open(xlog, "w") { |xfile| 
      xfile.puts "# Ordrenummer: #{order.ordrenummer} (#{order.sagsnummer})"
      xfile.puts tidsreg_keys.to_a 
    }

    # Find the order lines. These are aggregated tidsreg records and could
    # be coalesced into the previous select but then we would have to do
    # grouping and ordering in ruby. Note that the select clause in the pre
    order_lines = conn.structs %(
      select
        ordrenummer,
        medarbejderogrolle as "rolle",
        maaned,
        tekst + ': ' + md + ' ' + cast(aar as nvarchar) as "tekst",
        ydelse,
        sum(timer) as antal,
        pris
      from #{fakturalinie_view} f
      where #{line_constraint}
      group by ordrenummer, medarbejderogrolle, acid, tekst, md, maaned, aar, ydelse, pris
      order by ordrenummer, maaned, medarbejderogrolle, acid
    )
    emit "    #{order.ordrenummer} (#{order.sagsnummer}) #{order_lines.size} items"

    # Create json object for HTTP
    json = format_order_line(order, order_lines, end_date)

    # Transfer JSON document. Note that HTTP redirects are not supported
    # (yet)
    emit_exec("      Upload") { upload_json(insert_order_line_list_url, json) }

    # Update Sagsys database
    emit_exec("      Update") { update_tidsreg(tidsreg_keys, end_date) }

    # The whole operation completed successfully so we remove the transaction file
    FileUtils.rm_f(xlog)
  end

  emit "Done @ #{Time.now.strftime("%F %T")}"
  emit ""
end