his_emr_api_lab 0.0.2 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,6 +18,6 @@ module Lab
18
18
  ORDER_TYPE_NAME = 'Lab'
19
19
 
20
20
  # Programs
21
- LAB_PROGRAM_NAME = 'Lab Program'
21
+ LAB_PROGRAM_NAME = 'Laboratory Program'
22
22
  end
23
23
  end
@@ -5,44 +5,54 @@ module Lab
5
5
  module OrdersSearchService
6
6
  class << self
7
7
  def find_orders(filters)
8
- date = filters.delete(:date)
9
- status = filters.delete(:status)
8
+ extra_filters = pop_filters(filters, :date, :end_date, :status)
10
9
 
11
10
  orders = Lab::LabOrder.prefetch_relationships
12
11
  .where(filters)
13
12
  .order(start_date: :desc)
14
13
 
15
- orders = filter_orders_by_date(orders, date) if date
16
- orders = filter_orders_by_status(orders, status) if status
14
+ orders = filter_orders_by_status(orders, pop_filters(extra_filters, :status))
15
+ orders = filter_orders_by_date(orders, extra_filters)
17
16
 
18
17
  orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
19
18
  end
20
19
 
21
- def filter_orders_by_date(orders, date)
22
- orders.where('start_date < DATE(?)', date.to_date + 1.day)
23
- end
20
+ def filter_orders_by_date(orders, date: nil, end_date: nil)
21
+ date = date&.to_date
22
+ end_date = end_date&.to_date
24
23
 
25
- def filter_orders_by_status(orders, status)
26
- case status.downcase
27
- when 'ordered' then orders.where(concept_id: unknown_concept_id)
28
- when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
24
+ if date && end_date
25
+ return orders.where('start_date BETWEEN ? AND ?', date, end_date + 1.day)
29
26
  end
30
- end
31
27
 
32
- def unknown_concept_id
33
- ConceptName.find_by_name!('Unknown').concept_id
28
+ if date
29
+ return orders.where('start_date BETWEEN ? AND ?', date, date + 1.day)
30
+ end
31
+
32
+ return orders.where('start_date < ?', end_date + 1.day) if end_date
33
+
34
+ orders
34
35
  end
35
36
 
36
- def filter_orders_by_status(orders, status)
37
- case status.downcase
37
+ def filter_orders_by_status(orders, status: nil)
38
+ case status&.downcase
38
39
  when 'ordered' then orders.where(concept_id: unknown_concept_id)
39
40
  when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
41
+ else orders
40
42
  end
41
43
  end
42
44
 
43
45
  def unknown_concept_id
44
46
  ConceptName.find_by_name!('Unknown').concept_id
45
47
  end
48
+
49
+ def pop_filters(params, *filters)
50
+ filters.each_with_object({}) do |filter, popped_params|
51
+ next unless params.key?(filter)
52
+
53
+ popped_params[filter.to_sym] = params.delete(filter)
54
+ end
55
+ end
46
56
  end
47
57
  end
48
58
  end
@@ -27,7 +27,7 @@ module Lab
27
27
  # },
28
28
  # program_id: { type: :integer, required: false },
29
29
  # patient_id: { type: :integer, required: false }
30
- # specimen_type_id: { type: :object, properties: { concept_id: :integer }, required: %i[concept_id] },
30
+ # specimen: { type: :object, properties: { concept_id: :integer }, required: %i[concept_id] },
31
31
  # test_type_ids: {
32
32
  # type: :array,
33
33
  # items: {
@@ -71,11 +71,15 @@ module Lab
71
71
  end
72
72
 
73
73
  order = Lab::LabOrder.find(order_id)
74
- unless order.concept_id == unknown_concept_id
74
+ unless order.concept_id == unknown_concept_id || params[:force_update]&.to_s&.casecmp?('true')
75
75
  raise ::UnprocessableEntityError
76
76
  end
77
77
 
78
- order.update!(concept_id: specimen_id)
78
+ order.update!(concept_id: specimen_id,
79
+ discontinued: true,
80
+ discontinued_by: User.current.user_id,
81
+ discontinued_date: params[:date]&.to_date || Date.today,
82
+ discontinued_reason_non_coded: 'Sample drawn/updated')
79
83
  Lab::LabOrderSerializer.serialize_order(order)
80
84
  end
81
85
 
@@ -87,10 +91,7 @@ module Lab
87
91
  order.reason_for_test&.void(reason)
88
92
  order.target_lab&.void(reason)
89
93
 
