malawi_hiv_program_reports 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/malawi_hiv_program_reports/README.md +16 -0
  3. data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
  4. data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
  5. data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
  6. data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
  7. data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
  8. data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
  9. data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
  10. data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
  11. data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
  12. data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
  13. data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
  14. data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
  15. data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
  16. data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
  17. data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
  18. data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
  19. data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
  20. data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
  21. data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
  22. data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
  23. data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
  24. data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
  25. data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
  26. data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
  27. data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
  28. data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
  29. data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
  30. data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
  31. data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
  32. data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
  33. data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
  34. data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
  35. data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
  36. data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
  37. data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
  38. data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
  39. data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
  40. data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
  41. data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
  42. data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
  43. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
  44. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
  45. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
  46. data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
  47. data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
  48. data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
  49. data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
  50. data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
  51. data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
  52. data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
  53. data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
  54. data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
  55. data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
  56. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
  57. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
  58. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
  59. data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
  60. data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
  61. data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
  62. data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
  63. data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
  64. data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
  65. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
  66. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
  67. data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
  68. data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
  69. data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
  70. data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
  71. data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
  72. data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
  73. data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
  74. data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
  75. data/lib/malawi_hiv_program_reports/version.rb +1 -1
  76. metadata +74 -1
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Clinic
5
+ class RegimensByWeightAndGender
6
+ include MalawiHivProgramReports::Utils::ConcurrencyUtils
7
+
8
+ attr_reader :start_date, :end_date
9
+
10
+ def initialize(start_date:, end_date:, **kwargs)
11
+ @start_date = start_date
12
+ @end_date = end_date
13
+ @rebuild_outcomes = true
14
+ @occupation = kwargs[:occupation]
15
+ end
16
+
17
+ def find_report
18
+ regimen_counts
19
+ end
20
+
21
+ private
22
+
23
+ WEIGHT_BANDS = [
24
+ [3, 3.9],
25
+ [4, 4.9],
26
+ [6, 9.9],
27
+ [10, 13.9],
28
+ [14, 19.9],
29
+ [20, 24.9],
30
+ [25, 29.9],
31
+ [30, 34.9],
32
+ [35, 39.9],
33
+ [40, 49.9],
34
+ [50, Float::INFINITY],
35
+ [nil, nil] # To capture all those missing weight
36
+ ].freeze
37
+
38
+ def regimen_counts
39
+ with_lock(Cohort::LOCK_FILE) do
40
+ PatientsAliveAndOnTreatment.new(start_date:, end_date:, occupation: @occupation)
41
+ .refresh_outcomes_table
42
+
43
+ WEIGHT_BANDS.map do |start_weight, end_weight|
44
+ {
45
+ weight: weight_band_to_string(start_weight, end_weight),
46
+ males: regimen_counts_by_weight_and_gender(start_weight, end_weight, 'M'),
47
+ females: regimen_counts_by_weight_and_gender(start_weight, end_weight, 'F'),
48
+ unknown_gender: regimen_counts_by_weight_and_gender(start_weight, end_weight, nil)
49
+ }
50
+ end
51
+ end
52
+ end
53
+
54
+ def weight_band_to_string(start_weight, end_weight)
55
+ if start_weight.nil? && end_weight.nil?
56
+ 'Unknown'
57
+ elsif end_weight == Float::INFINITY
58
+ "#{start_weight} Kg +"
59
+ else
60
+ "#{start_weight} - #{end_weight} Kg"
61
+ end
62
+ end
63
+
64
+ # TODO: Refactor the queries in this module... Possibly
65
+ # prefer joins over the subqueries (ie if performance becomes an
66
+ # issue - it probably will eventually).
67
+
68
+ def regimen_counts_by_weight_and_gender(start_weight, end_weight, gender)
69
+ date = ActiveRecord::Base.connection.quote(end_date)
70
+
71
+ query = TempPatientOutcome.joins('INNER JOIN temp_earliest_start_date USING (patient_id)')
72
+ .select("patient_current_regimen(patient_id, #{date}) as regimen, count(*) AS count")
73
+ .where(patient_id: patients_in_weight_band(start_weight, end_weight))
74
+ .where(cum_outcome: 'On Antiretrovirals')
75
+ .group(:regimen)
76
+
77
+ query = gender ? query.where('gender LIKE ?', "#{gender}%") : query.where('gender IS NULL')
78
+
79
+ query.collect { |obs| { obs.regimen => obs.count } }
80
+ end
81
+
82
+ def patients_in_weight_band(start_weight, end_weight)
83
+ if start_weight.nil? && end_weight.nil?
84
+ # If no weight is provided then this must be all patients without a weight observation
85
+ return ::PatientProgram.select(:patient_id)
86
+ .where(program_id: ::ArtService::Constants::PROGRAM_ID)
87
+ .where.not(patient_id: patients_with_known_weight.select(:person_id))
88
+ end
89
+
90
+ max_weights = patients_with_known_weight.select('MAX(obs_datetime) AS obs_datetime, person_id').to_sql
91
+
92
+ patients_with_known_weight
93
+ .joins("INNER JOIN (#{max_weights}) AS max_weights
94
+ ON max_weights.person_id = obs.person_id AND max_weights.obs_datetime = obs.obs_datetime")
95
+ .where(value_numeric: (start_weight..end_weight))
96
+ .select(:person_id)
97
+ end
98
+
99
+ def patients_with_known_weight
100
+ ::Observation.joins('INNER JOIN temp_patient_outcomes AS outcomes ON outcomes.patient_id = obs.person_id')
101
+ .where(concept_id: ::ConceptName.where(name: 'Weight (kg)').select(:concept_id),
102
+ outcomes: { cum_outcome: 'On antiretrovirals' })
103
+ .where('DATE(obs.obs_datetime) <= ?', end_date)
104
+ .group(:person_id)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Retrieve patients who are completing their first 1st, 3rd, and 6th month on ART
4
+ # in the reporting period.
5
+ module MalawiHivProgramReports
6
+ module Clinic
7
+ class Retention
8
+ attr_reader :start_date, :end_date
9
+
10
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
11
+
12
+ DAYS_IN_MONTH = 28
13
+ MONTHS = [1, 3, 6].freeze
14
+
15
+ def initialize(start_date:, end_date:, **kwargs)
16
+ @start_date = start_date.to_s
17
+ @end_date = end_date.to_s
18
+ @use_filing_number = ::GlobalProperty.find_by(property: 'use.filing.numbers')
19
+ &.property_value
20
+ &.casecmp?('true')
21
+ @occupation = kwargs[:occupation]
22
+ end
23
+
24
+ def find_report
25
+ matched_patients = MONTHS.each_with_object({}) do |month, hash|
26
+ hash[month] = { retained: [], all: [] }
27
+ end
28
+
29
+ find_patients_retention_period(retained_patients(as_of: Date.parse(start_date) - MONTHS.max.months)) do |period, patient|
30
+ matched_patients[period][:retained] << {
31
+ patient_id: patient.patient_id,
32
+ arv_number: patient.arv_number,
33
+ start_date: patient.start_date,
34
+ gender: begin
35
+ patient.gender.upcase.first
36
+ rescue StandardError
37
+ nil
38
+ end,
39
+ age_group: patient.age_group,
40
+ end_date: patient.start_date + period.months
41
+ }
42
+ end
43
+
44
+ find_patients_retention_period(all_patients(as_of: Date.parse(start_date) - MONTHS.max.months)) do |period, patient|
45
+ matched_patients[period][:all] << {
46
+ patient_id: patient.patient_id,
47
+ arv_number: patient.arv_number,
48
+ gender: begin
49
+ patient.gender.upcase.first
50
+ rescue StandardError
51
+ nil
52
+ end,
53
+ age_group: patient.age_group,
54
+ start_date: patient.start_date
55
+ }
56
+ end
57
+
58
+ matched_patients
59
+ end
60
+
61
+ def find_patients_retention_period(patients)
62
+ patients.each do |patient|
63
+ retention_period = MONTHS.find do |period|
64
+ (start_date..end_date).cover?((patient.start_date + period.months).to_s)
65
+ end
66
+
67
+ next unless retention_period
68
+
69
+ yield retention_period, patient
70
+ end
71
+ end
72
+
73
+ # Pull all patients who started medication before the current reporting period but after
74
+ # the given `as_of` date and have any dispensation that ends in the current reporting
75
+ # period... That's a mouthful woah!!!
76
+ def retained_patients(as_of:)
77
+ start_date = ActiveRecord::Base.connection.quote(self.start_date)
78
+ end_date = ActiveRecord::Base.connection.quote(self.end_date)
79
+ as_of = ActiveRecord::Base.connection.quote(as_of)
80
+ ::Order.find_by_sql(
81
+ <<~SQL
82
+ SELECT
83
+ initial_orders.patient_id AS patient_id,
84
+ DATE(initial_orders.start_date) AS start_date,
85
+ last_orders.auto_expire_date AS auto_expire_date,
86
+ patient_identifier.identifier AS arv_number,
87
+ disaggregated_age_group(p.birthdate, DATE('#{@end_date}')) age_group,
88
+ p.gender gender
89
+ FROM orders initial_orders
90
+ INNER JOIN encounter initial_encounter ON initial_encounter.encounter_id = initial_orders.encounter_id AND initial_encounter.voided = 0 AND initial_encounter.program_id = 1
91
+ INNER JOIN person p ON p.person_id = initial_orders.patient_id AND p.voided = 0
92
+ LEFT JOIN patient_identifier ON patient_identifier.patient_id = initial_orders.patient_id AND patient_identifier.identifier_type = #{patient_identifier_type_id}
93
+ INNER JOIN (
94
+ SELECT last_order.patient_id, MAX(last_order.auto_expire_date) AS auto_expire_date
95
+ FROM orders last_order
96
+ INNER JOIN encounter last_encounter ON last_encounter.encounter_id = last_order.encounter_id AND last_encounter.voided = 0 AND last_encounter.program_id = 1
97
+ WHERE last_order.auto_expire_date BETWEEN #{start_date} AND #{end_date}
98
+ AND last_order.order_type_id = #{drug_order_type_id}
99
+ AND last_order.voided = 0
100
+ GROUP BY last_order.patient_id
101
+ ) last_orders ON initial_orders.patient_id = last_orders.patient_id
102
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = initial_orders.patient_id
103
+ WHERE initial_orders.start_date BETWEEN #{as_of} AND #{start_date}
104
+ AND initial_orders.order_type_id = #{drug_order_type_id} #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
105
+ AND initial_orders.auto_expire_date IS NOT NULL
106
+ AND initial_orders.patient_id NOT IN (
107
+ SELECT o.patient_id
108
+ FROM orders o
109
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.program_id = 1
110
+ WHERE o.order_type_id = #{drug_order_type_id} AND o.start_date < #{as_of} AND o.auto_expire_date IS NOT NULL AND o.voided = 0
111
+ )
112
+ GROUP BY initial_orders.patient_id
113
+ SQL
114
+ )
115
+ end
116
+
117
+ def all_patients(as_of:)
118
+ start_date = ActiveRecord::Base.connection.quote(self.start_date)
119
+ as_of = ActiveRecord::Base.connection.quote(as_of)
120
+
121
+ ::Order.find_by_sql(
122
+ <<~SQL
123
+ SELECT initial_order.patient_id AS patient_id,
124
+ DATE(initial_order.start_date) AS start_date,
125
+ patient_identifier.identifier AS arv_number,
126
+ disaggregated_age_group(p.birthdate, DATE('#{@end_date}')) age_group,
127
+ p.gender gender
128
+ FROM orders initial_order
129
+ INNER JOIN encounter initial_encounter ON initial_encounter.encounter_id = initial_order.encounter_id AND initial_encounter.program_id = 1
130
+ INNER JOIN person p ON p.person_id = initial_encounter.patient_id
131
+ LEFT JOIN patient_identifier ON patient_identifier.patient_id = initial_order.patient_id AND patient_identifier.identifier_type = #{patient_identifier_type_id}
132
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = initial_order.patient_id
133
+ WHERE initial_order.start_date BETWEEN #{as_of} AND #{start_date}
134
+ AND initial_order.voided = 0
135
+ AND initial_order.auto_expire_date IS NOT NULL
136
+ AND initial_order.order_type_id = #{drug_order_type_id}
137
+ AND p.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
138
+ AND initial_order.patient_id NOT IN (
139
+ SELECT orders.patient_id
140
+ FROM orders
141
+ INNER JOIN encounter ON encounter.encounter_id = orders.encounter_id AND encounter.program_id = 1 AND encounter.voided = 0
142
+ WHERE start_date < #{as_of} AND order_type_id = #{drug_order_type_id} AND orders.voided = 0
143
+ )
144
+ GROUP BY initial_order.patient_id
145
+ SQL
146
+ )
147
+ end
148
+
149
+ # def retained_patients(as_of:)
150
+ # start_date = ActiveRecord::Base.connection.quote(self.start_date)
151
+ # end_date = ActiveRecord::Base.connection.quote(self.end_date)
152
+ # as_of = ActiveRecord::Base.connection.quote(as_of)
153
+
154
+ # ::Order.find_by_sql(
155
+ # <<~SQL
156
+ # SELECT initial_order.patient_id AS patient_id,
157
+ # initial_order.start_date AS start_date,
158
+ # last_order.auto_expire_date AS auto_expire_date,
159
+ # patient_identifier.identifier AS arv_number,
160
+ # disaggregated_age_group(p.birthdate, DATE('#{@end_date}')) age_group,
161
+ # p.gender gender
162
+ # FROM orders initial_order
163
+ # INNER JOIN encounter initial_encounter ON initial_encounter.encounter_id = initial_order.encounter_id AND initial_encounter.program_id = 1
164
+ # INNER JOIN orders last_order ON last_order.patient_id = initial_order.patient_id
165
+ # INNER JOIN encounter last_encounter ON last_encounter.encounter_id = last_order.encounter_id
166
+ # INNER JOIN person p ON p.person_id = initial_encounter.patient_id
167
+ # LEFT JOIN patient_identifier ON patient_identifier.patient_id = initial_order.patient_id
168
+ # WHERE initial_order.start_date BETWEEN #{as_of} AND #{start_date}
169
+ # AND initial_order.voided = 0
170
+ # AND initial_order.auto_expire_date IS NOT NULL
171
+ # AND initial_order.order_type_id = #{drug_order_type_id}
172
+ # AND last_order.auto_expire_date BETWEEN #{start_date} AND #{end_date}
173
+ # AND last_order.order_type_id = #{drug_order_type_id}
174
+ # AND last_order.voided = 0
175
+ # AND p.voided = 0
176
+ # AND initial_order.start_date = (
177
+ # SELECT MIN(start_date) FROM orders
178
+ # WHERE patient_id = initial_order.patient_id
179
+ # AND start_date BETWEEN #{as_of} AND #{start_date}
180
+ # AND order_type_id = #{drug_order_type_id}
181
+ # AND voided = 0
182
+ # )
183
+ # AND initial_order.patient_id NOT IN (
184
+ # SELECT orders.patient_id
185
+ # FROM orders
186
+ # INNER JOIN encounter ON encounter.encounter_id = orders.encounter_id AND encounter.program_id = 1
187
+ # WHERE start_date < #{as_of} AND order_type_id = #{drug_order_type_id} AND orders.voided = 0
188
+ # )
189
+ # GROUP BY initial_order.patient_id
190
+ # SQL
191
+ # )
192
+ # end
193
+
194
+ # def all_patients(as_of:)
195
+ # start_date = ActiveRecord::Base.connection.quote(self.start_date)
196
+ # as_of = ActiveRecord::Base.connection.quote(as_of)
197
+
198
+ # ::Order.find_by_sql(
199
+ # <<~SQL
200
+ # SELECT initial_order.patient_id AS patient_id,
201
+ # initial_order.start_date AS start_date,
202
+ # patient_identifier.identifier AS arv_number,
203
+ # disaggregated_age_group(p.birthdate, DATE('#{@end_date}')) age_group,
204
+ # p.gender gender
205
+ # FROM orders initial_order
206
+ # INNER JOIN encounter initial_encounter ON initial_encounter.encounter_id = initial_order.encounter_id AND initial_encounter.program_id = 1
207
+ # INNER JOIN person p ON p.person_id = initial_encounter.patient_id
208
+ # LEFT JOIN patient_identifier ON patient_identifier.patient_id = initial_order.patient_id AND patient_identifier.identifier_type = #{patient_identifier_type_id}
209
+ # WHERE initial_order.start_date BETWEEN #{as_of} AND #{start_date}
210
+ # AND initial_order.voided = 0
211
+ # AND initial_order.auto_expire_date IS NOT NULL
212
+ # AND initial_order.order_type_id = #{drug_order_type_id}
213
+ # AND p.voided = 0
214
+ # AND initial_order.start_date = (
215
+ # SELECT MIN(start_date) FROM orders
216
+ # WHERE patient_id = initial_order.patient_id
217
+ # AND start_date BETWEEN #{as_of} AND #{start_date}
218
+ # AND order_type_id = #{drug_order_type_id}
219
+ # AND voided = 0
220
+ # )
221
+ # AND initial_order.patient_id NOT IN (
222
+ # SELECT orders.patient_id
223
+ # FROM orders
224
+ # INNER JOIN encounter ON encounter.encounter_id = orders.encounter_id AND encounter.program_id = 1
225
+ # WHERE start_date < #{as_of} AND order_type_id = #{drug_order_type_id} AND orders.voided = 0
226
+ # )
227
+ # GROUP BY initial_order.patient_id
228
+ # SQL
229
+ # )
230
+ # end
231
+
232
+ def drug_order_type_id
233
+ @drug_order_type_id ||= ::OrderType.find_by_name('Drug order').order_type_id
234
+ end
235
+
236
+ def patient_identifier_type_id
237
+ return @patient_identifier_type_id if @patient_identifier_type_id
238
+
239
+ identifier_type_name = @use_filing_number ? 'Filing Number' : 'ARV Number'
240
+ identifier_type = ::PatientIdentifierType.find_by_name!(identifier_type_name)
241
+
242
+ @patient_identifier_type_id ||= ActiveRecord::Base.connection.quote(identifier_type.id)
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Clinic
5
+ # Generates a stock card report for a clinic
6
+ class StockCardReport
7
+ def initialize(start_date:, end_date:, **_kwargs)
8
+ @start_date = ActiveRecord::Base.connection.quote(start_date)
9
+ @end_date = ActiveRecord::Base.connection.quote(end_date)
10
+ end
11
+
12
+ def find_report
13
+ # TODO: Implement this
14
+ stock_card_report
15
+ end
16
+
17
+ private
18
+
19
+ def stock_card_report
20
+ ActiveRecord::Base.connection.select_all <<~SQL
21
+ SELECT
22
+ pbi.drug_id AS drug_id,
23
+ COALESCE(d.name, 'Unkown') AS drug_name,
24
+ COALESCE(psb_opening.close_balance, 0)/pbi.pack_size AS opening_balance,
25
+ COALESCE(psb_closing.close_balance, 0)/pbi.pack_size AS closing_balance,
26
+ SUM(ABS(po.quantity))/pbi.pack_size AS dispensed_quantity,
27
+ pbi.pack_size
28
+ FROM pharmacy_batch_items AS pbi
29
+ INNER JOIN drug AS d ON d.drug_id = pbi.drug_id
30
+ LEFT JOIN pharmacy_obs AS po ON po.batch_item_id = pbi.id
31
+ AND po.voided = 0
32
+ AND po.pharmacy_encounter_type = 3 -- Pharmacy dispensing
33
+ AND po.transaction_date BETWEEN #{@start_date} AND #{@end_date}
34
+ AND po.dispensation_obs_id IS NOT NULL
35
+ LEFT JOIN (
36
+ SELECT
37
+ drug_id,
38
+ MAX(transaction_date) AS transaction_date,
39
+ pack_size
40
+ FROM pharmacy_stock_balances
41
+ WHERE transaction_date <= #{@end_date}
42
+ GROUP BY drug_id, pack_size
43
+ ) AS psb_max ON pbi.drug_id = psb_max.drug_id AND pbi.pack_size = psb_max.pack_size
44
+ LEFT JOIN (
45
+ SELECT
46
+ drug_id,
47
+ MAX(transaction_date) AS transaction_date,
48
+ pack_size
49
+ FROM pharmacy_stock_balances
50
+ WHERE transaction_date < #{@start_date} -- Opening balance can be anything less than end_date
51
+ GROUP BY drug_id, pack_size
52
+ ) AS psb_min ON pbi.drug_id = psb_min.drug_id AND pbi.pack_size = psb_min.pack_size
53
+ LEFT JOIN pharmacy_stock_balances AS psb_opening ON
54
+ pbi.drug_id = psb_opening.drug_id AND pbi.pack_size = psb_opening.pack_size
55
+ AND psb_opening.transaction_date = psb_min.transaction_date
56
+ LEFT JOIN pharmacy_stock_balances AS psb_closing ON
57
+ pbi.drug_id = psb_closing.drug_id AND pbi.pack_size = psb_closing.pack_size
58
+ AND psb_closing.transaction_date = psb_max.transaction_date
59
+ WHERE pbi.voided = 0
60
+ GROUP BY pbi.drug_id, pbi.pack_size
61
+ SQL
62
+ end
63
+ end
64
+ end
65
+ end