malawi_hiv_program_reports 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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