90
- order.tests.each do |test|
91
- test.result&.void(reason)
92
- test.void(reason)
93
- end
94
+ order.tests.each { |test| test.void(reason) }
94
95
 
95
96
  order.void(reason)
96
97
  end
@@ -119,7 +120,7 @@ module Lab
119
120
  program_id: program_id,
120
121
  type: EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME),
121
122
  encounter_datetime: order_params[:date] || Date.today,
122
- provider_id: order_params[:provider_id] || User.current&.user_id
123
+ provider_id: order_params[:provider_id] || User.current.person.person_id
123
124
  )
124
125
  end
125
126
 
@@ -3,14 +3,26 @@
3
3
  module Lab
4
4
  module ResultsService
5
5
  class << self
6
+ ##
7
+ # Attach results to a test
8
+ #
9
+ # Params:
10
+ # test_id: The tests id (maps to obs_id of the test's observation in OpenMRS)
11
+ # params: A hash comprising the following fields
12
+ # - encounter_id: Encounter to create result under (can be ommitted but provider_id has to specified)
13
+ # - provider_id: Specify a provider for an encounter the result is going to be created under
14
+ # - date: Retrospective date when the result was received (can be ommitted, defaults to today)
15
+ # - measures: An array of measures. A measure is an object of the following structure
16
+ # - indicator: An object that has a concept_id field (concept_id of the indicator)
17
+ # - value_type: An enum that's limited to 'numeric', 'boolean', 'text', and 'coded'
6
18
  def create_results(test_id, params)
7
19
  ActiveRecord::Base.transaction do
8
20
  test = Lab::LabTest.find(test_id)
9
21
  encounter = find_encounter(test, encounter_id: params[:encounter_id],
10
- date: params[:date],
22
+ date: params[:date]&.to_date,
11
23
  provider_id: params[:provider_id])
12
24
 
13
- results_obs = create_results_obs(encounter, test, params[:date])
25
+ results_obs = create_results_obs(encounter, test, params[:date], params[:comments])
14
26
  params[:measures].map { |measure| add_measure_to_results(results_obs, measure, params[:date]) }
15
27
 
16
28
  Lab::ResultSerializer.serialize(results_obs)
@@ -32,23 +44,41 @@ module Lab
32
44
  end
33
45
 
34
46
  # Creates the parent observation for results to which the different measures are attached
35
- def create_results_obs(encounter, test, date)
47
+ def create_results_obs(encounter, test, date, comments = nil)
48
+ void_existing_results_obs(encounter, test)
49
+
36
50
  Lab::LabResult.create!(
37
51
  person_id: encounter.patient_id,
38
52
  encounter_id: encounter.encounter_id,
39
- concept_id: ConceptName.find_by_name!(Lab::Metadata::TEST_RESULT_CONCEPT_NAME).concept_id,
53
+ concept_id: test_result_concept.concept_id,
40
54
  order_id: test.order_id,
41
55
  obs_group_id: test.obs_id,
42
- obs_datetime: date&.to_datetime || DateTime.now
56
+ obs_datetime: date&.to_datetime || DateTime.now,
57
+ comments: comments
43
58
  )
44
59
  end
45
60
 
61
+ def void_existing_results_obs(encounter, test)
62
+ result = Lab::LabResult.find_by(person_id: encounter.patient_id,
63
+ concept_id: test_result_concept.concept_id,
64
+ obs_group_id: test.obs_id)
65
+ return unless result
66
+
67
+ result.measures.map { |child_obs| child_obs.void("Updated/overwritten by #{User.current.username}") }
68
+ result.void("Updated/overwritten by #{User.current.username}")
69
+ end
70
+
71
+ def test_result_concept
72
+ ConceptName.find_by_name!(Lab::Metadata::TEST_RESULT_CONCEPT_NAME)
73
+ end
74
+
46
75
  def add_measure_to_results(results_obs, params, date)
47
76
  validate_measure_params(params)
48
77
 
