@@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react';
2
2
3
3
import * as d from 'date-fns' ;
4
4
5
+ import * as monthUtils from 'loot-core/src/shared/months' ;
5
6
import { format as formatDate_ } from 'loot-core/src/shared/months' ;
6
7
import {
7
8
amountToCurrency ,
@@ -133,13 +134,24 @@ function getFileType(filepath) {
133
134
return rawType ;
134
135
}
135
136
136
- function ParsedDate ( { parseDateFormat, dateFormat, date } ) {
137
+ function ParsedDate ( { parseDateFormat, dateFormat, date, installmentParcel } ) {
137
138
const parsed =
138
139
date &&
139
140
formatDate (
140
141
parseDateFormat ? parseDate ( date , parseDateFormat ) : date ,
141
142
dateFormat ,
142
143
) ;
144
+ const overrideDate =
145
+ parsed != null
146
+ ? d . format (
147
+ d . addMonths (
148
+ monthUtils . _parse ( parseDate ( parsed ) ) ,
149
+ installmentParcel - 1 ,
150
+ ) ,
151
+ dateFormat ,
152
+ )
153
+ : null ;
154
+
143
155
return (
144
156
< Text >
145
157
< Text >
@@ -153,6 +165,18 @@ function ParsedDate({ parseDateFormat, dateFormat, date }) {
153
165
< Text style = { { color : parsed ? theme . noticeTextLight : theme . errorText } } >
154
166
{ parsed || 'Invalid' }
155
167
</ Text >
168
+ { installmentParcel && (
169
+ < Text >
170
+ →{ ' ' }
171
+ < Text
172
+ style = { {
173
+ color : overrideDate ? theme . noticeTextLight : theme . errorText ,
174
+ } }
175
+ >
176
+ { overrideDate || 'Invalid' }
177
+ </ Text >
178
+ </ Text >
179
+ ) }
156
180
</ Text >
157
181
) ;
158
182
}
@@ -323,6 +347,17 @@ function parseCategoryFields(trans, categories) {
323
347
return match ;
324
348
}
325
349
350
+ function markInstallment ( transaction , mapping ) {
351
+ return (
352
+ < Checkbox
353
+ checked = {
354
+ transaction [ mapping [ 'notes' ] ] ?. match ( / \( ( \d { 2 } ) \/ ( \d { 2 } ) \) / ) ?? false
355
+ }
356
+ readOnly = { true }
357
+ />
358
+ ) ;
359
+ }
360
+
326
361
function Transaction ( {
327
362
transaction : rawTransaction ,
328
363
fieldMappings,
@@ -335,6 +370,8 @@ function Transaction({
335
370
flipAmount,
336
371
multiplierAmount,
337
372
categories,
373
+ detectInstallments,
374
+ updateDetectedInstallmentDate,
338
375
} ) {
339
376
const categoryList = categories . map ( category => category . name ) ;
340
377
const transaction = useMemo (
@@ -354,18 +391,33 @@ function Transaction({
354
391
multiplierAmount ,
355
392
) ;
356
393
394
+ let installmentParcel = null ;
395
+ if (
396
+ detectInstallments &&
397
+ updateDetectedInstallmentDate &&
398
+ transaction . date !== null &&
399
+ transaction . date !== undefined
400
+ ) {
401
+ const matches =
402
+ rawTransaction [ fieldMappings [ 'notes' ] ] ?. match ( / \( ( \d { 2 } ) \/ ( \d { 2 } ) \) / ) ;
403
+ if ( matches ) {
404
+ installmentParcel = parseInt ( matches [ 1 ] ) ;
405
+ }
406
+ }
407
+
357
408
return (
358
409
< Row
359
410
style = { {
360
411
backgroundColor : theme . tableBackground ,
361
412
} }
362
413
>
363
- < Field width = { 200 } >
414
+ < Field width = { updateDetectedInstallmentDate ? 300 : 200 } >
364
415
{ showParsed ? (
365
416
< ParsedDate
366
417
parseDateFormat = { parseDateFormat }
367
418
dateFormat = { dateFormat }
368
419
date = { transaction . date }
420
+ installmentParcel = { installmentParcel }
369
421
/>
370
422
) : (
371
423
formatDate ( transaction . date , dateFormat )
@@ -455,6 +507,18 @@ function Transaction({
455
507
</ Field >
456
508
</ >
457
509
) }
510
+ { detectInstallments && (
511
+ < Field
512
+ width = { 90 }
513
+ title = "Installment"
514
+ style = { { display : 'flex' , alignItems : 'flex-end' } }
515
+ contentStyle = { {
516
+ textAlign : 'right' ,
517
+ } }
518
+ >
519
+ { markInstallment ( rawTransaction , fieldMappings ) }
520
+ </ Field >
521
+ ) }
458
522
</ Row >
459
523
) ;
460
524
}
@@ -757,6 +821,13 @@ export function ImportTransactions({ modalProps, options }) {
757
821
const [ multiplierEnabled , setMultiplierEnabled ] = useState ( false ) ;
758
822
const [ reconcile , setReconcile ] = useState ( true ) ;
759
823
const { accountId, categories, onImported } = options ;
824
+ const [ detectInstallments , setDetectInstallments ] = useState ( false ) ;
825
+ const [ updateDetectedInstallmentDate , setUpdateDetectedInstallmentDate ] =
826
+ useState ( false ) ;
827
+ const [
828
+ ignoreAlreadyDetectedInstallments ,
829
+ setIgnoreAlreadyDetectedInstallments ,
830
+ ] = useState ( false ) ;
760
831
761
832
// This cannot be set after parsing the file, because changing it
762
833
// requires re-parsing the file. This is different from the other
@@ -916,10 +987,32 @@ export function ImportTransactions({ modalProps, options }) {
916
987
for ( let trans of transactions ) {
917
988
trans = fieldMappings ? applyFieldMappings ( trans , fieldMappings ) : trans ;
918
989
919
- const date =
990
+ let date =
920
991
isOfxFile ( filetype ) || isCamtFile ( filetype )
921
992
? trans . date
922
993
: parseDate ( trans . date , parseDateFormat ) ;
994
+
995
+ if ( updateDetectedInstallmentDate ) {
996
+ if (
997
+ detectInstallments &&
998
+ updateDetectedInstallmentDate &&
999
+ trans . date !== null &&
1000
+ trans . date !== undefined
1001
+ ) {
1002
+ const matches = trans . notes ?. match ( / \( ( \d { 2 } ) \/ ( \d { 2 } ) \) / ) ;
1003
+ if ( matches ) {
1004
+ const installmentParcel = parseInt ( matches [ 1 ] ) ;
1005
+ date =
1006
+ date != null
1007
+ ? d . format (
1008
+ d . addMonths ( monthUtils . _parse ( date ) , installmentParcel - 1 ) ,
1009
+ 'yyyy-MM-dd' ,
1010
+ )
1011
+ : null ;
1012
+ }
1013
+ }
1014
+ }
1015
+
923
1016
if ( date == null ) {
924
1017
errorMessage = `Unable to parse date ${
925
1018
trans . date || '(empty)'
@@ -984,6 +1077,9 @@ export function ImportTransactions({ modalProps, options }) {
984
1077
accountId ,
985
1078
finalTransactions ,
986
1079
reconcile ,
1080
+ detectInstallments ,
1081
+ updateDetectedInstallmentDate ,
1082
+ ignoreAlreadyDetectedInstallments ,
987
1083
) ;
988
1084
if ( didChange ) {
989
1085
await getPayees ( ) ;
@@ -997,7 +1093,7 @@ export function ImportTransactions({ modalProps, options }) {
997
1093
}
998
1094
999
1095
const headers = [
1000
- { name : 'Date' , width : 200 } ,
1096
+ { name : 'Date' , width : updateDetectedInstallmentDate ? 300 : 200 } ,
1001
1097
{ name : 'Payee' , width : 'flex' } ,
1002
1098
{ name : 'Notes' , width : 'flex' } ,
1003
1099
{ name : 'Category' , width : 'flex' } ,
@@ -1013,14 +1109,22 @@ export function ImportTransactions({ modalProps, options }) {
1013
1109
headers . push ( { name : 'Amount' , width : 90 , style : { textAlign : 'right' } } ) ;
1014
1110
}
1015
1111
1112
+ if ( detectInstallments ) {
1113
+ headers . push ( {
1114
+ name : 'Installment' ,
1115
+ width : 90 ,
1116
+ style : { textAlign : 'right' } ,
1117
+ } ) ;
1118
+ }
1119
+
1016
1120
return (
1017
1121
< Modal
1018
1122
title = {
1019
1123
'Import transactions' + ( filetype ? ` (${ filetype . toUpperCase ( ) } )` : '' )
1020
1124
}
1021
1125
{ ...modalProps }
1022
1126
loading = { loadingState === 'parsing' }
1023
- style = { { width : 800 } }
1127
+ style = { { width : 900 } }
1024
1128
>
1025
1129
{ error && ! error . parsed && (
1026
1130
< View style = { { alignItems : 'center' , marginBottom : 15 } } >
@@ -1072,6 +1176,8 @@ export function ImportTransactions({ modalProps, options }) {
1072
1176
flipAmount = { flipAmount }
1073
1177
multiplierAmount = { multiplierAmount }
1074
1178
categories = { categories . list }
1179
+ detectInstallments = { detectInstallments }
1180
+ updateDetectedInstallmentDate = { updateDetectedInstallmentDate }
1075
1181
/>
1076
1182
</ View >
1077
1183
) }
@@ -1222,6 +1328,9 @@ export function ImportTransactions({ modalProps, options }) {
1222
1328
checked = { reconcile }
1223
1329
onChange = { ( ) => {
1224
1330
setReconcile ( state => ! state ) ;
1331
+ setDetectInstallments ( false ) ;
1332
+ setUpdateDetectedInstallmentDate ( false ) ;
1333
+ setIgnoreAlreadyDetectedInstallments ( false ) ;
1225
1334
} }
1226
1335
>
1227
1336
Reconcile transactions
@@ -1270,6 +1379,45 @@ export function ImportTransactions({ modalProps, options }) {
1270
1379
onChangeAmount = { onMultiplierChange }
1271
1380
/>
1272
1381
</ View >
1382
+ < View style = { { marginRight : 25 , gap : 5 } } >
1383
+ < SectionLabel title = "MISC" />
1384
+ < CheckboxOption
1385
+ id = "form_detect_installments"
1386
+ checked = { detectInstallments }
1387
+ disabled = { ! reconcile }
1388
+ onChange = { ( ) => {
1389
+ setDetectInstallments ( ! detectInstallments ) ;
1390
+ setUpdateDetectedInstallmentDate ( false ) ;
1391
+ setIgnoreAlreadyDetectedInstallments ( false ) ;
1392
+ } }
1393
+ >
1394
+ Detect Installments
1395
+ </ CheckboxOption >
1396
+ < CheckboxOption
1397
+ id = "form_detect_installments_update_installment_date"
1398
+ checked = { updateDetectedInstallmentDate }
1399
+ disabled = { ! detectInstallments }
1400
+ onChange = { ( ) =>
1401
+ setUpdateDetectedInstallmentDate (
1402
+ ! updateDetectedInstallmentDate ,
1403
+ )
1404
+ }
1405
+ >
1406
+ Update installment parcel to right month date
1407
+ </ CheckboxOption >
1408
+ < CheckboxOption
1409
+ id = "form_detect_installments_already_detected"
1410
+ checked = { ignoreAlreadyDetectedInstallments }
1411
+ disabled = { ! detectInstallments }
1412
+ onChange = { ( ) =>
1413
+ setIgnoreAlreadyDetectedInstallments (
1414
+ ! ignoreAlreadyDetectedInstallments ,
1415
+ )
1416
+ }
1417
+ >
1418
+ Don't create installment if already exists
1419
+ </ CheckboxOption >
1420
+ </ View >
1273
1421
</ Stack >
1274
1422
</ View >
1275
1423
) }
0 commit comments