1
use chrono::{DateTime, Utc};
2
use finance::error::FinanceError;
3
use num_rational::Rational64;
4
use sqlx::types::Uuid;
5

            
6
use super::super::{CmdError, ReportFilter};
7
use super::filter::SqlParam;
8
use super::tree::{
9
    AccountAmounts, AccountRow, ConversionTarget, accumulate_split, accumulate_split_converted,
10
};
11

            
12
pub(super) struct BreakdownSplit {
13
    pub commodity_id: Uuid,
14
    pub commodity_symbol: String,
15
    pub value: Rational64,
16
    pub category: Option<String>,
17
}
18

            
19
18
pub(super) async fn fetch_accounts(
20
18
    conn: &mut sqlx::PgConnection,
21
18
) -> Result<Vec<AccountRow>, CmdError> {
22
18
    let rows = sqlx::query_file!("sql/report/accounts/with_names.sql")
23
18
        .fetch_all(conn)
24
18
        .await?;
25
18
    Ok(rows
26
18
        .into_iter()
27
18
        .map(|r| AccountRow {
28
45
            account_id: r.account_id,
29
45
            parent_id: r.parent_id,
30
45
            account_name: r.account_name,
31
45
            account_type: r.account_type,
32
45
        })
33
18
        .collect())
34
18
}
35

            
36
8
pub(super) async fn fetch_target_symbol(
37
8
    conn: &mut sqlx::PgConnection,
38
8
    target_commodity_id: Uuid,
39
8
) -> Result<String, CmdError> {
40
    Ok(
41
8
        sqlx::query_file_scalar!("sql/select/commodities/symbol.sql", &target_commodity_id)
42
8
            .fetch_optional(conn)
43
8
            .await?
44
8
            .unwrap_or_else(|| target_commodity_id.to_string()),
45
    )
46
8
}
47

            
48
4
pub(super) async fn fetch_balance_splits_no_conversion(
49
4
    conn: &mut sqlx::PgConnection,
50
4
    as_of: Option<DateTime<Utc>>,
51
4
) -> Result<AccountAmounts, CmdError> {
52
4
    let rows = sqlx::query_file!("sql/report/splits/all.sql", as_of)
53
4
        .fetch_all(conn)
54
4
        .await?;
55

            
56
4
    let mut amounts = AccountAmounts::new();
57
12
    for r in rows {
58
12
        accumulate_split(
59
12
            &mut amounts,
60
12
            r.account_id,
61
12
            r.commodity_id,
62
12
            Rational64::new(r.value_num, r.value_denom),
63
12
            &r.commodity_symbol,
64
12
        );
65
12
    }
66
4
    Ok(amounts)
67
4
}
68

            
69
5
pub(super) async fn fetch_balance_splits_with_conversion(
70
5
    conn: &mut sqlx::PgConnection,
71
5
    target_commodity_id: Uuid,
72
5
    target_symbol: &str,
73
5
    as_of: Option<DateTime<Utc>>,
74
5
) -> Result<AccountAmounts, CmdError> {
75
5
    let rows = sqlx::query_file!(
76
5
        "sql/report/splits/all_with_conversion.sql",
77
        &target_commodity_id,
78
        as_of
79
    )
80
5
    .fetch_all(conn)
81
5
    .await?;
82

            
83
5
    let target = ConversionTarget {
84
5
        commodity_id: target_commodity_id,
85
5
        symbol: target_symbol,
86
5
    };
87
5
    let mut amounts = AccountAmounts::new();
88
21
    for r in rows {
89
21
        accumulate_split_converted(
90
21
            &mut amounts,
91
21
            r.account_id,
92
21
            r.commodity_id,
93
21
            Rational64::new(r.value_num, r.value_denom),
94
21
            &r.commodity_symbol,
95
21
            &target,
96
21
            (r.price_num, r.price_denom),
97
        )
98
21
        .map_err(|e| CmdError::Finance(FinanceError::Report(e)))?;
99
    }
100
4
    Ok(amounts)
101
5
}
102

            
103
1
pub(super) async fn fetch_date_range_splits_no_conversion(
104
1
    conn: &mut sqlx::PgConnection,
105
1
    from: DateTime<Utc>,
106
1
    to: DateTime<Utc>,
107
1
) -> Result<AccountAmounts, CmdError> {
108
1
    let rows = sqlx::query_file!("sql/report/splits/date_range.sql", from, to)
109
1
        .fetch_all(conn)
110
1
        .await?;
111

            
112
1
    let mut amounts = AccountAmounts::new();
113
4
    for r in rows {
114
4
        accumulate_split(
115
4
            &mut amounts,
116
4
            r.account_id,
117
4
            r.commodity_id,
118
4
            Rational64::new(r.value_num, r.value_denom),
119
4
            &r.commodity_symbol,
120
4
        );
121
4
    }
122
1
    Ok(amounts)
123
1
}
124

            
125
1
pub(super) async fn fetch_date_range_splits_with_conversion(
126
1
    conn: &mut sqlx::PgConnection,
127
1
    target_commodity_id: Uuid,
128
1
    target_symbol: &str,
129
1
    from: DateTime<Utc>,
130
1
    to: DateTime<Utc>,
131
1
) -> Result<AccountAmounts, CmdError> {
132
1
    let rows = sqlx::query_file!(
133
1
        "sql/report/splits/date_range_with_conversion.sql",
134
        &target_commodity_id,
135
        from,
136
        to
137
    )
138
1
    .fetch_all(conn)
139
1
    .await?;
140

            
141
1
    let target = ConversionTarget {
142
1
        commodity_id: target_commodity_id,
143
1
        symbol: target_symbol,
144
1
    };
145
1
    let mut amounts = AccountAmounts::new();
146
4
    for r in rows {
147
4
        accumulate_split_converted(
148
4
            &mut amounts,
149
4
            r.account_id,
150
4
            r.commodity_id,
151
4
            Rational64::new(r.value_num, r.value_denom),
152
4
            &r.commodity_symbol,
153
4
            &target,
154
4
            (r.price_num, r.price_denom),
155
        )
156
4
        .map_err(|e| CmdError::Finance(FinanceError::Report(e)))?;
157
    }
158
1
    Ok(amounts)
159
1
}
160

            
161
1
pub(super) async fn fetch_balance_splits_filtered_no_conversion(
162
1
    conn: &mut sqlx::PgConnection,
163
1
    as_of: Option<DateTime<Utc>>,
164
1
    filter: &ReportFilter,
165
1
) -> Result<AccountAmounts, CmdError> {
166
    use sqlx::Row;
167

            
168
1
    let base_sql = concat!(
169
        "SELECT s.account_id, s.commodity_id, s.value_num, s.value_denom, ",
170
        "t_symbol.tag_value AS commodity_symbol ",
171
        "FROM splits AS s ",
172
        "INNER JOIN transactions AS t ON s.tx_id = t.id ",
173
        "INNER JOIN commodity_tags AS ct_symbol ON s.commodity_id = ct_symbol.commodity_id ",
174
        "INNER JOIN tags AS t_symbol ON (ct_symbol.tag_id = t_symbol.id AND t_symbol.tag_name = 'symbol') ",
175
        "WHERE ($1::timestamptz IS NULL OR t.post_date <= $1)"
176
    );
177

            
178
1
    let mut bind_offset: i32 = 1;
179
1
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
180
1
    let full_sql = format!("{base_sql} AND {where_clause}");
181

            
182
1
    let mut query = sqlx::query(&full_sql).bind(as_of);
183
1
    for p in &params {
184
1
        query = match p {
185
1
            SqlParam::Uuid(v) => query.bind(*v),
186
            SqlParam::UuidVec(v) => query.bind(v),
187
            SqlParam::I64(v) => query.bind(*v),
188
            SqlParam::String(v) => query.bind(v.as_str()),
189
            SqlParam::StringVec(v) => query.bind(v),
190
        };
191
    }
192

            
193
1
    let rows = query.fetch_all(&mut *conn).await?;
194

            
195
1
    let mut amounts = AccountAmounts::new();
196
1
    for r in &rows {
197
1
        let account_id: Uuid = r.get("account_id");
198
1
        let commodity_id: Uuid = r.get("commodity_id");
199
1
        let value_num: i64 = r.get("value_num");
200
1
        let value_denom: i64 = r.get("value_denom");
201
1
        let commodity_symbol: String = r.get("commodity_symbol");
202
1
        accumulate_split(
203
1
            &mut amounts,
204
1
            account_id,
205
1
            commodity_id,
206
1
            Rational64::new(value_num, value_denom),
207
1
            &commodity_symbol,
208
1
        );
209
1
    }
210
1
    Ok(amounts)
211
1
}
212

            
213
pub(super) async fn fetch_balance_splits_filtered_with_conversion(
214
    conn: &mut sqlx::PgConnection,
215
    target_commodity_id: Uuid,
216
    target_symbol: &str,
217
    as_of: Option<DateTime<Utc>>,
218
    filter: &ReportFilter,
219
) -> Result<AccountAmounts, CmdError> {
220
    use sqlx::Row;
221

            
222
    let base_sql = concat!(
223
        "SELECT s.id AS split_id, s.account_id, s.commodity_id, s.value_num, s.value_denom, ",
224
        "t_symbol.tag_value AS commodity_symbol, ",
225
        "p.value_num AS price_num, p.value_denom AS price_denom ",
226
        "FROM splits AS s ",
227
        "INNER JOIN transactions AS t ON s.tx_id = t.id ",
228
        "INNER JOIN commodity_tags AS ct_symbol ON s.commodity_id = ct_symbol.commodity_id ",
229
        "INNER JOIN tags AS t_symbol ON (ct_symbol.tag_id = t_symbol.id AND t_symbol.tag_name = 'symbol') ",
230
        "LEFT JOIN LATERAL (",
231
        "  SELECT pr.value_num, pr.value_denom FROM prices AS pr ",
232
        "  WHERE pr.commodity_split_id = s.id AND pr.currency_id = $1 ",
233
        "  ORDER BY pr.price_date DESC, pr.id LIMIT 1",
234
        ") AS p ON TRUE ",
235
        "WHERE ($2::timestamptz IS NULL OR t.post_date <= $2)"
236
    );
237

            
238
    let mut bind_offset: i32 = 2;
239
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
240
    let full_sql = format!("{base_sql} AND {where_clause}");
241

            
242
    let mut query = sqlx::query(&full_sql).bind(target_commodity_id).bind(as_of);
243
    for p in &params {
244
        query = match p {
245
            SqlParam::Uuid(v) => query.bind(*v),
246
            SqlParam::UuidVec(v) => query.bind(v),
247
            SqlParam::I64(v) => query.bind(*v),
248
            SqlParam::String(v) => query.bind(v.as_str()),
249
            SqlParam::StringVec(v) => query.bind(v),
250
        };
251
    }
252

            
253
    let rows = query.fetch_all(&mut *conn).await?;
254

            
255
    let target = ConversionTarget {
256
        commodity_id: target_commodity_id,
257
        symbol: target_symbol,
258
    };
259
    let mut amounts = AccountAmounts::new();
260
    for r in &rows {
261
        let account_id: Uuid = r.get("account_id");
262
        let commodity_id: Uuid = r.get("commodity_id");
263
        let value_num: i64 = r.get("value_num");
264
        let value_denom: i64 = r.get("value_denom");
265
        let commodity_symbol: String = r.get("commodity_symbol");
266
        let price_num: Option<i64> = r.get("price_num");
267
        let price_denom: Option<i64> = r.get("price_denom");
268
        accumulate_split_converted(
269
            &mut amounts,
270
            account_id,
271
            commodity_id,
272
            Rational64::new(value_num, value_denom),
273
            &commodity_symbol,
274
            &target,
275
            (price_num, price_denom),
276
        )
277
        .map_err(|e| CmdError::Finance(FinanceError::Report(e)))?;
278
    }
279
    Ok(amounts)
280
}
281

            
282
14
pub(super) async fn fetch_date_range_splits_filtered_no_conversion(
283
14
    conn: &mut sqlx::PgConnection,
284
14
    from: DateTime<Utc>,
285
14
    to: DateTime<Utc>,
286
14
    filter: &ReportFilter,
287
14
) -> Result<AccountAmounts, CmdError> {
288
    use sqlx::Row;
289

            
290
14
    let base_sql = concat!(
291
        "SELECT s.account_id, s.commodity_id, s.value_num, s.value_denom, t.post_date, ",
292
        "t_symbol.tag_value AS commodity_symbol ",
293
        "FROM splits AS s ",
294
        "INNER JOIN transactions AS t ON s.tx_id = t.id ",
295
        "INNER JOIN commodity_tags AS ct_symbol ON s.commodity_id = ct_symbol.commodity_id ",
296
        "INNER JOIN tags AS t_symbol ON (ct_symbol.tag_id = t_symbol.id AND t_symbol.tag_name = 'symbol') ",
297
        "WHERE t.post_date >= $1 AND t.post_date < $2"
298
    );
299

            
300
14
    let mut bind_offset: i32 = 2;
301
14
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
302
14
    let full_sql = format!("{base_sql} AND {where_clause}");
303

            
304
14
    let mut query = sqlx::query(&full_sql).bind(from).bind(to);
305
29
    for p in &params {
306
29
        query = match p {
307
3
            SqlParam::Uuid(v) => query.bind(*v),
308
            SqlParam::UuidVec(v) => query.bind(v),
309
            SqlParam::I64(v) => query.bind(*v),
310
26
            SqlParam::String(v) => query.bind(v.as_str()),
311
            SqlParam::StringVec(v) => query.bind(v),
312
        };
313
    }
314

            
315
14
    let rows = query.fetch_all(&mut *conn).await?;
316

            
317
14
    let mut amounts = AccountAmounts::new();
318
14
    for r in &rows {
319
14
        let account_id: Uuid = r.get("account_id");
320
14
        let commodity_id: Uuid = r.get("commodity_id");
321
14
        let value_num: i64 = r.get("value_num");
322
14
        let value_denom: i64 = r.get("value_denom");
323
14
        let commodity_symbol: String = r.get("commodity_symbol");
324
14
        accumulate_split(
325
14
            &mut amounts,
326
14
            account_id,
327
14
            commodity_id,
328
14
            Rational64::new(value_num, value_denom),
329
14
            &commodity_symbol,
330
14
        );
331
14
    }
332
14
    Ok(amounts)
333
14
}
334

            
335
pub(super) async fn fetch_date_range_splits_filtered_with_conversion(
336
    conn: &mut sqlx::PgConnection,
337
    target_commodity_id: Uuid,
338
    target_symbol: &str,
339
    from: DateTime<Utc>,
340
    to: DateTime<Utc>,
341
    filter: &ReportFilter,
342
) -> Result<AccountAmounts, CmdError> {
343
    use sqlx::Row;
344

            
345
    let base_sql = concat!(
346
        "SELECT s.id AS split_id, s.account_id, s.commodity_id, s.value_num, s.value_denom, ",
347
        "t_symbol.tag_value AS commodity_symbol, t.post_date, ",
348
        "p.value_num AS price_num, p.value_denom AS price_denom ",
349
        "FROM splits AS s ",
350
        "INNER JOIN transactions AS t ON s.tx_id = t.id ",
351
        "INNER JOIN commodity_tags AS ct_symbol ON s.commodity_id = ct_symbol.commodity_id ",
352
        "INNER JOIN tags AS t_symbol ON (ct_symbol.tag_id = t_symbol.id AND t_symbol.tag_name = 'symbol') ",
353
        "LEFT JOIN LATERAL (",
354
        "  SELECT pr.value_num, pr.value_denom FROM prices AS pr ",
355
        "  WHERE pr.commodity_split_id = s.id AND pr.currency_id = $1 ",
356
        "  ORDER BY pr.price_date DESC, pr.id LIMIT 1",
357
        ") AS p ON TRUE ",
358
        "WHERE t.post_date >= $2 AND t.post_date < $3"
359
    );
360

            
361
    let mut bind_offset: i32 = 3;
362
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
363
    let full_sql = format!("{base_sql} AND {where_clause}");
364

            
365
    let mut query = sqlx::query(&full_sql)
366
        .bind(target_commodity_id)
367
        .bind(from)
368
        .bind(to);
369
    for p in &params {
370
        query = match p {
371
            SqlParam::Uuid(v) => query.bind(*v),
372
            SqlParam::UuidVec(v) => query.bind(v),
373
            SqlParam::I64(v) => query.bind(*v),
374
            SqlParam::String(v) => query.bind(v.as_str()),
375
            SqlParam::StringVec(v) => query.bind(v),
376
        };
377
    }
378

            
379
    let rows = query.fetch_all(&mut *conn).await?;
380

            
381
    let target = ConversionTarget {
382
        commodity_id: target_commodity_id,
383
        symbol: target_symbol,
384
    };
385
    let mut amounts = AccountAmounts::new();
386
    for r in &rows {
387
        let account_id: Uuid = r.get("account_id");
388
        let commodity_id: Uuid = r.get("commodity_id");
389
        let value_num: i64 = r.get("value_num");
390
        let value_denom: i64 = r.get("value_denom");
391
        let commodity_symbol: String = r.get("commodity_symbol");
392
        let price_num: Option<i64> = r.get("price_num");
393
        let price_denom: Option<i64> = r.get("price_denom");
394
        accumulate_split_converted(
395
            &mut amounts,
396
            account_id,
397
            commodity_id,
398
            Rational64::new(value_num, value_denom),
399
            &commodity_symbol,
400
            &target,
401
            (price_num, price_denom),
402
        )
403
        .map_err(|e| CmdError::Finance(FinanceError::Report(e)))?;
404
    }
405
    Ok(amounts)
406
}
407

            
408
11
pub(super) async fn fetch_date_range_breakdown_filtered_no_conversion(
409
11
    conn: &mut sqlx::PgConnection,
410
11
    from: DateTime<Utc>,
411
11
    to: DateTime<Utc>,
412
11
    tag_name: &str,
413
11
    filter: &ReportFilter,
414
11
) -> Result<Vec<BreakdownSplit>, CmdError> {
415
    use sqlx::Row;
416

            
417
11
    let base_sql = concat!(
418
        "SELECT s.account_id, s.commodity_id, s.value_num, s.value_denom, ",
419
        "t_symbol.tag_value AS commodity_symbol, ",
420
        "COALESCE(",
421
        "(SELECT t_cat.tag_value FROM split_tags AS st_cat ",
422
        "INNER JOIN tags AS t_cat ON st_cat.tag_id = t_cat.id ",
423
        "WHERE st_cat.split_id = s.id AND t_cat.tag_name = $3 LIMIT 1), ",
424
        "(SELECT t_cat.tag_value FROM transaction_tags AS tt_cat ",
425
        "INNER JOIN tags AS t_cat ON tt_cat.tag_id = t_cat.id ",
426
        "WHERE tt_cat.tx_id = s.tx_id AND t_cat.tag_name = $3 LIMIT 1)",
427
        ") AS category_value ",
428
        "FROM splits AS s ",
429
        "INNER JOIN transactions AS t ON s.tx_id = t.id ",
430
        "INNER JOIN commodity_tags AS ct_symbol ON s.commodity_id = ct_symbol.commodity_id ",
431
        "INNER JOIN tags AS t_symbol ON (ct_symbol.tag_id = t_symbol.id AND t_symbol.tag_name = 'symbol') ",
432
        "WHERE t.post_date >= $1 AND t.post_date < $2"
433
    );
434

            
435
11
    let mut bind_offset: i32 = 3;
436
11
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
437
11
    let full_sql = format!("{base_sql} AND {where_clause}");
438

            
439
11
    let mut query = sqlx::query(&full_sql).bind(from).bind(to).bind(tag_name);
440
23
    for p in &params {
441
23
        query = match p {
442
1
            SqlParam::Uuid(v) => query.bind(*v),
443
            SqlParam::UuidVec(v) => query.bind(v),
444
            SqlParam::I64(v) => query.bind(*v),
445
11
            SqlParam::String(v) => query.bind(v.as_str()),
446
11
            SqlParam::StringVec(v) => query.bind(v),
447
        };
448
    }
449

            
450
11
    let rows = query.fetch_all(&mut *conn).await?;
451

            
452
11
    let mut out = Vec::with_capacity(rows.len());
453
17
    for r in &rows {
454
17
        let commodity_id: Uuid = r.get("commodity_id");
455
17
        let value_num: i64 = r.get("value_num");
456
17
        let value_denom: i64 = r.get("value_denom");
457
17
        let commodity_symbol: String = r.get("commodity_symbol");
458
17
        let category: Option<String> = r.get("category_value");
459
17
        out.push(BreakdownSplit {
460
17
            commodity_id,
461
17
            commodity_symbol,
462
17
            value: Rational64::new(value_num, value_denom),
463
17
            category,
464
17
        });
465
17
    }
466
11
    Ok(out)
467
11
}
468

            
469
2
pub(super) async fn fetch_date_range_breakdown_filtered_with_conversion(
470
2
    conn: &mut sqlx::PgConnection,
471
2
    target_commodity_id: Uuid,
472
2
    target_symbol: &str,
473
2
    from: DateTime<Utc>,
474
2
    to: DateTime<Utc>,
475
2
    tag_name: &str,
476
2
    filter: &ReportFilter,
477
2
) -> Result<Vec<BreakdownSplit>, CmdError> {
478
    use sqlx::Row;
479

            
480
2
    let base_sql = concat!(
481
        "SELECT s.id AS split_id, s.account_id, s.commodity_id, s.value_num, s.value_denom, ",
482
        "t_symbol.tag_value AS commodity_symbol, ",
483
        "p.value_num AS price_num, p.value_denom AS price_denom, ",
484
        "COALESCE(",
485
        "(SELECT t_cat.tag_value FROM split_tags AS st_cat ",
486
        "INNER JOIN tags AS t_cat ON st_cat.tag_id = t_cat.id ",
487
        "WHERE st_cat.split_id = s.id AND t_cat.tag_name = $4 LIMIT 1), ",
488
        "(SELECT t_cat.tag_value FROM transaction_tags AS tt_cat ",
489
        "INNER JOIN tags AS t_cat ON tt_cat.tag_id = t_cat.id ",
490
        "WHERE tt_cat.tx_id = s.tx_id AND t_cat.tag_name = $4 LIMIT 1)",
491
        ") AS category_value ",
492
        "FROM splits AS s ",
493
        "INNER JOIN transactions AS t ON s.tx_id = t.id ",
494
        "INNER JOIN commodity_tags AS ct_symbol ON s.commodity_id = ct_symbol.commodity_id ",
495
        "INNER JOIN tags AS t_symbol ON (ct_symbol.tag_id = t_symbol.id AND t_symbol.tag_name = 'symbol') ",
496
        "LEFT JOIN LATERAL (",
497
        "  SELECT pr.value_num, pr.value_denom FROM prices AS pr ",
498
        "  WHERE pr.commodity_split_id = s.id AND pr.currency_id = $1 ",
499
        "  ORDER BY pr.price_date DESC, pr.id LIMIT 1",
500
        ") AS p ON TRUE ",
501
        "WHERE t.post_date >= $2 AND t.post_date < $3"
502
    );
503

            
504
2
    let mut bind_offset: i32 = 4;
505
2
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
506
2
    let full_sql = format!("{base_sql} AND {where_clause}");
507

            
508
2
    let mut query = sqlx::query(&full_sql)
509
2
        .bind(target_commodity_id)
510
2
        .bind(from)
511
2
        .bind(to)
512
2
        .bind(tag_name);
513
4
    for p in &params {
514
4
        query = match p {
515
            SqlParam::Uuid(v) => query.bind(*v),
516
            SqlParam::UuidVec(v) => query.bind(v),
517
            SqlParam::I64(v) => query.bind(*v),
518
2
            SqlParam::String(v) => query.bind(v.as_str()),
519
2
            SqlParam::StringVec(v) => query.bind(v),
520
        };
521
    }
522

            
523
2
    let rows = query.fetch_all(&mut *conn).await?;
524

            
525
2
    let mut out = Vec::with_capacity(rows.len());
526
6
    for r in &rows {
527
6
        let commodity_id: Uuid = r.get("commodity_id");
528
6
        let value_num: i64 = r.get("value_num");
529
6
        let value_denom: i64 = r.get("value_denom");
530
6
        let commodity_symbol: String = r.get("commodity_symbol");
531
6
        let price_num: Option<i64> = r.get("price_num");
532
6
        let price_denom: Option<i64> = r.get("price_denom");
533
6
        let category: Option<String> = r.get("category_value");
534

            
535
6
        let value = Rational64::new(value_num, value_denom);
536
6
        let converted = if commodity_id == target_commodity_id {
537
6
            value
538
        } else if let (Some(pn), Some(pd)) = (price_num, price_denom) {
539
            value * Rational64::new(pn, pd)
540
        } else {
541
            return Err(CmdError::Finance(FinanceError::Report(
542
                finance::error::ReportError::MissingConversion {
543
                    from_commodity: commodity_symbol,
544
                    to_commodity: target_symbol.to_owned(),
545
                },
546
            )));
547
        };
548
6
        out.push(BreakdownSplit {
549
6
            commodity_id: target_commodity_id,
550
6
            commodity_symbol: target_symbol.to_owned(),
551
6
            value: converted,
552
6
            category,
553
6
        });
554
    }
555
2
    Ok(out)
556
2
}