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.
- checksums.yaml +4 -4
- data/app/services/malawi_hiv_program_reports/README.md +16 -0
- data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
- data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
- data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
- data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
- data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
- data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
- data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
- data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
- data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
- data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
- data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
- data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
- data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
- data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
- data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
- data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
- data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
- data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
- data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
- data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
- data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
- data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
- data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
- data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
- data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
- data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
- data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
- data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
- data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
- data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
- data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
- data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
- data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
- data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
- data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
- data/lib/malawi_hiv_program_reports/version.rb +1 -1
- metadata +74 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Retrieve all patients who are taking ARVs in a given time period.
|
|
4
|
+
module MalawiHivProgramReports
|
|
5
|
+
module Clinic
|
|
6
|
+
class PatientsOnAntiretrovirals
|
|
7
|
+
attr_reader :start_date, :end_date
|
|
8
|
+
|
|
9
|
+
def initialize(start_date:, end_date:, **_kwargs)
|
|
10
|
+
@start_date = start_date
|
|
11
|
+
@end_date = end_date
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.within(start_date, end_date)
|
|
15
|
+
PatientsOnAntiretrovirals.new(start_date:, end_date:)
|
|
16
|
+
.patients
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_report
|
|
20
|
+
patients
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def patients
|
|
24
|
+
::DrugOrder.select('orders.patient_id AS patient_id')
|
|
25
|
+
.joins(:order)
|
|
26
|
+
.merge(art_orders)
|
|
27
|
+
.where(drug_inventory_id: ArtService::RegimenEngine.arv_drugs,
|
|
28
|
+
quantity: 1..Float::INFINITY)
|
|
29
|
+
.where('start_date BETWEEN :start_date AND :end_date
|
|
30
|
+
OR auto_expire_date BETWEEN :start_date AND :end_date
|
|
31
|
+
OR (start_date <= :start_date AND auto_expire_date >= :end_date)',
|
|
32
|
+
start_date:, end_date:)
|
|
33
|
+
.group('orders.patient_id')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def art_orders
|
|
39
|
+
::Order.joins(:encounter)
|
|
40
|
+
.where('encounter.program_id = ?', ::ArtService::Constants::PROGRAM_ID)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MalawiHivProgramReports
|
|
4
|
+
module Clinic
|
|
5
|
+
class PatientsOnDtg
|
|
6
|
+
attr_reader :start_date, :end_date
|
|
7
|
+
|
|
8
|
+
HIV_PROGRAM_ID = 1
|
|
9
|
+
ARV_NUMBER_TYPE_ID = 4
|
|
10
|
+
|
|
11
|
+
def initialize(start_date:, end_date:, **_)
|
|
12
|
+
@start_date = start_date
|
|
13
|
+
@end_date = end_date
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find_report
|
|
17
|
+
::DrugOrder.joins(:order)
|
|
18
|
+
.joins('INNER JOIN encounter USING (encounter_id)')
|
|
19
|
+
.joins('LEFT JOIN patient_identifier ON patient_identifier.patient_id = orders.patient_id')
|
|
20
|
+
.where(drug: dtg_drugs,
|
|
21
|
+
encounter: { program_id: HIV_PROGRAM_ID },
|
|
22
|
+
patient_identifier: { identifier_type: ARV_NUMBER_TYPE_ID })
|
|
23
|
+
.where('start_date BETWEEN ? AND ?', start_date, end_date)
|
|
24
|
+
.group('orders.patient_id')
|
|
25
|
+
.select('identifier')
|
|
26
|
+
.map(&:identifier)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def dtg_drugs
|
|
32
|
+
::Drug.where(concept_id: ::ConceptName.find_by_name('Dolutegravir').concept_id)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reports on all patients that were on treatment in given time period
|
|
4
|
+
# regardless of whether they have a terminal state or not within the
|
|
5
|
+
# period.
|
|
6
|
+
module MalawiHivProgramReports
|
|
7
|
+
module Clinic
|
|
8
|
+
class PatientsOnTreatment
|
|
9
|
+
attr_reader :start_date, :end_date
|
|
10
|
+
|
|
11
|
+
def initialize(start_date:, end_date:)
|
|
12
|
+
@start_date = start_date
|
|
13
|
+
@end_date = end_date
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns patients that were on treatment within the given time period.
|
|
17
|
+
def self.within(start_date, end_date)
|
|
18
|
+
sql_conditions =
|
|
19
|
+
<<~SQL
|
|
20
|
+
((start_date >= :start_date AND start_date <= :end_date)
|
|
21
|
+
OR (end_date >= :start_date AND end_date <= :end_date)
|
|
22
|
+
OR (start_date < :start_date AND end_date IS NULL))
|
|
23
|
+
AND state = :state
|
|
24
|
+
SQL
|
|
25
|
+
|
|
26
|
+
on_arvs = ::PatientState.where(sql_conditions, start_date:,
|
|
27
|
+
end_date:,
|
|
28
|
+
state: ::ArtService::Constants::States::ON_ANTIRETROVIRALS)
|
|
29
|
+
|
|
30
|
+
::PatientProgram.select('DISTINCT patient_program.patient_id')
|
|
31
|
+
.joins(:patient_states)
|
|
32
|
+
.merge(on_arvs)
|
|
33
|
+
.where(program_id: ::ArtService::Constants::PROGRAM_ID)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# We an interface to satisfy, let's be good citizens
|
|
37
|
+
def find_report
|
|
38
|
+
within(start_date, end_date).map(&:patient_id)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Retrieves all current(neither dead nor transferred out) patients missing
|
|
4
|
+
# any demographics or haven't had their demographics updated since the
|
|
5
|
+
# given date
|
|
6
|
+
module MalawiHivProgramReports
|
|
7
|
+
module Clinic
|
|
8
|
+
class PatientsWithOutdatedDemographics
|
|
9
|
+
attr_reader :date, :variant
|
|
10
|
+
|
|
11
|
+
def initialize(start_date:, variant: 'poc', **_kwargs)
|
|
12
|
+
@date = start_date
|
|
13
|
+
@variant = parse_variant(variant)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find_report
|
|
17
|
+
patients = if variant == 'poc'
|
|
18
|
+
poc_patients_missing_demographics
|
|
19
|
+
else
|
|
20
|
+
emastercard_patients_missing_demographics
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
patients.collect do |patient|
|
|
24
|
+
{
|
|
25
|
+
patient_id: patient.patient_id,
|
|
26
|
+
arv_number: patient.arv_number,
|
|
27
|
+
given_name: patient.given_name,
|
|
28
|
+
family_name: patient.family_name,
|
|
29
|
+
birthdate: patient[:birthdate],
|
|
30
|
+
gender: patient[:gender],
|
|
31
|
+
current_village: patient.current_village,
|
|
32
|
+
current_traditional_authority: patient.current_traditional_authority,
|
|
33
|
+
current_district: patient.current_district,
|
|
34
|
+
home_village: patient.home_village,
|
|
35
|
+
home_traditional_authority: patient.home_traditional_authority,
|
|
36
|
+
home_district: patient.home_district,
|
|
37
|
+
landmark: patient.landmark,
|
|
38
|
+
address_last_updated_date: patient.address_last_updated_date
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Find all patients missing any of the following variables:
|
|
44
|
+
# - birthdate
|
|
45
|
+
# - gender
|
|
46
|
+
# - name
|
|
47
|
+
# - address
|
|
48
|
+
def poc_patients_missing_demographics
|
|
49
|
+
quoted_date = ActiveRecord::Base.connection.quote(date)
|
|
50
|
+
|
|
51
|
+
::Patient.find_by_sql(
|
|
52
|
+
<<~SQL
|
|
53
|
+
SELECT patient.patient_id AS patient_id,
|
|
54
|
+
patient_identifier.identifier AS arv_number,
|
|
55
|
+
person_name.given_name AS given_name,
|
|
56
|
+
person_name.family_name AS family_name,
|
|
57
|
+
person.birthdate AS birthdate,
|
|
58
|
+
person.gender AS gender,
|
|
59
|
+
person_address.city_village AS current_village,
|
|
60
|
+
person_address.township_division AS current_traditional_authority,
|
|
61
|
+
person_address.state_province AS current_district,
|
|
62
|
+
person_address.neighborhood_cell AS home_village,
|
|
63
|
+
person_address.county_district AS home_traditional_authority,
|
|
64
|
+
person_address.address2 AS home_district,
|
|
65
|
+
landmark.value AS landmark,
|
|
66
|
+
person_address.date_created AS address_last_updated_date
|
|
67
|
+
FROM patient
|
|
68
|
+
INNER JOIN person ON person.person_id = patient.patient_id AND person.voided = 0
|
|
69
|
+
INNER JOIN patient_program ON patient_program.patient_id = patient.patient_id AND patient_program.program_id = 1
|
|
70
|
+
INNER JOIN patient_state ON patient_state.patient_program_id = patient_program.patient_program_id
|
|
71
|
+
LEFT JOIN patient_identifier ON patient_identifier.patient_id = patient.patient_id
|
|
72
|
+
AND patient_identifier.voided = 0 AND patient_identifier.identifier_type = #{arv_number_id}
|
|
73
|
+
LEFT JOIN person_name ON person_name.person_id = patient.patient_id AND person_name.voided = 0
|
|
74
|
+
LEFT JOIN person_address ON person_address.person_id = patient.patient_id AND person_address.voided = 0
|
|
75
|
+
LEFT JOIN person_attribute landmark ON landmark.person_id = patient.patient_id
|
|
76
|
+
AND landmark.person_attribute_type_id = #{landmark_attribute_type_id}
|
|
77
|
+
WHERE patient.voided = 0
|
|
78
|
+
AND (patient_state.state != #{died_state_id}
|
|
79
|
+
OR (patient_state.state = #{transferred_out_state_id} AND patient_state.start_date < #{quoted_date}
|
|
80
|
+
AND (patient_state.end_date IS NULL OR patient_state.end_date > #{quoted_date})))
|
|
81
|
+
AND ((person.birthdate IS NULL OR TRIM(COALESCE(person.gender, '')) = '')
|
|
82
|
+
OR (TRIM(COALESCE(person_name.given_name, '')) = '' OR TRIM(COALESCE(person_name.family_name, '')) = '')
|
|
83
|
+
OR (TRIM(COALESCE(person_address.city_village, '')) = ''
|
|
84
|
+
OR TRIM(COALESCE(person_address.township_division, '')) = ''
|
|
85
|
+
OR TRIM(COALESCE(person_address.state_province, '')) = ''
|
|
86
|
+
OR TRIM(COALESCE(person_address.neighborhood_cell, '') = '')
|
|
87
|
+
OR TRIM(COALESCE(person_address.county_district, '') = '')
|
|
88
|
+
OR TRIM(COALESCE(person_address.address2, '') = '')
|
|
89
|
+
OR person_address.date_created < #{quoted_date}))
|
|
90
|
+
GROUP BY patient.patient_id
|
|
91
|
+
SQL
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Find all patients missing any of the following variables:
|
|
96
|
+
# - birthdate
|
|
97
|
+
# - gender
|
|
98
|
+
# - name
|
|
99
|
+
# - landmark
|
|
100
|
+
#
|
|
101
|
+
# NOTE: Old emastercard application did not collect a detailed and
|
|
102
|
+
# properly formatted address. The address was collected as free text and
|
|
103
|
+
# there was no standard provided for the address thus it's treated as
|
|
104
|
+
# a landmark by this system. Even for the new eMastercard application
|
|
105
|
+
# running atop this API, a free text alternative is provided if the
|
|
106
|
+
def emastercard_patients_missing_demographics
|
|
107
|
+
quoted_date = ActiveRecord::Base.connection.quote(date.to_s)
|
|
108
|
+
|
|
109
|
+
::Patient.find_by_sql(
|
|
110
|
+
<<~SQL
|
|
111
|
+
SELECT patient.patient_id AS patient_id,
|
|
112
|
+
patient_identifier.identifier AS arv_number,
|
|
113
|
+
person_name.given_name AS given_name,
|
|
114
|
+
person_name.family_name AS family_name,
|
|
115
|
+
person.birthdate AS birthdate,
|
|
116
|
+
person.gender AS gender,
|
|
117
|
+
person_address.city_village AS current_village,
|
|
118
|
+
person_address.township_division AS current_traditional_authority,
|
|
119
|
+
person_address.state_province AS current_district,
|
|
120
|
+
person_address.neighborhood_cell AS home_village,
|
|
121
|
+
person_address.county_district AS home_traditional_authority,
|
|
122
|
+
person_address.address2 AS home_district,
|
|
123
|
+
landmark.value AS landmark,
|
|
124
|
+
person_address.date_created AS address_last_updated_date
|
|
125
|
+
FROM patient
|
|
126
|
+
INNER JOIN person ON person.person_id = patient.patient_id AND person.voided = 0
|
|
127
|
+
INNER JOIN patient_program ON patient_program.patient_id = patient.patient_id AND patient_program.program_id = 1
|
|
128
|
+
INNER JOIN patient_state ON patient_state.patient_program_id = patient_program.patient_program_id
|
|
129
|
+
LEFT JOIN patient_identifier ON patient_identifier.patient_id = patient.patient_id
|
|
130
|
+
AND patient_identifier.voided = 0 AND patient_identifier.identifier_type = #{arv_number_id}
|
|
131
|
+
LEFT JOIN person_name ON person_name.person_id = patient.patient_id AND person_name.voided = 0
|
|
132
|
+
LEFT JOIN person_address ON person_address.person_id = patient.patient_id AND person_address.voided = 0
|
|
133
|
+
LEFT JOIN person_attribute landmark ON landmark.person_id = patient.patient_id
|
|
134
|
+
AND landmark.person_attribute_type_id = #{landmark_attribute_type_id}
|
|
135
|
+
WHERE patient.voided = 0
|
|
136
|
+
AND (patient_state.state != #{died_state_id}
|
|
137
|
+
OR (patient_state.state = #{transferred_out_state_id} AND patient_state.start_date < #{quoted_date}
|
|
138
|
+
AND (patient_state.end_date IS NULL OR patient_state.end_date > #{quoted_date})))
|
|
139
|
+
AND ((person.birthdate IS NULL OR TRIM(COALESCE(person.gender, '')) = '')
|
|
140
|
+
OR (TRIM(COALESCE(person_name.given_name, '')) = '' OR TRIM(COALESCE(person_name.family_name, '')) = '')
|
|
141
|
+
OR (TRIM(COALESCE(landmark.value, '')) = '' OR landmark.date_created < #{quoted_date}))
|
|
142
|
+
GROUP BY patient.patient_id
|
|
143
|
+
SQL
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def arv_number_id
|
|
148
|
+
@arv_number_id ||= ::PatientIdentifierType.find_by(name: 'ARV Number').patient_identifier_type_id
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def died_state_id
|
|
152
|
+
@died_state_id ||= ::ProgramWorkflowState.find_by_name_and_program(name: 'Died', program_id: 1).id
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def transferred_out_state_id
|
|
156
|
+
@transferred_out_state_id ||= ::ProgramWorkflowState.find_by_name_and_program(name: 'Patient transferred out',
|
|
157
|
+
program_id: 1).id
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def landmark_attribute_type_id
|
|
161
|
+
@landmark_attribute_type_id ||= ::PersonAttributeType.find_by_name('Landmark Or Plot Number').id
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def parse_variant(variant)
|
|
165
|
+
variant = variant.downcase
|
|
166
|
+
|
|
167
|
+
return variant if %w[poc emastercard].include?(variant)
|
|
168
|
+
|
|
169
|
+
raise ::InvalidParameterError, "Invalid report variant '#{variant}'; expected poc or emastercard"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Retrieve all pregnant females in a given time period
|
|
4
|
+
module MalawiHivProgramReports
|
|
5
|
+
module Clinic
|
|
6
|
+
class PregnantPatients
|
|
7
|
+
attr_reader :start_date, :end_date
|
|
8
|
+
|
|
9
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
|
10
|
+
|
|
11
|
+
def initialize(start_date:, end_date:, **kwargs)
|
|
12
|
+
@start_date = start_date
|
|
13
|
+
@end_date = end_date
|
|
14
|
+
@occupation = kwargs[:occupation]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find_report
|
|
18
|
+
all_pregnant.map do |patient|
|
|
19
|
+
{
|
|
20
|
+
patient_id: patient.patient_id,
|
|
21
|
+
arv_number: patient.arv_number,
|
|
22
|
+
given_name: patient.given_name,
|
|
23
|
+
family_name: patient.family_name,
|
|
24
|
+
gender: patient.gender,
|
|
25
|
+
birthdate: patient.birthdate,
|
|
26
|
+
last_reported_date: patient.last_reported_date
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def all_pregnant
|
|
34
|
+
str_start_date = ActiveRecord::Base.connection.quote(start_date)
|
|
35
|
+
str_end_date = ActiveRecord::Base.connection.quote(end_date)
|
|
36
|
+
|
|
37
|
+
::Person.find_by_sql(
|
|
38
|
+
<<~SQL
|
|
39
|
+
SELECT obs.person_id AS patient_id,
|
|
40
|
+
patient_identifier.identifier AS arv_number,
|
|
41
|
+
given_name,
|
|
42
|
+
family_name,
|
|
43
|
+
gender,
|
|
44
|
+
birthdate,
|
|
45
|
+
obs.obs_datetime AS last_reported_date
|
|
46
|
+
FROM obs
|
|
47
|
+
INNER JOIN person ON person.person_id = obs.person_id
|
|
48
|
+
INNER JOIN person_name ON person_name.person_id = obs.person_id
|
|
49
|
+
LEFT JOIN patient_identifier ON patient_identifier.patient_id = obs.person_id
|
|
50
|
+
AND patient_identifier.identifier_type = #{arv_number_type_id}
|
|
51
|
+
INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id
|
|
52
|
+
AND encounter.program_id = #{hiv_program_id}
|
|
53
|
+
LEFT JOIN (#{current_occupation_query}) a ON a.person_id = obs.person_id
|
|
54
|
+
WHERE obs.concept_id IN (#{pregnant_concepts.select(:concept_id).to_sql})
|
|
55
|
+
AND obs.value_coded = #{yes_concept_id}
|
|
56
|
+
AND obs.person_id IN (#{patients_on_treatment.to_sql})
|
|
57
|
+
AND obs.obs_datetime = (
|
|
58
|
+
SELECT MAX(obs_date.obs_datetime) FROM obs obs_date
|
|
59
|
+
INNER JOIN encounter USING (encounter_id)
|
|
60
|
+
WHERE obs_date.concept_id IN (#{pregnant_concepts.select(:concept_id).to_sql})
|
|
61
|
+
AND DATE(obs_date.obs_datetime) BETWEEN #{str_start_date} AND #{str_end_date}
|
|
62
|
+
AND obs_date.person_id = obs.person_id
|
|
63
|
+
AND program_id = #{hiv_program_id}
|
|
64
|
+
) #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
|
|
65
|
+
GROUP BY obs.person_id
|
|
66
|
+
SQL
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def patients_on_treatment
|
|
71
|
+
PatientsOnTreatment.within(start_date, end_date)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def pregnant_concepts
|
|
75
|
+
::ConceptName.where(name: ['Is patient pregnant?', 'patient_pregnant'])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def yes_concept_id
|
|
79
|
+
::ConceptName.find_by_name('Yes').concept_id
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def arv_number_type_id
|
|
83
|
+
::PatientIdentifierType.find_by_name('ARV Number').id
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def hiv_program_id
|
|
87
|
+
::ArtService::Constants::PROGRAM_ID
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# this report is used to generate regimen data
|
|
4
|
+
module MalawiHivProgramReports
|
|
5
|
+
module Clinic
|
|
6
|
+
class RegimenDispensationData
|
|
7
|
+
attr_reader :start_date, :end_date, :type
|
|
8
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
|
9
|
+
include MalawiHivProgramReports::Adapters::Moh::Custom
|
|
10
|
+
include Utils
|
|
11
|
+
|
|
12
|
+
def initialize(start_date:, end_date:, **kwargs)
|
|
13
|
+
@start_date = ActiveRecord::Base.connection.quote(start_date.to_date.strftime('%Y-%m-%d 00:00:00'))
|
|
14
|
+
@end_date = ActiveRecord::Base.connection.quote(end_date.to_date.strftime('%Y-%m-%d 23:59:59'))
|
|
15
|
+
@type = kwargs[:type]
|
|
16
|
+
@occupation = kwargs[:occupation]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_report
|
|
20
|
+
drop_regimen_data
|
|
21
|
+
create_filtered_data
|
|
22
|
+
process_clients
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# rubocop:disable Metrics/AbcSize
|
|
28
|
+
def drop_regimen_data
|
|
29
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_dispensation'
|
|
30
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_drug_dispensed'
|
|
31
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_regimen_names'
|
|
32
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_patient_regimen'
|
|
33
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_reg_outcome'
|
|
34
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_vl_results'
|
|
35
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_vl_results'
|
|
36
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_regimen_patient_weight'
|
|
37
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_regimen_data'
|
|
38
|
+
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_patient_start_date'
|
|
39
|
+
end
|
|
40
|
+
# rubocop:enable Metrics/AbcSize
|
|
41
|
+
|
|
42
|
+
def create_filtered_data
|
|
43
|
+
create_temp_current_dispensation
|
|
44
|
+
create_temp_drug_dispensed
|
|
45
|
+
create_temp_current_regimen
|
|
46
|
+
create_temp_current_patient_regimen
|
|
47
|
+
create_temp_patient_outcome
|
|
48
|
+
create_temp_vl_result
|
|
49
|
+
create_temp_current_vl_results
|
|
50
|
+
create_temp_regimen_patient_weight
|
|
51
|
+
create_temp_patient_start_date
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def create_temp_current_dispensation
|
|
55
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
56
|
+
CREATE TABLE temp_current_dispensation
|
|
57
|
+
SELECT o.patient_id, MAX(o.start_date) AS start_date
|
|
58
|
+
FROM orders o
|
|
59
|
+
INNER JOIN drug_order od ON od.order_id = o.order_id AND od.quantity > 0 AND od.drug_inventory_id IN (SELECT drug_id FROM arv_drug)
|
|
60
|
+
LEFT JOIN (#{current_occupation_query}) a ON a.person_id = o.patient_id
|
|
61
|
+
WHERE o.start_date > #{end_date} - INTERVAL 18 MONTH AND o.start_date < #{end_date} + INTERVAL 1 DAY
|
|
62
|
+
AND o.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
|
|
63
|
+
AND o.order_type_id = 1 -- drug order
|
|
64
|
+
GROUP BY o.patient_id
|
|
65
|
+
SQL
|
|
66
|
+
ActiveRecord::Base.connection.execute 'create index current_disp on temp_current_dispensation (patient_id, start_date)'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create_temp_drug_dispensed
|
|
70
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
71
|
+
CREATE table temp_drug_dispensed
|
|
72
|
+
SELECT o.patient_id, od.drug_inventory_id, d.name, o.start_date, od.quantity
|
|
73
|
+
FROM orders o
|
|
74
|
+
INNER JOIN temp_current_dispensation tcd ON tcd.patient_id = o.patient_id AND tcd.start_date = o.start_date
|
|
75
|
+
INNER JOIN drug_order od ON od.order_id = o.order_id AND od.quantity > 0 AND od.drug_inventory_id IN (SELECT drug_id FROM arv_drug)
|
|
76
|
+
INNER JOIN drug d ON d.drug_id = od.drug_inventory_id AND d.retired = 0
|
|
77
|
+
WHERE o.voided = 0
|
|
78
|
+
AND o.order_type_id = 1 -- drug order
|
|
79
|
+
SQL
|
|
80
|
+
ActiveRecord::Base.connection.execute 'create index drug_disp on temp_drug_dispensed (patient_id, start_date)'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def create_temp_current_regimen
|
|
84
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
85
|
+
CREATE table temp_current_regimen_names
|
|
86
|
+
SELECT GROUP_CONCAT(drug.drug_id ORDER BY drug.drug_id ASC) AS drugs, regimen_name.name AS name
|
|
87
|
+
FROM moh_regimen_combination AS combo
|
|
88
|
+
INNER JOIN moh_regimen_combination_drug AS drug USING (regimen_combination_id)
|
|
89
|
+
INNER JOIN moh_regimen_name AS regimen_name USING (regimen_name_id)
|
|
90
|
+
GROUP BY combo.regimen_combination_id
|
|
91
|
+
SQL
|
|
92
|
+
ActiveRecord::Base.connection.execute 'create index regimen_names on temp_current_regimen_names (drugs(50))'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_temp_current_patient_regimen
|
|
96
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
97
|
+
CREATE table temp_current_patient_regimen
|
|
98
|
+
SELECT patient_id, name
|
|
99
|
+
FROM temp_current_regimen_names tcrn
|
|
100
|
+
INNER JOIN (
|
|
101
|
+
SELECT patient_id, GROUP_CONCAT(drug_inventory_id ORDER BY drug_inventory_id ASC) AS drugs
|
|
102
|
+
FROM temp_drug_dispensed
|
|
103
|
+
GROUP BY patient_id
|
|
104
|
+
) d ON d.drugs = tcrn.drugs
|
|
105
|
+
SQL
|
|
106
|
+
ActiveRecord::Base.connection.execute 'create index patient_regimen on temp_current_patient_regimen (patient_id)'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def create_temp_patient_outcome
|
|
110
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
111
|
+
CREATE TABLE temp_reg_outcome
|
|
112
|
+
SELECT patient_id, #{type == 'pepfar' ? "pepfar_patient_outcome(patient_id, #{end_date})" : "patient_outcome(patient_id, #{end_date})"} outcome
|
|
113
|
+
FROM temp_current_dispensation
|
|
114
|
+
SQL
|
|
115
|
+
ActiveRecord::Base.connection.execute 'create index reg_outcome on temp_reg_outcome (patient_id)'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def create_temp_vl_result
|
|
119
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
120
|
+
CREATE table temp_vl_results
|
|
121
|
+
SELECT
|
|
122
|
+
lab_result_obs.obs_datetime AS result_date,
|
|
123
|
+
CONCAT (COALESCE(measure.value_modifier, '='),' ',COALESCE(measure.value_numeric, measure.value_text, '')) AS result,
|
|
124
|
+
lab_result_obs.person_id AS patient_id
|
|
125
|
+
FROM obs AS lab_result_obs
|
|
126
|
+
INNER JOIN orders ON orders.order_id = lab_result_obs.order_id AND orders.voided = 0
|
|
127
|
+
INNER JOIN obs AS measure ON measure.obs_group_id = lab_result_obs.obs_id AND measure.voided = 0
|
|
128
|
+
INNER JOIN (
|
|
129
|
+
SELECT concept_id, name
|
|
130
|
+
FROM concept_name
|
|
131
|
+
INNER JOIN concept USING (concept_id)
|
|
132
|
+
WHERE concept.retired = 0
|
|
133
|
+
AND name NOT LIKE 'Lab test result'
|
|
134
|
+
GROUP BY concept_id
|
|
135
|
+
) AS measure_concept ON measure_concept.concept_id = measure.concept_id
|
|
136
|
+
WHERE lab_result_obs.voided = 0
|
|
137
|
+
AND measure.person_id IN (SELECT patient_id FROM temp_reg_outcome WHERE outcome = 'On antiretrovirals')
|
|
138
|
+
AND (measure.value_numeric IS NOT NULL || measure.value_text IS NOT NULL)
|
|
139
|
+
AND lab_result_obs.obs_datetime < #{end_date} + INTERVAL 1 DAY
|
|
140
|
+
ORDER BY lab_result_obs.obs_datetime DESC
|
|
141
|
+
SQL
|
|
142
|
+
ActiveRecord::Base.connection.execute 'create index vl_result on temp_vl_results (patient_id, result_date)'
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def create_temp_current_vl_results
|
|
146
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
147
|
+
CREATE TABLE temp_current_vl_results
|
|
148
|
+
SELECT t.*
|
|
149
|
+
FROM temp_vl_results t
|
|
150
|
+
LEFT JOIN temp_vl_results td ON td.patient_id = t.patient_id AND td.result_date > t.result_date
|
|
151
|
+
WHERE td.patient_id IS NULL
|
|
152
|
+
SQL
|
|
153
|
+
ActiveRecord::Base.connection.execute 'create index current_vl on temp_current_vl_results (patient_id)'
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def create_temp_regimen_patient_weight
|
|
157
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
158
|
+
CREATE TABLE temp_regimen_patient_weight
|
|
159
|
+
SELECT tro.patient_id, o.value_numeric AS weight
|
|
160
|
+
FROM temp_reg_outcome tro
|
|
161
|
+
INNER JOIN (
|
|
162
|
+
SELECT person_id, MAX(obs_datetime) AS max_datetime
|
|
163
|
+
FROM obs
|
|
164
|
+
WHERE concept_id = 5089 AND voided = 0 AND obs_datetime < #{end_date} + INTERVAL 1 DAY
|
|
165
|
+
GROUP BY person_id
|
|
166
|
+
) latest_obs ON latest_obs.person_id = tro.patient_id
|
|
167
|
+
INNER JOIN obs o ON o.person_id = latest_obs.person_id AND o.concept_id = 5089 AND o.obs_datetime = latest_obs.max_datetime AND o.voided = 0 AND o.obs_datetime < #{end_date} + INTERVAL 1 DAY
|
|
168
|
+
WHERE tro.outcome = 'On antiretrovirals'
|
|
169
|
+
SQL
|
|
170
|
+
ActiveRecord::Base.connection.execute 'create index patient_weight on temp_regimen_patient_weight (patient_id)'
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def create_temp_patient_start_date
|
|
174
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
|
175
|
+
CREATE TABLE temp_patient_start_date
|
|
176
|
+
SELECT
|
|
177
|
+
`p`.`patient_id` AS `patient_id`,
|
|
178
|
+
cast(patient_date_enrolled(`p`.`patient_id`) as date) AS `date_enrolled`,
|
|
179
|
+
date_antiretrovirals_started(`p`.`patient_id`, min(`s`.`start_date`)) AS `earliest_start_date`
|
|
180
|
+
FROM
|
|
181
|
+
((`patient_program` `p`
|
|
182
|
+
LEFT JOIN `person` `pe` ON ((`pe`.`person_id` = `p`.`patient_id`))
|
|
183
|
+
LEFT JOIN `patient_state` `s` ON ((`p`.`patient_program_id` = `s`.`patient_program_id`)))
|
|
184
|
+
LEFT JOIN `person` ON ((`person`.`person_id` = `p`.`patient_id`)))
|
|
185
|
+
WHERE
|
|
186
|
+
((`p`.`voided` = 0)
|
|
187
|
+
AND (`s`.`voided` = 0)
|
|
188
|
+
AND (`p`.`program_id` = 1)
|
|
189
|
+
AND (`s`.`state` = 7))
|
|
190
|
+
AND (`s`.`start_date` < #{end_date} + INTERVAL 1 DAY)
|
|
191
|
+
AND p.patient_id IN (SELECT patient_id FROM temp_reg_outcome WHERE outcome = 'On antiretrovirals')
|
|
192
|
+
GROUP BY `p`.`patient_id`;
|
|
193
|
+
SQL
|
|
194
|
+
ActiveRecord::Base.connection.execute 'create index start_date on temp_patient_start_date (patient_id)'
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def clients_alive_on_treatment
|
|
198
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
199
|
+
SELECT
|
|
200
|
+
trc.patient_id,
|
|
201
|
+
p.gender,
|
|
202
|
+
p.birthdate,
|
|
203
|
+
pn.given_name,
|
|
204
|
+
pn.family_name,
|
|
205
|
+
i.identifier arv_number,
|
|
206
|
+
tpw.weight,
|
|
207
|
+
tcvr.result_date,
|
|
208
|
+
tcvr.result,
|
|
209
|
+
tcp.name regimen,
|
|
210
|
+
trc.earliest_start_date
|
|
211
|
+
FROM temp_patient_start_date trc
|
|
212
|
+
INNER JOIN temp_current_dispensation tcd ON tcd.patient_id = trc.patient_id AND tcd.start_date BETWEEN #{@start_date} AND #{@end_date}
|
|
213
|
+
INNER JOIN person p ON p.person_id = trc.patient_id AND p.voided = 0
|
|
214
|
+
INNER JOIN person_name pn ON pn.person_id = p.person_id AND pn.voided = 0
|
|
215
|
+
LEFT JOIN patient_identifier i ON i.patient_id = p.person_id AND i.identifier_type = 4 AND i.voided = 0
|
|
216
|
+
LEFT JOIN temp_regimen_patient_weight tpw ON tpw.patient_id = trc.patient_id
|
|
217
|
+
LEFT JOIN temp_current_vl_results tcvr ON tcvr.patient_id = trc.patient_id
|
|
218
|
+
LEFT JOIN temp_current_patient_regimen tcp ON tcp.patient_id = trc.patient_id
|
|
219
|
+
GROUP BY trc.patient_id
|
|
220
|
+
SQL
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def alive_clients
|
|
224
|
+
clients = ActiveRecord::Base.connection.select_all <<~SQL
|
|
225
|
+
SELECT trc.patient_id
|
|
226
|
+
FROM temp_reg_outcome trc
|
|
227
|
+
INNER JOIN person p ON p.person_id = trc.patient_id AND p.voided = 0
|
|
228
|
+
where trc.outcome = 'On antiretrovirals' AND LEFT(p.gender, 1) = 'F'
|
|
229
|
+
SQL
|
|
230
|
+
clients.map { |client| client['patient_id'] }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def process_clients
|
|
234
|
+
clients = {}
|
|
235
|
+
@maternal_status = maternal_status
|
|
236
|
+
clients_alive_on_treatment.each do |client|
|
|
237
|
+
clients[client['patient_id']] = {
|
|
238
|
+
arv_number: client['arv_number'],
|
|
239
|
+
given_name: client['given_name'],
|
|
240
|
+
family_name: client['family_name'],
|
|
241
|
+
birthdate: client['birthdate'],
|
|
242
|
+
gender: client['gender'] == 'M' ? 'M' : fetch_maternal_status(client['patient_id']),
|
|
243
|
+
current_regimen: client['regimen'],
|
|
244
|
+
current_weight: client['weight'],
|
|
245
|
+
art_start_date: client['earliest_start_date'],
|
|
246
|
+
medication: fetch_medication(client['patient_id']),
|
|
247
|
+
vl_result: client['result'],
|
|
248
|
+
vl_result_date: client['result_date']
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
clients
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def maternal_status
|
|
255
|
+
MalawiHivProgramReports::Pepfar::ViralLoadCoverage2.new(start_date: @start_date,
|
|
256
|
+
end_date: @end_date).vl_maternal_status(alive_clients)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def fetch_maternal_status(patient_id)
|
|
260
|
+
return nil if patient_id.blank?
|
|
261
|
+
|
|
262
|
+
gender = 'FNP'
|
|
263
|
+
gender = 'FP' if @maternal_status[:FP].include?(patient_id)
|
|
264
|
+
gender = 'FBf' if @maternal_status[:FBf].include?(patient_id)
|
|
265
|
+
gender
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def fetch_medication(patient_id)
|
|
269
|
+
result = []
|
|
270
|
+
data = ActiveRecord::Base.connection.select_all <<~SQL
|
|
271
|
+
SELECT tdd.name, DATE(tdd.start_date) start_date, tdd.quantity
|
|
272
|
+
FROM temp_drug_dispensed tdd
|
|
273
|
+
WHERE tdd.patient_id = #{patient_id}
|
|
274
|
+
SQL
|
|
275
|
+
data.each do |row|
|
|
276
|
+
result << { medication: row['name'], start_date: row['start_date'], quantity: row['quantity'] }
|
|
277
|
+
end
|
|
278
|
+
result
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|