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
48
pub(super) async fn fetch_accounts(
20
48
    conn: &mut sqlx::PgConnection,
21
48
) -> Result<Vec<AccountRow>, CmdError> {
22
43
    let rows = sqlx::query_file!("sql/report/accounts/with_names.sql")
23
43
        .fetch_all(conn)
24
43
        .await?;
25
43
    Ok(rows
26
43
        .into_iter()
27
43
        .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
43
        .collect())
34
43
}
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
        "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
        "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(sqlx::AssertSqlSafe(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(sqlx::AssertSqlSafe(full_sql))
243
        .bind(target_commodity_id)
244
        .bind(as_of);
245
    for p in &params {
246
        query = match p {
247
            SqlParam::Uuid(v) => query.bind(*v),
248
            SqlParam::UuidVec(v) => query.bind(v),
249
            SqlParam::I64(v) => query.bind(*v),
250
            SqlParam::String(v) => query.bind(v.as_str()),
251
            SqlParam::StringVec(v) => query.bind(v),
252
        };
253
    }
254

            
255
    let rows = query.fetch_all(&mut *conn).await?;
256

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

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

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

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

            
306
64
    let mut query = sqlx::query(sqlx::AssertSqlSafe(full_sql))
307
64
        .bind(from)
308
64
        .bind(to);
309
129
    for p in &params {
310
129
        query = match p {
311
3
            SqlParam::Uuid(v) => query.bind(*v),
312
            SqlParam::UuidVec(v) => query.bind(v),
313
            SqlParam::I64(v) => query.bind(*v),
314
126
            SqlParam::String(v) => query.bind(v.as_str()),
315
            SqlParam::StringVec(v) => query.bind(v),
316
        };
317
    }
318

            
319
64
    let rows = query.fetch_all(&mut *conn).await?;
320

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

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

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

            
365
    let mut bind_offset: i32 = 3;
366
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
367
    let full_sql = format!("{base_sql} AND {where_clause}");
368

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

            
383
    let rows = query.fetch_all(&mut *conn).await?;
384

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

            
412
41
pub(super) async fn fetch_date_range_breakdown_filtered_no_conversion(
413
41
    conn: &mut sqlx::PgConnection,
414
41
    from: DateTime<Utc>,
415
41
    to: DateTime<Utc>,
416
41
    tag_name: &str,
417
41
    filter: &ReportFilter,
418
41
) -> Result<Vec<BreakdownSplit>, CmdError> {
419
    use sqlx::Row;
420

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

            
439
36
    let mut bind_offset: i32 = 3;
440
36
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
441
36
    let full_sql = format!("{base_sql} AND {where_clause}");
442

            
443
36
    let mut query = sqlx::query(sqlx::AssertSqlSafe(full_sql))
444
36
        .bind(from)
445
36
        .bind(to)
446
36
        .bind(tag_name);
447
73
    for p in &params {
448
73
        query = match p {
449
1
            SqlParam::Uuid(v) => query.bind(*v),
450
            SqlParam::UuidVec(v) => query.bind(v),
451
            SqlParam::I64(v) => query.bind(*v),
452
36
            SqlParam::String(v) => query.bind(v.as_str()),
453
36
            SqlParam::StringVec(v) => query.bind(v),
454
        };
455
    }
456

            
457
36
    let rows = query.fetch_all(&mut *conn).await?;
458

            
459
36
    let mut out = Vec::with_capacity(rows.len());
460
42
    for r in &rows {
461
17
        let commodity_id: Uuid = r.get("commodity_id");
462
17
        let value_num: i64 = r.get("value_num");
463
17
        let value_denom: i64 = r.get("value_denom");
464
17
        let commodity_symbol: String = r.get("commodity_symbol");
465
17
        let category: Option<String> = r.get("category_value");
466
17
        out.push(BreakdownSplit {
467
17
            commodity_id,
468
17
            commodity_symbol,
469
17
            value: Rational64::new(value_num, value_denom),
470
17
            category,
471
17
        });
472
17
    }
473
36
    Ok(out)
474
36
}
475

            
476
2
pub(super) async fn fetch_date_range_breakdown_filtered_with_conversion(
477
2
    conn: &mut sqlx::PgConnection,
478
2
    target_commodity_id: Uuid,
479
2
    target_symbol: &str,
480
2
    from: DateTime<Utc>,
481
2
    to: DateTime<Utc>,
482
2
    tag_name: &str,
483
2
    filter: &ReportFilter,
484
2
) -> Result<Vec<BreakdownSplit>, CmdError> {
485
    use sqlx::Row;
486

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

            
511
2
    let mut bind_offset: i32 = 4;
512
2
    let (where_clause, params) = filter.to_sql(&mut bind_offset);
513
2
    let full_sql = format!("{base_sql} AND {where_clause}");
514

            
515
2
    let mut query = sqlx::query(sqlx::AssertSqlSafe(full_sql))
516
2
        .bind(target_commodity_id)
517
2
        .bind(from)
518
2
        .bind(to)
519
2
        .bind(tag_name);
520
4
    for p in &params {
521
4
        query = match p {
522
            SqlParam::Uuid(v) => query.bind(*v),
523
            SqlParam::UuidVec(v) => query.bind(v),
524
            SqlParam::I64(v) => query.bind(*v),
525
2
            SqlParam::String(v) => query.bind(v.as_str()),
526
2
            SqlParam::StringVec(v) => query.bind(v),
527
        };
528
    }
529

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

            
532
2
    let mut out = Vec::with_capacity(rows.len());
533
6
    for r in &rows {
534
6
        let commodity_id: Uuid = r.get("commodity_id");
535
6
        let value_num: i64 = r.get("value_num");
536
6
        let value_denom: i64 = r.get("value_denom");
537
6
        let commodity_symbol: String = r.get("commodity_symbol");
538
6
        let price_num: Option<i64> = r.get("price_num");
539
6
        let price_denom: Option<i64> = r.get("price_denom");
540
6
        let category: Option<String> = r.get("category_value");
541

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