Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions docs/cli/indicators/R/055.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# TODO The title of the indicator (R055)

TODO A one-sentence description of the indicator.

## Methodology

TODO

:::{admonition} Example
:class: seealso

TODO
:::

:::{admonition} Why is this a red flag?
:class: hint

TODO
:::

<small>Based on "TODO" in [*TODO*](TODO).</small>

## Output

The indicator's value is TODO.

## Configuration

All configuration is optional. To override the default TODO:

```ini
[R055]
TODO
```

## Exclusions

A contracting process is excluded if:

- TODO

## Assumptions

TODO

## Demonstration

*Input*

:::{literalinclude} ../../../examples/R/055.jsonl
:language: json
:::

*Output*

```console
$ ocdscardinal indicators --settings docs/examples/settings.ini --no-meta docs/examples/R/055.jsonl
{}

```
5 changes: 5 additions & 0 deletions docs/cli/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ $ ocdscardinal init -
; threshold = 10
; minimum_contracting_processes = 20

[R055]
; threshold = 66593
; start_date = 2022-01-01
; end_date = 2022-12-31

[R058]
; threshold = 0.5

Expand Down
1 change: 1 addition & 0 deletions docs/examples/R/055.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions docs/examples/settings.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
[R036]
[R038]
[R048]
[R055]
[R058]
38 changes: 37 additions & 1 deletion src/indicators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ pub mod r035;
pub mod r036;
pub mod r038;
pub mod r048;
pub mod r055;
pub mod r058;
pub mod util;

use std::collections::{HashMap, HashSet};
use std::ops::AddAssign;

use chrono::NaiveDate;
use indexmap::IndexMap;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};

// Settings.
Expand Down Expand Up @@ -119,6 +121,32 @@ pub struct R048 {
pub minimum_contracting_processes: Option<usize>,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct R055 {
pub threshold: Option<f64>,
#[serde(deserialize_with = "naive_date_from_str")]
pub start_date: Option<NaiveDate>,
#[serde(deserialize_with = "naive_date_from_str")]
pub end_date: Option<NaiveDate>,
}

// https://serde.rs/field-attrs.html#deserialize_with
fn naive_date_from_str<'de, D>(deserializer: D) -> Result<Option<NaiveDate>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
s.map_or_else(
|| Ok(None),
|s| {
NaiveDate::parse_from_str(&s, "%Y-%m-%d")
.map(Some)
.map_err(de::Error::custom)
},
)
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(non_snake_case)]
Expand All @@ -144,6 +172,7 @@ pub struct Settings {
pub R036: Option<Empty>,
pub R038: Option<R038>,
pub R048: Option<R048>,
pub R055: Option<R055>,
pub R058: Option<FloatThreshold>, // ratio
}

Expand All @@ -169,6 +198,7 @@ pub enum Indicator {
R036,
R038,
R048,
R055,
R058,
}

Expand Down Expand Up @@ -216,6 +246,12 @@ pub struct Indicators {
pub r038_tenderer: HashMap<String, Fraction>,
/// The item classifications for each `bids/details/tenderers/id`.
pub r048_classifications: HashMap<String, (usize, HashSet<String>)>,
/// The `tender/value/amount` for each `ocid` when `tender/procurementMethod` is 'open'.
pub r055_open_tender_amount: HashMap<String, f64>,
/// The total awarded amount for each `buyer/id` and `awards/suppliers/id` when `tender/procurementMethod` is 'direct'.
pub r055_direct_awarded_amount_supplier_buyer: HashMap<(String, String), f64>,
/// The total awarded amount for each `tender/procuringEntity/id` and `awards/suppliers/id` when `tender/procurementMethod` is 'direct'.
pub r055_direct_awarded_amount_supplier_procuring_entity: HashMap<(String, String), f64>,
/// Whether to map contracting processes to organizations.
pub map: bool,
}
Expand Down
133 changes: 133 additions & 0 deletions src/indicators/r055.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use chrono::{Duration, NaiveDate, Utc};
use log::warn;
use serde_json::{Map, Value};

use crate::indicators::{set_meta, set_result, sum, Calculate, Indicators, Settings};

#[derive(Default)]
pub struct R055 {
threshold: Option<f64>, // resolved in finalize()
start_date: NaiveDate,
end_date: NaiveDate,
currency: Option<String>,
}