49
78
  Observation.create!(
50
79
  person_id: results_obs.person_id,
51
80
  encounter_id: results_obs.encounter_id,
81
+ order_id: results_obs.order_id,
52
82
  concept_id: params[:indicator][:concept_id],
53
83
  obs_group_id: results_obs.obs_id,
54
84
  obs_datetime: date&.to_datetime || DateTime.now,
@@ -57,7 +87,9 @@ module Lab
57
87
  end
58
88
 
59
89
  def validate_measure_params(params)
60
- raise InvalidParameterError, 'measures.value is required' if params[:value].blank?
90
+ if params[:value].blank?
91
+ raise InvalidParameterError, 'measures.value is required'
92
+ end
61
93
 
62
94
  if params[:indicator]&.[](:concept_id).blank?
63
95
  raise InvalidParameterError, 'measures.indicator.concept_id is required'
data/config/routes.rb CHANGED
@@ -6,6 +6,8 @@ Lab::Engine.routes.draw do
6
6
  resources :results, only: %i[index create destroy]
7
7
  end
8
8
 
9
+ get 'api/v1/lab/labels/order', to: 'labels#print_order_label'
10
+
9
11
  # Metadata
10
12
  # TODO: Move the following to namespace /concepts
11
13
  resources :specimen_types, only: %i[index], path: 'api/v1/lab/specimen_types'
data/lib/auto12epl.rb ADDED
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/ruby
2
+ # Jeremy Espino MD MS
3
+ # 28-JAN-2016
4
+
5
+
6
+ class Float
7
+ # function to round down a float to an integer value
8
+ def round_down n=0
9
+ n < 1 ? self.to_i.to_f : (self - 0.5 / 10**n).round(n)
10
+ end
11
+ end
12
+
13
+ # Generates EPL code that conforms to the Auto12-A standard for specimen labeling
14
+ class Auto12Epl
15
+
16
+ attr_accessor :element_font
17
+ attr_accessor :barcode_human_font
18
+
19
+ DPI = 203
20
+ LABEL_WIDTH_IN = 2.0
21
+ LABEL_HEIGHT_IN = 0.5
22
+
23
+ # font constants
24
+ FONT_X_DOTS = [8, 10, 12, 14, 32]
25
+ FONT_Y_DOTS = [12, 16, 20, 24, 24]
26
+ FONT_PAD_DOTS = 2
27
+
28
+ # element heights
29
+ HEIGHT_MARGIN = 0.031
30
+ HEIGHT_ELEMENT = 0.1
31
+ HEIGHT_ELEMENT_SPACE = 0.01
32
+ HEIGHT_PID = 0.1
33
+ HEIGHT_BARCODE = 0.200
34
+ HEIGHT_BARCODE_HUMAN = 0.050
35
+
36
+ # element widths
37
+ WIDTH_ELEMENT = 1.94
38
+ WIDTH_BARCODE = 1.395
39
+ WIDTH_BARCODE_HUMAN = 1.688
40
+
41
+ # margins
42
+ L_MARGIN = 0.031
43
+ L_MARGIN_BARCODE = 0.25
44
+
45
+ # stat locations
46
+ L_MARGIN_BARCODE_W_STAT = 0.200
47
+ L_MARGIN_W_STAT = 0.150
48
+ STAT_WIDTH_ELEMENT = 1.78
49
+ STAT_WIDTH_BARCODE = 1.150
50
+ STAT_WIDTH_BARCODE_HUMAN = 1.400
51
+
52
+ # constants for generated EPL code
53
+ BARCODE_TYPE = '1A'
54
+ BARCODE_NARROW_WIDTH = '2'
55
+ BARCODE_WIDE_WIDTH = '2'
56
+ BARCODE_ROTATION = '0'
57
+ BARCODE_IS_HUMAN_READABLE = 'N'
58
+ ASCII_HORZ_MULT = 1
59
+ ASCII_VERT_MULT = 1
60
+
61
+
62
+ def initialize(element_font = 1, barcode_human_font = 1)
63
+ @element_font = element_font
64
+ @barcode_human_font = barcode_human_font
65
+ end
66
+
67
+ # Calculate the number of characters that will fit in a given length
68
+ def max_characters(font, length)
69
+
70
+ dots_per_char = FONT_X_DOTS.at(font-1) + FONT_PAD_DOTS
71
+
72
+ num_char = ( (length * DPI) / dots_per_char).round_down
73
+
74
+ num_char.to_int
75
+ end
76
+
77
+ # Use basic truncation rule to truncate the name element i.e., if > maxCharacters cutoff and trail with +
78
+ def truncate_name(last_name, first_name, middle_initial, is_stat)
79
+ if is_stat
80
+ name_max_characters = max_characters(@element_font, STAT_WIDTH_ELEMENT)
81
+ else
82
+ name_max_characters = max_characters(@element_font, WIDTH_ELEMENT)
83
+ end
84
+
85
+ if concatName(last_name, first_name, middle_initial).length > name_max_characters
86
+ # truncate last?
87
+ if last_name.length > 12
88
+ last_name = last_name[0..11] + '+'
89
+ end
90
+
91
+ # truncate first?
92
+ if concatName(last_name, first_name, middle_initial).length > name_max_characters && first_name.length > 7
93
+ first_name = first_name[0..7] + '+'
94
+ end
95
+ end
96
+
97
+ concatName(last_name, first_name, middle_initial)
98
+
99
+ end
100
+
101
+ def concatName(last_name, first_name, middle_initial)
102
+ last_name + ', ' + first_name + (middle_initial == nil ? '' : ' ' + middle_initial)
103
+ end
104
+
105
+ # The main function to generate the EPL
106
+ def generate_epl(last_name, first_name, middle_initial, pid, dob, age, gender, col_date_time, col_name, tests, stat, acc_num, schema_track)
107
+
108
+ # format text and set margin
109
+ if stat == nil
110
+ name_text = truncate_name(last_name, first_name, middle_initial, false)
111
+ pid_dob_age_gender_text = full_justify(pid, dob + ' ' + age + ' ' + gender, @element_font, WIDTH_ELEMENT)
112
+ l_margin = L_MARGIN
113
+ l_margin_barcode = L_MARGIN_BARCODE
114
+ else
115
+ name_text = truncate_name(last_name, first_name, middle_initial, true)
116
+ pid_dob_age_gender_text = full_justify(pid, dob + ' ' + age + ' ' + gender, @element_font, STAT_WIDTH_ELEMENT)
117
+ stat_element_text = pad_stat_w_space(stat)
118
+ l_margin = L_MARGIN_W_STAT
119
+ l_margin_barcode = L_MARGIN_BARCODE_W_STAT
120
+ end
121
+ barcode_human_text = "#{acc_num} * #{schema_track.gsub(/\-/i, '')}"
122
+ collector_element_text = "Col: #{col_date_time} #{col_name}"
123
+ tests_element_text = tests
124
+
125
+ # generate EPL statements
126
+ name_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN), 0, @element_font, false, name_text)
127
+ pid_dob_age_gender_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE), 0, @element_font, false, pid_dob_age_gender_text)
128
+ barcode_human_element = generate_ascii_element(to_dots(l_margin_barcode), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_BARCODE), 0, @barcode_human_font, false, barcode_human_text)
129
+ collector_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_BARCODE + HEIGHT_BARCODE_HUMAN + HEIGHT_ELEMENT_SPACE), 0, @element_font, false, collector_element_text)
130
+ tests_element = generate_ascii_element(to_dots(l_margin), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_BARCODE + HEIGHT_BARCODE_HUMAN + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE), 0, @element_font, false, tests_element_text)
131
+ barcode_element = generate_barcode_element(to_dots(l_margin_barcode), to_dots(HEIGHT_MARGIN + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE + HEIGHT_ELEMENT + HEIGHT_ELEMENT_SPACE), to_dots(HEIGHT_BARCODE)-4, schema_track)
132
+ stat_element = generate_ascii_element(to_dots(L_MARGIN)+FONT_Y_DOTS.at(@element_font - 1)+FONT_PAD_DOTS, to_dots(HEIGHT_MARGIN), 1, @element_font, true, stat_element_text)
133
+
134
+ # combine EPL statements
135
+ if stat == nil
136
+ "\nN\nR216,0\nZT\nS1\n#{name_element}\n#{pid_dob_age_gender_element}\n#{barcode_element}\n#{barcode_human_element}\n#{collector_element}\n#{tests_element}\nP3\n"
137
+ else
138
+ "\nN\nR216,0\nZT\nS1\n#{name_element}\n#{pid_dob_age_gender_element}\n#{barcode_element}\n#{barcode_human_element}\n#{collector_element}\n#{tests_element}\n#{stat_element}\nP3\n"
139
+ end
140
+
141
+ end
142
+
143
+ # Add spaces before and after the stat text so that black bars appear across the left edge of label
144
+ def pad_stat_w_space(stat)
145
+ num_char = max_characters(@element_font, LABEL_HEIGHT_IN)
146
+ spaces_needed = (num_char - stat.length) / 1
147
+ space = ''
148
+ spaces_needed.times do
149
+ space = space + ' '
150
+ end
151
+ space + stat + space
152
+ end
153
+
154
+ # Add spaces between the NPID and the dob/age/gender so that line is fully justified
155
+ def full_justify(pid, dag, font, length)
156
+ max_char = max_characters(font, length)
157
+ spaces_needed = max_char - pid.length - dag.length
158
+ space = ''
159
+ spaces_needed.times do
160
+ space = space + ' '
161
+ end
162
+ pid + space + dag
163
+ end
164
+
165
+ # convert inches to number of dots using DPI
166
+ def to_dots(inches)
167
+ (inches * DPI).round
168
+ end
169
+
170
+ # generate ascii EPL
171
+ def generate_ascii_element(x, y, rotation, font, is_reverse, text)
172
+ "A#{x.to_s},#{y.to_s},#{rotation.to_s},#{font.to_s},#{ASCII_HORZ_MULT},#{ASCII_VERT_MULT},#{is_reverse ? 'R' : 'N'},\"#{text}\""
173
+ end
174
+
175
+ # generate barcode EPL
176
+ def generate_barcode_element(x, y, height, schema_track)
177
+ schema_track = schema_track.gsub("-", "").strip
178
+ "B#{x.to_s},#{y.to_s},#{BARCODE_ROTATION},#{BARCODE_TYPE},#{BARCODE_NARROW_WIDTH},#{BARCODE_WIDE_WIDTH},#{height.to_s},#{BARCODE_IS_HUMAN_READABLE},\"#{schema_track}\""
179
+ end
180
+
181
+ end
182
+
183
+ if __FILE__ == $0
184
+
185
+ auto = Auto12Epl.new
186
+
187
+ puts auto.generate_epl("Banda", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", nil, "KCH-16-00001234", "1600001234")
188
+ puts "\n"
189
+ puts auto.generate_epl("Banda", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
190
+ puts "\n"
191
+ puts auto.generate_epl("Bandajustrightlas", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
192
+ puts "\n"
193
+ puts auto.generate_epl("Bandasuperlonglastnamethatwonfit", "Marysuperlonglastnamethatwonfit", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
194
+ puts "\n"
195
+ puts auto.generate_epl("Bandasuperlonglastnamethatwonfit", "Mary", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
196
+ puts "\n"
197
+ puts auto.generate_epl("Banda", "Marysuperlonglastnamethatwonfit", "U", "Q23-HGF", "12-SEP-1997", "19y", "F", "01-JAN-2016 14:21", "byGD", "CHEM7,Ca,Mg", "STAT CHEM", "KCH-16-00001234", "1600001234")
198
+
199
+
200
+
201
+ end
@@ -8,8 +8,12 @@ require 'couchrest'
8
8
  #
9
9
  # See: https://github.com/couchrest/couchrest
10
10
  class CouchBum
11
+ cattr_accessor :logger
12
+
11
13
  def initialize(database:, protocol: 'http', host: 'localhost', port: 5984, username: nil, password: nil)
12
14
  @connection_string = make_connection_string(protocol, username, password, host, port, database)
15
+
16
+ CouchBum.logger ||= Logger.new(STDOUT)
13
17
  end
14
18
 
15
19
  ##
@@ -20,22 +24,33 @@ class CouchBum
20
24
  # within the passed block.
21
25
  def binge_changes(since: 0, limit: nil, include_docs: nil, &block)
22
26
  catch(:choke) do
23
- extra_params = stringify_params(limit: limit, include_docs: include_docs)
27
+ logger.debug("Binging #{limit} changes from '#{since}'")
28
+ params = stringify_params(limit: limit, include_docs: include_docs)
29
+ params = "since=#{since}&#{params}" unless since.blank?
24
30
 
25
- changes = couch_rest(:get, "_changes?since=#{since}&#{extra_params}")
31
+ changes = couch_rest(:get, "_changes?#{params}")
26
32
  context = BingeContext.new(changes)
27
- changes['results'].each { |change| context.instance_exec(change, &block) }
33
+ changes['results'].each do |change|
34
+ context.current_seq = change['seq']
35
+ context.instance_exec(change, &block)
36
+ end
28
37
  end
29
38
  end
30
39
 
31
40
  def couch_rest(method, route, *args, **kwargs)
32
- CouchRest.send(method, expand_route(route), *args, **kwargs)
41
+ url = expand_route(route)
42
+ CouchRest.send(method, url, *args, **kwargs)
43
+ rescue CouchRest::Exception => e
44
+ logger.error("Failed to communicate with CouchDB: Status: #{e.http_code} - #{e.http_body}")
45
+ raise e
33
46
  end
34
47
 
35
48
  private
36
49
 
37
50
  # Context under which the callback passed to binge_changes is executed.
38
51
  class BingeContext
52
+ attr_accessor :current_seq
53
+
39
54
  def initialize(changes)
40
55
  @changes = changes
41
56
  end