impl Calculate for R055 {
fn new(settings: &mut Settings) -> Self {
let setting = std::mem::take(&mut settings.R055).unwrap_or_default();
let today = Utc::now().date_naive();

Self {
threshold: setting.threshold,
start_date: setting.start_date.unwrap_or(today - Duration::days(365)),
end_date: setting.end_date.unwrap_or(today),
currency: settings.currency.clone(),
}
}

fn fold(&self, item: &mut Indicators, release: &Map<String, Value>, ocid: &str) {
if let Some(Value::Object(tender)) = release.get("tender")
&& let Some(Value::Object(tender_period)) = tender.get("tenderPeriod")
&& let Some(Value::String(date)) = tender_period.get("startDate")
&& let Some(Value::String(method)) = tender.get("procurementMethod")
&& method == "open"
&& let Ok(date) = NaiveDate::parse_from_str(date, "%Y-%m-%dT%H:%M:%S%z")
&& date >= self.start_date
&& date <= self.end_date
&& let Some(Value::Object(value)) = tender.get("value")
&& let Some(Value::Number(amount)) = value.get("amount")
&& let Some(Value::String(currency)) = value.get("currency")
&& let Some(amount) = amount.as_f64()
{
if currency
== item
.currency
.get_or_insert_with(|| self.currency.as_ref().map_or_else(|| currency.clone(), Clone::clone))
{
item.r055_open_tender_amount.insert(ocid.to_owned(), amount);
} else {
warn!("{} is not {:?}, skipping.", currency, item.currency);
}
}

if let Some(Value::Array(awards)) = release.get("awards") {
for award in awards {
if let Some(Value::String(status)) = award.get("status")
&& let Some(Value::Array(suppliers)) = award.get("suppliers")
&& suppliers.len() == 1
&& let Some(Value::String(supplier_id)) = suppliers[0].get("id")
&& status == "active"
&& let Some(Value::String(date)) = award.get("date")
&& let Ok(date) = NaiveDate::parse_from_str(date, "%Y-%m-%dT%H:%M:%S%z")
&& date >= self.start_date
&& date <= self.end_date
&& let Some(Value::Object(value)) = award.get("value")
&& let Some(Value::Number(amount)) = value.get("amount")
&& let Some(Value::String(currency)) = value.get("currency")
&& let Some(amount) = amount.as_f64()
{
if currency
== item.currency.get_or_insert_with(|| {
self.currency.as_ref().map_or_else(|| currency.clone(), Clone::clone)
})
{
if let Some(Value::Object(buyer)) = release.get("buyer")
&& let Some(Value::String(id)) = buyer.get("id")
{
item.r055_direct_awarded_amount_supplier_buyer
.insert((id.clone(), supplier_id.clone()), amount);
}
if let Some(Value::Object(tender)) = release.get("tender")
&& let Some(Value::Object(procuring_entity)) = tender.get("procuringEntity")
&& let Some(Value::String(id)) = procuring_entity.get("id")
{
item.r055_direct_awarded_amount_supplier_procuring_entity
.insert((id.clone(), supplier_id.clone()), amount);
}
} else {
warn!("{} is not {:?}, skipping.", currency, item.currency);
}
}
}
}
}

fn reduce(&self, item: &mut Indicators, other: &mut Indicators) {
let mut min = 0.0;
for (key, value) in std::mem::take(&mut other.r055_open_tender_amount) {
if min <= value {
item.r055_open_tender_amount.insert(key, value);
min = value;
}
}
sum!(item, other, r055_direct_awarded_amount_supplier_buyer);
sum!(item, other, r055_direct_awarded_amount_supplier_procuring_entity);
}

fn finalize(&self, item: &mut Indicators) {
let min_amount = self.threshold.map_or_else(
|| {
std::mem::take(&mut item.r055_open_tender_amount)
.values()
.copied()
.last()
.unwrap_or(0.0)
},
|v| v,
);
set_meta!(item, R055, "lower_open_amount", min_amount);

for (id, amount) in &item.r055_direct_awarded_amount_supplier_buyer {
if *amount >= min_amount {
set_result!(item, Buyer, id.0, R055, *amount);
set_result!(item, Tenderer, id.1, R055, *amount);
}
}
for (id, amount) in &item.r055_direct_awarded_amount_supplier_procuring_entity {
if *amount >= min_amount {
set_result!(item, ProcuringEntity, id.0, R055, *amount);
set_result!(item, Tenderer, id.1, R055, *amount);
}
}
}
}
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::indicators::r035::R035;
use crate::indicators::r036::R036;
use crate::indicators::r038::R038;
use crate::indicators::r048::R048;
use crate::indicators::r055::R055;
use crate::indicators::r058::R058;
use crate::indicators::util::{SecondLowestBidRatio, Tenderers};
pub use crate::indicators::{Calculate, Codelist, Exclusions, Group, Indicator, Indicators, Modifications, Settings};
Expand Down Expand Up @@ -144,6 +145,11 @@ pub fn init(path: &PathBuf, force: &bool) -> std::io::Result<bool> {
; threshold = 10
; minimum_contracting_processes = 20

[R055]
; threshold = 66593
; start_date = 2022-01-01
; end_date = 2022-12-31

[R058]
; threshold = 0.5
";
Expand Down Expand Up @@ -255,6 +261,7 @@ impl Indicators {
R036,
R038,
R048,
R055,
R058,
);

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/indicators/R055.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions tests/fixtures/indicators/R055.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}