mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-01-03 17:11:22 +00:00
Compare commits
1156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81bef28607 | ||
|
|
d46ba4325f | ||
|
|
e044670c55 | ||
|
|
66c13f35e7 | ||
|
|
f2cb675267 | ||
|
|
6f9b69a032 | ||
|
|
3a3eb4e84f | ||
|
|
c81955d84a | ||
|
|
0543733e3d | ||
|
|
336fb5f5a6 | ||
|
|
6d592d45b0 | ||
|
|
45d2467772 | ||
|
|
643248f9e4 | ||
|
|
94b18798ed | ||
|
|
4a18178a86 | ||
|
|
7bdde6822b | ||
|
|
a118588fe7 | ||
|
|
88bc81b9f5 | ||
|
|
1af68843b0 | ||
|
|
f704ade86b | ||
|
|
2dff8aec69 | ||
|
|
aae26c5da9 | ||
|
|
2dbdcf73ed | ||
|
|
9960b063e7 | ||
|
|
13834a276e | ||
|
|
a50945ad53 | ||
|
|
068b9e388f | ||
|
|
6158288295 | ||
|
|
b9fc298a0a | ||
|
|
a697b0735f | ||
|
|
6b82a8b29a | ||
|
|
158377a522 | ||
|
|
3d32b8eb59 | ||
|
|
9bc53c6644 | ||
|
|
a1b9baae30 | ||
|
|
6b6132b91b | ||
|
|
fdf26905c6 | ||
|
|
8f3731dabe | ||
|
|
e3521692ca | ||
|
|
dc8df37f5a | ||
|
|
141ae05cd6 | ||
|
|
584f4fb286 | ||
|
|
fdec8ab6a2 | ||
|
|
3f79ffecaf | ||
|
|
5ee071fdc7 | ||
|
|
88e0985776 | ||
|
|
a942313f85 | ||
|
|
f2446d46aa | ||
|
|
78c8680300 | ||
|
|
1cb979404b | ||
|
|
44a017db99 | ||
|
|
69a67ca977 | ||
|
|
dd579cc19b | ||
|
|
0346b09cb0 | ||
|
|
eb15d2bbd8 | ||
|
|
6a20f3113a | ||
|
|
b4e8bb1e0f | ||
|
|
83754960a6 | ||
|
|
892f262261 | ||
|
|
0bccf0d734 | ||
|
|
826dce324f | ||
|
|
d9dad4387e | ||
|
|
62c4a9a7fc | ||
|
|
386b069c83 | ||
|
|
6a48c354b7 | ||
|
|
ff0b1d0ec9 | ||
|
|
ceaba5a11c | ||
|
|
560bd6a92b | ||
|
|
05103c0676 | ||
|
|
88ea2aad28 | ||
|
|
55cd717df4 | ||
|
|
812e3d4a74 | ||
|
|
5946e4b9b5 | ||
|
|
ef54e0a845 | ||
|
|
20597c5ee1 | ||
|
|
057e76acd0 | ||
|
|
3582d79530 | ||
|
|
1c72e742cf | ||
|
|
2ac582cd18 | ||
|
|
e36302315c | ||
|
|
e48d14e14a | ||
|
|
608aa5c31e | ||
|
|
533398115d | ||
|
|
a8d69f850a | ||
|
|
98cb74e00c | ||
|
|
49c5c9ba15 | ||
|
|
64979fd941 | ||
|
|
dccafee383 | ||
|
|
0444ad5221 | ||
|
|
693c6fb71b | ||
|
|
d5bcaf42ac | ||
|
|
b002635d1b | ||
|
|
19e3a10a28 | ||
|
|
1d87da7745 | ||
|
|
0840883546 | ||
|
|
9d283b85e2 | ||
|
|
f031b6fa3f | ||
|
|
4b03bc92c0 | ||
|
|
487c81eeb2 | ||
|
|
26f71400e9 | ||
|
|
daeb06ede8 | ||
|
|
fe9705d33e | ||
|
|
25ea1c8f5f | ||
|
|
80a2fd485e | ||
|
|
18f8b102c3 | ||
|
|
b99c8dd32a | ||
|
|
52f1d96cfb | ||
|
|
c143d54c97 | ||
|
|
ac3881779c | ||
|
|
65e0700f2c | ||
|
|
733cee5c8c | ||
|
|
6b09466819 | ||
|
|
b29c0f9d74 | ||
|
|
5d239b896c | ||
|
|
52770a970b | ||
|
|
bbf923b849 | ||
|
|
d735c5d10b | ||
|
|
2417224991 | ||
|
|
d515bda868 | ||
|
|
8e332119e3 | ||
|
|
5ac3dccb37 | ||
|
|
e9354e1b6a | ||
|
|
8700393e61 | ||
|
|
8d76eaf633 | ||
|
|
99b6eb3cd9 | ||
|
|
36ffa46441 | ||
|
|
d110fea710 | ||
|
|
09bd94d44e | ||
|
|
3b8b45d12d | ||
|
|
39045fe2fc | ||
|
|
b7e7c82bd7 | ||
|
|
473eeaa206 | ||
|
|
36b93d6d2b | ||
|
|
f24feb62c8 | ||
|
|
69b82a5043 | ||
|
|
547db2f27b | ||
|
|
6b6599eb79 | ||
|
|
fcad295809 | ||
|
|
9a1e3701a3 | ||
|
|
ba3b82cc87 | ||
|
|
c768949034 | ||
|
|
4749c854bc | ||
|
|
3887231051 | ||
|
|
3078adac2d | ||
|
|
ec40d91f85 | ||
|
|
399cb3bc3f | ||
|
|
fe2bba2c40 | ||
|
|
2bbdd76ee1 | ||
|
|
a8ace43470 | ||
|
|
9c5c5b56ef | ||
|
|
109b9000d2 | ||
|
|
2194cd9da1 | ||
|
|
ac2834954c | ||
|
|
136043bb23 | ||
|
|
459c0fc136 | ||
|
|
22ab01c9ff | ||
|
|
cb7466247c | ||
|
|
a87b1fca1b | ||
|
|
0d4b971bd0 | ||
|
|
2b6fe7cae7 | ||
|
|
beb1ac4c9d | ||
|
|
923cdcde16 | ||
|
|
4ff4ca642c | ||
|
|
a968525d3f | ||
|
|
f73a23db97 | ||
|
|
4437cb67b9 | ||
|
|
abfb47674c | ||
|
|
f4deaf78e7 | ||
|
|
38f588da1d | ||
|
|
d7be6fc9f6 | ||
|
|
dd4b640424 | ||
|
|
ffbe400e33 | ||
|
|
533ea67278 | ||
|
|
9ad0bea76d | ||
|
|
7d6bc09f18 | ||
|
|
40a8a5a8e7 | ||
|
|
47c0a2cf03 | ||
|
|
2c787dd96d | ||
|
|
de340130b8 | ||
|
|
abc71e4cd6 | ||
|
|
444c2af5b0 | ||
|
|
07277b7282 | ||
|
|
41f1bb1dbd | ||
|
|
ee337b9476 | ||
|
|
1cbe63e293 | ||
|
|
eae3bf4d0e | ||
|
|
7502497827 | ||
|
|
a66541fd04 | ||
|
|
522d99477c | ||
|
|
3a4869205d | ||
|
|
5ae49e8e3e | ||
|
|
915321db61 | ||
|
|
bcd5b580cb | ||
|
|
70226a24c9 | ||
|
|
dd4995e02b | ||
|
|
46d3b70154 | ||
|
|
540c03670b | ||
|
|
8037e4a4c4 | ||
|
|
15a13be329 | ||
|
|
9509825552 | ||
|
|
e46ef2f92c | ||
|
|
dc21afe14a | ||
|
|
7667c0fa4a | ||
|
|
7f747cd4ef | ||
|
|
1e3693101b | ||
|
|
d31640734c | ||
|
|
ce3478041a | ||
|
|
ad2752a62c | ||
|
|
d29ebdbf26 | ||
|
|
491b3547b5 | ||
|
|
71cb8fe038 | ||
|
|
816b291ed3 | ||
|
|
b9f6119c68 | ||
|
|
c87d7458fc | ||
|
|
aed5ef3fba | ||
|
|
523824225e | ||
|
|
2a2a18f378 | ||
|
|
c29fb13941 | ||
|
|
30549da044 | ||
|
|
e81775fbce | ||
|
|
e7520a82c4 | ||
|
|
cf0e089616 | ||
|
|
df8207bd9f | ||
|
|
d65d555d63 | ||
|
|
159920296a | ||
|
|
bfd7e009cc | ||
|
|
682f9283a6 | ||
|
|
bbe8a97945 | ||
|
|
f93d11643f | ||
|
|
78bae33433 | ||
|
|
7cab89a291 | ||
|
|
95e91b9af8 | ||
|
|
5e08461385 | ||
|
|
632d95f1fb | ||
|
|
edd1b25330 | ||
|
|
ffaf48dda7 | ||
|
|
0e465ade48 | ||
|
|
ab80803f0f | ||
|
|
88d3db4dc8 | ||
|
|
da4dbb7319 | ||
|
|
808b405dd6 | ||
|
|
a429aaa6fd | ||
|
|
8db9641480 | ||
|
|
8dec769d64 | ||
|
|
06ad377729 | ||
|
|
569f736831 | ||
|
|
7702ed027b | ||
|
|
7c068afe05 | ||
|
|
211d136b7a | ||
|
|
434e0653fe | ||
|
|
cb134a7e13 | ||
|
|
08db90f606 | ||
|
|
964d552d28 | ||
|
|
40d3d9906a | ||
|
|
122467799f | ||
|
|
b1ca56a296 | ||
|
|
cb3504642c | ||
|
|
a8f10bbb09 | ||
|
|
b75849d4ab | ||
|
|
0c88bea772 | ||
|
|
7aaddf2a60 | ||
|
|
35d6db5e73 | ||
|
|
6642e93581 | ||
|
|
4aaacf8e2e | ||
|
|
0a8e6e9f07 | ||
|
|
062272a6e8 | ||
|
|
d610302e79 | ||
|
|
69846bfb18 | ||
|
|
c4bca4c214 | ||
|
|
14a8c686c0 | ||
|
|
c6cc750166 | ||
|
|
3cce84dfa7 | ||
|
|
c8f4c6bd49 | ||
|
|
17501c6cad | ||
|
|
aa64e7947e | ||
|
|
9adfbf1668 | ||
|
|
c6f2d4a0c4 | ||
|
|
4b7fc7b80b | ||
|
|
5c30146ee0 | ||
|
|
28af10444d | ||
|
|
a6f0faa058 | ||
|
|
b6227e633c | ||
|
|
47dd8610c6 | ||
|
|
6ffdbcafc5 | ||
|
|
d89f45d534 | ||
|
|
f014828423 | ||
|
|
f26c672421 | ||
|
|
b1b9804aca | ||
|
|
5c2c947785 | ||
|
|
1319cb2e4e | ||
|
|
1ecf3d04d9 | ||
|
|
b8d4776671 | ||
|
|
43b829bf02 | ||
|
|
6c150f9d06 | ||
|
|
bf818e1ede | ||
|
|
4022b6b9cd | ||
|
|
ab5ff6446e | ||
|
|
ef4bc0ca0c | ||
|
|
07e31d6745 | ||
|
|
e0bcbb0396 | ||
|
|
6a5bdebb7c | ||
|
|
bfcc1d8f03 | ||
|
|
eed6d6c49d | ||
|
|
cf05970ccf | ||
|
|
fa6e82dd25 | ||
|
|
16aaa0b5a6 | ||
|
|
70e146aedd | ||
|
|
338d7587b2 | ||
|
|
50837af607 | ||
|
|
989e931edf | ||
|
|
9238efbd3a | ||
|
|
ea2af2378d | ||
|
|
094ddfcf5f | ||
|
|
7329098ed8 | ||
|
|
0f229e4d7b | ||
|
|
b08eccd076 | ||
|
|
4e6e7c0562 | ||
|
|
eb331104df | ||
|
|
d6b664375e | ||
|
|
90c4f449e8 | ||
|
|
d5e8f5810d | ||
|
|
6215ec8cf6 | ||
|
|
f484785395 | ||
|
|
4eb93dd139 | ||
|
|
2da3f74057 | ||
|
|
4b2abb6f25 | ||
|
|
c38429d3a3 | ||
|
|
d78a5d1225 | ||
|
|
907ff89609 | ||
|
|
7758dcaaba | ||
|
|
ed6da918e1 | ||
|
|
30373db89c | ||
|
|
7e032fdbb3 | ||
|
|
d232a7abbb | ||
|
|
6b6b9ae242 | ||
|
|
241bcce6c1 | ||
|
|
f490d9ebe8 | ||
|
|
fb09843e5a | ||
|
|
ab95698181 | ||
|
|
28b3134ae2 | ||
|
|
ec47c9cd00 | ||
|
|
5e062908a5 | ||
|
|
d0cefd7005 | ||
|
|
c1276e789a | ||
|
|
7ac4cd4665 | ||
|
|
1797bf8108 | ||
|
|
8c32e619cb | ||
|
|
a0d800bbb9 | ||
|
|
0f276f10ee | ||
|
|
473c6aad84 | ||
|
|
f9b60fef85 | ||
|
|
f7ea140018 | ||
|
|
0287416aa5 | ||
|
|
0dfcfbb97e | ||
|
|
d82c386402 | ||
|
|
3c3a32b1fa | ||
|
|
8e2f78226a | ||
|
|
617360c631 | ||
|
|
0f2bd34a62 | ||
|
|
23a028f011 | ||
|
|
6c8da850a7 | ||
|
|
1bf5ae2d3d | ||
|
|
37a25625de | ||
|
|
206887bc71 | ||
|
|
1e8551da4a | ||
|
|
6dd88d25dd | ||
|
|
81089e989b | ||
|
|
8a4355e786 | ||
|
|
20947bc8c1 | ||
|
|
da0436c60e | ||
|
|
04af43a9fa | ||
|
|
9a2d87eb42 | ||
|
|
cb66925f22 | ||
|
|
c7b6688250 | ||
|
|
129dacbfe2 | ||
|
|
568ef92f79 | ||
|
|
75671dc982 | ||
|
|
4d55dbea6c | ||
|
|
67b7f90c26 | ||
|
|
3efd112d8b | ||
|
|
df5830b5f9 | ||
|
|
a6d8b985b2 | ||
|
|
e76a6dc00f | ||
|
|
77ee046b3f | ||
|
|
464f86eb1d | ||
|
|
b9bbd2872e | ||
|
|
7a70b8234c | ||
|
|
e6bf9faa3e | ||
|
|
ede7b1b35c | ||
|
|
6c095d8efa | ||
|
|
95bb8fa6ec | ||
|
|
bbe8fff1e6 | ||
|
|
5dc4cf4c17 | ||
|
|
5b135ada24 | ||
|
|
65b3dab916 | ||
|
|
e2643ead1a | ||
|
|
0dcfddc169 | ||
|
|
b36589b2e6 | ||
|
|
57bea64ad4 | ||
|
|
5b5bbd29c3 | ||
|
|
e80822f1c5 | ||
|
|
c7978150c4 | ||
|
|
70ed0af475 | ||
|
|
57d196fdce | ||
|
|
8dd61e1f86 | ||
|
|
d36801c589 | ||
|
|
9f62df36dc | ||
|
|
aa66cfaae7 | ||
|
|
e36abd2dfb | ||
|
|
9d5a021b1e | ||
|
|
5ddad51911 | ||
|
|
ef5a950ecb | ||
|
|
8419cfae2b | ||
|
|
289e144521 | ||
|
|
97ab06ea22 | ||
|
|
3d06fba918 | ||
|
|
f9aa33315a | ||
|
|
8eb6092f91 | ||
|
|
06a50a5e45 | ||
|
|
4759713d9d | ||
|
|
7061730abc | ||
|
|
f62dee262c | ||
|
|
df3a4d0d45 | ||
|
|
33d1fc085b | ||
|
|
c5eb3923c1 | ||
|
|
596dd03bb4 | ||
|
|
d72bdcfe9b | ||
|
|
0c3174c2b5 | ||
|
|
6abb219499 | ||
|
|
eea1ddc288 | ||
|
|
42cbb7b723 | ||
|
|
a06097b012 | ||
|
|
d7b8115724 | ||
|
|
c7703cd70c | ||
|
|
8dec3717eb | ||
|
|
c0673b10b5 | ||
|
|
91c3f2fb2d | ||
|
|
f83afd82d1 | ||
|
|
cf2cda6694 | ||
|
|
455c6e0d39 | ||
|
|
72b7900ce2 | ||
|
|
dba3d89027 | ||
|
|
8e4a480f05 | ||
|
|
7fe2ed81e2 | ||
|
|
58db856b9d | ||
|
|
62a854c2ea | ||
|
|
acb15e5209 | ||
|
|
ebf4395bc9 | ||
|
|
8af45e8091 | ||
|
|
32d6951c34 | ||
|
|
a172762558 | ||
|
|
7abedd176f | ||
|
|
f63be8a47d | ||
|
|
86aa27d92c | ||
|
|
972480269a | ||
|
|
b829e5c0f6 | ||
|
|
fe4b3c6f36 | ||
|
|
cad3921c95 | ||
|
|
24a84de463 | ||
|
|
3abf949e3f | ||
|
|
b561d86410 | ||
|
|
59ff1ff856 | ||
|
|
88a911f6b7 | ||
|
|
55aa3f1198 | ||
|
|
a6f78aad73 | ||
|
|
54bfbf174b | ||
|
|
be6757c290 | ||
|
|
dda2080d01 | ||
|
|
8ba63338fc | ||
|
|
8ebb39b8d5 | ||
|
|
0f230834a1 | ||
|
|
2dc41df0c8 | ||
|
|
995b879a28 | ||
|
|
b72e8559f6 | ||
|
|
733b5c62af | ||
|
|
b192b5366a | ||
|
|
14aa261aca | ||
|
|
14a218e319 | ||
|
|
4ac5f25279 | ||
|
|
c37ade8f5b | ||
|
|
1a179c7a45 | ||
|
|
237983cecf | ||
|
|
c1985b2fa2 | ||
|
|
fc13df9a09 | ||
|
|
8389b87556 | ||
|
|
b77a783f94 | ||
|
|
cdffd4a995 | ||
|
|
e75d17ed24 | ||
|
|
087b4d5c7f | ||
|
|
7ce631d529 | ||
|
|
829d3680d9 | ||
|
|
dbad6e4e8c | ||
|
|
7453f89827 | ||
|
|
3d639c7d45 | ||
|
|
9348316b12 | ||
|
|
d7d874d48c | ||
|
|
7aec367a3c | ||
|
|
6a29c64f08 | ||
|
|
0ee825212e | ||
|
|
8dfd2b9b07 | ||
|
|
343909f87a | ||
|
|
5e85d5ef32 | ||
|
|
1b14d7aa2d | ||
|
|
139282a39e | ||
|
|
e9724d8b41 | ||
|
|
adc9f92125 | ||
|
|
1c93153d83 | ||
|
|
c73a32f76d | ||
|
|
068af6b0d5 | ||
|
|
01a2d5e017 | ||
|
|
ca9f0fde9b | ||
|
|
b16a9053b4 | ||
|
|
5e6e7ed152 | ||
|
|
93d1c06892 | ||
|
|
e22745d1ff | ||
|
|
3308bc8a0f | ||
|
|
f94193ad53 | ||
|
|
6f5eb45144 | ||
|
|
9bfa7acdee | ||
|
|
e38b259094 | ||
|
|
394e92d538 | ||
|
|
02ee9451d8 | ||
|
|
093fe22ab5 | ||
|
|
2bb4d7c8b9 | ||
|
|
ea2732b38c | ||
|
|
6bb3cf5719 | ||
|
|
54e277e773 | ||
|
|
dec29b3e22 | ||
|
|
9974f71f3e | ||
|
|
25256601a6 | ||
|
|
d3e9fe37eb | ||
|
|
9ccbebdc4f | ||
|
|
73ddd239aa | ||
|
|
598eec219f | ||
|
|
f2f6239b59 | ||
|
|
ce53cdd4a4 | ||
|
|
4a97a6403d | ||
|
|
6ec397b934 | ||
|
|
23be3afc9d | ||
|
|
bf6b12cf57 | ||
|
|
3936251363 | ||
|
|
b50d97c3b7 | ||
|
|
ef29d4efe8 | ||
|
|
92ddf9bb2d | ||
|
|
875d5d50b8 | ||
|
|
83fa13ea4b | ||
|
|
8b24716372 | ||
|
|
670ac4a34f | ||
|
|
82eb923689 | ||
|
|
18aeda4713 | ||
|
|
a388493030 | ||
|
|
c3f8573950 | ||
|
|
36778bb87e | ||
|
|
1a89e379a4 | ||
|
|
0aae349816 | ||
|
|
434291b592 | ||
|
|
668ceda86c | ||
|
|
e3ef729adf | ||
|
|
baafba1774 | ||
|
|
2ccf82749f | ||
|
|
5663fd386a | ||
|
|
6d1ebf3952 | ||
|
|
0ea28c646a | ||
|
|
fab5dc64e9 | ||
|
|
a5013ecbc3 | ||
|
|
5f82e947b2 | ||
|
|
d00b9515de | ||
|
|
eebbbdd0dc | ||
|
|
46adb7644a | ||
|
|
49e7b4f4ea | ||
|
|
55b1c533cf | ||
|
|
f1a7f30167 | ||
|
|
9f5c2b74eb | ||
|
|
78b44af7a2 | ||
|
|
a8442ca0bb | ||
|
|
926b1ae28b | ||
|
|
109c1ce245 | ||
|
|
637ccf1b2a | ||
|
|
a428aedf14 | ||
|
|
2747049986 | ||
|
|
9a52fcf66d | ||
|
|
a2b2e01e39 | ||
|
|
be48a2ed91 | ||
|
|
245db56771 | ||
|
|
ba4616371b | ||
|
|
62a3bbbcf5 | ||
|
|
235c7a3bb8 | ||
|
|
9a1219c70c | ||
|
|
5e692e10bc | ||
|
|
22b6916ac2 | ||
|
|
e628ec6ff0 | ||
|
|
863f812dbb | ||
|
|
21dbf0f14b | ||
|
|
9c392461e8 | ||
|
|
5b9178b18d | ||
|
|
7f4a80f17f | ||
|
|
2798837450 | ||
|
|
381ca70517 | ||
|
|
fe2beaf96a | ||
|
|
c962ec34c8 | ||
|
|
ff390fcb7c | ||
|
|
35ab4a5ff4 | ||
|
|
cac498fd9e | ||
|
|
06fcf8f079 | ||
|
|
5dad1fe2af | ||
|
|
81ae860134 | ||
|
|
91558049d9 | ||
|
|
aa78948c90 | ||
|
|
7cbe0768f2 | ||
|
|
05be2fe25a | ||
|
|
59f46b8265 | ||
|
|
c5cdd748fc | ||
|
|
6fcbe5a37f | ||
|
|
4d595c1380 | ||
|
|
7684e966fc | ||
|
|
40639dfa37 | ||
|
|
5769d0121e | ||
|
|
1d6f3fc57f | ||
|
|
2609f3425b | ||
|
|
bd924b993b | ||
|
|
c4fe9a6a51 | ||
|
|
4694e31e35 | ||
|
|
ca0f09c8f7 | ||
|
|
684c9773c9 | ||
|
|
df443aa34c | ||
|
|
33e381b5da | ||
|
|
4b5d363f55 | ||
|
|
4c3dbc6deb | ||
|
|
519ad64e1d | ||
|
|
93068659e5 | ||
|
|
4b46a3d298 | ||
|
|
3e64028e29 | ||
|
|
9b17715175 | ||
|
|
7d8876f03c | ||
|
|
b955486f14 | ||
|
|
6068cfbd70 | ||
|
|
9cda8c8bcf | ||
|
|
b7522288b5 | ||
|
|
18bc91734f | ||
|
|
ef21ac3d5a | ||
|
|
08af0aab75 | ||
|
|
a29292e018 | ||
|
|
ebb37c09e5 | ||
|
|
6666d1a2f4 | ||
|
|
431bcf20ea | ||
|
|
7b3ef0e3ab | ||
|
|
34894fb76b | ||
|
|
e5afcbd013 | ||
|
|
253a98143c | ||
|
|
59a7feafef | ||
|
|
0b2c3d7ca8 | ||
|
|
bd8afc67dd | ||
|
|
08e5b018b8 | ||
|
|
3d58a0c0f3 | ||
|
|
56f7ca388d | ||
|
|
ddf0ee9972 | ||
|
|
298e6d38a0 | ||
|
|
7bb5d243a0 | ||
|
|
207b0194c2 | ||
|
|
f1081e058c | ||
|
|
7974319c73 | ||
|
|
e84b37fc66 | ||
|
|
4219edc089 | ||
|
|
244dcc0465 | ||
|
|
5e423a8ede | ||
|
|
fe24c8971f | ||
|
|
2410c767f8 | ||
|
|
a785fd27ec | ||
|
|
5d3df4579e | ||
|
|
3182256580 | ||
|
|
84b5c3c789 | ||
|
|
4a1a70fa46 | ||
|
|
fe457d149c | ||
|
|
9a29747bbf | ||
|
|
c04eb6dc2a | ||
|
|
5a9a6a4680 | ||
|
|
1633994fbd | ||
|
|
f9c85d4d81 | ||
|
|
ec636c95a1 | ||
|
|
2915fae942 | ||
|
|
f9b5468481 | ||
|
|
287f37eba5 | ||
|
|
26740668da | ||
|
|
bdd72f0d30 | ||
|
|
7e79f25949 | ||
|
|
2d917e166c | ||
|
|
ad4a811d0a | ||
|
|
ab1aa97af4 | ||
|
|
018941c5b3 | ||
|
|
cc1439fb7b | ||
|
|
f684a2900b | ||
|
|
83b721a322 | ||
|
|
a879528ed8 | ||
|
|
accbff3ccb | ||
|
|
0375f77b73 | ||
|
|
9d01162f42 | ||
|
|
3436dc1564 | ||
|
|
089293079f | ||
|
|
00e2cd7c04 | ||
|
|
f5abb933b0 | ||
|
|
848386bbaf | ||
|
|
5562a3c2ae | ||
|
|
9e8738660b | ||
|
|
178772d50d | ||
|
|
8d7f29c7c9 | ||
|
|
255f87f1c3 | ||
|
|
975354d081 | ||
|
|
ec6890ced1 | ||
|
|
290efb4b62 | ||
|
|
ab3ea76244 | ||
|
|
21d07aef15 | ||
|
|
6031cd02b9 | ||
|
|
2cdb4cab12 | ||
|
|
679217f2ef | ||
|
|
3aacc0a258 | ||
|
|
254cf6cc5b | ||
|
|
c06095259a | ||
|
|
30fd98c8d7 | ||
|
|
3a410f04fe | ||
|
|
4a0a58cdc1 | ||
|
|
c834f93539 | ||
|
|
35bdb84e57 | ||
|
|
36a3a09fcc | ||
|
|
2c07b490e8 | ||
|
|
5790311cbd | ||
|
|
fde3322117 | ||
|
|
5664e133d1 | ||
|
|
a1dc38a83f | ||
|
|
df974f93ee | ||
|
|
3dde64cdac | ||
|
|
6b4887df0f | ||
|
|
ca8ef3961e | ||
|
|
114e0e5b1e | ||
|
|
c7a247cbba | ||
|
|
3a641a7020 | ||
|
|
0977859bf5 | ||
|
|
a644253ee9 | ||
|
|
3403356be8 | ||
|
|
4a9ec0d0d4 | ||
|
|
250c5c0745 | ||
|
|
120b5880b1 | ||
|
|
18eff73795 | ||
|
|
89b9dbb1b4 | ||
|
|
dde4b3ab93 | ||
|
|
b1cc655ba5 | ||
|
|
5645a12d5b | ||
|
|
ce1a935c06 | ||
|
|
58b90ad6c9 | ||
|
|
4740e63e76 | ||
|
|
319932c734 | ||
|
|
7cf415288a | ||
|
|
fe67c1db8f | ||
|
|
09afbd89b8 | ||
|
|
824f7d827c | ||
|
|
3a8ca56f95 | ||
|
|
137e3fd083 | ||
|
|
1285944b1f | ||
|
|
cd8e9deab7 | ||
|
|
de424eac64 | ||
|
|
d106a09766 | ||
|
|
963e8ccd2c | ||
|
|
3474b8f2d0 | ||
|
|
e27b449ae9 | ||
|
|
7656818456 | ||
|
|
d214be1215 | ||
|
|
c8678c3ee5 | ||
|
|
108ee40c3f | ||
|
|
de51d205dc | ||
|
|
1f76246edc | ||
|
|
f91974b766 | ||
|
|
df85936145 | ||
|
|
f39aa0f52a | ||
|
|
180812394b | ||
|
|
291fa9597f | ||
|
|
6c4d8c25fc | ||
|
|
bf4b32a8e9 | ||
|
|
2e9dac7678 | ||
|
|
369314c1cb | ||
|
|
6ab86898af | ||
|
|
f3aa65d219 | ||
|
|
edf4f19de9 | ||
|
|
91eaaae6d7 | ||
|
|
ca0ae5a165 | ||
|
|
86d788d294 | ||
|
|
e345cea9be | ||
|
|
779f7841c2 | ||
|
|
15fdca46b8 | ||
|
|
97e099c70f | ||
|
|
d5a58bb763 | ||
|
|
2774df0d30 | ||
|
|
93a1799a4b | ||
|
|
f65f051c7c | ||
|
|
c46952dd4e | ||
|
|
1dad6b6118 | ||
|
|
e964621f2c | ||
|
|
46ae62f693 | ||
|
|
7d578f5852 | ||
|
|
467f257ad3 | ||
|
|
55186c0a49 | ||
|
|
1a0a4f7112 | ||
|
|
658290ae80 | ||
|
|
cb9f29c6fc | ||
|
|
d8f4955292 | ||
|
|
9803932324 | ||
|
|
a3a416b5e2 | ||
|
|
1453a318fe | ||
|
|
9ffa01d318 | ||
|
|
29b779c873 | ||
|
|
598307f676 | ||
|
|
aa9d546bf5 | ||
|
|
f2ef245eca | ||
|
|
458606381e | ||
|
|
244f174dc9 | ||
|
|
8a577f3197 | ||
|
|
2f1b24cc14 | ||
|
|
d5f836db7a | ||
|
|
ffd7c36cb2 | ||
|
|
dba543d539 | ||
|
|
9753c0f8eb | ||
|
|
c50b4d3d47 | ||
|
|
907f886cf0 | ||
|
|
218a2d2004 | ||
|
|
0e0f2c6833 | ||
|
|
e59090d3b6 | ||
|
|
0404be8bef | ||
|
|
337895cbaa | ||
|
|
743547096e | ||
|
|
81fb3df45e | ||
|
|
12624cab5b | ||
|
|
7d62ea88d2 | ||
|
|
abcc277430 | ||
|
|
4c4c9426ee | ||
|
|
88fdeb2bf2 | ||
|
|
f9ab868022 | ||
|
|
148d2cec8f | ||
|
|
5869f157f6 | ||
|
|
69a2902161 | ||
|
|
c19b048249 | ||
|
|
cd2c8acdb2 | ||
|
|
7d0d1c764f | ||
|
|
b5acf1d529 | ||
|
|
72b08384ad | ||
|
|
f51e48f282 | ||
|
|
2e6d1f3642 | ||
|
|
716af4ed93 | ||
|
|
31de86c9eb | ||
|
|
04a2cd1f1f | ||
|
|
bc0ef11a8c | ||
|
|
90c8420a4c | ||
|
|
657d5e0d74 | ||
|
|
03fa4d957c | ||
|
|
8d4530f1f2 | ||
|
|
6d15c503c3 | ||
|
|
75ddb34398 | ||
|
|
6fbfe6fb72 | ||
|
|
db500e911c | ||
|
|
c0c5ced6ad | ||
|
|
726270f8bc | ||
|
|
c3908450a0 | ||
|
|
f7ba05f465 | ||
|
|
dea7f7d5d6 | ||
|
|
ed7a0a2b9d | ||
|
|
ac6b4db0f8 | ||
|
|
b5d659c13c | ||
|
|
919cb5d1f2 | ||
|
|
e504d3cc35 | ||
|
|
1d67939e76 | ||
|
|
5d1e90d29c | ||
|
|
1678eba9cc | ||
|
|
cc142b2ba1 | ||
|
|
48cbffba14 | ||
|
|
d909bb1b25 | ||
|
|
3313e66fc2 | ||
|
|
085892a4c8 | ||
|
|
817e059230 | ||
|
|
7ce8891246 | ||
|
|
439b2589f9 | ||
|
|
e7b5cf66d2 | ||
|
|
b29fabf76c | ||
|
|
5d837c3ee4 | ||
|
|
4c0396ad1c | ||
|
|
97b62fce79 | ||
|
|
e904a38735 | ||
|
|
ae72187aed | ||
|
|
e4ebeefa61 | ||
|
|
417373ba70 | ||
|
|
36f9ded08e | ||
|
|
858925b8c8 | ||
|
|
d36c96fba9 | ||
|
|
2be6fb329e | ||
|
|
da56363ef9 | ||
|
|
90d73228f3 | ||
|
|
1c76fcd26b | ||
|
|
c26c4ddf15 | ||
|
|
537bdb62da | ||
|
|
b2ac2bd97a | ||
|
|
aee6ea56b5 | ||
|
|
ff5506c842 | ||
|
|
94c5340fbf | ||
|
|
0e11026b60 | ||
|
|
1d8e953ebc | ||
|
|
8708fba4bc | ||
|
|
bc1294ae61 | ||
|
|
308f05101e | ||
|
|
f368739303 | ||
|
|
1bca1b921b | ||
|
|
3e513e92b1 | ||
|
|
194fe178c0 | ||
|
|
5d10a19bfa | ||
|
|
8f52a68526 | ||
|
|
c67d0d59bb | ||
|
|
4477064f17 | ||
|
|
9dbe24b37c | ||
|
|
46a85295e8 | ||
|
|
63170324a8 | ||
|
|
4fd157b5f4 | ||
|
|
e5406a0ea3 | ||
|
|
9b64ba21fd | ||
|
|
18fcf07971 | ||
|
|
d3404c6570 | ||
|
|
1878b5287b | ||
|
|
7f74545586 | ||
|
|
a920894a8f | ||
|
|
2c912456ce | ||
|
|
ebda475972 | ||
|
|
05aace84e1 | ||
|
|
1cfa1faccc | ||
|
|
1caa393974 | ||
|
|
ef357ab6e5 | ||
|
|
c39c5492ea | ||
|
|
1a5d54f74f | ||
|
|
13dc6c7dfb | ||
|
|
4c7dee69c2 | ||
|
|
dc9cf7689d | ||
|
|
778a408c6c | ||
|
|
2b7f3061d0 | ||
|
|
92903e1ec3 | ||
|
|
c5a621010e | ||
|
|
0b5b636578 | ||
|
|
8fcdb91ba3 | ||
|
|
f67d5f1197 | ||
|
|
6e0e6203cc | ||
|
|
2694ce4148 | ||
|
|
7c02b032f6 | ||
|
|
deb7754cb9 | ||
|
|
624f3c60bd | ||
|
|
8a8e792faa | ||
|
|
bc836011bc | ||
|
|
107eedfb49 | ||
|
|
61a7dcda23 | ||
|
|
873ca4f438 | ||
|
|
29221c2901 | ||
|
|
eac9613df7 | ||
|
|
184d5d25a6 | ||
|
|
ae33411566 | ||
|
|
479ebcc3fa | ||
|
|
1ae572cf30 | ||
|
|
ac6e16688d | ||
|
|
69159e0271 | ||
|
|
cae35d6a5a | ||
|
|
df9b65e296 | ||
|
|
fb0b34a6a0 | ||
|
|
db3b822aef | ||
|
|
f4994ef151 | ||
|
|
8e27291417 | ||
|
|
aae9ad78e5 | ||
|
|
fb3efbfc66 | ||
|
|
0f8a66609a | ||
|
|
b3bb8c386f | ||
|
|
b3b5e0e155 | ||
|
|
99a0bf1286 | ||
|
|
650f0ee752 | ||
|
|
2ddcf1120f | ||
|
|
d45c74915c | ||
|
|
cd373791ac | ||
|
|
7b03b0c5fc | ||
|
|
57d6677131 | ||
|
|
05e73344eb | ||
|
|
fff8f96490 | ||
|
|
1ccfb34246 | ||
|
|
b30bd00993 | ||
|
|
6b2d9fe816 | ||
|
|
531114279d | ||
|
|
944aaff0fa | ||
|
|
6bafa6819d | ||
|
|
8d7091285a | ||
|
|
579e95219a | ||
|
|
4a746881e3 | ||
|
|
9122f1b642 | ||
|
|
8b033ed4a8 | ||
|
|
8e53a63243 | ||
|
|
4de01e7387 | ||
|
|
f85ab74e2b | ||
|
|
8bb7d5de3f | ||
|
|
68424e485c | ||
|
|
ac34285eed | ||
|
|
e605a82573 | ||
|
|
1061764426 | ||
|
|
8d93e410f7 | ||
|
|
9336cdcc5d | ||
|
|
7639c03646 | ||
|
|
2da55bff9c | ||
|
|
6ea5d45dec | ||
|
|
e9504a3899 | ||
|
|
7bff2fd1e5 | ||
|
|
694712e933 | ||
|
|
aa675559e6 | ||
|
|
2af19d675a | ||
|
|
4588d16fc2 | ||
|
|
5dbd3031be | ||
|
|
59c16eba77 | ||
|
|
8c21ec96d1 | ||
|
|
2c00a8353d | ||
|
|
19e7a76f85 | ||
|
|
f16a186faf | ||
|
|
456dd39ec4 | ||
|
|
5b69a697e4 | ||
|
|
20d279fee4 | ||
|
|
cd55d819af | ||
|
|
dce51da92f | ||
|
|
6118cf6041 | ||
|
|
4ae8bffd4d | ||
|
|
5ebe86d5b6 | ||
|
|
317d97a310 | ||
|
|
082aa1d8e3 | ||
|
|
2f2963676f | ||
|
|
f2d388f742 | ||
|
|
d79008495a | ||
|
|
43f1867fb8 | ||
|
|
043c28628c | ||
|
|
9540854c8a | ||
|
|
b951a6ca66 | ||
|
|
26561a395d | ||
|
|
0a884c8718 | ||
|
|
b393e3c662 | ||
|
|
c36062210f | ||
|
|
861381fbb6 | ||
|
|
b9019f9590 | ||
|
|
fcfa877e89 | ||
|
|
d61849eefe | ||
|
|
bc83bd6ec8 | ||
|
|
c18c94e565 | ||
|
|
77d077ec08 | ||
|
|
021d0e6359 | ||
|
|
7f4fd2c52f | ||
|
|
8ffe098e3b | ||
|
|
a2ca86ca74 | ||
|
|
deef018806 | ||
|
|
c3f8b2ea56 | ||
|
|
e3696b6055 | ||
|
|
b82b340668 | ||
|
|
a8b01c439b | ||
|
|
6ec686ca7e | ||
|
|
fdb664fecb | ||
|
|
09f838089b | ||
|
|
db0fe918b8 | ||
|
|
8ca696d547 | ||
|
|
b7ccb1ce5c | ||
|
|
5f0033e466 | ||
|
|
477adba3e1 | ||
|
|
eb15ca8ebd | ||
|
|
dbf5907c20 | ||
|
|
e95deb9443 | ||
|
|
e19c36b5c4 | ||
|
|
fdd589db4f | ||
|
|
cd4e45d8f5 | ||
|
|
e6e31b9cab | ||
|
|
b676b1fef9 | ||
|
|
58bfb35fa6 | ||
|
|
09131e8c36 | ||
|
|
1d6ca91c01 | ||
|
|
fa00ba2edd | ||
|
|
65114c8483 | ||
|
|
d033985141 | ||
|
|
5badfbee93 | ||
|
|
44a61842f0 | ||
|
|
70b61af572 | ||
|
|
53a53d393d | ||
|
|
6c95b5757b | ||
|
|
693b3e5a6f | ||
|
|
aac1338bdd | ||
|
|
2979137102 | ||
|
|
0068675f3b | ||
|
|
6c1a22ea7f | ||
|
|
2869b92e6b | ||
|
|
8a38ce1964 | ||
|
|
3a7c5566ed | ||
|
|
1bed8c2b08 | ||
|
|
e24500b306 | ||
|
|
6251914419 | ||
|
|
8d9e943d57 | ||
|
|
f71bc5d323 | ||
|
|
a8082b2b3f | ||
|
|
510b72303d | ||
|
|
4666d4bbaa | ||
|
|
025f1e9502 | ||
|
|
b4c05098a9 | ||
|
|
3c8ee6f7f7 | ||
|
|
8b34135b08 | ||
|
|
d4b7c6689f | ||
|
|
d72aadfb00 | ||
|
|
00ff8ae166 | ||
|
|
cffcf49b26 | ||
|
|
ad55aa8b85 | ||
|
|
accbdea942 | ||
|
|
22144b78ea | ||
|
|
c03ab269f0 | ||
|
|
0c6c4d5959 | ||
|
|
e4e1873770 | ||
|
|
3b3579025d | ||
|
|
5cf8f2f4f4 | ||
|
|
c4ff05b1ba | ||
|
|
2bf54d0b8e | ||
|
|
2a63888546 | ||
|
|
cb8294cbd2 | ||
|
|
c9a99be183 | ||
|
|
5d2e026e5a | ||
|
|
568fdad52a | ||
|
|
174fb65fed | ||
|
|
2e77c45ae8 | ||
|
|
1b674a0abf | ||
|
|
4beb8b1f6b | ||
|
|
3c4abb7b60 | ||
|
|
970c73c221 | ||
|
|
6797c1255f | ||
|
|
0ed159b553 | ||
|
|
faa8679804 | ||
|
|
e3d2253958 | ||
|
|
3f7b7996c0 | ||
|
|
9229ff54df | ||
|
|
204ab8f427 | ||
|
|
8ee6c0789d | ||
|
|
be188e7266 | ||
|
|
1a43b62750 | ||
|
|
eaa8321aa8 | ||
|
|
f01f0071dd | ||
|
|
ec69cc3590 | ||
|
|
102dac1eb8 | ||
|
|
d54c5f1f99 | ||
|
|
c2e0e00c47 | ||
|
|
f24e387363 | ||
|
|
09715b83e4 | ||
|
|
e4f197bbc9 | ||
|
|
de64cef48d | ||
|
|
b85329b371 | ||
|
|
215f9a4245 | ||
|
|
80ce7a64ba | ||
|
|
65f3dc0656 | ||
|
|
f9b6b841b6 | ||
|
|
d4f2d649e8 | ||
|
|
15b057fe68 | ||
|
|
9cb549f4a1 | ||
|
|
4d67578458 | ||
|
|
71faaf1c19 | ||
|
|
0187f29b4b | ||
|
|
9b6ccdd43a |
8
.github/CONTRIBUTING.md
vendored
8
.github/CONTRIBUTING.md
vendored
@@ -6,14 +6,6 @@
|
||||
|
||||
I am always interested in expanding Firefly III's many features. If you are requesting a new feature, please check out the list of [often requested features](https://firefly-iii.github.io/requested-features/).
|
||||
|
||||
## Bugs
|
||||
|
||||
First of all: thank you for reporting a bug instead of ditching the tool altogether. If you find a bug, please take the time and see if the [demo site](https://firefly-iii.nder.be/) is also suffering from this bug. Include as many log files and details as you think are necessary. Bugs have a lot of priority!
|
||||
|
||||
## Installation problems
|
||||
|
||||
Please take the time to read the [installation guide FAQ](https://firefly-iii.github.io/installation-guide-faq/) and make sure you search through closed issues for the problems other people have had. Your problem may be among them! If not, open an issue and I will help where I can.
|
||||
|
||||
## Pull requests
|
||||
|
||||
When contributing to Firefly III, please first discuss the change you wish to make via issue, email, or any other method. I can only accept pull requests against the `develop` branch, never the `master` branch.
|
||||
|
||||
11
.github/SUPPORT.md
vendored
Normal file
11
.github/SUPPORT.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Welcome to Firefly III on Github!
|
||||
|
||||
:+1::tada: Thank you for taking the time to contribute something to Firefly III!
|
||||
|
||||
## Bugs
|
||||
|
||||
First of all: thank you for reporting a bug instead of ditching the tool altogether. If you find a bug, please take the time and see if the [demo site](https://firefly-iii.nder.be/) is also suffering from this bug. Include as many log files and details as you think are necessary. Bugs have a lot of priority!
|
||||
|
||||
## Installation problems
|
||||
|
||||
Please take the time to read the [installation guide FAQ](https://firefly-iii.github.io/installation-guide-faq/) and make sure you search through closed issues for the problems other people have had. Your problem may be among them! If not, open an issue and I will help where I can.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ Homestead.json
|
||||
Homestead.yaml
|
||||
.env
|
||||
public/google*.html
|
||||
report.html
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
- 7.1
|
||||
|
||||
cache:
|
||||
|
||||
110
CHANGELOG.md
110
CHANGELOG.md
@@ -2,6 +2,116 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [4.6.5] - 2017-09-09
|
||||
|
||||
### Added
|
||||
- #616, The ability to link transactions
|
||||
- #763, as suggested by @tannie
|
||||
- #770, as suggested by @skibbipl
|
||||
- #780, as suggested by @skibbipl
|
||||
- #784, as suggested by @SmilingWorlock
|
||||
- Lots of code for future support of automated Bunq imports
|
||||
|
||||
### Changed
|
||||
- Rewrote the export routine
|
||||
- #782, as suggested by @NiceGuyIT
|
||||
- #800, as suggested by @jleeong
|
||||
|
||||
### Fixed
|
||||
- #724, reported by @skibbipl
|
||||
- #738, reported by @skibbipl
|
||||
- #760, reported by @leander091
|
||||
- #764, reported by @tannie
|
||||
- #792, reported by @jleeong
|
||||
- #793, reported by @nicoschreiner
|
||||
- #797, reported by @leander091
|
||||
- #801, reported by @pkoziol
|
||||
- #803, reported by @pkoziol
|
||||
- #805, reported by @pkoziol
|
||||
- #806, reported by @pkoziol
|
||||
- #807, reported by @pkoziol
|
||||
- #808, reported by @pkoziol
|
||||
- #809, reported by @pkoziol
|
||||
- #814, reported by @nicoschreiner
|
||||
- #818, reported by @gavu
|
||||
- #819, reported by @DieBauer
|
||||
- #820, reported by @simonsmiley
|
||||
- Various other fixes
|
||||
|
||||
|
||||
## [4.6.4] - 2017-08-13
|
||||
### Added
|
||||
- PHP7.1 support
|
||||
- Routine to decrypt attachments from the command line, for issue #671
|
||||
- A routine that can check if your password has been stolen in the past.
|
||||
- Split transaction shows amount left to be split
|
||||
|
||||
|
||||
### Changed
|
||||
- Importer can (potentially) handle new import routines such as banks.
|
||||
- Importer can fall back from JSON errors
|
||||
|
||||
### Deprecated
|
||||
- Initial release.
|
||||
|
||||
### Removed
|
||||
- PHP7.0 support
|
||||
- Support for extended tag modes
|
||||
- Remove "time jumps" to non-empty periods
|
||||
|
||||
|
||||
### Fixed
|
||||
- #717, reported by @NiceGuyIT
|
||||
- #718, reported by @wtercato
|
||||
- #722, reported by @simonsmiley
|
||||
- #648, reported by @skibbipl
|
||||
- #730, reported by @ragnarkarlsson
|
||||
- #733, reported by @xpfgsyb
|
||||
- #735, reported by @kristophr
|
||||
- #739, reported by @skibbipl
|
||||
- #515, reported by @schwalberich
|
||||
- #743, reported by @simonsmiley
|
||||
- #746, reported by @tannie
|
||||
- #747, reported by @tannie
|
||||
|
||||
### Security
|
||||
- Initial release.
|
||||
|
||||
|
||||
|
||||
## [4.6.3.1] - 2017-07-23
|
||||
### Fixed
|
||||
- Hotfix to close issue #715
|
||||
|
||||
## [4.6.3] - 2017-07-23
|
||||
|
||||
This will be the last release to support PHP 7.0.
|
||||
|
||||
### Added
|
||||
- New guidelines and new introduction tour to aid new users.
|
||||
- Rules can now be applied at will to transactions, not just rule groups.
|
||||
|
||||
### Changed
|
||||
- Improved category overview.
|
||||
- Improved budget overview.
|
||||
- Improved budget report.
|
||||
- Improved command line import responsiveness and speed.
|
||||
- All code comparisons are now strict.
|
||||
- Improve search page.
|
||||
- Charts are easier to read thanks to @simonsmiley
|
||||
- Fixed #708.
|
||||
|
||||
### Fixed
|
||||
- Fixed bug where import would not respect default account. #694
|
||||
- Fixed various broken paths
|
||||
- Fixed several import inconsistencies.
|
||||
- Various bug fixes.
|
||||
|
||||
### Security
|
||||
- Initial release.
|
||||
|
||||
|
||||
|
||||
## [4.6.2] - 2017-07-08
|
||||
### Added
|
||||
- Links added to boxes, idea by @simonsmiley
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@@ -23,28 +23,18 @@ RUN docker-php-ext-install -j$(nproc) curl gd intl json mcrypt readline tidy zip
|
||||
# Generate locales supported by firefly
|
||||
RUN echo "en_US.UTF-8 UTF-8\nde_DE.UTF-8 UTF-8\nnl_NL.UTF-8 UTF-8\npt_BR.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
|
||||
|
||||
COPY docker/apache2.conf /etc/apache2/apache2.conf
|
||||
# Enable apache mod rewrite..
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# Setup the Composer installer
|
||||
RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer && \
|
||||
curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig && \
|
||||
php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" && \
|
||||
chmod +x /tmp/composer-setup.php && \
|
||||
php /tmp/composer-setup.php && \
|
||||
mv composer.phar /usr/local/bin/composer && \
|
||||
rm -f /tmp/composer-setup.{php,sig}
|
||||
run curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
|
||||
ADD . /var/www/firefly-iii
|
||||
RUN chown -R www-data:www-data /var/www/
|
||||
RUN cd /var/www && composer create-project grumpydictator/firefly-iii --no-dev --prefer-dist firefly-iii 4.6.4
|
||||
COPY docker/entrypoint.sh /var/www/firefly-iii/docker/entrypoint.sh
|
||||
ADD docker/apache-firefly.conf /etc/apache2/sites-available/000-default.conf
|
||||
|
||||
USER www-data
|
||||
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www/firefly-iii/storage
|
||||
|
||||
WORKDIR /var/www/firefly-iii
|
||||
|
||||
RUN composer install --no-scripts --no-dev
|
||||
|
||||
USER root
|
||||
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["/var/www/firefly-iii/docker/entrypoint.sh"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Firefly III: A personal finances manager
|
||||
|
||||
[](https://secure.php.net/downloads.php) [](https://packagist.org/packages/grumpydictator/firefly-iii) [](https://creativecommons.org/licenses/by-sa/4.0/) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA)
|
||||
[](https://secure.php.net/downloads.php) [](https://packagist.org/packages/grumpydictator/firefly-iii) [](https://creativecommons.org/licenses/by-sa/4.0/) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA)
|
||||
|
||||
[](https://i.nder.be/h2b37243) [](https://i.nder.be/hv70pbwc)
|
||||
|
||||
|
||||
@@ -14,10 +14,14 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Console\Commands;
|
||||
|
||||
use Artisan;
|
||||
use FireflyIII\Import\Logging\CommandHandler;
|
||||
use FireflyIII\Import\Routine\ImportRoutine;
|
||||
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Log;
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
|
||||
/**
|
||||
* Class CreateImport
|
||||
@@ -50,7 +54,10 @@ class CreateImport extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the command.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) // cannot be helped
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five exactly.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
@@ -73,36 +80,63 @@ class CreateImport extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info(sprintf('Going to create a job to import file: %s', $file));
|
||||
$this->info(sprintf('Using configuration file: %s', $configuration));
|
||||
$this->info(sprintf('Import into user: #%d (%s)', $user->id, $user->email));
|
||||
$this->info(sprintf('Type of import: %s', $type));
|
||||
$this->line(sprintf('Going to create a job to import file: %s', $file));
|
||||
$this->line(sprintf('Using configuration file: %s', $configuration));
|
||||
$this->line(sprintf('Import into user: #%d (%s)', $user->id, $user->email));
|
||||
$this->line(sprintf('Type of import: %s', $type));
|
||||
|
||||
|
||||
/** @var ImportJobRepositoryInterface $jobRepository */
|
||||
$jobRepository = app(ImportJobRepositoryInterface::class);
|
||||
$jobRepository->setUser($user);
|
||||
$job = $jobRepository->create($type);
|
||||
$this->line(sprintf('Created job "%s"...', $job->key));
|
||||
$this->line(sprintf('Created job "%s"', $job->key));
|
||||
|
||||
|
||||
Artisan::call('firefly:encrypt-file', ['file' => $file, 'key' => $job->key]);
|
||||
$this->line('Stored import data...');
|
||||
|
||||
|
||||
$job->configuration = $configurationData;
|
||||
$job->status = 'configured';
|
||||
$job->save();
|
||||
$this->line('Stored configuration...');
|
||||
|
||||
|
||||
if ($this->option('start') === true) {
|
||||
$this->line('The import will start in a moment. This process is not visible...');
|
||||
Log::debug('Go for import!');
|
||||
Artisan::call('firefly:start-import', ['key' => $job->key]);
|
||||
$this->line('Done!');
|
||||
|
||||
// normally would refer to other firefly:start-import but that doesn't seem to work all to well...
|
||||
$monolog = Log::getMonolog();
|
||||
$handler = new CommandHandler($this);
|
||||
$formatter = new LineFormatter(null, null, false, true);
|
||||
$handler->setFormatter($formatter);
|
||||
$monolog->pushHandler($handler);
|
||||
|
||||
|
||||
// start the actual routine:
|
||||
/** @var ImportRoutine $routine */
|
||||
$routine = app(ImportRoutine::class);
|
||||
$routine->setJob($job);
|
||||
$routine->run();
|
||||
|
||||
// give feedback.
|
||||
/** @var MessageBag $error */
|
||||
foreach ($routine->errors as $index => $error) {
|
||||
$this->error(sprintf('Error importing line #%d: %s', $index, $error));
|
||||
}
|
||||
$this->line(
|
||||
sprintf('The import has finished. %d transactions have been imported out of %d records.', $routine->journals->count(), $routine->lines)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify user inserts correct arguments.
|
||||
*
|
||||
* @return bool
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five exactly.
|
||||
*/
|
||||
|
||||
104
app/Console/Commands/DecryptAttachment.php
Normal file
104
app/Console/Commands/DecryptAttachment.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* DecryptAttachment.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Console\Commands;
|
||||
|
||||
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
|
||||
use Illuminate\Console\Command;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class DecryptAttachment
|
||||
*
|
||||
* @package FireflyIII\Console\Commands
|
||||
*/
|
||||
class DecryptAttachment extends Command
|
||||
{
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Decrypts an attachment and dumps the content in a file in the given directory.';
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature
|
||||
= 'firefly:decrypt-attachment {id:The ID of the attachment.} {name:The file name of the attachment.}
|
||||
{directory:Where the file must be stored.}';
|
||||
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
/** @var AttachmentRepositoryInterface $repository */
|
||||
$repository = app(AttachmentRepositoryInterface::class);
|
||||
$attachmentId = intval($this->argument('id'));
|
||||
$attachment = $repository->findWithoutUser($attachmentId);
|
||||
$attachmentName = trim($this->argument('name'));
|
||||
$storagePath = realpath(trim($this->argument('directory')));
|
||||
if (is_null($attachment->id)) {
|
||||
$this->error(sprintf('No attachment with id #%d', $attachmentId));
|
||||
Log::error(sprintf('DecryptAttachment: No attachment with id #%d', $attachmentId));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($attachmentName !== $attachment->filename) {
|
||||
$this->error('File name does not match.');
|
||||
Log::error('DecryptAttachment: File name does not match.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_dir($storagePath)) {
|
||||
$this->error(sprintf('Path "%s" is not a directory.', $storagePath));
|
||||
Log::error(sprintf('DecryptAttachment: Path "%s" is not a directory.', $storagePath));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_writable($storagePath)) {
|
||||
$this->error(sprintf('Path "%s" is not writable.', $storagePath));
|
||||
Log::error(sprintf('DecryptAttachment: Path "%s" is not writable.', $storagePath));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$fullPath = $storagePath . DIRECTORY_SEPARATOR . $attachment->filename;
|
||||
$content = $repository->getContent($attachment);
|
||||
$this->line(sprintf('Going to write content for attachment #%d into file "%s"', $attachment->id, $fullPath));
|
||||
$result = file_put_contents($fullPath, $content);
|
||||
if ($result === false) {
|
||||
$this->error('Could not write to file.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->info(sprintf('%d bytes written. Exiting now..', $result));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class Import extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Run the import routine.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
@@ -91,6 +91,8 @@ class Import extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if job is valid to be imported.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return bool
|
||||
|
||||
@@ -20,23 +20,23 @@ use FireflyIII\Models\AccountMeta;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
use FireflyIII\Models\LimitRepetition;
|
||||
use FireflyIII\Models\PiggyBankEvent;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
use Preferences;
|
||||
use Schema;
|
||||
use Steam;
|
||||
|
||||
/**
|
||||
* Class UpgradeDatabase
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) // it just touches a lot of things.
|
||||
*
|
||||
* @package FireflyIII\Console\Commands
|
||||
*/
|
||||
class UpgradeDatabase extends Command
|
||||
@@ -70,73 +70,26 @@ class UpgradeDatabase extends Command
|
||||
{
|
||||
$this->setTransactionIdentifier();
|
||||
$this->migrateRepetitions();
|
||||
$this->repairPiggyBanks();
|
||||
$this->updateAccountCurrencies();
|
||||
$this->updateJournalCurrencies();
|
||||
$this->currencyInfoToTransactions();
|
||||
$this->verifyCurrencyInfo();
|
||||
$this->line('Updating currency information..');
|
||||
$this->updateTransferCurrencies();
|
||||
$this->updateOtherCurrencies();
|
||||
$this->info('Firefly III database is up to date.');
|
||||
|
||||
return;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the currency id info to the transaction instead of the journal.
|
||||
* Migrate budget repetitions to new format where the end date is in the budget limit as well,
|
||||
* making the limit_repetition table obsolete.
|
||||
*/
|
||||
private function currencyInfoToTransactions()
|
||||
public function migrateRepetitions(): void
|
||||
{
|
||||
$count = 0;
|
||||
$set = TransactionJournal::with('transactions')->get();
|
||||
/** @var TransactionJournal $journal */
|
||||
foreach ($set as $journal) {
|
||||
/** @var Transaction $transaction */
|
||||
foreach ($journal->transactions as $transaction) {
|
||||
if (is_null($transaction->transaction_currency_id)) {
|
||||
$transaction->transaction_currency_id = $journal->transaction_currency_id;
|
||||
$transaction->save();
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// read and use the foreign amounts when present.
|
||||
if ($journal->hasMeta('foreign_amount')) {
|
||||
$amount = Steam::positive($journal->getMeta('foreign_amount'));
|
||||
|
||||
// update both transactions:
|
||||
foreach ($journal->transactions as $transaction) {
|
||||
$transaction->foreign_amount = $amount;
|
||||
if (bccomp($transaction->amount, '0') === -1) {
|
||||
// update with negative amount:
|
||||
$transaction->foreign_amount = bcmul($amount, '-1');
|
||||
}
|
||||
// set foreign currency id:
|
||||
$transaction->foreign_currency_id = intval($journal->getMeta('foreign_currency_id'));
|
||||
$transaction->save();
|
||||
}
|
||||
$journal->deleteMeta('foreign_amount');
|
||||
$journal->deleteMeta('foreign_currency_id');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->line(sprintf('Updated currency information for %d transactions', $count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate budget repetitions to new format.
|
||||
*/
|
||||
private function migrateRepetitions()
|
||||
{
|
||||
if (!Schema::hasTable('budget_limits')) {
|
||||
return;
|
||||
}
|
||||
// get all budget limits with end_date NULL
|
||||
$set = BudgetLimit::whereNull('end_date')->get();
|
||||
if ($set->count() > 0) {
|
||||
$this->line(sprintf('Found %d budget limit(s) to update', $set->count()));
|
||||
}
|
||||
/** @var BudgetLimit $budgetLimit */
|
||||
foreach ($set as $budgetLimit) {
|
||||
// get limit repetition (should be just one):
|
||||
/** @var LimitRepetition $repetition */
|
||||
$repetition = $budgetLimit->limitrepetitions()->first();
|
||||
if (!is_null($repetition)) {
|
||||
@@ -146,56 +99,30 @@ class UpgradeDatabase extends Command
|
||||
$repetition->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure there are only transfers linked to piggy bank events.
|
||||
* This method gives all transactions which are part of a split journal (so more than 2) a sort of "order" so they are easier
|
||||
* to easier to match to their counterpart. When a journal is split, it has two or three transactions: -3, -4 and -5 for example.
|
||||
*
|
||||
* In the database this is reflected as 6 transactions: -3/+3, -4/+4, -5/+5.
|
||||
*
|
||||
* When either of these are the same amount, FF3 can't keep them apart: +3/-3, +3/-3, +3/-3. This happens more often than you would
|
||||
* think. So each set gets a number (1,2,3) to keep them apart.
|
||||
*/
|
||||
private function repairPiggyBanks()
|
||||
{
|
||||
// if table does not exist, return false
|
||||
if (!Schema::hasTable('piggy_bank_events')) {
|
||||
return;
|
||||
}
|
||||
$set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
|
||||
/** @var PiggyBankEvent $event */
|
||||
foreach ($set as $event) {
|
||||
|
||||
if (is_null($event->transaction_journal_id)) {
|
||||
continue;
|
||||
}
|
||||
/** @var TransactionJournal $journal */
|
||||
$journal = $event->transactionJournal()->first();
|
||||
if (is_null($journal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $journal->transactionType->type;
|
||||
if ($type !== TransactionType::TRANSFER) {
|
||||
$event->transaction_journal_id = null;
|
||||
$event->save();
|
||||
$this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is strangely complex, because the HAVING modifier is a no-no. And subqueries in Laravel are weird.
|
||||
*/
|
||||
private function setTransactionIdentifier()
|
||||
public function setTransactionIdentifier(): void
|
||||
{
|
||||
// if table does not exist, return false
|
||||
if (!Schema::hasTable('transaction_journals')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->whereNull('transactions.deleted_at')
|
||||
->groupBy(['transaction_journals.id'])
|
||||
->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]);
|
||||
|
||||
$subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->whereNull('transactions.deleted_at')
|
||||
->groupBy(['transaction_journals.id'])
|
||||
->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]);
|
||||
$result = DB::table(DB::raw('(' . $subQuery->toSql() . ') AS derived'))
|
||||
->mergeBindings($subQuery->getQuery())
|
||||
->where('t_count', '>', 2)
|
||||
@@ -203,59 +130,172 @@ class UpgradeDatabase extends Command
|
||||
$journalIds = array_unique($result->pluck('id')->toArray());
|
||||
|
||||
foreach ($journalIds as $journalId) {
|
||||
$this->updateJournal(intval($journalId));
|
||||
$this->updateJournalidentifiers(intval($journalId));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account.
|
||||
*/
|
||||
private function updateAccountCurrencies()
|
||||
public function updateAccountCurrencies(): void
|
||||
{
|
||||
$accounts = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
|
||||
->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->get(['accounts.*']);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($accounts as $account) {
|
||||
// get users preference, fall back to system pref.
|
||||
$defaultCurrencyCode = Preferences::getForUser($account->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data;
|
||||
$defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first();
|
||||
$accountCurrency = intval($account->getMeta('currency_id'));
|
||||
$openingBalance = $account->getOpeningBalance();
|
||||
$openingBalanceCurrency = intval($openingBalance->transaction_currency_id);
|
||||
$accounts->each(
|
||||
function (Account $account) {
|
||||
// get users preference, fall back to system pref.
|
||||
$defaultCurrencyCode = Preferences::getForUser($account->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data;
|
||||
$defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first();
|
||||
$accountCurrency = intval($account->getMeta('currency_id'));
|
||||
$openingBalance = $account->getOpeningBalance();
|
||||
$obCurrency = intval($openingBalance->transaction_currency_id);
|
||||
|
||||
// both 0? set to default currency:
|
||||
if ($accountCurrency === 0 && $openingBalanceCurrency === 0) {
|
||||
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]);
|
||||
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
|
||||
continue;
|
||||
}
|
||||
// both 0? set to default currency:
|
||||
if ($accountCurrency === 0 && $obCurrency === 0) {
|
||||
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]);
|
||||
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
|
||||
|
||||
// opening balance 0, account not zero? just continue:
|
||||
if ($accountCurrency > 0 && $openingBalanceCurrency === 0) {
|
||||
continue;
|
||||
}
|
||||
// account is set to 0, opening balance is not?
|
||||
if ($accountCurrency === 0 && $openingBalanceCurrency > 0) {
|
||||
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $openingBalanceCurrency]);
|
||||
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// both are equal, just continue:
|
||||
if ($accountCurrency === $openingBalanceCurrency) {
|
||||
continue;
|
||||
// account is set to 0, opening balance is not?
|
||||
if ($accountCurrency === 0 && $obCurrency > 0) {
|
||||
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $obCurrency]);
|
||||
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// do not match and opening balance id is not null.
|
||||
if ($accountCurrency !== $obCurrency && $openingBalance->id > 0) {
|
||||
// update opening balance:
|
||||
$openingBalance->transaction_currency_id = $accountCurrency;
|
||||
$openingBalance->save();
|
||||
$this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// do not match:
|
||||
if ($accountCurrency !== $openingBalanceCurrency) {
|
||||
// update opening balance:
|
||||
$openingBalance->transaction_currency_id = $accountCurrency;
|
||||
$openingBalance->save();
|
||||
$this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name));
|
||||
continue;
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for
|
||||
* the accounts they are linked to.
|
||||
*
|
||||
* Both source and destination must match the respective currency preference of the related asset account.
|
||||
* So FF3 must verify all transactions.
|
||||
*/
|
||||
public function updateOtherCurrencies(): void
|
||||
{
|
||||
/** @var CurrencyRepositoryInterface $repository */
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$set = TransactionJournal
|
||||
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
|
||||
->whereIn('transaction_types.type', [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE])
|
||||
->get(['transaction_journals.*']);
|
||||
|
||||
$set->each(
|
||||
function (TransactionJournal $journal) use ($repository) {
|
||||
// get the transaction with the asset account in it:
|
||||
/** @var Transaction $transaction */
|
||||
$transaction = $journal->transactions()
|
||||
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
|
||||
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
|
||||
->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->first(['transactions.*']);
|
||||
/** @var Account $account */
|
||||
$account = $transaction->account;
|
||||
$currency = $repository->find(intval($account->getMeta('currency_id')));
|
||||
$transactions = $journal->transactions()->get();
|
||||
$transactions->each(
|
||||
function (Transaction $transaction) use ($currency) {
|
||||
if (is_null($transaction->transaction_currency_id)) {
|
||||
$transaction->transaction_currency_id = $currency->id;
|
||||
$transaction->save();
|
||||
}
|
||||
|
||||
// when mismatch in transaction:
|
||||
if ($transaction->transaction_currency_id !== $currency->id) {
|
||||
$transaction->foreign_currency_id = $transaction->transaction_currency_id;
|
||||
$transaction->foreign_amount = $transaction->amount;
|
||||
$transaction->transaction_currency_id = $currency->id;
|
||||
$transaction->save();
|
||||
}
|
||||
}
|
||||
);
|
||||
// also update the journal, of course:
|
||||
$journal->transaction_currency_id = $currency->id;
|
||||
$journal->save();
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
|
||||
* For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
|
||||
* like it or not. Previous routines MUST have set the currency setting for both accounts for this to work.
|
||||
*
|
||||
* A transfer always has the
|
||||
*
|
||||
* Both source and destination must match the respective currency preference. So FF3 must verify ALL
|
||||
* transactions.
|
||||
*/
|
||||
public function updateTransferCurrencies()
|
||||
{
|
||||
$set = TransactionJournal
|
||||
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
|
||||
->where('transaction_types.type', TransactionType::TRANSFER)
|
||||
->get(['transaction_journals.*']);
|
||||
|
||||
$set->each(
|
||||
function (TransactionJournal $transfer) {
|
||||
// select all "source" transactions:
|
||||
/** @var Collection $transactions */
|
||||
$transactions = $transfer->transactions()->where('amount', '<', 0)->get();
|
||||
$transactions->each(
|
||||
function (Transaction $transaction) {
|
||||
$this->updateTransactionCurrency($transaction);
|
||||
$this->updateJournalCurrency($transaction);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method makes sure that the transaction journal uses the currency given in the transaction.
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
*/
|
||||
private function updateJournalCurrency(Transaction $transaction): void
|
||||
{
|
||||
/** @var CurrencyRepositoryInterface $repository */
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$currency = $repository->find(intval($transaction->account->getMeta('currency_id')));
|
||||
$journal = $transaction->transactionJournal;
|
||||
|
||||
if (!(intval($currency->id) === intval($journal->transaction_currency_id))) {
|
||||
$this->line(
|
||||
sprintf(
|
||||
'Transfer #%d ("%s") has been updated to use %s instead of %s.', $journal->id, $journal->description, $currency->code,
|
||||
$journal->transactionCurrency->code
|
||||
)
|
||||
);
|
||||
$journal->transaction_currency_id = $currency->id;
|
||||
$journal->save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,7 +304,7 @@ class UpgradeDatabase extends Command
|
||||
*
|
||||
* @param int $journalId
|
||||
*/
|
||||
private function updateJournal(int $journalId)
|
||||
private function updateJournalidentifiers(int $journalId): void
|
||||
{
|
||||
$identifier = 0;
|
||||
$processed = [];
|
||||
@@ -292,121 +332,128 @@ class UpgradeDatabase extends Command
|
||||
if (!is_null($opposing)) {
|
||||
// give both a new identifier:
|
||||
$transaction->identifier = $identifier;
|
||||
$opposing->identifier = $identifier;
|
||||
$transaction->save();
|
||||
$opposing->identifier = $identifier;
|
||||
$opposing->save();
|
||||
$processed[] = $transaction->id;
|
||||
$processed[] = $opposing->id;
|
||||
}
|
||||
$identifier++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that withdrawals, deposits and transfers have
|
||||
* a currency setting matching their respective accounts
|
||||
*/
|
||||
private function updateJournalCurrencies()
|
||||
{
|
||||
$types = [
|
||||
TransactionType::WITHDRAWAL => '<',
|
||||
TransactionType::DEPOSIT => '>',
|
||||
];
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$notification = '%s #%d uses %s but should use %s. It has been updated. Please verify this in Firefly III.';
|
||||
$transfer = 'Transfer #%d has been updated to use the correct currencies. Please verify this in Firefly III.';
|
||||
$driver = DB::connection()->getDriverName();
|
||||
$pgsql = ['pgsql', 'postgresql'];
|
||||
|
||||
foreach ($types as $type => $operator) {
|
||||
$query = TransactionJournal
|
||||
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')->leftJoin(
|
||||
'transactions', function (JoinClause $join) use ($operator) {
|
||||
$join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', $operator, '0');
|
||||
}
|
||||
)
|
||||
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
|
||||
->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id')
|
||||
->where('transaction_types.type', $type)
|
||||
->where('account_meta.name', 'currency_id');
|
||||
if (in_array($driver, $pgsql)) {
|
||||
$query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('cast(account_meta.data as int)'));
|
||||
}
|
||||
if (!in_array($driver, $pgsql)) {
|
||||
$query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data'));
|
||||
}
|
||||
|
||||
$set = $query->get(['transaction_journals.*', 'account_meta.data as expected_currency_id', 'transactions.amount as transaction_amount']);
|
||||
/** @var TransactionJournal $journal */
|
||||
foreach ($set as $journal) {
|
||||
$expectedCurrency = $repository->find(intval($journal->expected_currency_id));
|
||||
$line = sprintf($notification, $type, $journal->id, $journal->transactionCurrency->code, $expectedCurrency->code);
|
||||
|
||||
$journal->setMeta('foreign_amount', $journal->transaction_amount);
|
||||
$journal->setMeta('foreign_currency_id', $journal->transaction_currency_id);
|
||||
$journal->transaction_currency_id = $expectedCurrency->id;
|
||||
$journal->save();
|
||||
$this->line($line);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* For transfers it's slightly different. Both source and destination
|
||||
* must match the respective currency preference. So we must verify ALL
|
||||
* transactions.
|
||||
*/
|
||||
$set = TransactionJournal
|
||||
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
|
||||
->where('transaction_types.type', TransactionType::TRANSFER)
|
||||
->get(['transaction_journals.*']);
|
||||
/** @var TransactionJournal $journal */
|
||||
foreach ($set as $journal) {
|
||||
$updated = false;
|
||||
/** @var Transaction $sourceTransaction */
|
||||
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
|
||||
$sourceCurrency = $repository->find(intval($sourceTransaction->account->getMeta('currency_id')));
|
||||
|
||||
if ($sourceCurrency->id !== $journal->transaction_currency_id) {
|
||||
$updated = true;
|
||||
$journal->transaction_currency_id = $sourceCurrency->id;
|
||||
$journal->save();
|
||||
}
|
||||
|
||||
// destination
|
||||
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
|
||||
$destinationCurrency = $repository->find(intval($destinationTransaction->account->getMeta('currency_id')));
|
||||
|
||||
if ($destinationCurrency->id !== $journal->transaction_currency_id) {
|
||||
$updated = true;
|
||||
$journal->deleteMeta('foreign_amount');
|
||||
$journal->deleteMeta('foreign_currency_id');
|
||||
$journal->setMeta('foreign_amount', $destinationTransaction->amount);
|
||||
$journal->setMeta('foreign_currency_id', $destinationCurrency->id);
|
||||
}
|
||||
if ($updated) {
|
||||
$line = sprintf($transfer, $journal->id);
|
||||
$this->line($line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method makes sure that the tranaction uses the same currency as the source account does.
|
||||
* If not, the currency is updated to include a reference to its original currency as the "foreign" currency.
|
||||
*
|
||||
* The transaction that is sent to this function MUST be the source transaction (amount negative).
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
*/
|
||||
private function verifyCurrencyInfo()
|
||||
private function updateTransactionCurrency(Transaction $transaction): void
|
||||
{
|
||||
$count = 0;
|
||||
$transactions = Transaction::get();
|
||||
/** @var Transaction $transaction */
|
||||
foreach ($transactions as $transaction) {
|
||||
$currencyId = intval($transaction->transaction_currency_id);
|
||||
$foreignId = intval($transaction->foreign_currency_id);
|
||||
if ($currencyId === $foreignId) {
|
||||
$transaction->foreign_currency_id = null;
|
||||
$transaction->foreign_amount = null;
|
||||
$transaction->save();
|
||||
$count++;
|
||||
}
|
||||
/** @var CurrencyRepositoryInterface $repository */
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$currency = $repository->find(intval($transaction->account->getMeta('currency_id')));
|
||||
|
||||
// has no currency ID? Must have, so fill in using account preference:
|
||||
if (is_null($transaction->transaction_currency_id)) {
|
||||
$transaction->transaction_currency_id = $currency->id;
|
||||
Log::debug(sprintf('Transaction #%d has no currency setting, now set to %s', $transaction->id, $currency->code));
|
||||
$transaction->save();
|
||||
}
|
||||
$this->line(sprintf('Updated currency information for %d transactions', $count));
|
||||
|
||||
// does not match the source account (see above)? Can be fixed
|
||||
// when mismatch in transaction and NO foreign amount is set:
|
||||
if ($transaction->transaction_currency_id !== $currency->id && is_null($transaction->foreign_amount)) {
|
||||
Log::debug(
|
||||
sprintf(
|
||||
'Transaction #%d has a currency setting (#%d) that should be #%d. Amount remains %s, currency is changed.', $transaction->id,
|
||||
$transaction->transaction_currency_id, $currency->id, $transaction->amount
|
||||
)
|
||||
);
|
||||
$transaction->transaction_currency_id = $currency->id;
|
||||
$transaction->save();
|
||||
}
|
||||
|
||||
// grab opposing transaction:
|
||||
/** @var TransactionJournal $journal */
|
||||
$journal = $transaction->transactionJournal;
|
||||
/** @var Transaction $opposing */
|
||||
$opposing = $journal->transactions()->where('amount', '>', 0)->where('identifier', $transaction->identifier)->first();
|
||||
$opposingCurrency = $repository->find(intval($opposing->account->getMeta('currency_id')));
|
||||
|
||||
if (is_null($opposingCurrency->id)) {
|
||||
Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $opposing->account->id, $opposing->account->name));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if the destination account currency is the same, both foreign_amount and foreign_currency_id must be NULL for both transactions:
|
||||
if ($opposingCurrency->id === $currency->id) {
|
||||
// update both transactions to match:
|
||||
$transaction->foreign_amount = null;
|
||||
$transaction->foreign_currency_id = null;
|
||||
$opposing->foreign_amount = null;
|
||||
$opposing->foreign_currency_id = null;
|
||||
$opposing->transaction_currency_id = $currency->id;
|
||||
$transaction->save();
|
||||
$opposing->save();
|
||||
Log::debug(sprintf('Cleaned up transaction #%d and #%d', $transaction->id, $opposing->id));
|
||||
|
||||
return;
|
||||
}
|
||||
// if destination account currency is different, both transactions must have this currency as foreign currency id.
|
||||
if ($opposingCurrency->id !== $currency->id) {
|
||||
$transaction->foreign_currency_id = $opposingCurrency->id;
|
||||
$opposing->foreign_currency_id = $opposingCurrency->id;
|
||||
$transaction->save();
|
||||
$opposing->save();
|
||||
Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $transaction->id, $opposing->id));
|
||||
}
|
||||
|
||||
// if foreign amount of one is null and the other is not, use this to restore:
|
||||
if (is_null($transaction->foreign_amount) && !is_null($opposing->foreign_amount)) {
|
||||
$transaction->foreign_amount = bcmul(strval($opposing->foreign_amount), '-1');
|
||||
$transaction->save();
|
||||
Log::debug(sprintf('Restored foreign amount of transaction (1) #%d to %s', $transaction->id, $transaction->foreign_amount));
|
||||
}
|
||||
|
||||
// if foreign amount of one is null and the other is not, use this to restore (other way around)
|
||||
if (is_null($opposing->foreign_amount) && !is_null($transaction->foreign_amount)) {
|
||||
$opposing->foreign_amount = bcmul(strval($transaction->foreign_amount), '-1');
|
||||
$opposing->save();
|
||||
Log::debug(sprintf('Restored foreign amount of transaction (2) #%d to %s', $opposing->id, $opposing->foreign_amount));
|
||||
}
|
||||
|
||||
// when both are zero, try to grab it from journal:
|
||||
if (is_null($opposing->foreign_amount) && is_null($transaction->foreign_amount)) {
|
||||
|
||||
$foreignAmount = $journal->getMeta('foreign_amount');
|
||||
if (is_null($foreignAmount)) {
|
||||
Log::debug(sprintf('Journal #%d has missing foreign currency data, forced to do 1:1 conversion :(.', $transaction->transaction_journal_id));
|
||||
$transaction->foreign_amount = bcmul(strval($transaction->amount), '-1');
|
||||
$opposing->foreign_amount = bcmul(strval($opposing->amount), '-1');
|
||||
$transaction->save();
|
||||
$opposing->save();
|
||||
|
||||
return;
|
||||
}
|
||||
$foreignPositive = app('steam')->positive(strval($foreignAmount));
|
||||
Log::debug(
|
||||
sprintf(
|
||||
'Journal #%d has missing foreign currency info, try to restore from meta-data ("%s").', $transaction->transaction_journal_id, $foreignAmount
|
||||
)
|
||||
);
|
||||
$transaction->foreign_amount = bcmul($foreignPositive, '-1');
|
||||
$opposing->foreign_amount = $foreignPositive;
|
||||
$transaction->save();
|
||||
$opposing->save();
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ class UpgradeFireflyInstructions extends Command
|
||||
public function handle()
|
||||
{
|
||||
|
||||
if ($this->argument('task') == 'update') {
|
||||
if ($this->argument('task') === 'update') {
|
||||
$this->updateInstructions();
|
||||
}
|
||||
if ($this->argument('task') == 'install') {
|
||||
if ($this->argument('task') === 'install') {
|
||||
$this->installInstructions();
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,9 @@ class UpgradeFireflyInstructions extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render instructions.
|
||||
*/
|
||||
private function installInstructions()
|
||||
{
|
||||
/** @var string $version */
|
||||
@@ -102,7 +105,7 @@ class UpgradeFireflyInstructions extends Command
|
||||
$this->boxed('');
|
||||
if (is_null($text)) {
|
||||
|
||||
$this->boxed(sprintf('Thank you for installin Firefly III, v%s!', $version));
|
||||
$this->boxed(sprintf('Thank you for installing Firefly III, v%s!', $version));
|
||||
$this->boxedInfo('There are no extra installation instructions.');
|
||||
$this->boxed('Firefly III should be ready for use.');
|
||||
$this->boxed('');
|
||||
@@ -131,6 +134,9 @@ class UpgradeFireflyInstructions extends Command
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render upgrade instructions.
|
||||
*/
|
||||
private function updateInstructions()
|
||||
{
|
||||
/** @var string $version */
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* UseEncryption.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* UseEncryption.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class UseEncryption
|
||||
*
|
||||
* @package FireflyIII\Console\Commands
|
||||
*/
|
||||
class UseEncryption extends Command
|
||||
{
|
||||
/**
|
||||
@@ -34,7 +58,6 @@ class UseEncryption extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
$this->handleObjects('Account', 'name', 'encrypted');
|
||||
$this->handleObjects('Bill', 'name', 'name_encrypted');
|
||||
$this->handleObjects('Bill', 'match', 'match_encrypted');
|
||||
@@ -45,6 +68,8 @@ class UseEncryption extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Run each object and encrypt them (or not).
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $field
|
||||
* @param string $indicator
|
||||
|
||||
@@ -17,6 +17,7 @@ use Crypt;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\PiggyBankEvent;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
@@ -94,6 +95,40 @@ class VerifyDatabase extends Command
|
||||
// report on journals with the wrong types of accounts.
|
||||
$this->reportIncorrectJournals();
|
||||
|
||||
// report (and fix) piggy banks
|
||||
$this->repairPiggyBanks();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure there are only transfers linked to piggy bank events.
|
||||
*/
|
||||
private function repairPiggyBanks(): void
|
||||
{
|
||||
$set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
|
||||
$set->each(
|
||||
function (PiggyBankEvent $event) {
|
||||
if (is_null($event->transaction_journal_id)) {
|
||||
return true;
|
||||
}
|
||||
/** @var TransactionJournal $journal */
|
||||
$journal = $event->transactionJournal()->first();
|
||||
if (is_null($journal)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$type = $journal->transactionType->type;
|
||||
if ($type !== TransactionType::TRANSFER) {
|
||||
$event->transaction_journal_id = null;
|
||||
$event->save();
|
||||
$this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,6 +204,9 @@ class VerifyDatabase extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report on journals with bad account types linked to them.
|
||||
*/
|
||||
private function reportIncorrectJournals()
|
||||
{
|
||||
$configuration = [
|
||||
@@ -235,7 +273,7 @@ class VerifyDatabase extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Report on journals without transactions.
|
||||
*/
|
||||
private function reportNoTransactions()
|
||||
{
|
||||
@@ -253,13 +291,15 @@ class VerifyDatabase extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Report on things with no linked journals.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
private function reportObject(string $name)
|
||||
{
|
||||
$plural = str_plural($name);
|
||||
$class = sprintf('FireflyIII\Models\%s', ucfirst($name));
|
||||
$field = $name == 'tag' ? 'tag' : 'name';
|
||||
$field = $name === 'tag' ? 'tag' : 'name';
|
||||
$set = $class::leftJoin($name . '_transaction_journal', $plural . '.id', '=', $name . '_transaction_journal.' . $name . '_id')
|
||||
->leftJoin('users', $plural . '.user_id', '=', 'users.id')
|
||||
->distinct()
|
||||
@@ -324,7 +364,7 @@ class VerifyDatabase extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Report on transfers that have budgets.
|
||||
*/
|
||||
private function reportTransfersBudgets()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Console;
|
||||
|
||||
use FireflyIII\Console\Commands\CreateImport;
|
||||
use FireflyIII\Console\Commands\DecryptAttachment;
|
||||
use FireflyIII\Console\Commands\EncryptFile;
|
||||
use FireflyIII\Console\Commands\Import;
|
||||
use FireflyIII\Console\Commands\ScanAttachments;
|
||||
@@ -63,6 +64,7 @@ class Kernel extends ConsoleKernel
|
||||
ScanAttachments::class,
|
||||
UpgradeDatabase::class,
|
||||
UseEncryption::class,
|
||||
DecryptAttachment::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace FireflyIII\Export\Collector;
|
||||
|
||||
use Crypt;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Log;
|
||||
use Storage;
|
||||
|
||||
@@ -50,14 +51,6 @@ class UploadCollector extends BasicCollector implements CollectorInterface
|
||||
public function run(): bool
|
||||
{
|
||||
Log::debug('Going to collect attachments', ['key' => $this->job->key]);
|
||||
|
||||
// file names associated with the old import routine.
|
||||
$this->vintageFormat = sprintf('csv-upload-%d-', $this->job->user->id);
|
||||
|
||||
// collect old upload files (names beginning with "csv-upload".
|
||||
$this->collectVintageUploads();
|
||||
|
||||
// then collect current upload files:
|
||||
$this->collectModernUploads();
|
||||
|
||||
return true;
|
||||
@@ -70,7 +63,8 @@ class UploadCollector extends BasicCollector implements CollectorInterface
|
||||
*/
|
||||
private function collectModernUploads(): bool
|
||||
{
|
||||
$set = $this->job->user->importJobs()->where('status', 'import_complete')->get(['import_jobs.*']);
|
||||
$set = $this->job->user->importJobs()->whereIn('status', ['import_complete', 'finished'])->get(['import_jobs.*']);
|
||||
Log::debug(sprintf('Found %d import jobs', $set->count()));
|
||||
$keys = [];
|
||||
if ($set->count() > 0) {
|
||||
$keys = $set->pluck('key')->toArray();
|
||||
@@ -83,59 +77,6 @@ class UploadCollector extends BasicCollector implements CollectorInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method collects all the uploads that are uploaded using the "old" importer. So from before the summer of 2016.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function collectVintageUploads(): bool
|
||||
{
|
||||
// grab upload directory.
|
||||
$files = $this->uploadDisk->files();
|
||||
|
||||
foreach ($files as $entry) {
|
||||
$this->processVintageUpload($entry);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method tells you when the vintage upload file was actually uploaded.
|
||||
*
|
||||
* @param string $entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getVintageUploadDate(string $entry): string
|
||||
{
|
||||
// this is an original upload.
|
||||
$parts = explode('-', str_replace(['.csv.encrypted', $this->vintageFormat], '', $entry));
|
||||
$originalUpload = intval($parts[1]);
|
||||
$date = date('Y-m-d \a\t H-i-s', $originalUpload);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells you if a file name is a vintage upload.
|
||||
*
|
||||
* @param string $entry
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isVintageImport(string $entry): bool
|
||||
{
|
||||
$len = strlen($this->vintageFormat);
|
||||
// file is part of the old import routine:
|
||||
if (substr($entry, 0, $len) === $this->vintageFormat) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
@@ -153,7 +94,7 @@ class UploadCollector extends BasicCollector implements CollectorInterface
|
||||
$content = '';
|
||||
try {
|
||||
$content = Crypt::decrypt($this->uploadDisk->get(sprintf('%s.upload', $key)));
|
||||
} catch (DecryptException $e) {
|
||||
} catch (FileNotFoundException | DecryptException $e) {
|
||||
Log::error(sprintf('Could not decrypt old import file "%s". Skipped because: %s', $key, $e->getMessage()));
|
||||
}
|
||||
|
||||
@@ -168,47 +109,4 @@ class UploadCollector extends BasicCollector implements CollectorInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the file is a vintage upload, process it.
|
||||
*
|
||||
* @param string $entry
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function processVintageUpload(string $entry): bool
|
||||
{
|
||||
if ($this->isVintageImport($entry)) {
|
||||
$this->saveVintageImportFile($entry);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This will store the content of the old vintage upload somewhere.
|
||||
*
|
||||
* @param string $entry
|
||||
*/
|
||||
private function saveVintageImportFile(string $entry)
|
||||
{
|
||||
$content = '';
|
||||
try {
|
||||
$content = Crypt::decrypt($this->uploadDisk->get($entry));
|
||||
} catch (DecryptException $e) {
|
||||
Log::error('Could not decrypt old CSV import file ' . $entry . '. Skipped because ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if (strlen($content) > 0) {
|
||||
// add to export disk.
|
||||
$date = $this->getVintageUploadDate($entry);
|
||||
$file = $this->job->key . '-Old import dated ' . $date . '.csv';
|
||||
$this->exportDisk->put($file, $content);
|
||||
$this->getEntries()->push($file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Export\Entry;
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use Steam;
|
||||
|
||||
/**
|
||||
@@ -37,24 +38,43 @@ final class Entry
|
||||
{
|
||||
// @formatter:off
|
||||
public $journal_id;
|
||||
public $transaction_id = 0;
|
||||
|
||||
public $date;
|
||||
public $description;
|
||||
|
||||
public $currency_code;
|
||||
public $amount;
|
||||
public $foreign_currency_code = '';
|
||||
public $foreign_amount = '0';
|
||||
|
||||
public $transaction_type;
|
||||
|
||||
public $source_account_id;
|
||||
public $source_account_name;
|
||||
public $asset_account_id;
|
||||
public $asset_account_name;
|
||||
public $asset_account_iban;
|
||||
public $asset_account_bic;
|
||||
public $asset_account_number;
|
||||
public $asset_currency_code;
|
||||
|
||||
public $destination_account_id;
|
||||
public $destination_account_name;
|
||||
public $opposing_account_id;
|
||||
public $opposing_account_name;
|
||||
public $opposing_account_iban;
|
||||
public $opposing_account_bic;
|
||||
public $opposing_account_number;
|
||||
public $opposing_currency_code;
|
||||
|
||||
public $budget_id;
|
||||
public $budget_name;
|
||||
|
||||
public $category_id;
|
||||
public $category_name;
|
||||
|
||||
public $bill_id;
|
||||
public $bill_name;
|
||||
|
||||
public $notes;
|
||||
public $tags;
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
@@ -71,29 +91,99 @@ final class Entry
|
||||
*/
|
||||
public static function fromObject($object): Entry
|
||||
{
|
||||
$entry = new self;
|
||||
$entry->journal_id = $object->transaction_journal_id;
|
||||
$entry->description = Steam::decrypt(intval($object->journal_encrypted), $object->journal_description);
|
||||
$entry->amount = $object->amount;
|
||||
$entry->date = $object->date;
|
||||
$entry->transaction_type = $object->transaction_type;
|
||||
$entry->currency_code = $object->transaction_currency_code;
|
||||
$entry->source_account_id = $object->account_id;
|
||||
$entry->source_account_name = Steam::decrypt(intval($object->account_name_encrypted), $object->account_name);
|
||||
$entry->destination_account_id = $object->opposing_account_id;
|
||||
$entry->destination_account_name = Steam::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name);
|
||||
$entry->category_id = $object->category_id ?? '';
|
||||
$entry->category_name = $object->category_name ?? '';
|
||||
$entry->budget_id = $object->budget_id ?? '';
|
||||
$entry->budget_name = $object->budget_name ?? '';
|
||||
$entry = new self;
|
||||
$entry->journal_id = $object->transaction_journal_id;
|
||||
$entry->description = Steam::decrypt(intval($object->journal_encrypted), $object->journal_description);
|
||||
$entry->amount = $object->amount;
|
||||
$entry->date = $object->date;
|
||||
$entry->transaction_type = $object->transaction_type;
|
||||
$entry->currency_code = $object->transaction_currency_code;
|
||||
$entry->asset_account_id = $object->account_id;
|
||||
$entry->asset_account_name = Steam::decrypt(intval($object->account_name_encrypted), $object->account_name);
|
||||
$entry->opposing_account_id = $object->opposing_account_id;
|
||||
$entry->opposing_account_name = Steam::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name);
|
||||
$entry->category_id = $object->category_id ?? '';
|
||||
$entry->category_name = $object->category_name ?? '';
|
||||
$entry->budget_id = $object->budget_id ?? '';
|
||||
$entry->budget_name = $object->budget_name ?? '';
|
||||
|
||||
// update description when transaction description is different:
|
||||
if (!is_null($object->description) && $object->description != $entry->description) {
|
||||
if (!is_null($object->description) && $object->description !== $entry->description) {
|
||||
$entry->description = $entry->description . ' (' . $object->description . ')';
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given transaction (as collected by the collector) into an export entry.
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
*
|
||||
* @return Entry
|
||||
*/
|
||||
public static function fromTransaction(Transaction $transaction): Entry
|
||||
{
|
||||
$entry = new self;
|
||||
$entry->journal_id = $transaction->journal_id;
|
||||
$entry->transaction_id = $transaction->id;
|
||||
$entry->date = $transaction->date->format('Ymd');
|
||||
$entry->description = $transaction->description;
|
||||
if (strlen(strval($transaction->transaction_description)) > 0) {
|
||||
$entry->description = $transaction->transaction_description . '(' . $transaction->description . ')';
|
||||
}
|
||||
$entry->currency_code = $transaction->transactionCurrency->code;
|
||||
$entry->amount = round($transaction->transaction_amount, $transaction->transactionCurrency->decimal_places);
|
||||
|
||||
$entry->foreign_currency_code = is_null($transaction->foreign_currency_id) ? null : $transaction->foreignCurrency->code;
|
||||
$entry->foreign_amount = is_null($transaction->foreign_currency_id)
|
||||
? null
|
||||
: strval(
|
||||
round(
|
||||
$transaction->transaction_foreign_amount, $transaction->foreignCurrency->decimal_places
|
||||
)
|
||||
);
|
||||
|
||||
$entry->transaction_type = $transaction->transaction_type_type;
|
||||
$entry->asset_account_id = $transaction->account_id;
|
||||
$entry->asset_account_name = app('steam')->tryDecrypt($transaction->account_name);
|
||||
$entry->asset_account_iban = $transaction->account_iban;
|
||||
$entry->asset_account_number = $transaction->account_number;
|
||||
$entry->asset_account_bic = $transaction->account_bic;
|
||||
$entry->asset_currency_code = $transaction->account_currency_code;
|
||||
|
||||
$entry->opposing_account_id = $transaction->opposing_account_id;
|
||||
$entry->opposing_account_name = app('steam')->tryDecrypt($transaction->opposing_account_name);
|
||||
$entry->opposing_account_iban = $transaction->opposing_account_iban;
|
||||
$entry->opposing_account_number = $transaction->opposing_account_number;
|
||||
$entry->opposing_account_bic = $transaction->opposing_account_bic;
|
||||
$entry->opposing_currency_code = $transaction->opposing_currency_code;
|
||||
|
||||
/** budget */
|
||||
$entry->budget_id = $transaction->transaction_budget_id;
|
||||
$entry->budget_name = app('steam')->tryDecrypt($transaction->transaction_budget_name);
|
||||
if (is_null($transaction->transaction_budget_id)) {
|
||||
$entry->budget_id = $transaction->transaction_journal_budget_id;
|
||||
$entry->budget_name = app('steam')->tryDecrypt($transaction->transaction_journal_budget_name);
|
||||
}
|
||||
|
||||
/** category */
|
||||
$entry->category_id = $transaction->transaction_category_id;
|
||||
$entry->category_name = app('steam')->tryDecrypt($transaction->transaction_category_name);
|
||||
if (is_null($transaction->transaction_category_id)) {
|
||||
$entry->category_id = $transaction->transaction_journal_category_id;
|
||||
$entry->category_name = app('steam')->tryDecrypt($transaction->transaction_journal_category_name);
|
||||
}
|
||||
|
||||
/** budget */
|
||||
$entry->bill_id = $transaction->bill_id;
|
||||
$entry->bill_name = app('steam')->tryDecrypt($transaction->bill_name);
|
||||
|
||||
$entry->tags = $transaction->tags;
|
||||
$entry->notes = $transaction->notes;
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
339
app/Export/ExpandedProcessor.php
Normal file
339
app/Export/ExpandedProcessor.php
Normal file
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
/**
|
||||
* ExpandedProcessor.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Export;
|
||||
|
||||
|
||||
use Crypt;
|
||||
use DB;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Export\Collector\AttachmentCollector;
|
||||
use FireflyIII\Export\Collector\UploadCollector;
|
||||
use FireflyIII\Export\Entry\Entry;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\InternalTransferFilter;
|
||||
use FireflyIII\Models\AccountMeta;
|
||||
use FireflyIII\Models\ExportJob;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournalMeta;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
use Storage;
|
||||
use ZipArchive;
|
||||
|
||||
/**
|
||||
* Class ExpandedProcessor
|
||||
*
|
||||
* @package FireflyIII\Export
|
||||
*/
|
||||
class ExpandedProcessor implements ProcessorInterface
|
||||
{
|
||||
|
||||
/** @var Collection */
|
||||
public $accounts;
|
||||
/** @var string */
|
||||
public $exportFormat;
|
||||
/** @var bool */
|
||||
public $includeAttachments;
|
||||
/** @var bool */
|
||||
public $includeOldUploads;
|
||||
/** @var ExportJob */
|
||||
public $job;
|
||||
/** @var array */
|
||||
public $settings;
|
||||
/** @var Collection */
|
||||
private $exportEntries;
|
||||
/** @var Collection */
|
||||
private $files;
|
||||
/** @var Collection */
|
||||
private $journals;
|
||||
|
||||
/**
|
||||
* Processor constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->journals = new Collection;
|
||||
$this->exportEntries = new Collection;
|
||||
$this->files = new Collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function collectAttachments(): bool
|
||||
{
|
||||
/** @var AttachmentCollector $attachmentCollector */
|
||||
$attachmentCollector = app(AttachmentCollector::class);
|
||||
$attachmentCollector->setJob($this->job);
|
||||
$attachmentCollector->setDates($this->settings['startDate'], $this->settings['endDate']);
|
||||
$attachmentCollector->run();
|
||||
$this->files = $this->files->merge($attachmentCollector->getEntries());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function collectJournals(): bool
|
||||
{
|
||||
// use journal collector thing.
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($this->accounts)->setRange($this->settings['startDate'], $this->settings['endDate'])
|
||||
->withOpposingAccount()->withBudgetInformation()->withCategoryInformation()
|
||||
->removeFilter(InternalTransferFilter::class);
|
||||
$transactions = $collector->getJournals();
|
||||
// get some more meta data for each entry:
|
||||
$ids = $transactions->pluck('journal_id')->toArray();
|
||||
$assetIds = $transactions->pluck('account_id')->toArray();
|
||||
$opposingIds = $transactions->pluck('opposing_account_id')->toArray();
|
||||
$notes = $this->getNotes($ids);
|
||||
$tags = $this->getTags($ids);
|
||||
$ibans = $this->getIbans($assetIds) + $this->getIbans($opposingIds);
|
||||
$currencies = $this->getAccountCurrencies($ibans);
|
||||
$transactions->each(
|
||||
function (Transaction $transaction) use ($notes, $tags, $ibans, $currencies) {
|
||||
$journalId = intval($transaction->journal_id);
|
||||
$accountId = intval($transaction->account_id);
|
||||
$opposingId = intval($transaction->opposing_account_id);
|
||||
$currencyId = $ibans[$accountId]['currency_id'] ?? 0;
|
||||
$opposingCurrencyId = $ibans[$opposingId]['currency_id'] ?? 0;
|
||||
$transaction->notes = $notes[$journalId] ?? '';
|
||||
$transaction->tags = join(',', $tags[$journalId] ?? []);
|
||||
$transaction->account_number = $ibans[$accountId]['accountNumber'] ?? '';
|
||||
$transaction->account_bic = $ibans[$accountId]['BIC'] ?? '';
|
||||
$transaction->account_currency_code = $currencies[$currencyId] ?? '';
|
||||
$transaction->opposing_account_number = $ibans[$opposingId]['accountNumber'] ?? '';
|
||||
$transaction->opposing_account_bic = $ibans[$opposingId]['BIC'] ?? '';
|
||||
$transaction->opposing_currency_code = $currencies[$opposingCurrencyId] ?? '';
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
$this->journals = $transactions;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function collectOldUploads(): bool
|
||||
{
|
||||
/** @var UploadCollector $uploadCollector */
|
||||
$uploadCollector = app(UploadCollector::class);
|
||||
$uploadCollector->setJob($this->job);
|
||||
$uploadCollector->run();
|
||||
|
||||
$this->files = $this->files->merge($uploadCollector->getEntries());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function convertJournals(): bool
|
||||
{
|
||||
$this->journals->each(
|
||||
function (Transaction $transaction) {
|
||||
$this->exportEntries->push(Entry::fromTransaction($transaction));
|
||||
}
|
||||
);
|
||||
Log::debug(sprintf('Count %d entries in exportEntries (convertJournals)', $this->exportEntries->count()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function createZipFile(): bool
|
||||
{
|
||||
$zip = new ZipArchive;
|
||||
$file = $this->job->key . '.zip';
|
||||
$fullPath = storage_path('export') . '/' . $file;
|
||||
|
||||
if ($zip->open($fullPath, ZipArchive::CREATE) !== true) {
|
||||
throw new FireflyException('Cannot store zip file.');
|
||||
}
|
||||
// for each file in the collection, add it to the zip file.
|
||||
$disk = Storage::disk('export');
|
||||
foreach ($this->getFiles() as $entry) {
|
||||
// is part of this job?
|
||||
$zipFileName = str_replace($this->job->key . '-', '', $entry);
|
||||
$zip->addFromString($zipFileName, $disk->get($entry));
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
// delete the files:
|
||||
$this->deleteFiles();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function exportJournals(): bool
|
||||
{
|
||||
$exporterClass = config('firefly.export_formats.' . $this->exportFormat);
|
||||
$exporter = app($exporterClass);
|
||||
$exporter->setJob($this->job);
|
||||
$exporter->setEntries($this->exportEntries);
|
||||
$exporter->run();
|
||||
$this->files->push($exporter->getFileName());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getFiles(): Collection
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save export job settings to class.
|
||||
*
|
||||
* @param array $settings
|
||||
*/
|
||||
public function setSettings(array $settings)
|
||||
{
|
||||
// save settings
|
||||
$this->settings = $settings;
|
||||
$this->accounts = $settings['accounts'];
|
||||
$this->exportFormat = $settings['exportFormat'];
|
||||
$this->includeAttachments = $settings['includeAttachments'];
|
||||
$this->includeOldUploads = $settings['includeOldUploads'];
|
||||
$this->job = $settings['job'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function deleteFiles()
|
||||
{
|
||||
$disk = Storage::disk('export');
|
||||
foreach ($this->getFiles() as $file) {
|
||||
$disk->delete($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getAccountCurrencies(array $array): array
|
||||
{
|
||||
/** @var CurrencyRepositoryInterface $repository */
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$return = [];
|
||||
$ids = [];
|
||||
$repository->setUser($this->job->user);
|
||||
foreach ($array as $value) {
|
||||
$ids[] = $value['currency_id'] ?? 0;
|
||||
}
|
||||
$ids = array_unique($ids);
|
||||
$result = $repository->getByIds($ids);
|
||||
|
||||
foreach ($result as $currency) {
|
||||
$return[$currency->id] = $currency->code;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all IBAN / SWIFT / account numbers
|
||||
*
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getIbans(array $array): array
|
||||
{
|
||||
$array = array_unique($array);
|
||||
$return = [];
|
||||
$set = AccountMeta::whereIn('account_id', $array)
|
||||
->leftJoin('accounts', 'accounts.id', 'account_meta.account_id')
|
||||
->where('accounts.user_id', $this->job->user_id)
|
||||
->whereIn('account_meta.name', ['accountNumber', 'BIC', 'currency_id'])
|
||||
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']);
|
||||
/** @var AccountMeta $meta */
|
||||
foreach ($set as $meta) {
|
||||
$id = intval($meta->account_id);
|
||||
$return[$id][$meta->name] = $meta->data;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, if present, for the given journal ID's the notes.
|
||||
*
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getNotes(array $array): array
|
||||
{
|
||||
$array = array_unique($array);
|
||||
$set = TransactionJournalMeta::whereIn('journal_meta.transaction_journal_id', $array)
|
||||
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
|
||||
->where('transaction_journals.user_id', $this->job->user_id)
|
||||
->where('journal_meta.name', 'notes')->get(
|
||||
['journal_meta.transaction_journal_id', 'journal_meta.data', 'journal_meta.id']
|
||||
);
|
||||
$return = [];
|
||||
/** @var TransactionJournalMeta $meta */
|
||||
foreach ($set as $meta) {
|
||||
$id = intval($meta->transaction_journal_id);
|
||||
$return[$id] = $meta->data;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a comma joined list of all the users tags linked to these journals.
|
||||
*
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getTags(array $array): array
|
||||
{
|
||||
$set = DB::table('tag_transaction_journal')
|
||||
->whereIn('tag_transaction_journal.transaction_journal_id', $array)
|
||||
->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id')
|
||||
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'tag_transaction_journal.transaction_journal_id')
|
||||
->where('transaction_journals.user_id', $this->job->user_id)
|
||||
->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag']);
|
||||
$result = [];
|
||||
foreach ($set as $entry) {
|
||||
$id = intval($entry->transaction_journal_id);
|
||||
$result[$id] = isset($result[$id]) ? $result[$id] : [];
|
||||
$result[$id][] = Crypt::decrypt($entry->tag);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,12 @@ class CsvExporter extends BasicExporter implements ExporterInterface
|
||||
|
||||
// get field names for header row:
|
||||
$first = $this->getEntries()->first();
|
||||
$headers = array_keys(get_object_vars($first));
|
||||
$rows[] = $headers;
|
||||
$headers = [];
|
||||
if (!is_null($first)) {
|
||||
$headers = array_keys(get_object_vars($first));
|
||||
}
|
||||
|
||||
$rows[] = $headers;
|
||||
|
||||
/** @var Entry $entry */
|
||||
foreach ($this->getEntries() as $entry) {
|
||||
|
||||
@@ -110,7 +110,14 @@ class ChartJsGenerator implements GeneratorInterface
|
||||
];
|
||||
|
||||
// sort by value, keep keys.
|
||||
// different sort when values are positive and when they're negative.
|
||||
asort($data);
|
||||
$next = next($data);
|
||||
if (!is_bool($next) && bccomp($next, '0') === 1) {
|
||||
// next is positive, sort other way around.
|
||||
arsort($data);
|
||||
}
|
||||
unset($next);
|
||||
|
||||
$index = 0;
|
||||
foreach ($data as $key => $value) {
|
||||
|
||||
@@ -99,7 +99,7 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function saveAttachmentsForModel(Model $model, array $files = null): bool
|
||||
public function saveAttachmentsForModel(Model $model, ?array $files): bool
|
||||
{
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $entry) {
|
||||
|
||||
@@ -55,6 +55,6 @@ interface AttachmentHelperInterface
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function saveAttachmentsForModel(Model $model, array $files = null): bool;
|
||||
public function saveAttachmentsForModel(Model $model, ?array $files): bool;
|
||||
|
||||
}
|
||||
|
||||
@@ -147,13 +147,13 @@ class BalanceLine
|
||||
if ($this->getBudget() instanceof BudgetModel && !is_null($this->getBudget()->id)) {
|
||||
return $this->getBudget()->name;
|
||||
}
|
||||
if ($this->getRole() == self::ROLE_DEFAULTROLE) {
|
||||
if ($this->getRole() === self::ROLE_DEFAULTROLE) {
|
||||
return strval(trans('firefly.no_budget'));
|
||||
}
|
||||
if ($this->getRole() == self::ROLE_TAGROLE) {
|
||||
if ($this->getRole() === self::ROLE_TAGROLE) {
|
||||
return strval(trans('firefly.coveredWithTags'));
|
||||
}
|
||||
if ($this->getRole() == self::ROLE_DIFFROLE) {
|
||||
if ($this->getRole() === self::ROLE_DIFFROLE) {
|
||||
return strval(trans('firefly.leftUnbalanced'));
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class Bill
|
||||
{
|
||||
$set = $this->bills->sortBy(
|
||||
function (BillLine $bill) {
|
||||
$active = intval($bill->getBill()->active) == 0 ? 1 : 0;
|
||||
$active = intval($bill->getBill()->active) === 0 ? 1 : 0;
|
||||
$name = $bill->getBill()->name;
|
||||
|
||||
return $active . $name;
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace FireflyIII\Helpers\Collector;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Crypt;
|
||||
use DB;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Filter\FilterInterface;
|
||||
@@ -31,7 +30,6 @@ use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
@@ -64,33 +62,33 @@ class JournalCollector implements JournalCollectorInterface
|
||||
'transaction_journals.bill_id',
|
||||
'bills.name as bill_name',
|
||||
'bills.name_encrypted as bill_name_encrypted',
|
||||
'transactions.id as id',
|
||||
|
||||
'transactions.id as id',
|
||||
'transactions.description as transaction_description',
|
||||
'transactions.account_id',
|
||||
'transactions.identifier',
|
||||
'transactions.transaction_journal_id',
|
||||
|
||||
'transactions.amount as transaction_amount',
|
||||
|
||||
'transactions.transaction_currency_id as transaction_currency_id',
|
||||
|
||||
'transaction_currencies.code as transaction_currency_code',
|
||||
'transaction_currencies.symbol as transaction_currency_symbol',
|
||||
'transaction_currencies.decimal_places as transaction_currency_dp',
|
||||
|
||||
'transactions.foreign_amount as transaction_foreign_amount',
|
||||
'transactions.foreign_currency_id as foreign_currency_id',
|
||||
|
||||
'foreign_currencies.code as foreign_currency_code',
|
||||
'foreign_currencies.symbol as foreign_currency_symbol',
|
||||
'foreign_currencies.decimal_places as foreign_currency_dp',
|
||||
|
||||
'accounts.name as account_name',
|
||||
'accounts.encrypted as account_encrypted',
|
||||
'accounts.iban as account_iban',
|
||||
'account_types.type as account_type',
|
||||
|
||||
];
|
||||
/** @var bool */
|
||||
private $filterInternalTransfers;
|
||||
/** @var bool */
|
||||
private $filterTransfers = false;
|
||||
/** @var array */
|
||||
private $filters = [InternalTransferFilter::class];
|
||||
@@ -177,12 +175,10 @@ class JournalCollector implements JournalCollectorInterface
|
||||
if (!is_null($transaction->bill_name)) {
|
||||
$transaction->bill_name = Steam::decrypt(intval($transaction->bill_name_encrypted), $transaction->bill_name);
|
||||
}
|
||||
$transaction->opposing_account_name = app('steam')->tryDecrypt($transaction->opposing_account_name);
|
||||
$transaction->account_iban = app('steam')->tryDecrypt($transaction->account_iban);
|
||||
$transaction->opposing_account_iban = app('steam')->tryDecrypt($transaction->opposing_account_iban);
|
||||
|
||||
try {
|
||||
$transaction->opposing_account_name = Crypt::decrypt($transaction->opposing_account_name);
|
||||
} catch (DecryptException $e) {
|
||||
// if this fails its already decrypted.
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
@@ -401,6 +397,10 @@ class JournalCollector implements JournalCollectorInterface
|
||||
*/
|
||||
public function setPage(int $page): JournalCollectorInterface
|
||||
{
|
||||
if ($page < 1) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$this->page = $page;
|
||||
|
||||
if ($page > 0) {
|
||||
@@ -508,7 +508,8 @@ class JournalCollector implements JournalCollectorInterface
|
||||
->where('transaction_journals.user_id', $this->user->id)
|
||||
->orderBy('transaction_journals.date', 'DESC')
|
||||
->orderBy('transaction_journals.order', 'ASC')
|
||||
->orderBy('transaction_journals.id', 'DESC');
|
||||
->orderBy('transaction_journals.id', 'DESC')
|
||||
->orderBy('transaction_journals.description', 'DESC');
|
||||
|
||||
$this->query = $query;
|
||||
|
||||
@@ -618,6 +619,8 @@ class JournalCollector implements JournalCollectorInterface
|
||||
$this->query->leftJoin('budgets as transaction_journal_budgets', 'transaction_journal_budgets.id', '=', 'budget_transaction_journal.budget_id');
|
||||
$this->query->leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id');
|
||||
$this->query->leftJoin('budgets as transaction_budgets', 'transaction_budgets.id', '=', 'budget_transaction.budget_id');
|
||||
$this->query->whereNull('transaction_journal_budgets.deleted_at');
|
||||
$this->query->whereNull('transaction_budgets.deleted_at');
|
||||
|
||||
$this->fields[] = 'budget_transaction_journal.budget_id as transaction_journal_budget_id';
|
||||
$this->fields[] = 'transaction_journal_budgets.encrypted as transaction_journal_budget_encrypted';
|
||||
@@ -674,9 +677,12 @@ class JournalCollector implements JournalCollectorInterface
|
||||
$this->query->leftJoin('account_types as opposing_account_types', 'opposing_accounts.account_type_id', '=', 'opposing_account_types.id');
|
||||
$this->query->whereNull('opposing.deleted_at');
|
||||
|
||||
$this->fields[] = 'opposing.account_id as opposing_account_id';
|
||||
$this->fields[] = 'opposing_accounts.name as opposing_account_name';
|
||||
$this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted';
|
||||
$this->fields[] = 'opposing.id as opposing_id';
|
||||
$this->fields[] = 'opposing.account_id as opposing_account_id';
|
||||
$this->fields[] = 'opposing_accounts.name as opposing_account_name';
|
||||
$this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted';
|
||||
$this->fields[] = 'opposing_accounts.iban as opposing_account_iban';
|
||||
|
||||
$this->fields[] = 'opposing_account_types.type as opposing_account_type';
|
||||
$this->joinedOpposing = true;
|
||||
Log::debug('joinedOpposing is now true!');
|
||||
|
||||
@@ -41,11 +41,13 @@ class TransferFilter implements FilterInterface
|
||||
continue;
|
||||
}
|
||||
// make property string:
|
||||
$journalId = $transaction->transaction_journal_id;
|
||||
$amount = Steam::positive($transaction->transaction_amount);
|
||||
$accountIds = [intval($transaction->account_id), intval($transaction->opposing_account_id)];
|
||||
$journalId = $transaction->transaction_journal_id;
|
||||
$amount = Steam::positive($transaction->transaction_amount);
|
||||
$accountIds = [intval($transaction->account_id), intval($transaction->opposing_account_id)];
|
||||
$transactionIds = [$transaction->id, intval($transaction->opposing_id)];
|
||||
sort($accountIds);
|
||||
$key = $journalId . '-' . join(',', $accountIds) . '-' . $amount;
|
||||
sort($transactionIds);
|
||||
$key = $journalId . '-' . join(',', $transactionIds) . '-' . join(',', $accountIds) . '-' . $amount;
|
||||
if (!isset($count[$key])) {
|
||||
// not yet counted? add to new set and count it:
|
||||
$new->push($transaction);
|
||||
|
||||
@@ -27,6 +27,7 @@ use Route;
|
||||
*/
|
||||
class Help implements HelpInterface
|
||||
{
|
||||
const CACHEKEY = 'help_%s_%s';
|
||||
/** @var string */
|
||||
protected $userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36';
|
||||
|
||||
@@ -38,7 +39,7 @@ class Help implements HelpInterface
|
||||
*/
|
||||
public function getFromCache(string $route, string $language): string
|
||||
{
|
||||
$line = sprintf('help.%s.%s', $route, $language);
|
||||
$line = sprintf(self::CACHEKEY, $route, $language);
|
||||
|
||||
return Cache::get($line);
|
||||
}
|
||||
@@ -64,12 +65,12 @@ class Help implements HelpInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
Log::debug(sprintf('Status code is %d', $result->status_code));
|
||||
|
||||
if ($result->status_code === 200) {
|
||||
$content = trim($result->body);
|
||||
}
|
||||
|
||||
if (strlen($content) > 0) {
|
||||
Log::debug('Content is longer than zero. Expect something.');
|
||||
$converter = new CommonMarkConverter();
|
||||
@@ -98,7 +99,7 @@ class Help implements HelpInterface
|
||||
*/
|
||||
public function inCache(string $route, string $language): bool
|
||||
{
|
||||
$line = sprintf('help.%s.%s', $route, $language);
|
||||
$line = sprintf(self::CACHEKEY, $route, $language);
|
||||
$result = Cache::has($line);
|
||||
if ($result) {
|
||||
Log::debug(sprintf('Cache has this entry: %s', 'help.' . $route . '.' . $language));
|
||||
@@ -120,7 +121,7 @@ class Help implements HelpInterface
|
||||
*/
|
||||
public function putInCache(string $route, string $language, string $content)
|
||||
{
|
||||
$key = sprintf('help.%s.%s', $route, $language);
|
||||
$key = sprintf(self::CACHEKEY, $route, $language);
|
||||
if (strlen($content) > 0) {
|
||||
Log::debug(sprintf('Will store entry in cache: %s', $key));
|
||||
Cache::put($key, $content, 10080); // a week.
|
||||
|
||||
@@ -14,16 +14,12 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Helpers\Report;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use FireflyIII\Helpers\Collection\Balance;
|
||||
use FireflyIII\Helpers\Collection\BalanceEntry;
|
||||
use FireflyIII\Helpers\Collection\BalanceHeader;
|
||||
use FireflyIII\Helpers\Collection\BalanceLine;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
@@ -74,14 +70,9 @@ class BalanceReportHelper implements BalanceReportHelperInterface
|
||||
$line = $this->createBalanceLine($budgetLimit, $accounts);
|
||||
$balance->addBalanceLine($line);
|
||||
}
|
||||
Log::debug('Create rest of the things.');
|
||||
$noBudgetLine = $this->createNoBudgetLine($accounts, $start, $end);
|
||||
$coveredByTagLine = $this->createTagsBalanceLine($accounts, $start, $end);
|
||||
$leftUnbalancedLine = $this->createLeftUnbalancedLine($noBudgetLine, $coveredByTagLine);
|
||||
$noBudgetLine = $this->createNoBudgetLine($accounts, $start, $end);
|
||||
|
||||
$balance->addBalanceLine($noBudgetLine);
|
||||
$balance->addBalanceLine($coveredByTagLine);
|
||||
$balance->addBalanceLine($leftUnbalancedLine);
|
||||
$balance->setBalanceHeader($header);
|
||||
|
||||
Log::debug('Clear unused budgets.');
|
||||
@@ -93,54 +84,6 @@ class BalanceReportHelper implements BalanceReportHelperInterface
|
||||
return $balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method collects all transfers that are part of a "balancing act" tag
|
||||
* and groups the amounts of those transfers by their destination account.
|
||||
*
|
||||
* This is used to indicate which expenses, usually outside of budgets, have been
|
||||
* corrected by transfers from a savings account.
|
||||
*
|
||||
* @param Collection $accounts
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function allCoveredByBalancingActs(Collection $accounts, Carbon $start, Carbon $end): Collection
|
||||
{
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
$set = auth()->user()->tags()
|
||||
->leftJoin('tag_transaction_journal', 'tag_transaction_journal.tag_id', '=', 'tags.id')
|
||||
->leftJoin('transaction_journals', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
|
||||
->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id')
|
||||
->leftJoin(
|
||||
'transactions AS t_source', function (JoinClause $join) {
|
||||
$join->on('transaction_journals.id', '=', 't_source.transaction_journal_id')->where('t_source.amount', '<', 0);
|
||||
}
|
||||
)
|
||||
->leftJoin(
|
||||
'transactions AS t_destination', function (JoinClause $join) {
|
||||
$join->on('transaction_journals.id', '=', 't_destination.transaction_journal_id')->where('t_destination.amount', '>', 0);
|
||||
}
|
||||
)
|
||||
->where('tags.tagMode', 'balancingAct')
|
||||
->where('transaction_types.type', TransactionType::TRANSFER)
|
||||
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
|
||||
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->whereIn('t_source.account_id', $ids)
|
||||
->whereIn('t_destination.account_id', $ids)
|
||||
->groupBy('t_destination.account_id')
|
||||
->get(
|
||||
[
|
||||
't_destination.account_id',
|
||||
DB::raw('SUM(t_destination.amount) AS sum'),
|
||||
]
|
||||
);
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param BudgetLimit $budgetLimit
|
||||
@@ -168,40 +111,6 @@ class BalanceReportHelper implements BalanceReportHelperInterface
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BalanceLine $noBudgetLine
|
||||
* @param BalanceLine $coveredByTagLine
|
||||
*
|
||||
* @return BalanceLine
|
||||
*/
|
||||
private function createLeftUnbalancedLine(BalanceLine $noBudgetLine, BalanceLine $coveredByTagLine): BalanceLine
|
||||
{
|
||||
$line = new BalanceLine;
|
||||
$line->setRole(BalanceLine::ROLE_DIFFROLE);
|
||||
$noBudgetEntries = $noBudgetLine->getBalanceEntries();
|
||||
$tagEntries = $coveredByTagLine->getBalanceEntries();
|
||||
|
||||
foreach ($noBudgetEntries as $entry) {
|
||||
$account = $entry->getAccount();
|
||||
$tagEntry = $tagEntries->filter(
|
||||
function (BalanceEntry $current) use ($account) {
|
||||
return $current->getAccount()->id === $account->id;
|
||||
}
|
||||
);
|
||||
if ($tagEntry->first()) {
|
||||
// found corresponding entry. As we should:
|
||||
$newEntry = new BalanceEntry;
|
||||
$newEntry->setAccount($account);
|
||||
$spent = bcadd($tagEntry->first()->getLeft(), $entry->getSpent());
|
||||
$newEntry->setSpent($spent);
|
||||
$line->addBalanceEntry($newEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return $line;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $accounts
|
||||
@@ -227,41 +136,6 @@ class BalanceReportHelper implements BalanceReportHelperInterface
|
||||
return $empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $accounts
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return BalanceLine
|
||||
*/
|
||||
private function createTagsBalanceLine(Collection $accounts, Carbon $start, Carbon $end): BalanceLine
|
||||
{
|
||||
$tags = new BalanceLine;
|
||||
$tagsLeft = $this->allCoveredByBalancingActs($accounts, $start, $end);
|
||||
|
||||
$tags->setRole(BalanceLine::ROLE_TAGROLE);
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$leftEntry = $tagsLeft->filter(
|
||||
function (Tag $tag) use ($account) {
|
||||
return $tag->account_id == $account->id;
|
||||
}
|
||||
);
|
||||
$left = '0';
|
||||
if (!is_null($leftEntry->first())) {
|
||||
$left = $leftEntry->first()->sum;
|
||||
}
|
||||
|
||||
// balanced by tags
|
||||
$tagEntry = new BalanceEntry;
|
||||
$tagEntry->setAccount($account);
|
||||
$tagEntry->setLeft($left);
|
||||
$tags->addBalanceEntry($tagEntry);
|
||||
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Balance $balance
|
||||
|
||||
@@ -56,7 +56,7 @@ class BudgetReportHelper implements BudgetReportHelperInterface
|
||||
/** @var Budget $budget */
|
||||
foreach ($set as $budget) {
|
||||
$budgetLimits = $this->repository->getBudgetLimits($budget, $start, $end);
|
||||
if ($budgetLimits->count() == 0) { // no budget limit(s) for this budget
|
||||
if ($budgetLimits->count() === 0) { // no budget limit(s) for this budget
|
||||
|
||||
$spent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end);// spent for budget in time range
|
||||
if (bccomp($spent, '0') === -1) {
|
||||
|
||||
@@ -194,7 +194,7 @@ class AccountController extends Controller
|
||||
|
||||
return view(
|
||||
'accounts.edit', compact(
|
||||
'allCurrencies', 'currencySelectList', 'account', 'currency', 'subTitle', 'subTitleIcon', 'openingBalance', 'what', 'roles'
|
||||
'allCurrencies', 'currencySelectList', 'account', 'currency', 'subTitle', 'subTitleIcon', 'what', 'roles', 'preFilled'
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -253,7 +253,7 @@ class AccountController extends Controller
|
||||
$currencyRepos = app(CurrencyRepositoryInterface::class);
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type);
|
||||
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
$chartUri = route('chart.account.single', [$account->id]);
|
||||
$start = null;
|
||||
@@ -274,55 +274,31 @@ class AccountController extends Controller
|
||||
if (strlen($moment) > 0 && $moment !== 'all') {
|
||||
$start = new Carbon($moment);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
$subTitle = trans(
|
||||
'firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
|
||||
'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
$fStart = $start->formatLocalized($this->monthAndDayFormat);
|
||||
$fEnd = $end->formatLocalized($this->monthAndDayFormat);
|
||||
$subTitle = trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]);
|
||||
$chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d')]);
|
||||
$periods = $this->getPeriodOverview($account);
|
||||
}
|
||||
|
||||
// prep for current period
|
||||
// prep for current period view
|
||||
if (strlen($moment) === 0) {
|
||||
$start = clone session('start', Navigation::startOfPeriod(new Carbon, $range));
|
||||
$end = clone session('end', Navigation::endOfPeriod(new Carbon, $range));
|
||||
$subTitle = trans(
|
||||
'firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
|
||||
'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
$fStart = $start->formatLocalized($this->monthAndDayFormat);
|
||||
$fEnd = $end->formatLocalized($this->monthAndDayFormat);
|
||||
$subTitle = trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]);
|
||||
$periods = $this->getPeriodOverview($account);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$loop = 0;
|
||||
// grab journals, but be prepared to jump a period back to get the right ones:
|
||||
Log::info('Now at loop start.');
|
||||
while ($count === 0 && $loop < 3) {
|
||||
$loop++;
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
Log::info('Count is zero, search for journals.');
|
||||
$collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page);
|
||||
if (!is_null($start)) {
|
||||
$collector->setRange($start, $end);
|
||||
}
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('accounts/show/' . $account->id . '/' . $moment);
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0 && $loop < 3) {
|
||||
$start->subDay();
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
}
|
||||
// grab journals:
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page);
|
||||
if (!is_null($start)) {
|
||||
$collector->setRange($start, $end);
|
||||
}
|
||||
|
||||
if ($moment != 'all' && $loop > 1) {
|
||||
$subTitle = trans(
|
||||
'firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
|
||||
'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath(route('accounts.show', [$account->id, $moment]));
|
||||
|
||||
return view(
|
||||
'accounts.show',
|
||||
@@ -421,7 +397,7 @@ class AccountController extends Controller
|
||||
$start = $repository->oldestJournalDate($account);
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range, null);
|
||||
$entries = new Collection;
|
||||
|
||||
// properties for cache
|
||||
|
||||
229
app/Http/Controllers/Admin/LinkController.php
Normal file
229
app/Http/Controllers/Admin/LinkController.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
/**
|
||||
* LinkController.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Admin;
|
||||
|
||||
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Http\Requests\LinkTypeFormRequest;
|
||||
use FireflyIII\Models\LinkType;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Preferences;
|
||||
use View;
|
||||
|
||||
/**
|
||||
* Class LinkController
|
||||
*
|
||||
* @package FireflyIII\Http\Controllers\Admin
|
||||
*/
|
||||
class LinkController extends Controller
|
||||
{
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
View::share('title', strval(trans('firefly.administration')));
|
||||
View::share('mainTitleIcon', 'fa-hand-spock-o');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$subTitle = trans('firefly.create_new_link_type');
|
||||
$subTitleIcon = 'fa-link';
|
||||
|
||||
// put previous url in session if not redirect from store (not "create another").
|
||||
if (session('link_types.create.fromStore') !== true) {
|
||||
$this->rememberPreviousUri('link_types.create.uri');
|
||||
}
|
||||
|
||||
return view('admin.link.create', compact('subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param LinkTypeRepositoryInterface $repository
|
||||
* @param LinkType $linkType
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
|
||||
*/
|
||||
public function delete(Request $request, LinkTypeRepositoryInterface $repository, LinkType $linkType)
|
||||
{
|
||||
if (!$linkType->editable) {
|
||||
$request->session()->flash('error', strval(trans('firefly.cannot_edit_link_type', ['name' => $linkType->name])));
|
||||
|
||||
return redirect(route('admin.links.index'));
|
||||
}
|
||||
|
||||
$subTitle = trans('firefly.delete_link_type', ['name' => $linkType->name]);
|
||||
$otherTypes = $repository->get();
|
||||
$count = $repository->countJournals($linkType);
|
||||
$moveTo = [];
|
||||
$moveTo[0] = trans('firefly.do_not_save_connection');
|
||||
/** @var LinkType $otherType */
|
||||
foreach ($otherTypes as $otherType) {
|
||||
if ($otherType->id !== $linkType->id) {
|
||||
$moveTo[$otherType->id] = sprintf('%s (%s / %s)', $otherType->name, $otherType->inward, $otherType->outward);
|
||||
}
|
||||
}
|
||||
// put previous url in session
|
||||
$this->rememberPreviousUri('link_types.delete.uri');
|
||||
|
||||
return view('admin.link.delete', compact('linkType', 'subTitle', 'moveTo', 'count'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param LinkTypeRepositoryInterface $repository
|
||||
* @param LinkType $linkType
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function destroy(Request $request, LinkTypeRepositoryInterface $repository, LinkType $linkType)
|
||||
{
|
||||
$name = $linkType->name;
|
||||
$moveTo = $repository->find(intval($request->get('move_link_type_before_delete')));
|
||||
$repository->destroy($linkType, $moveTo);
|
||||
|
||||
$request->session()->flash('success', strval(trans('firefly.deleted_link_type', ['name' => $name])));
|
||||
Preferences::mark();
|
||||
|
||||
return redirect($this->getPreviousUri('link_types.delete.uri'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param LinkType $linkType
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
|
||||
*/
|
||||
public function edit(Request $request, LinkType $linkType)
|
||||
{
|
||||
if (!$linkType->editable) {
|
||||
$request->session()->flash('error', strval(trans('firefly.cannot_edit_link_type', ['name' => $linkType->name])));
|
||||
|
||||
return redirect(route('admin.links.index'));
|
||||
}
|
||||
$subTitle = trans('firefly.edit_link_type', ['name' => $linkType->name]);
|
||||
$subTitleIcon = 'fa-link';
|
||||
|
||||
// put previous url in session if not redirect from store (not "return_to_edit").
|
||||
if (session('link_types.edit.fromUpdate') !== true) {
|
||||
$this->rememberPreviousUri('link_types.edit.uri');
|
||||
}
|
||||
$request->session()->forget('link_types.edit.fromUpdate');
|
||||
|
||||
return view('admin.link.edit', compact('subTitle', 'subTitleIcon', 'linkType'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LinkTypeRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function index(LinkTypeRepositoryInterface $repository)
|
||||
{
|
||||
$subTitle = trans('firefly.journal_link_configuration');
|
||||
$subTitleIcon = 'fa-link';
|
||||
$linkTypes = $repository->get();
|
||||
$linkTypes->each(
|
||||
function (LinkType $linkType) use ($repository) {
|
||||
$linkType->journalCount = $repository->countJournals($linkType);
|
||||
}
|
||||
);
|
||||
|
||||
return view('admin.link.index', compact('subTitle', 'subTitleIcon', 'linkTypes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LinkType $linkType
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function show(LinkType $linkType)
|
||||
{
|
||||
$subTitle = trans('firefly.overview_for_link', ['name' => $linkType->name]);
|
||||
$subTitleIcon = 'fa-link';
|
||||
$links = $linkType->transactionJournalLinks()->get();
|
||||
|
||||
return view('admin.link.show', compact('subTitle', 'subTitleIcon', 'linkType', 'links'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LinkTypeFormRequest $request
|
||||
* @param LinkTypeRepositoryInterface $repository
|
||||
*
|
||||
* @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function store(LinkTypeFormRequest $request, LinkTypeRepositoryInterface $repository)
|
||||
{
|
||||
$data = [
|
||||
'name' => $request->string('name'),
|
||||
'inward' => $request->string('inward'),
|
||||
'outward' => $request->string('outward'),
|
||||
];
|
||||
$linkType = $repository->store($data);
|
||||
$request->session()->flash('success', strval(trans('firefly.stored_new_link_type', ['name' => $linkType->name])));
|
||||
|
||||
if (intval($request->get('create_another')) === 1) {
|
||||
// set value so create routine will not overwrite URL:
|
||||
$request->session()->put('link_types.create.fromStore', true);
|
||||
|
||||
return redirect(route('link_types.create', [$request->input('what')]))->withInput();
|
||||
}
|
||||
|
||||
// redirect to previous URL.
|
||||
return redirect($this->getPreviousUri('link_types.create.uri'));
|
||||
}
|
||||
|
||||
public function update(LinkTypeFormRequest $request, LinkTypeRepositoryInterface $repository, LinkType $linkType)
|
||||
{
|
||||
if (!$linkType->editable) {
|
||||
$request->session()->flash('error', strval(trans('firefly.cannot_edit_link_type', ['name' => $linkType->name])));
|
||||
|
||||
return redirect(route('admin.links.index'));
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $request->string('name'),
|
||||
'inward' => $request->string('inward'),
|
||||
'outward' => $request->string('outward'),
|
||||
];
|
||||
$repository->update($linkType, $data);
|
||||
|
||||
$request->session()->flash('success', strval(trans('firefly.updated_link_type', ['name' => $linkType->name])));
|
||||
Preferences::mark();
|
||||
|
||||
if (intval($request->get('return_to_edit')) === 1) {
|
||||
// set value so edit routine will not overwrite URL:
|
||||
$request->session()->put('link_types.edit.fromUpdate', true);
|
||||
|
||||
return redirect(route('admin.links.edit', [$linkType->id]))->withInput(['return_to_edit' => 1]);
|
||||
}
|
||||
|
||||
// redirect to previous URL.
|
||||
return redirect($this->getPreviousUri('link_types.edit.uri'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -153,7 +153,7 @@ class AttachmentController extends Controller
|
||||
$image = 'images/page_green.png';
|
||||
|
||||
|
||||
if ($attachment->mime == 'application/pdf') {
|
||||
if ($attachment->mime === 'application/pdf') {
|
||||
$image = 'images/page_white_acrobat.png';
|
||||
}
|
||||
$file = public_path($image);
|
||||
|
||||
@@ -17,6 +17,7 @@ use Config;
|
||||
use FireflyConfig;
|
||||
use FireflyIII\Events\RegisteredUser;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Http\Requests\UserRegistrationRequest;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -52,11 +53,11 @@ class RegisterController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param UserRegistrationRequest|Request $request
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
|
||||
*/
|
||||
public function register(Request $request)
|
||||
public function register(UserRegistrationRequest $request)
|
||||
{
|
||||
// is allowed to?
|
||||
$singleUserMode = FireflyConfig::get('single_user_mode', Config::get('firefly.configuration.single_user_mode'))->data;
|
||||
|
||||
@@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Amount;
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Http\Requests\BillFormRequest;
|
||||
@@ -129,6 +130,11 @@ class BillController extends Controller
|
||||
if (session('bills.edit.fromUpdate') !== true) {
|
||||
$this->rememberPreviousUri('bills.edit.uri');
|
||||
}
|
||||
|
||||
$currency = Amount::getDefaultCurrency();
|
||||
$bill->amount_min = round($bill->amount_min, $currency->decimal_places);
|
||||
$bill->amount_max = round($bill->amount_max, $currency->decimal_places);
|
||||
|
||||
$request->session()->forget('bills.edit.fromUpdate');
|
||||
$request->session()->flash('gaEventCategory', 'bills');
|
||||
$request->session()->flash('gaEventAction', 'edit');
|
||||
@@ -175,7 +181,7 @@ class BillController extends Controller
|
||||
*/
|
||||
public function rescan(Request $request, BillRepositoryInterface $repository, Bill $bill)
|
||||
{
|
||||
if (intval($bill->active) == 0) {
|
||||
if (intval($bill->active) === 0) {
|
||||
$request->session()->flash('warning', strval(trans('firefly.cannot_scan_inactive_bill')));
|
||||
|
||||
return redirect(URL::previous());
|
||||
@@ -206,7 +212,7 @@ class BillController extends Controller
|
||||
/** @var Carbon $date */
|
||||
$date = session('start');
|
||||
$year = $date->year;
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
$yearAverage = $repository->getYearAverage($bill, $date);
|
||||
$overallAverage = $repository->getOverallAverage($bill);
|
||||
@@ -217,7 +223,7 @@ class BillController extends Controller
|
||||
$collector->setAllAssetAccounts()->setBills(new Collection([$bill]))->setLimit($pageSize)->setPage($page)->withBudgetInformation()
|
||||
->withCategoryInformation();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('/bills/show/' . $bill->id);
|
||||
$journals->setPath(route('bills.show', [$bill->id]));
|
||||
|
||||
$bill->nextExpectedMatch = $repository->nextExpectedMatch($bill, new Carbon);
|
||||
$hideBill = true;
|
||||
|
||||
@@ -75,13 +75,11 @@ class BudgetController extends Controller
|
||||
*/
|
||||
public function amount(Request $request, Budget $budget)
|
||||
{
|
||||
$amount = intval($request->get('amount'));
|
||||
/** @var Carbon $start */
|
||||
$start = session('start', Carbon::now()->startOfMonth());
|
||||
/** @var Carbon $end */
|
||||
$end = session('end', Carbon::now()->endOfMonth());
|
||||
$amount = intval($request->get('amount'));
|
||||
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
|
||||
$budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount);
|
||||
if ($amount == 0) {
|
||||
if ($amount === 0) {
|
||||
$budgetLimit = null;
|
||||
}
|
||||
Preferences::mark();
|
||||
@@ -243,7 +241,7 @@ class BudgetController extends Controller
|
||||
compact(
|
||||
'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText',
|
||||
'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets',
|
||||
'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start'
|
||||
'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start', 'end'
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -293,37 +291,15 @@ class BudgetController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
|
||||
$count = 0;
|
||||
$loop = 0;
|
||||
// grab journals, but be prepared to jump a period back to get the right ones:
|
||||
Log::info('Now at no-budget loop start.');
|
||||
while ($count === 0 && $loop < 3) {
|
||||
$loop++;
|
||||
Log::info(sprintf('Count is zero, search for journals between %s and %s.', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page)
|
||||
->withoutBudget()->withOpposingAccount();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('/budgets/list/no-budget');
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0 && $loop < 3) {
|
||||
$start->subDay();
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
}
|
||||
}
|
||||
|
||||
if ($moment != 'all' && $loop > 1) {
|
||||
$subTitle = trans(
|
||||
'firefly.without_budget_between',
|
||||
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page)
|
||||
->withoutBudget()->withOpposingAccount();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath(route('budgets.no-budget'));
|
||||
|
||||
return view('budgets.no-budget', compact('journals', 'subTitle', 'moment', 'periods', 'start', 'end'));
|
||||
}
|
||||
@@ -335,15 +311,15 @@ class BudgetController extends Controller
|
||||
*/
|
||||
public function postUpdateIncome(BudgetIncomeRequest $request)
|
||||
{
|
||||
$start = session('start', new Carbon);
|
||||
$end = session('end', new Carbon);
|
||||
$start = Carbon::createFromFormat('Y-m-d', $request->string('start'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $request->string('end'));
|
||||
$defaultCurrency = Amount::getDefaultCurrency();
|
||||
$amount = $request->get('amount');
|
||||
|
||||
$this->repository->setAvailableBudget($defaultCurrency, $start, $end, $amount);
|
||||
Preferences::mark();
|
||||
|
||||
return redirect(route('budgets.index'));
|
||||
return redirect(route('budgets.index', [$start->format('Y-m-d')]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,7 +333,7 @@ class BudgetController extends Controller
|
||||
/** @var Carbon $start */
|
||||
$start = session('first', Carbon::create()->startOfYear());
|
||||
$end = new Carbon;
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
$limits = $this->getLimits($budget, $start, $end);
|
||||
$repetition = null;
|
||||
@@ -366,7 +342,7 @@ class BudgetController extends Controller
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setBudget($budget)->setLimit($pageSize)->setPage($page)->withCategoryInformation();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('/budgets/show/' . $budget->id);
|
||||
$journals->setPath(route('budgets.show', [$budget->id]));
|
||||
|
||||
|
||||
$subTitle = trans('firefly.all_journals_for_budget', ['name' => $budget->name]);
|
||||
@@ -384,11 +360,11 @@ class BudgetController extends Controller
|
||||
*/
|
||||
public function showByBudgetLimit(Request $request, Budget $budget, BudgetLimit $budgetLimit)
|
||||
{
|
||||
if ($budgetLimit->budget->id != $budget->id) {
|
||||
if ($budgetLimit->budget->id !== $budget->id) {
|
||||
throw new FireflyException('This budget limit is not part of this budget.');
|
||||
}
|
||||
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
$subTitle = trans(
|
||||
'firefly.budget_in_period', [
|
||||
@@ -404,7 +380,7 @@ class BudgetController extends Controller
|
||||
$collector->setAllAssetAccounts()->setRange($budgetLimit->start_date, $budgetLimit->end_date)
|
||||
->setBudget($budget)->setLimit($pageSize)->setPage($page)->withCategoryInformation();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('/budgets/show/' . $budget->id . '/' . $budgetLimit->id);
|
||||
$journals->setPath(route('budgets.show', [$budget->id, $budgetLimit->id]));
|
||||
|
||||
|
||||
$start = session('first', Carbon::create()->startOfYear());
|
||||
@@ -465,15 +441,16 @@ class BudgetController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @return View
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function updateIncome()
|
||||
public function updateIncome(Carbon $start, Carbon $end)
|
||||
{
|
||||
$start = session('start', new Carbon);
|
||||
$end = session('end', new Carbon);
|
||||
$defaultCurrency = Amount::getDefaultCurrency();
|
||||
$available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end);
|
||||
|
||||
$available = round($available, $defaultCurrency->decimal_places);
|
||||
|
||||
return view('budgets.income', compact('available', 'start', 'end'));
|
||||
}
|
||||
@@ -567,7 +544,7 @@ class BudgetController extends Controller
|
||||
$start = $first->date ?? new Carbon;
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range, null);
|
||||
$entries = new Collection;
|
||||
|
||||
// properties for cache
|
||||
|
||||
@@ -164,10 +164,12 @@ class CategoryController extends Controller
|
||||
public function noCategory(Request $request, JournalRepositoryInterface $repository, string $moment = '')
|
||||
{
|
||||
// default values:
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = null;
|
||||
$end = null;
|
||||
$periods = new Collection;
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = null;
|
||||
$end = null;
|
||||
$periods = new Collection;
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
|
||||
// prep for "all" view.
|
||||
if ($moment === 'all') {
|
||||
@@ -199,37 +201,13 @@ class CategoryController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
|
||||
$count = 0;
|
||||
$loop = 0;
|
||||
// grab journals, but be prepared to jump a period back to get the right ones:
|
||||
Log::info('Now at no-cat loop start.');
|
||||
while ($count === 0 && $loop < 3) {
|
||||
$loop++;
|
||||
Log::info('Count is zero, search for journals.');
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutCategory()->withOpposingAccount();
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('/categories/list/no-category');
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0 && $loop < 3) {
|
||||
$start->subDay();
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
}
|
||||
}
|
||||
|
||||
if ($moment != 'all' && $loop > 1) {
|
||||
$subTitle = trans(
|
||||
'firefly.without_category_between',
|
||||
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutCategory()->withOpposingAccount();
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath(route('categories.no-category'));
|
||||
|
||||
return view('categories.no-category', compact('journals', 'subTitle', 'moment', 'periods', 'start', 'end'));
|
||||
}
|
||||
@@ -247,10 +225,8 @@ class CategoryController extends Controller
|
||||
// default values:
|
||||
$subTitle = $category->name;
|
||||
$subTitleIcon = 'fa-bar-chart';
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
$count = 0;
|
||||
$loop = 0;
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = null;
|
||||
$end = null;
|
||||
@@ -287,34 +263,15 @@ class CategoryController extends Controller
|
||||
'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
// grab journals, but be prepared to jump a period back to get the right ones:
|
||||
Log::info('Now at category loop start.');
|
||||
while ($count === 0 && $loop < 3) {
|
||||
$loop++;
|
||||
Log::info('Count is zero, search for journals.');
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
|
||||
->setCategory($category)->withBudgetInformation()->withCategoryInformation();
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('categories/show/' . $category->id);
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0 && $loop < 3) {
|
||||
$start->subDay();
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
}
|
||||
}
|
||||
|
||||
if ($moment != 'all' && $loop > 1) {
|
||||
$subTitle = trans(
|
||||
'firefly.journals_in_period_for_category',
|
||||
['name' => $category->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
|
||||
'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
|
||||
->setCategory($category)->withBudgetInformation()->withCategoryInformation();
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath(route('categories.show', [$category->id]));
|
||||
|
||||
|
||||
return view('categories.show', compact('category', 'moment', 'journals', 'periods', 'subTitle', 'subTitleIcon', 'start', 'end'));
|
||||
}
|
||||
@@ -382,7 +339,7 @@ class CategoryController extends Controller
|
||||
$start = $first->date ?? new Carbon;
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range, null);
|
||||
$entries = new Collection;
|
||||
|
||||
// properties for cache
|
||||
@@ -463,12 +420,12 @@ class CategoryController extends Controller
|
||||
$accountRepository = app(AccountRepositoryInterface::class);
|
||||
$accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
|
||||
$first = $repository->firstUseDate($category);
|
||||
if ($first->year == 1900) {
|
||||
if ($first->year === 1900) {
|
||||
$first = new Carbon;
|
||||
}
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$first = Navigation::startOfPeriod($first, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range, null);
|
||||
$entries = new Collection;
|
||||
|
||||
// properties for entries with their amounts.
|
||||
|
||||
@@ -124,7 +124,7 @@ class BudgetController extends Controller
|
||||
*/
|
||||
public function budgetLimit(Budget $budget, BudgetLimit $budgetLimit)
|
||||
{
|
||||
if ($budgetLimit->budget->id != $budget->id) {
|
||||
if ($budgetLimit->budget->id !== $budget->id) {
|
||||
throw new FireflyException('This budget limit is not part of this budget.');
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ class BudgetController extends Controller
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function expenseAsset(Budget $budget, BudgetLimit $budgetLimit = null)
|
||||
public function expenseAsset(Budget $budget, ?BudgetLimit $budgetLimit)
|
||||
{
|
||||
$cache = new CacheProperties;
|
||||
$cache->addProperty($budget->id);
|
||||
@@ -208,7 +208,7 @@ class BudgetController extends Controller
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function expenseCategory(Budget $budget, BudgetLimit $budgetLimit = null)
|
||||
public function expenseCategory(Budget $budget, ?BudgetLimit $budgetLimit)
|
||||
{
|
||||
$cache = new CacheProperties;
|
||||
$cache->addProperty($budget->id);
|
||||
@@ -255,7 +255,7 @@ class BudgetController extends Controller
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function expenseExpense(Budget $budget, BudgetLimit $budgetLimit = null)
|
||||
public function expenseExpense(Budget $budget, ?BudgetLimit $budgetLimit)
|
||||
{
|
||||
$cache = new CacheProperties;
|
||||
$cache->addProperty($budget->id);
|
||||
|
||||
@@ -67,7 +67,7 @@ class CategoryController extends Controller
|
||||
|
||||
$start = $repository->firstUseDate($category);
|
||||
|
||||
if ($start->year == 1900) {
|
||||
if ($start->year === 1900) {
|
||||
$start = new Carbon;
|
||||
}
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ class CategoryReportController extends Controller
|
||||
// remove all empty entries to prevent cluttering:
|
||||
$newSet = [];
|
||||
foreach ($chartData as $key => $entry) {
|
||||
if (!array_sum($entry['entries']) == 0) {
|
||||
if (!array_sum($entry['entries']) === 0) {
|
||||
$newSet[$key] = $chartData[$key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ class TagReportController extends Controller
|
||||
// remove all empty entries to prevent cluttering:
|
||||
$newSet = [];
|
||||
foreach ($chartData as $key => $entry) {
|
||||
if (!array_sum($entry['entries']) == 0) {
|
||||
if (!array_sum($entry['entries']) === 0) {
|
||||
$newSet[$key] = $chartData[$key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@ use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Log;
|
||||
use Route;
|
||||
use Session;
|
||||
use URL;
|
||||
use View;
|
||||
@@ -47,22 +50,50 @@ class Controller extends BaseController
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// for transaction lists:
|
||||
View::share('hideBudgets', false);
|
||||
View::share('hideCategories', false);
|
||||
View::share('hideBills', false);
|
||||
View::share('hideTags', false);
|
||||
|
||||
// is site a demo site?
|
||||
$isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data;
|
||||
View::share('IS_DEMO_SITE', $isDemoSite);
|
||||
View::share('DEMO_USERNAME', env('DEMO_USERNAME', ''));
|
||||
View::share('DEMO_PASSWORD', env('DEMO_PASSWORD', ''));
|
||||
|
||||
// translations:
|
||||
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
// translations for specific strings:
|
||||
$this->monthFormat = (string)trans('config.month');
|
||||
$this->monthAndDayFormat = (string)trans('config.month_and_day');
|
||||
$this->dateTimeFormat = (string)trans('config.date_time');
|
||||
|
||||
// get shown-intro-preference:
|
||||
if (auth()->check()) {
|
||||
// some routes have a "what" parameter, which indicates a special page:
|
||||
$specificPage = is_null(Route::current()->parameter('what')) ? '' : '_' . Route::current()->parameter('what');
|
||||
$page = str_replace('.', '_', Route::currentRouteName());
|
||||
|
||||
// indicator if user has seen the help for this page ( + special page):
|
||||
$key = 'shown_demo_' . $page . $specificPage;
|
||||
// is there an intro for this route?
|
||||
$intro = config('intro.' . $page);
|
||||
$specialIntro = config('intro.' . $page . $specificPage);
|
||||
$shownDemo = true;
|
||||
|
||||
// either must be array and either must be > 0
|
||||
if ((is_array($intro) || is_array($specialIntro)) && (count($intro) > 0 || count($specialIntro) > 0)) {
|
||||
$shownDemo = Preferences::get($key, false)->data;
|
||||
Log::debug(sprintf('Check if user has already seen intro with key "%s". Result is %d', $key, $shownDemo));
|
||||
}
|
||||
|
||||
View::share('shownDemo', $shownDemo);
|
||||
View::share('current_route_name', $page);
|
||||
View::share('original_route_name', Route::currentRouteName());
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -137,13 +137,14 @@ class ExportController extends Controller
|
||||
public function postIndex(ExportFormRequest $request, AccountRepositoryInterface $repository, ExportJobRepositoryInterface $jobs)
|
||||
{
|
||||
$job = $jobs->findByKey($request->get('job'));
|
||||
$accounts = $request->get('accounts') ?? [];
|
||||
$settings = [
|
||||
'accounts' => $repository->getAccountsById($request->get('accounts')),
|
||||
'accounts' => $repository->getAccountsById($accounts),
|
||||
'startDate' => new Carbon($request->get('export_start_range')),
|
||||
'endDate' => new Carbon($request->get('export_end_range')),
|
||||
'exportFormat' => $request->get('exportFormat'),
|
||||
'includeAttachments' => intval($request->get('include_attachments')) === 1,
|
||||
'includeOldUploads' => intval($request->get('include_old_uploads')) === 1,
|
||||
'includeAttachments' => $request->boolean('include_attachments'),
|
||||
'includeOldUploads' => $request->boolean('include_old_uploads'),
|
||||
'job' => $job,
|
||||
];
|
||||
|
||||
@@ -159,12 +160,14 @@ class ExportController extends Controller
|
||||
$jobs->changeStatus($job, 'export_status_collecting_journals');
|
||||
$processor->collectJournals();
|
||||
$jobs->changeStatus($job, 'export_status_collected_journals');
|
||||
|
||||
/*
|
||||
* Transform to exportable entries:
|
||||
*/
|
||||
$jobs->changeStatus($job, 'export_status_converting_to_export_format');
|
||||
$processor->convertJournals();
|
||||
$jobs->changeStatus($job, 'export_status_converted_to_export_format');
|
||||
|
||||
/*
|
||||
* Transform to (temporary) file:
|
||||
*/
|
||||
@@ -180,6 +183,7 @@ class ExportController extends Controller
|
||||
$jobs->changeStatus($job, 'export_status_collected_attachments');
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Collect old uploads
|
||||
*/
|
||||
|
||||
@@ -25,62 +25,95 @@ use Response;
|
||||
*/
|
||||
class HelpController extends Controller
|
||||
{
|
||||
|
||||
/** @var HelpInterface */
|
||||
private $help;
|
||||
|
||||
/**
|
||||
* HelpController constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->help = app(HelpInterface::class);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HelpInterface $help
|
||||
* @param $route
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function show(HelpInterface $help, string $route)
|
||||
public function show(string $route)
|
||||
{
|
||||
|
||||
$language = Preferences::get('language', config('firefly.default_language', 'en_US'))->data;
|
||||
$content = '<p>' . strval(trans('firefly.route_has_no_help')) . '</p>';
|
||||
$html = $this->getHelpText($route, $language);
|
||||
|
||||
if (!$help->hasRoute($route)) {
|
||||
return Response::json(['html' => $html]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string $language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getHelpText(string $route, string $language): string
|
||||
{
|
||||
// get language and default variables.
|
||||
|
||||
$content = '<p>' . strval(trans('firefly.route_has_no_help')) . '</p>';
|
||||
|
||||
// if no such route, log error and return default text.
|
||||
if (!$this->help->hasRoute($route)) {
|
||||
Log::error('No such route: ' . $route);
|
||||
|
||||
return Response::json($content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ($help->inCache($route, $language)) {
|
||||
$content = $help->getFromCache($route, $language);
|
||||
// help content may be cached:
|
||||
if ($this->help->inCache($route, $language)) {
|
||||
$content = $this->help->getFromCache($route, $language);
|
||||
Log::debug(sprintf('Help text %s was in cache.', $language));
|
||||
|
||||
return Response::json($content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
$content = $help->getFromGithub($route, $language);
|
||||
$notYourLanguage = '<p><em>' . strval(trans('firefly.help_may_not_be_your_language')) . '</em></p>';
|
||||
// get help content from Github:
|
||||
$content = $this->help->getFromGithub($route, $language);
|
||||
|
||||
// get backup language content (try English):
|
||||
// content will have 0 length when Github failed. Try en_US when it does:
|
||||
if (strlen($content) === 0) {
|
||||
$language = 'en_US';
|
||||
if ($help->inCache($route, $language)) {
|
||||
|
||||
// also check cache first:
|
||||
if ($this->help->inCache($route, $language)) {
|
||||
Log::debug(sprintf('Help text %s was in cache.', $language));
|
||||
$content = $notYourLanguage . $help->getFromCache($route, $language);
|
||||
}
|
||||
if (!$help->inCache($route, $language)) {
|
||||
$content = trim($notYourLanguage . $help->getFromGithub($route, $language));
|
||||
$content = $this->help->getFromCache($route, $language);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
$content = $this->help->getFromGithub($route, $language);
|
||||
|
||||
}
|
||||
|
||||
if ($content === $notYourLanguage) {
|
||||
$content = '<p>' . strval(trans('firefly.route_has_no_help')) . '</p>';
|
||||
// help still empty?
|
||||
if (strlen($content) !== 0) {
|
||||
$this->help->putInCache($route, $language, $content);
|
||||
|
||||
return $content;
|
||||
|
||||
}
|
||||
|
||||
$help->putInCache($route, $language, $content);
|
||||
|
||||
return Response::json($content);
|
||||
|
||||
return '<p>' . strval(trans('firefly.route_has_no_help')) . '</p>';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
use Preferences;
|
||||
use Route as RouteFacade;
|
||||
use Session;
|
||||
use View;
|
||||
|
||||
@@ -80,6 +82,14 @@ class HomeController extends Controller
|
||||
*/
|
||||
public function displayError()
|
||||
{
|
||||
Log::debug('This is a test message at the DEBUG level.');
|
||||
Log::info('This is a test message at the INFO level.');
|
||||
Log::notice('This is a test message at the NOTICE level.');
|
||||
Log::warning('This is a test message at the WARNING level.');
|
||||
Log::error('This is a test message at the ERROR level.');
|
||||
Log::critical('This is a test message at the CRITICAL level.');
|
||||
Log::alert('This is a test message at the ALERT level.');
|
||||
Log::emergency('This is a test message at the EMERGENCY level.');
|
||||
throw new FireflyException('A very simple test error.');
|
||||
}
|
||||
|
||||
@@ -107,7 +117,7 @@ class HomeController extends Controller
|
||||
$types = config('firefly.accountTypesByIdentifier.asset');
|
||||
$count = $repository->count($types);
|
||||
|
||||
if ($count == 0) {
|
||||
if ($count === 0) {
|
||||
return redirect(route('new-user.index'));
|
||||
}
|
||||
|
||||
@@ -120,7 +130,6 @@ class HomeController extends Controller
|
||||
$start = session('start', Carbon::now()->startOfMonth());
|
||||
/** @var Carbon $end */
|
||||
$end = session('end', Carbon::now()->endOfMonth());
|
||||
$showTour = Preferences::get('tour', true)->data;
|
||||
$accounts = $repository->getAccountsById($frontPage->data);
|
||||
$showDepositsFrontpage = Preferences::get('showDepositsFrontpage', false)->data;
|
||||
|
||||
@@ -137,10 +146,34 @@ class HomeController extends Controller
|
||||
}
|
||||
|
||||
return view(
|
||||
'index', compact('count', 'showTour', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage', 'billCount')
|
||||
'index', compact('count', 'subTitle', 'transactions', 'showDepositsFrontpage', 'billCount')
|
||||
);
|
||||
}
|
||||
|
||||
public function routes()
|
||||
{
|
||||
$set = RouteFacade::getRoutes();
|
||||
$ignore = ['chart.', 'javascript.', 'json.', 'report-data.', 'popup.', 'debugbar.'];
|
||||
/** @var Route $route */
|
||||
foreach ($set as $route) {
|
||||
$name = $route->getName();
|
||||
if (!is_null($name) && in_array('GET', $route->methods()) && strlen($name) > 0) {
|
||||
$found = false;
|
||||
foreach ($ignore as $string) {
|
||||
if (strpos($name, $string) !== false) {
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
echo 'touch ' . $route->getName() . '.md;';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
|
||||
154
app/Http/Controllers/Import/BankController.php
Normal file
154
app/Http/Controllers/Import/BankController.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* BankController.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Import;
|
||||
|
||||
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Support\Import\Information\InformationInterface;
|
||||
use FireflyIII\Support\Import\Prerequisites\PrerequisitesInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Log;
|
||||
use Session;
|
||||
|
||||
class BankController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* This method must ask the user all parameters necessary to start importing data. This may not be enough
|
||||
* to finish the import itself (ie. mapping) but it should be enough to begin: accounts to import from,
|
||||
* accounts to import into, data ranges, etc.
|
||||
*
|
||||
* @param string $bank
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
|
||||
*/
|
||||
public function form(string $bank)
|
||||
{
|
||||
$class = config(sprintf('firefly.import_pre.%s', $bank));
|
||||
/** @var PrerequisitesInterface $object */
|
||||
$object = app($class);
|
||||
$object->setUser(auth()->user());
|
||||
|
||||
if ($object->hasPrerequisites()) {
|
||||
return redirect(route('import.bank.prerequisites', [$bank]));
|
||||
}
|
||||
$class = config(sprintf('firefly.import_info.%s', $bank));
|
||||
/** @var InformationInterface $object */
|
||||
$object = app($class);
|
||||
$object->setUser(auth()->user());
|
||||
$remoteAccounts = $object->getAccounts();
|
||||
|
||||
return view('import.bank.form', compact('remoteAccounts', 'bank'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* With the information given in the submitted form Firefly III will call upon the bank's classes to return transaction
|
||||
* information as requested. The user will be able to map unknown data and continue. Or maybe, it's put into some kind of
|
||||
* fake CSV file and forwarded to the import routine.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $bank
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|null
|
||||
*/
|
||||
public function postForm(Request $request, string $bank)
|
||||
{
|
||||
|
||||
$class = config(sprintf('firefly.import_pre.%s', $bank));
|
||||
/** @var PrerequisitesInterface $object */
|
||||
$object = app($class);
|
||||
$object->setUser(auth()->user());
|
||||
|
||||
if ($object->hasPrerequisites()) {
|
||||
return redirect(route('import.bank.prerequisites', [$bank]));
|
||||
}
|
||||
$remoteAccounts = $request->get('do_import');
|
||||
if (!is_array($remoteAccounts) || count($remoteAccounts) === 0) {
|
||||
Session::flash('error', 'Must select accounts');
|
||||
|
||||
return redirect(route('import.bank.form', [$bank]));
|
||||
}
|
||||
$remoteAccounts = array_keys($remoteAccounts);
|
||||
|
||||
$class = config(sprintf('firefly.import_pre.%s', $bank));
|
||||
// get import file
|
||||
|
||||
// get import config
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method processes the prerequisites the user has entered in the previous step.
|
||||
*
|
||||
* Whatever storePrerequisites does, it should make sure that the system is ready to continue immediately. So
|
||||
* no extra calls or stuff, except maybe to open a session
|
||||
*
|
||||
* @see PrerequisitesInterface::storePrerequisites
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $bank
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function postPrerequisites(Request $request, string $bank)
|
||||
{
|
||||
Log::debug(sprintf('Now in postPrerequisites for %s', $bank));
|
||||
$class = config(sprintf('firefly.import_pre.%s', $bank));
|
||||
/** @var PrerequisitesInterface $object */
|
||||
$object = app($class);
|
||||
$object->setUser(auth()->user());
|
||||
if (!$object->hasPrerequisites()) {
|
||||
Log::debug(sprintf('No more prerequisites for %s, move to form.', $bank));
|
||||
|
||||
return redirect(route('import.bank.form', [$bank]));
|
||||
}
|
||||
Log::debug('Going to store entered preprerequisites.');
|
||||
// store post data
|
||||
$result = $object->storePrerequisites($request);
|
||||
|
||||
if ($result->count() > 0) {
|
||||
Session::flash('error', $result->first());
|
||||
|
||||
return redirect(route('import.bank.prerequisites', [$bank]));
|
||||
}
|
||||
|
||||
return redirect(route('import.bank.form', [$bank]));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows you, if necessary, a form that allows you to enter any required values, such as API keys,
|
||||
* login passwords or other values.
|
||||
*
|
||||
* @param string $bank
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
|
||||
*/
|
||||
public function prerequisites(string $bank)
|
||||
{
|
||||
$class = config(sprintf('firefly.import_pre.%s', $bank));
|
||||
/** @var PrerequisitesInterface $object */
|
||||
$object = app($class);
|
||||
$object->setUser(auth()->user());
|
||||
|
||||
if ($object->hasPrerequisites()) {
|
||||
$view = $object->getView();
|
||||
$parameters = $object->getViewParameters();
|
||||
|
||||
return view($view, $parameters);
|
||||
}
|
||||
|
||||
return redirect(route('import.bank.form', [$bank]));
|
||||
}
|
||||
|
||||
}
|
||||
306
app/Http/Controllers/Import/FileController.php
Normal file
306
app/Http/Controllers/Import/FileController.php
Normal file
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
/**
|
||||
* FileController.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Import;
|
||||
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Http\Requests\ImportUploadRequest;
|
||||
use FireflyIII\Import\Configurator\ConfiguratorInterface;
|
||||
use FireflyIII\Import\Routine\ImportRoutine;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
|
||||
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response as LaravelResponse;
|
||||
use Log;
|
||||
use Response;
|
||||
use Session;
|
||||
use View;
|
||||
|
||||
|
||||
/**
|
||||
* Class FileController.
|
||||
*
|
||||
* @package FireflyIII\Http\Controllers\Import
|
||||
*/
|
||||
class FileController extends Controller
|
||||
{
|
||||
/** @var ImportJobRepositoryInterface */
|
||||
public $repository;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
View::share('mainTitleIcon', 'fa-archive');
|
||||
View::share('title', trans('firefly.import_index_title'));
|
||||
$this->repository = app(ImportJobRepositoryInterface::class);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 3. This repeats until the job is configured.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return View
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function configure(ImportJob $job)
|
||||
{
|
||||
// create configuration class:
|
||||
$configurator = $this->makeConfigurator($job);
|
||||
|
||||
// is the job already configured?
|
||||
if ($configurator->isJobConfigured()) {
|
||||
$this->repository->updateStatus($job, 'configured');
|
||||
|
||||
return redirect(route('import.file.status', [$job->key]));
|
||||
}
|
||||
$view = $configurator->getNextView();
|
||||
$data = $configurator->getNextData();
|
||||
$subTitle = trans('firefly.import_config_bread_crumb');
|
||||
$subTitleIcon = 'fa-wrench';
|
||||
|
||||
return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON file of the job's configuration and send it to the user.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return LaravelResponse
|
||||
*/
|
||||
public function download(ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in download()', ['job' => $job->key]);
|
||||
$config = $job->configuration;
|
||||
|
||||
// This is CSV import specific:
|
||||
$config['column-roles-complete'] = false;
|
||||
$config['column-mapping-complete'] = false;
|
||||
$config['initial-config-complete'] = false;
|
||||
$config['delimiter'] = $config['delimiter'] === "\t" ? 'tab' : $config['delimiter'];
|
||||
|
||||
$result = json_encode($config, JSON_PRETTY_PRINT);
|
||||
$name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\'));
|
||||
|
||||
/** @var LaravelResponse $response */
|
||||
$response = response($result, 200);
|
||||
$response->header('Content-disposition', 'attachment; filename=' . $name)
|
||||
->header('Content-Type', 'application/json')
|
||||
->header('Content-Description', 'File Transfer')
|
||||
->header('Connection', 'Keep-Alive')
|
||||
->header('Expires', '0')
|
||||
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
|
||||
->header('Pragma', 'public')
|
||||
->header('Content-Length', strlen($result));
|
||||
|
||||
return $response;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 1. Upload a file.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$subTitle = trans('firefly.import_index_sub_title');
|
||||
$subTitleIcon = 'fa-home';
|
||||
$importFileTypes = [];
|
||||
$defaultImportType = config('firefly.default_import_format');
|
||||
|
||||
foreach (array_keys(config('firefly.import_formats')) as $type) {
|
||||
$importFileTypes[$type] = trans('firefly.import_file_type_' . $type);
|
||||
}
|
||||
|
||||
return view('import.file.index', compact('subTitle', 'subTitleIcon', 'importFileTypes', 'defaultImportType'));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 2. It creates an Import Job. Stores the import.
|
||||
*
|
||||
* @param ImportUploadRequest $request
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function initialize(ImportUploadRequest $request)
|
||||
{
|
||||
Log::debug('Now in initialize()');
|
||||
|
||||
// create import job:
|
||||
$type = $request->get('import_file_type');
|
||||
$job = $this->repository->create($type);
|
||||
Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]);
|
||||
|
||||
// process file:
|
||||
$this->repository->processFile($job, $request->files->get('import_file'));
|
||||
|
||||
// process config, if present:
|
||||
if ($request->files->has('configuration_file')) {
|
||||
$this->repository->processConfiguration($job, $request->files->get('configuration_file'));
|
||||
}
|
||||
|
||||
$this->repository->updateStatus($job, 'initialized');
|
||||
|
||||
return redirect(route('import.file.configure', [$job->key]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Show status of import job in JSON.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function json(ImportJob $job)
|
||||
{
|
||||
$result = [
|
||||
'started' => false,
|
||||
'finished' => false,
|
||||
'running' => false,
|
||||
'errors' => array_values($job->extended_status['errors']),
|
||||
'percentage' => 0,
|
||||
'show_percentage' => false,
|
||||
'steps' => $job->extended_status['steps'],
|
||||
'done' => $job->extended_status['done'],
|
||||
'statusText' => trans('firefly.import_status_job_' . $job->status),
|
||||
'status' => $job->status,
|
||||
'finishedText' => '',
|
||||
];
|
||||
|
||||
if ($job->extended_status['steps'] !== 0) {
|
||||
$result['percentage'] = round(($job->extended_status['done'] / $job->extended_status['steps']) * 100, 0);
|
||||
$result['show_percentage'] = true;
|
||||
}
|
||||
|
||||
if ($job->status === 'finished') {
|
||||
$tagId = $job->extended_status['tag'];
|
||||
/** @var TagRepositoryInterface $repository */
|
||||
$repository = app(TagRepositoryInterface::class);
|
||||
$tag = $repository->find($tagId);
|
||||
$result['finished'] = true;
|
||||
$result['finishedText'] = trans('firefly.import_status_finished_job', ['link' => route('tags.show', [$tag->id, 'all']), 'tag' => $tag->tag]);
|
||||
}
|
||||
|
||||
if ($job->status === 'running') {
|
||||
$result['started'] = true;
|
||||
$result['running'] = true;
|
||||
}
|
||||
|
||||
return Response::json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4. Save the configuration.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function postConfigure(Request $request, ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in postConfigure()', ['job' => $job->key]);
|
||||
$configurator = $this->makeConfigurator($job);
|
||||
|
||||
// is the job already configured?
|
||||
if ($configurator->isJobConfigured()) {
|
||||
return redirect(route('import.file.status', [$job->key]));
|
||||
}
|
||||
$data = $request->all();
|
||||
$configurator->configureJob($data);
|
||||
|
||||
// get possible warning from configurator:
|
||||
$warning = $configurator->getWarningMessage();
|
||||
|
||||
if (strlen($warning) > 0) {
|
||||
Session::flash('warning', $warning);
|
||||
}
|
||||
|
||||
// return to configure
|
||||
return redirect(route('import.file.configure', [$job->key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function start(ImportJob $job)
|
||||
{
|
||||
/** @var ImportRoutine $routine */
|
||||
$routine = app(ImportRoutine::class);
|
||||
$routine->setJob($job);
|
||||
$result = $routine->run();
|
||||
if ($result) {
|
||||
return Response::json(['run' => 'ok']);
|
||||
}
|
||||
|
||||
throw new FireflyException('Job did not complete succesfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
|
||||
*/
|
||||
public function status(ImportJob $job)
|
||||
{
|
||||
$statuses = ['configured', 'running', 'finished'];
|
||||
if (!in_array($job->status, $statuses)) {
|
||||
return redirect(route('import.file.configure', [$job->key]));
|
||||
}
|
||||
$subTitle = trans('firefly.import_status_sub_title');
|
||||
$subTitleIcon = 'fa-star';
|
||||
|
||||
return view('import.file.status', compact('job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return ConfiguratorInterface
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function makeConfigurator(ImportJob $job): ConfiguratorInterface
|
||||
{
|
||||
$type = $job->file_type;
|
||||
$key = sprintf('firefly.import_configurators.%s', $type);
|
||||
$className = config($key);
|
||||
if (is_null($className)) {
|
||||
throw new FireflyException('Cannot find configurator class for this job.'); // @codeCoverageIgnore
|
||||
}
|
||||
/** @var ConfiguratorInterface $configurator */
|
||||
$configurator = app($className);
|
||||
$configurator->setJob($job);
|
||||
|
||||
|
||||
return $configurator;
|
||||
}
|
||||
}
|
||||
@@ -12,17 +12,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Http\Requests\ImportUploadRequest;
|
||||
use FireflyIII\Import\Configurator\ConfiguratorInterface;
|
||||
use FireflyIII\Import\Routine\ImportRoutine;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
|
||||
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response as LaravelResponse;
|
||||
use Log;
|
||||
use Response;
|
||||
use View;
|
||||
|
||||
/**
|
||||
@@ -53,72 +43,9 @@ class ImportController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 3. This repeats until the job is configured.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return View
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function configure(ImportJob $job)
|
||||
{
|
||||
// create configuration class:
|
||||
$configurator = $this->makeConfigurator($job);
|
||||
|
||||
// is the job already configured?
|
||||
if ($configurator->isJobConfigured()) {
|
||||
$this->repository->updateStatus($job, 'configured');
|
||||
|
||||
return redirect(route('import.status', [$job->key]));
|
||||
}
|
||||
$view = $configurator->getNextView();
|
||||
$data = $configurator->getNextData();
|
||||
$subTitle = trans('firefly.import_config_bread_crumb');
|
||||
$subTitleIcon = 'fa-wrench';
|
||||
|
||||
return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON file of the job's configuration and send it to the user.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return LaravelResponse
|
||||
*/
|
||||
public function download(ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in download()', ['job' => $job->key]);
|
||||
$config = $job->configuration;
|
||||
|
||||
// TODO this is CSV import specific:
|
||||
$config['column-roles-complete'] = false;
|
||||
$config['column-mapping-complete'] = false;
|
||||
$config['initial-config-complete'] = false;
|
||||
$config['delimiter'] = $config['delimiter'] === "\t" ? 'tab' : $config['delimiter'];
|
||||
|
||||
$result = json_encode($config, JSON_PRETTY_PRINT);
|
||||
$name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\'));
|
||||
|
||||
/** @var LaravelResponse $response */
|
||||
$response = response($result, 200);
|
||||
$response->header('Content-disposition', 'attachment; filename=' . $name)
|
||||
->header('Content-Type', 'application/json')
|
||||
->header('Content-Description', 'File Transfer')
|
||||
->header('Connection', 'Keep-Alive')
|
||||
->header('Expires', '0')
|
||||
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
|
||||
->header('Pragma', 'public')
|
||||
->header('Content-Length', strlen($result));
|
||||
|
||||
return $response;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 1. Upload a file.
|
||||
* General import index
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
@@ -136,160 +63,4 @@ class ImportController extends Controller
|
||||
return view('import.index', compact('subTitle', 'subTitleIcon', 'importFileTypes', 'defaultImportType'));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 2. It creates an Import Job. Stores the import.
|
||||
*
|
||||
* @param ImportUploadRequest $request
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function initialize(ImportUploadRequest $request)
|
||||
{
|
||||
Log::debug('Now in initialize()');
|
||||
|
||||
// create import job:
|
||||
$type = $request->get('import_file_type');
|
||||
$job = $this->repository->create($type);
|
||||
Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]);
|
||||
|
||||
// process file:
|
||||
$this->repository->processFile($job, $request->files->get('import_file'));
|
||||
|
||||
// process config, if present:
|
||||
if ($request->files->has('configuration_file')) {
|
||||
$this->repository->processConfiguration($job, $request->files->get('configuration_file'));
|
||||
}
|
||||
|
||||
$this->repository->updateStatus($job, 'initialized');
|
||||
|
||||
return redirect(route('import.configure', [$job->key]));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Show status of import job in JSON.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function json(ImportJob $job)
|
||||
{
|
||||
$result = [
|
||||
'started' => false,
|
||||
'finished' => false,
|
||||
'running' => false,
|
||||
'errors' => array_values($job->extended_status['errors']),
|
||||
'percentage' => 0,
|
||||
'show_percentage' => false,
|
||||
'steps' => $job->extended_status['steps'],
|
||||
'done' => $job->extended_status['done'],
|
||||
'statusText' => trans('firefly.import_status_job_' . $job->status),
|
||||
'status' => $job->status,
|
||||
'finishedText' => '',
|
||||
];
|
||||
|
||||
if ($job->extended_status['steps'] !== 0) {
|
||||
$result['percentage'] = round(($job->extended_status['done'] / $job->extended_status['steps']) * 100, 0);
|
||||
$result['show_percentage'] = true;
|
||||
}
|
||||
|
||||
if ($job->status === 'finished') {
|
||||
$tagId = $job->extended_status['tag'];
|
||||
/** @var TagRepositoryInterface $repository */
|
||||
$repository = app(TagRepositoryInterface::class);
|
||||
$tag = $repository->find($tagId);
|
||||
$result['finished'] = true;
|
||||
$result['finishedText'] = trans('firefly.import_status_finished_job', ['link' => route('tags.show', [$tag->id, 'all']), 'tag' => $tag->tag]);
|
||||
}
|
||||
|
||||
if ($job->status === 'running') {
|
||||
$result['started'] = true;
|
||||
$result['running'] = true;
|
||||
}
|
||||
|
||||
return Response::json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4. Save the configuration.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function postConfigure(Request $request, ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in postConfigure()', ['job' => $job->key]);
|
||||
$configurator = $this->makeConfigurator($job);
|
||||
|
||||
// is the job already configured?
|
||||
if ($configurator->isJobConfigured()) {
|
||||
return redirect(route('import.status', [$job->key]));
|
||||
}
|
||||
$data = $request->all();
|
||||
$configurator->configureJob($data);
|
||||
|
||||
// return to configure
|
||||
return redirect(route('import.configure', [$job->key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function start(ImportJob $job)
|
||||
{
|
||||
/** @var ImportRoutine $routine */
|
||||
$routine = app(ImportRoutine::class);
|
||||
$routine->setJob($job);
|
||||
$result = $routine->run();
|
||||
if ($result) {
|
||||
return Response::json(['run' => 'ok']);
|
||||
}
|
||||
|
||||
throw new FireflyException('Job did not complete succesfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
|
||||
*/
|
||||
public function status(ImportJob $job)
|
||||
{
|
||||
$statuses = ['configured', 'running', 'finished'];
|
||||
if (!in_array($job->status, $statuses)) {
|
||||
return redirect(route('import.configure', [$job->key]));
|
||||
}
|
||||
$subTitle = trans('firefly.import_status_sub_title');
|
||||
$subTitleIcon = 'fa-star';
|
||||
|
||||
return view('import.status', compact('job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return ConfiguratorInterface
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function makeConfigurator(ImportJob $job): ConfiguratorInterface
|
||||
{
|
||||
$type = $job->file_type;
|
||||
$key = sprintf('firefly.import_configurators.%s', $type);
|
||||
$className = config($key);
|
||||
if (is_null($className)) {
|
||||
throw new FireflyException('Cannot find configurator class for this job.'); // @codeCoverageIgnore
|
||||
}
|
||||
/** @var ConfiguratorInterface $configurator */
|
||||
$configurator = app($className);
|
||||
$configurator->setJob($job);
|
||||
|
||||
|
||||
return $configurator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Amount;
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
@@ -19,9 +20,9 @@ use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Log;
|
||||
use Navigation;
|
||||
use Preferences;
|
||||
use Session;
|
||||
|
||||
/**
|
||||
* Class JavascriptController
|
||||
@@ -88,11 +89,6 @@ class JavascriptController extends Controller
|
||||
*/
|
||||
public function variables(Request $request)
|
||||
{
|
||||
$picker = $this->getDateRangePicker();
|
||||
$start = Session::get('start');
|
||||
$end = Session::get('end');
|
||||
$linkTitle = sprintf('%s - %s', $start->formatLocalized($this->monthAndDayFormat), $end->formatLocalized($this->monthAndDayFormat));
|
||||
$firstDate = session('first')->format('Y-m-d');
|
||||
$localeconv = localeconv();
|
||||
$accounting = Amount::getJsConfig($localeconv);
|
||||
$localeconv = localeconv();
|
||||
@@ -100,15 +96,16 @@ class JavascriptController extends Controller
|
||||
$localeconv['frac_digits'] = $defaultCurrency->decimal_places;
|
||||
$pref = Preferences::get('language', config('firefly.default_language', 'en_US'));
|
||||
$lang = $pref->data;
|
||||
$data = [
|
||||
'picker' => $picker,
|
||||
'linkTitle' => $linkTitle,
|
||||
'firstDate' => $firstDate,
|
||||
'currencyCode' => Amount::getCurrencyCode(),
|
||||
'currencySymbol' => Amount::getCurrencySymbol(),
|
||||
'accounting' => $accounting,
|
||||
'localeconv' => $localeconv,
|
||||
'language' => $lang,
|
||||
$dateRange = $this->getDateRangeConfig();
|
||||
|
||||
$data = [
|
||||
'currencyCode' => Amount::getCurrencyCode(),
|
||||
'currencySymbol' => Amount::getCurrencySymbol(),
|
||||
'accounting' => $accounting,
|
||||
'localeconv' => $localeconv,
|
||||
'language' => $lang,
|
||||
'dateRangeTitle' => $dateRange['title'],
|
||||
'dateRangeConfig' => $dateRange['configuration'],
|
||||
];
|
||||
$request->session()->keep(['two-factor-secret']);
|
||||
|
||||
@@ -119,40 +116,73 @@ class JavascriptController extends Controller
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getDateRangePicker(): array
|
||||
private function getDateRangeConfig(): array
|
||||
{
|
||||
$viewRange = Preferences::get('viewRange', '1M')->data;
|
||||
$start = Session::get('start');
|
||||
$end = Session::get('end');
|
||||
$start = session('start');
|
||||
$end = session('end');
|
||||
$first = session('first');
|
||||
$title = sprintf('%s - %s', $start->formatLocalized($this->monthAndDayFormat), $end->formatLocalized($this->monthAndDayFormat));
|
||||
$isCustom = session('is_custom_range');
|
||||
$ranges = [
|
||||
// first range is the current range:
|
||||
$title => [$start, $end],
|
||||
];
|
||||
Log::debug(sprintf('viewRange is %s', $viewRange));
|
||||
|
||||
$prevStart = clone $start;
|
||||
$prevEnd = clone $start;
|
||||
$nextStart = clone $end;
|
||||
$nextEnd = clone $end;
|
||||
if ($viewRange === 'custom') {
|
||||
$days = $start->diffInDays($end);
|
||||
$prevStart->subDays($days);
|
||||
$nextEnd->addDays($days);
|
||||
unset($days);
|
||||
// get the format for the ranges:
|
||||
$format = $this->getFormatByRange($viewRange);
|
||||
|
||||
// when current range is a custom range, add the current period as the next range.
|
||||
if ($isCustom) {
|
||||
Log::debug('Custom is true.');
|
||||
$index = $start->formatLocalized($format);
|
||||
$customPeriodStart = Navigation::startOfPeriod($start, $viewRange);
|
||||
$customPeriodEnd = Navigation::endOfPeriod($customPeriodStart, $viewRange);
|
||||
$ranges[$index] = [$customPeriodStart, $customPeriodEnd];
|
||||
}
|
||||
// then add previous range and next range
|
||||
$previousDate = Navigation::subtractPeriod($start, $viewRange);
|
||||
$index = $previousDate->formatLocalized($format);
|
||||
$previousStart = Navigation::startOfPeriod($previousDate, $viewRange);
|
||||
$previousEnd = Navigation::endOfPeriod($previousStart, $viewRange);
|
||||
$ranges[$index] = [$previousStart, $previousEnd];
|
||||
|
||||
if ($viewRange !== 'custom') {
|
||||
$prevStart = Navigation::subtractPeriod($start, $viewRange);// subtract for previous period
|
||||
$prevEnd = Navigation::endOfPeriod($prevStart, $viewRange);
|
||||
$nextStart = Navigation::addPeriod($start, $viewRange, 0); // add for previous period
|
||||
$nextEnd = Navigation::endOfPeriod($nextStart, $viewRange);
|
||||
}
|
||||
$nextDate = Navigation::addPeriod($start, $viewRange, 0);
|
||||
$index = $nextDate->formatLocalized($format);
|
||||
$nextStart = Navigation::startOfPeriod($nextDate, $viewRange);
|
||||
$nextEnd = Navigation::endOfPeriod($nextStart, $viewRange);
|
||||
$ranges[$index] = [$nextStart, $nextEnd];
|
||||
|
||||
$ranges = [];
|
||||
$ranges['current'] = [$start->format('Y-m-d'), $end->format('Y-m-d')];
|
||||
$ranges['previous'] = [$prevStart->format('Y-m-d'), $prevEnd->format('Y-m-d')];
|
||||
$ranges['next'] = [$nextStart->format('Y-m-d'), $nextEnd->format('Y-m-d')];
|
||||
// everything
|
||||
$index = strval(trans('firefly.everything'));
|
||||
$ranges[$index] = [$first, new Carbon];
|
||||
|
||||
$return = [
|
||||
'title' => $title,
|
||||
'configuration' => [
|
||||
'apply' => strval(trans('firefly.apply')),
|
||||
'cancel' => strval(trans('firefly.cancel')),
|
||||
'from' => strval(trans('firefly.from')),
|
||||
'to' => strval(trans('firefly.to')),
|
||||
'customRange' => strval(trans('firefly.customRange')),
|
||||
'start' => $start->format('Y-m-d'),
|
||||
'end' => $end->format('Y-m-d'),
|
||||
'ranges' => $ranges,
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
return $return;
|
||||
|
||||
}
|
||||
|
||||
private function getFormatByRange(string $viewRange): string
|
||||
{
|
||||
switch ($viewRange) {
|
||||
default:
|
||||
throw new FireflyException('The date picker does not yet support "' . $viewRange . '".'); // @codeCoverageIgnore
|
||||
throw new FireflyException(sprintf('The date picker does not yet support "%s".', $viewRange)); // @codeCoverageIgnore
|
||||
case '1D':
|
||||
case 'custom':
|
||||
$format = (string)trans('config.month_and_day');
|
||||
@@ -174,18 +204,6 @@ class JavascriptController extends Controller
|
||||
break;
|
||||
}
|
||||
|
||||
$current = $start->formatLocalized($format);
|
||||
$next = $nextStart->formatLocalized($format);
|
||||
$prev = $prevStart->formatLocalized($format);
|
||||
|
||||
return [
|
||||
'start' => $start->format('Y-m-d'),
|
||||
'end' => $end->format('Y-m-d'),
|
||||
'current' => $current,
|
||||
'previous' => $prev,
|
||||
'next' => $next,
|
||||
'ranges' => $ranges,
|
||||
];
|
||||
return $format;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
171
app/Http/Controllers/Json/AutoCompleteController.php
Normal file
171
app/Http/Controllers/Json/AutoCompleteController.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/**
|
||||
* AutoCompleteController.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Json;
|
||||
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use Response;
|
||||
|
||||
/**
|
||||
* Class AutoCompleteController
|
||||
*
|
||||
* @package FireflyIII\Http\Controllers\Json
|
||||
*/
|
||||
class AutoCompleteController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns a JSON list of all accounts.
|
||||
*
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
*/
|
||||
public function allAccounts(AccountRepositoryInterface $repository)
|
||||
{
|
||||
$return = array_unique(
|
||||
$repository->getAccountsByType(
|
||||
[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET]
|
||||
)->pluck('name')->toArray()
|
||||
);
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalCollectorInterface $collector
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function allTransactionJournals(JournalCollectorInterface $collector)
|
||||
{
|
||||
$collector->setLimit(250)->setPage(1);
|
||||
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON list of all beneficiaries.
|
||||
*
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
*/
|
||||
public function expenseAccounts(AccountRepositoryInterface $repository)
|
||||
{
|
||||
$set = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]);
|
||||
$filtered = $set->filter(
|
||||
function (Account $account) {
|
||||
if ($account->active) {
|
||||
return $account;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
$return = array_unique($filtered->pluck('name')->toArray());
|
||||
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalCollectorInterface $collector
|
||||
*
|
||||
* @param TransactionJournal $except
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse|mixed
|
||||
*/
|
||||
public function journalsWithId(JournalCollectorInterface $collector, TransactionJournal $except)
|
||||
{
|
||||
|
||||
$cache = new CacheProperties;
|
||||
$cache->addProperty('recent-journals-id');
|
||||
|
||||
if ($cache->has()) {
|
||||
return $cache->get(); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$collector->setLimit(400)->setPage(1);
|
||||
$set = $collector->getJournals()->pluck('description', 'journal_id')->toArray();
|
||||
$return = [];
|
||||
foreach ($set as $id => $description) {
|
||||
$id = intval($id);
|
||||
if ($id !== $except->id) {
|
||||
$return[] = [
|
||||
'id' => $id,
|
||||
'name' => $id . ': ' . $description,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$cache->store($return);
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
*/
|
||||
public function revenueAccounts(AccountRepositoryInterface $repository)
|
||||
{
|
||||
$set = $repository->getAccountsByType([AccountType::REVENUE]);
|
||||
$filtered = $set->filter(
|
||||
function (Account $account) {
|
||||
if ($account->active) {
|
||||
return $account;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
$return = array_unique($filtered->pluck('name')->toArray());
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalCollectorInterface $collector
|
||||
* @param string $what
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function transactionJournals(JournalCollectorInterface $collector, string $what)
|
||||
{
|
||||
$type = config('firefly.transactionTypesByWhat.' . $what);
|
||||
$types = [$type];
|
||||
|
||||
$collector->setTypes($types)->setLimit(250)->setPage(1);
|
||||
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
}
|
||||
166
app/Http/Controllers/Json/IntroController.php
Normal file
166
app/Http/Controllers/Json/IntroController.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* IntroController.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Json;
|
||||
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use Log;
|
||||
use Response;
|
||||
|
||||
/**
|
||||
* Class IntroController
|
||||
*
|
||||
* @package FireflyIII\Http\Controllers\Json
|
||||
*/
|
||||
class IntroController
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string $specificPage
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getIntroSteps(string $route, string $specificPage = '')
|
||||
{
|
||||
$steps = $this->getBasicSteps($route);
|
||||
$specificSteps = $this->getSpecificSteps($route, $specificPage);
|
||||
if (count($specificSteps) === 0) {
|
||||
return Response::json($steps);
|
||||
}
|
||||
if ($this->hasOutroStep($route)) {
|
||||
// save last step:
|
||||
$lastStep = $steps[count($steps) - 1];
|
||||
// remove last step:
|
||||
array_pop($steps);
|
||||
// merge arrays and add last step again
|
||||
$steps = array_merge($steps, $specificSteps);
|
||||
$steps[] = $lastStep;
|
||||
|
||||
}
|
||||
if (!$this->hasOutroStep($route)) {
|
||||
$steps = array_merge($steps, $specificSteps);
|
||||
}
|
||||
|
||||
return Response::json($steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasOutroStep(string $route): bool
|
||||
{
|
||||
$routeKey = str_replace('.', '_', $route);
|
||||
$elements = config(sprintf('intro.%s', $routeKey));
|
||||
if (!is_array($elements)) {
|
||||
return false;
|
||||
}
|
||||
$keys = array_keys($elements);
|
||||
|
||||
return in_array('outro', $keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string $specialPage
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function postEnable(string $route, string $specialPage = '')
|
||||
{
|
||||
$route = str_replace('.', '_', $route);
|
||||
$key = 'shown_demo_' . $route;
|
||||
if ($specialPage !== '') {
|
||||
$key .= '_' . $specialPage;
|
||||
}
|
||||
Log::debug(sprintf('Going to mark the following route as NOT done: %s with special "%s" (%s)', $route, $specialPage, $key));
|
||||
Preferences::set($key, false);
|
||||
|
||||
return Response::json(['message' => trans('firefly.intro_boxes_after_refresh')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string $specialPage
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function postFinished(string $route, string $specialPage = '')
|
||||
{
|
||||
$key = 'shown_demo_' . $route;
|
||||
if ($specialPage !== '') {
|
||||
$key .= '_' . $specialPage;
|
||||
}
|
||||
Log::debug(sprintf('Going to mark the following route as done: %s with special "%s" (%s)', $route, $specialPage, $key));
|
||||
Preferences::set($key, true);
|
||||
|
||||
return Response::json(['result' => sprintf('Reported demo watched for route "%s".', $route)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getBasicSteps(string $route): array
|
||||
{
|
||||
$routeKey = str_replace('.', '_', $route);
|
||||
$elements = config(sprintf('intro.%s', $routeKey));
|
||||
$steps = [];
|
||||
if (is_array($elements) && count($elements) > 0) {
|
||||
foreach ($elements as $key => $options) {
|
||||
$currentStep = $options;
|
||||
|
||||
// get the text:
|
||||
$currentStep['intro'] = trans('intro.' . $route . '_' . $key);
|
||||
|
||||
|
||||
// save in array:
|
||||
$steps[] = $currentStep;
|
||||
}
|
||||
}
|
||||
|
||||
return $steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string $specificPage
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getSpecificSteps(string $route, string $specificPage): array
|
||||
{
|
||||
$steps = [];
|
||||
|
||||
// user is on page with specific instructions:
|
||||
if (strlen($specificPage) > 0) {
|
||||
$routeKey = str_replace('.', '_', $route);
|
||||
$elements = config(sprintf('intro.%s', $routeKey . '_' . $specificPage));
|
||||
if (is_array($elements) && count($elements) > 0) {
|
||||
foreach ($elements as $key => $options) {
|
||||
$currentStep = $options;
|
||||
|
||||
// get the text:
|
||||
$currentStep['intro'] = trans('intro.' . $route . '_' . $specificPage . '_' . $key);
|
||||
|
||||
// save in array:
|
||||
$steps[] = $currentStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $steps;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,11 +15,8 @@ namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Amount;
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
|
||||
@@ -27,7 +24,6 @@ use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
||||
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use Illuminate\Http\Request;
|
||||
use Preferences;
|
||||
use Response;
|
||||
|
||||
/**
|
||||
@@ -64,43 +60,6 @@ class JsonController extends Controller
|
||||
return Response::json(['html' => $view]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON list of all accounts.
|
||||
*
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
*/
|
||||
public function allAccounts(AccountRepositoryInterface $repository)
|
||||
{
|
||||
$return = array_unique(
|
||||
$repository->getAccountsByType(
|
||||
[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET]
|
||||
)->pluck('name')->toArray()
|
||||
);
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalCollectorInterface $collector
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function allTransactionJournals(JournalCollectorInterface $collector)
|
||||
{
|
||||
$collector->setLimit(100)->setPage(1);
|
||||
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BillRepositoryInterface $repository
|
||||
*
|
||||
@@ -237,46 +196,6 @@ class JsonController extends Controller
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function endTour()
|
||||
{
|
||||
Preferences::set('tour', false);
|
||||
|
||||
return Response::json('true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON list of all beneficiaries.
|
||||
*
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
*/
|
||||
public function expenseAccounts(AccountRepositoryInterface $repository)
|
||||
{
|
||||
$return = array_unique($repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY])->pluck('name')->toArray());
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
*/
|
||||
public function revenueAccounts(AccountRepositoryInterface $repository)
|
||||
{
|
||||
$return = array_unique($repository->getAccountsByType([AccountType::REVENUE])->pluck('name')->toArray());
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON list of all beneficiaries.
|
||||
*
|
||||
@@ -293,54 +212,6 @@ class JsonController extends Controller
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function tour()
|
||||
{
|
||||
$pref = Preferences::get('tour', true);
|
||||
if (!$pref) {
|
||||
throw new FireflyException('Cannot find preference for tour. Exit.'); // @codeCoverageIgnore
|
||||
}
|
||||
$headers = ['main-content', 'sidebar-toggle', 'account-menu', 'budget-menu', 'report-menu', 'transaction-menu', 'option-menu', 'main-content-end'];
|
||||
$steps = [];
|
||||
foreach ($headers as $header) {
|
||||
$steps[] = [
|
||||
'element' => '#' . $header,
|
||||
'title' => trans('help.' . $header . '-title'),
|
||||
'content' => trans('help.' . $header . '-text'),
|
||||
];
|
||||
}
|
||||
$steps[0]['orphan'] = true;// orphan and backdrop for first element.
|
||||
$steps[0]['backdrop'] = true;
|
||||
$steps[1]['placement'] = 'left';// sidebar position left:
|
||||
$steps[7]['orphan'] = true; // final in the center again.
|
||||
$steps[7]['backdrop'] = true;
|
||||
$template = view('json.tour')->render();
|
||||
|
||||
return Response::json(['steps' => $steps, 'template' => $template]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalCollectorInterface $collector
|
||||
* @param string $what
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function transactionJournals(JournalCollectorInterface $collector, string $what)
|
||||
{
|
||||
$type = config('firefly.transactionTypesByWhat.' . $what);
|
||||
$types = [$type];
|
||||
|
||||
$collector->setTypes($types)->setLimit(100)->setPage(1);
|
||||
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
|
||||
sort($return);
|
||||
|
||||
return Response::json($return);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalRepositoryInterface $repository
|
||||
*
|
||||
@@ -365,10 +236,11 @@ class JsonController extends Controller
|
||||
$keys = array_keys(config('firefly.rule-triggers'));
|
||||
$triggers = [];
|
||||
foreach ($keys as $key) {
|
||||
if ($key != 'user_action') {
|
||||
if ($key !== 'user_action') {
|
||||
$triggers[$key] = trans('firefly.rule_trigger_' . $key . '_choice');
|
||||
}
|
||||
}
|
||||
asort($triggers);
|
||||
|
||||
$view = view('rules.partials.trigger', compact('triggers', 'count'))->render();
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ class NewUserController extends Controller
|
||||
View::share('title', trans('firefly.welcome'));
|
||||
View::share('mainTitleIcon', 'fa-fire');
|
||||
|
||||
|
||||
$types = config('firefly.accountTypesByIdentifier.asset');
|
||||
$count = $repository->count($types);
|
||||
|
||||
@@ -74,30 +73,13 @@ class NewUserController extends Controller
|
||||
*/
|
||||
public function submit(NewUserFormRequest $request, AccountRepositoryInterface $repository)
|
||||
{
|
||||
$count = 1;
|
||||
// create normal asset account:
|
||||
$this->createAssetAccount($request, $repository);
|
||||
|
||||
// create savings account
|
||||
$savingBalance = strval($request->get('savings_balance')) === '' ? '0' : strval($request->get('savings_balance'));
|
||||
if (bccomp($savingBalance, '0') !== 0) {
|
||||
$this->createSavingsAccount($request, $repository);
|
||||
$count++;
|
||||
}
|
||||
$this->createSavingsAccount($request, $repository);
|
||||
|
||||
|
||||
// create credit card.
|
||||
$limit = strval($request->get('credit_card_limit')) === '' ? '0' : strval($request->get('credit_card_limit'));
|
||||
if (bccomp($limit, '0') !== 0) {
|
||||
$this->storeCreditCard($request, $repository);
|
||||
$count++;
|
||||
}
|
||||
$message = strval(trans('firefly.stored_new_accounts_new_user'));
|
||||
if ($count == 1) {
|
||||
$message = strval(trans('firefly.stored_new_account_new_user'));
|
||||
}
|
||||
|
||||
Session::flash('success', $message);
|
||||
Session::flash('success', strval(trans('firefly.stored_new_accounts_new_user')));
|
||||
Preferences::mark();
|
||||
|
||||
return redirect(route('index'));
|
||||
@@ -152,29 +134,4 @@ class NewUserController extends Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NewUserFormRequest $request
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function storeCreditCard(NewUserFormRequest $request, AccountRepositoryInterface $repository): bool
|
||||
{
|
||||
$creditAccount = [
|
||||
'name' => 'Credit card',
|
||||
'iban' => null,
|
||||
'accountType' => 'asset',
|
||||
'virtualBalance' => round($request->get('credit_card_limit'), 12),
|
||||
'active' => true,
|
||||
'accountRole' => 'ccAsset',
|
||||
'openingBalance' => null,
|
||||
'openingBalanceDate' => null,
|
||||
'openingBalanceCurrency' => intval($request->input('amount_currency_id_credit_card_limit')),
|
||||
'ccType' => 'monthlyFull',
|
||||
'ccMonthlyPaymentDate' => Carbon::now()->year . '-01-01',
|
||||
];
|
||||
$repository->store($creditAccount);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class PiggyBankController extends Controller
|
||||
/** @var Carbon $date */
|
||||
$date = session('end', Carbon::now()->endOfMonth());
|
||||
$leftOnAccount = $piggyBank->leftOnAccount($date);
|
||||
$savedSoFar = $piggyBank->currentRelevantRep()->currentamount?? '0';
|
||||
$savedSoFar = $piggyBank->currentRelevantRep()->currentamount ?? '0';
|
||||
$leftToSave = bcsub($piggyBank->targetamount, $savedSoFar);
|
||||
$maxAmount = min($leftOnAccount, $leftToSave);
|
||||
|
||||
|
||||
@@ -13,12 +13,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use ExpandedForm;
|
||||
use FireflyIII\Http\Requests\RuleFormRequest;
|
||||
use FireflyIII\Http\Requests\SelectTransactionsRequest;
|
||||
use FireflyIII\Http\Requests\TestRuleFormRequest;
|
||||
use FireflyIII\Jobs\ExecuteRuleOnExistingTransactions;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleAction;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
use FireflyIII\Models\RuleTrigger;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
|
||||
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
|
||||
use FireflyIII\Rules\TransactionMatcher;
|
||||
@@ -160,10 +166,13 @@ class RuleController extends Controller
|
||||
*/
|
||||
public function edit(Request $request, RuleRepositoryInterface $repository, Rule $rule)
|
||||
{
|
||||
$oldTriggers = $this->getCurrentTriggers($rule);
|
||||
$triggerCount = count($oldTriggers);
|
||||
$oldActions = $this->getCurrentActions($rule);
|
||||
$actionCount = count($oldActions);
|
||||
/** @var RuleGroupRepositoryInterface $ruleGroupRepository */
|
||||
$ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
|
||||
$oldTriggers = $this->getCurrentTriggers($rule);
|
||||
$triggerCount = count($oldTriggers);
|
||||
$oldActions = $this->getCurrentActions($rule);
|
||||
$actionCount = count($oldActions);
|
||||
$ruleGroups = ExpandedForm::makeSelectList($ruleGroupRepository->get());
|
||||
|
||||
// has old input?
|
||||
if ($request->old()) {
|
||||
@@ -185,7 +194,47 @@ class RuleController extends Controller
|
||||
Session::flash('gaEventCategory', 'rules');
|
||||
Session::flash('gaEventAction', 'edit-rule');
|
||||
|
||||
return view('rules.rule.edit', compact('rule', 'subTitle', 'primaryTrigger', 'oldTriggers', 'oldActions', 'triggerCount', 'actionCount'));
|
||||
return view(
|
||||
'rules.rule.edit', compact(
|
||||
'rule', 'subTitle',
|
||||
'primaryTrigger', 'oldTriggers', 'oldActions', 'triggerCount', 'actionCount', 'ruleGroups'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given rule on a set of existing transactions
|
||||
*
|
||||
* @param SelectTransactionsRequest $request
|
||||
* @param AccountRepositoryInterface $repository
|
||||
* @param Rule $rule
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @internal param RuleGroup $ruleGroup
|
||||
*/
|
||||
public function execute(SelectTransactionsRequest $request, AccountRepositoryInterface $repository, Rule $rule)
|
||||
{
|
||||
// Get parameters specified by the user
|
||||
$accounts = $repository->getAccountsById($request->get('accounts'));
|
||||
$startDate = new Carbon($request->get('start_date'));
|
||||
$endDate = new Carbon($request->get('end_date'));
|
||||
|
||||
// Create a job to do the work asynchronously
|
||||
$job = new ExecuteRuleOnExistingTransactions($rule);
|
||||
|
||||
// Apply parameters to the job
|
||||
$job->setUser(auth()->user());
|
||||
$job->setAccounts($accounts);
|
||||
$job->setStartDate($startDate);
|
||||
$job->setEndDate($endDate);
|
||||
|
||||
// Dispatch a new job to execute it in a queue
|
||||
$this->dispatch($job);
|
||||
|
||||
// Tell the user that the job is queued
|
||||
Session::flash('success', strval(trans('firefly.applied_rule_selection', ['title' => $rule->title])));
|
||||
|
||||
return redirect()->route('rules.index');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,6 +287,25 @@ class RuleController extends Controller
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccountRepositoryInterface $repository
|
||||
* @param Rule $rule
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function selectTransactions(AccountRepositoryInterface $repository, Rule $rule)
|
||||
{
|
||||
// does the user have shared accounts?
|
||||
$accounts = $repository->getAccountsByType([AccountType::ASSET]);
|
||||
$accountList = ExpandedForm::makeSelectList($accounts);
|
||||
$checkedAccounts = array_keys($accountList);
|
||||
$first = session('first')->format('Y-m-d');
|
||||
$today = Carbon::create()->format('Y-m-d');
|
||||
$subTitle = (string)trans('firefly.apply_rule_selection', ['title' => $rule->title]);
|
||||
|
||||
return view('rules.rule.select-transactions', compact('checkedAccounts', 'accountList', 'first', 'today', 'rule', 'subTitle'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RuleFormRequest $request
|
||||
* @param RuleRepositoryInterface $repository
|
||||
@@ -282,7 +350,7 @@ class RuleController extends Controller
|
||||
// build trigger array from response
|
||||
$triggers = $this->getValidTriggerList($request);
|
||||
|
||||
if (count($triggers) == 0) {
|
||||
if (count($triggers) === 0) {
|
||||
return Response::json(['html' => '', 'warning' => trans('firefly.warning_no_valid_triggers')]);
|
||||
}
|
||||
|
||||
@@ -294,14 +362,61 @@ class RuleController extends Controller
|
||||
$matcher->setLimit($limit);
|
||||
$matcher->setRange($range);
|
||||
$matcher->setTriggers($triggers);
|
||||
$matchingTransactions = $matcher->findMatchingTransactions();
|
||||
$matchingTransactions = $matcher->findTransactionsByTriggers();
|
||||
|
||||
// Warn the user if only a subset of transactions is returned
|
||||
$warning = '';
|
||||
if (count($matchingTransactions) == $limit) {
|
||||
if (count($matchingTransactions) === $limit) {
|
||||
$warning = trans('firefly.warning_transaction_subset', ['max_num_transactions' => $limit]);
|
||||
}
|
||||
if (count($matchingTransactions) == 0) {
|
||||
if (count($matchingTransactions) === 0) {
|
||||
$warning = trans('firefly.warning_no_matching_transactions', ['num_transactions' => $range]);
|
||||
}
|
||||
|
||||
// Return json response
|
||||
$view = view('list.journals-tiny', ['transactions' => $matchingTransactions])->render();
|
||||
|
||||
return Response::json(['html' => $view, 'warning' => $warning]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows the user to test a certain set of rule triggers. The rule triggers are grabbed from
|
||||
* the rule itself.
|
||||
*
|
||||
* This method will parse and validate those rules and create a "TransactionMatcher" which will attempt
|
||||
* to find transaction journals matching the users input. A maximum range of transactions to try (range) and
|
||||
* a maximum number of transactions to return (limit) are set as well.
|
||||
*
|
||||
*
|
||||
* @param Rule $rule
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function testTriggersByRule(Rule $rule)
|
||||
{
|
||||
|
||||
$triggers = $rule->ruleTriggers;
|
||||
|
||||
if (count($triggers) === 0) {
|
||||
return Response::json(['html' => '', 'warning' => trans('firefly.warning_no_valid_triggers')]);
|
||||
}
|
||||
|
||||
$limit = config('firefly.test-triggers.limit');
|
||||
$range = config('firefly.test-triggers.range');
|
||||
|
||||
/** @var TransactionMatcher $matcher */
|
||||
$matcher = app(TransactionMatcher::class);
|
||||
$matcher->setLimit($limit);
|
||||
$matcher->setRange($range);
|
||||
$matcher->setRule($rule);
|
||||
$matchingTransactions = $matcher->findTransactionsByRule();
|
||||
|
||||
// Warn the user if only a subset of transactions is returned
|
||||
$warning = '';
|
||||
if (count($matchingTransactions) === $limit) {
|
||||
$warning = trans('firefly.warning_transaction_subset', ['max_num_transactions' => $limit]);
|
||||
}
|
||||
if (count($matchingTransactions) === 0) {
|
||||
$warning = trans('firefly.warning_no_matching_transactions', ['num_transactions' => $range]);
|
||||
}
|
||||
|
||||
@@ -416,7 +531,7 @@ class RuleController extends Controller
|
||||
$actions[] = view(
|
||||
'rules.partials.action',
|
||||
[
|
||||
'oldTrigger' => $entry->action_type,
|
||||
'oldAction' => $entry->action_type,
|
||||
'oldValue' => $entry->action_value,
|
||||
'oldChecked' => $entry->stop_processing,
|
||||
'count' => $count,
|
||||
@@ -440,7 +555,7 @@ class RuleController extends Controller
|
||||
|
||||
/** @var RuleTrigger $entry */
|
||||
foreach ($rule->ruleTriggers as $entry) {
|
||||
if ($entry->trigger_type != 'user_action') {
|
||||
if ($entry->trigger_type !== 'user_action') {
|
||||
$count = ($index + 1);
|
||||
$triggers[] = view(
|
||||
'rules.partials.trigger',
|
||||
|
||||
@@ -178,7 +178,7 @@ class RuleGroupController extends Controller
|
||||
$this->dispatch($job);
|
||||
|
||||
// Tell the user that the job is queued
|
||||
Session::flash('success', strval(trans('firefly.executed_group_on_existing_transactions', ['title' => $ruleGroup->title])));
|
||||
Session::flash('success', strval(trans('firefly.applied_rule_group_selection', ['title' => $ruleGroup->title])));
|
||||
|
||||
return redirect()->route('rules.index');
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class RuleGroupController extends Controller
|
||||
$checkedAccounts = array_keys($accountList);
|
||||
$first = session('first')->format('Y-m-d');
|
||||
$today = Carbon::create()->format('Y-m-d');
|
||||
$subTitle = (string)trans('firefly.execute_on_existing_transactions');
|
||||
$subTitle = (string)trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]);
|
||||
|
||||
return view('rules.rule-group.select-transactions', compact('checkedAccounts', 'accountList', 'first', 'today', 'ruleGroup', 'subTitle'));
|
||||
}
|
||||
@@ -246,14 +246,14 @@ class RuleGroupController extends Controller
|
||||
* @param RuleGroupRepositoryInterface $repository
|
||||
* @param RuleGroup $ruleGroup
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function update(RuleGroupFormRequest $request, RuleGroupRepositoryInterface $repository, RuleGroup $ruleGroup)
|
||||
{
|
||||
$data = [
|
||||
'title' => $request->input('title'),
|
||||
'description' => $request->input('description'),
|
||||
'active' => intval($request->input('active')) == 1,
|
||||
'active' => intval($request->input('active')) === 1,
|
||||
];
|
||||
|
||||
$repository->update($ruleGroup, $data);
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use FireflyIII\Support\Search\SearchInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Response;
|
||||
use View;
|
||||
|
||||
/**
|
||||
@@ -51,47 +51,29 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function index(Request $request, SearchInterface $searcher)
|
||||
{
|
||||
// yes, hard coded values:
|
||||
$minSearchLen = 1;
|
||||
$limit = 20;
|
||||
$fullQuery = $request->get('q');
|
||||
|
||||
// ui stuff:
|
||||
$subTitle = '';
|
||||
// parse search terms:
|
||||
$searcher->parseQuery($fullQuery);
|
||||
$query = $searcher->getWordsAsString();
|
||||
$subTitle = trans('breadcrumbs.search_result', ['query' => $query]);
|
||||
|
||||
// query stuff
|
||||
$query = null;
|
||||
$result = [];
|
||||
$rawQuery = $request->get('q');
|
||||
$hasModifiers = true;
|
||||
$modifiers = [];
|
||||
return view('search.index', compact('query', 'fullQuery', 'subTitle'));
|
||||
}
|
||||
|
||||
if (!is_null($request->get('q')) && strlen($request->get('q')) >= $minSearchLen) {
|
||||
// parse query, find modifiers:
|
||||
// set limit for search
|
||||
$searcher->setLimit($limit);
|
||||
$searcher->parseQuery($request->get('q'));
|
||||
public function search(Request $request, SearchInterface $searcher)
|
||||
{
|
||||
$fullQuery = $request->get('query');
|
||||
|
||||
$transactions = $searcher->searchTransactions();
|
||||
$accounts = new Collection;
|
||||
$categories = new Collection;
|
||||
$tags = new Collection;
|
||||
$budgets = new Collection;
|
||||
// parse search terms:
|
||||
$searcher->parseQuery($fullQuery);
|
||||
$searcher->setLimit(20);
|
||||
$transactions = $searcher->searchTransactions();
|
||||
$html = view('search.search', compact('transactions'))->render();
|
||||
|
||||
// no special search thing?
|
||||
if (!$searcher->hasModifiers()) {
|
||||
$hasModifiers = false;
|
||||
$accounts = $searcher->searchAccounts();
|
||||
$categories = $searcher->searchCategories();
|
||||
$budgets = $searcher->searchBudgets();
|
||||
$tags = $searcher->searchTags();
|
||||
}
|
||||
$query = $searcher->getWordsAsString();
|
||||
$subTitle = trans('firefly.search_results_for', ['query' => $query]);
|
||||
$result = ['transactions' => $transactions, 'accounts' => $accounts, 'categories' => $categories, 'budgets' => $budgets, 'tags' => $tags];
|
||||
return Response::json(['count' => $transactions->count(), 'html' => $html]);
|
||||
|
||||
}
|
||||
|
||||
return view('search.index', compact('rawQuery', 'hasModifiers', 'modifiers', 'subTitle', 'limit', 'query', 'result'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ use FireflyIII\Repositories\Tag\TagRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
use Navigation;
|
||||
use Preferences;
|
||||
use Session;
|
||||
@@ -44,9 +43,6 @@ use View;
|
||||
class TagController extends Controller
|
||||
{
|
||||
|
||||
/** @var array */
|
||||
public $tagOptions = [];
|
||||
|
||||
/** @var TagRepositoryInterface */
|
||||
protected $repository;
|
||||
|
||||
@@ -61,17 +57,8 @@ class TagController extends Controller
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->repository = app(TagRepositoryInterface::class);
|
||||
$this->tagOptions = [
|
||||
'nothing' => trans('firefly.regular_tag'),
|
||||
'balancingAct' => trans('firefly.balancing_act'),
|
||||
'advancePayment' => trans('firefly.advance_payment'),
|
||||
];
|
||||
|
||||
|
||||
View::share('title', strval(trans('firefly.tags')));
|
||||
View::share('mainTitleIcon', 'fa-tags');
|
||||
View::share('tagOptions', $this->tagOptions);
|
||||
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
@@ -79,22 +66,16 @@ class TagController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* Create a new tag.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function create(Request $request)
|
||||
public function create()
|
||||
{
|
||||
$subTitle = trans('firefly.new_tag');
|
||||
$subTitleIcon = 'fa-tag';
|
||||
$apiKey = env('GOOGLE_MAPS_API_KEY', '');
|
||||
|
||||
$preFilled = [
|
||||
'tagMode' => 'nothing',
|
||||
];
|
||||
if (!$request->old('tagMode')) {
|
||||
Session::flash('preFilled', $preFilled);
|
||||
}
|
||||
// put previous url in session if not redirect from store (not "create another").
|
||||
if (session('tags.create.fromStore') !== true) {
|
||||
$this->rememberPreviousUri('tags.create.uri');
|
||||
@@ -107,6 +88,8 @@ class TagController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tag
|
||||
*
|
||||
* @param Tag $tag
|
||||
*
|
||||
* @return View
|
||||
@@ -141,38 +124,18 @@ class TagController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* Edit a tag
|
||||
*
|
||||
* @param TagRepositoryInterface $repository
|
||||
* @param Tag $tag
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function edit(Tag $tag, TagRepositoryInterface $repository)
|
||||
public function edit(Tag $tag)
|
||||
{
|
||||
$subTitle = trans('firefly.edit_tag', ['tag' => $tag->tag]);
|
||||
$subTitleIcon = 'fa-tag';
|
||||
$apiKey = env('GOOGLE_MAPS_API_KEY', '');
|
||||
|
||||
/*
|
||||
* Default tag options (again)
|
||||
*/
|
||||
$tagOptions = $this->tagOptions;
|
||||
|
||||
/*
|
||||
* Can this tag become another type?
|
||||
*/
|
||||
$allowAdvance = $repository->tagAllowAdvance($tag);
|
||||
$allowToBalancingAct = $repository->tagAllowBalancing($tag);
|
||||
|
||||
// edit tag options:
|
||||
if ($allowAdvance === false) {
|
||||
unset($tagOptions['advancePayment']);
|
||||
}
|
||||
if ($allowToBalancingAct === false) {
|
||||
unset($tagOptions['balancingAct']);
|
||||
}
|
||||
|
||||
|
||||
// put previous url in session if not redirect from store (not "return_to_edit").
|
||||
if (session('tags.edit.fromUpdate') !== true) {
|
||||
$this->rememberPreviousUri('tags.edit.uri');
|
||||
@@ -181,46 +144,34 @@ class TagController extends Controller
|
||||
Session::flash('gaEventCategory', 'tags');
|
||||
Session::flash('gaEventAction', 'edit');
|
||||
|
||||
return view('tags.edit', compact('tag', 'subTitle', 'subTitleIcon', 'tagOptions', 'apiKey'));
|
||||
return view('tags.edit', compact('tag', 'subTitle', 'subTitleIcon', 'apiKey'));
|
||||
}
|
||||
|
||||
/**
|
||||
* View all tags
|
||||
*
|
||||
* @param TagRepositoryInterface $repository
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function index(TagRepositoryInterface $repository)
|
||||
{
|
||||
$title = 'Tags';
|
||||
$mainTitleIcon = 'fa-tags';
|
||||
$types = ['nothing', 'balancingAct', 'advancePayment'];
|
||||
$count = $repository->count();
|
||||
|
||||
// loop each types and get the tags, group them by year.
|
||||
$collection = [];
|
||||
foreach ($types as $type) {
|
||||
// collect tags by year:
|
||||
/** @var Carbon $start */
|
||||
$start = clone(session('first'));
|
||||
$now = new Carbon;
|
||||
$clouds = [];
|
||||
$clouds['no-date'] = $repository->tagCloud(null);
|
||||
while ($now > $start) {
|
||||
$year = $now->year;
|
||||
$clouds[$year] = $repository->tagCloud($year);
|
||||
|
||||
/** @var Collection $tags */
|
||||
$tags = $repository->getByType($type);
|
||||
$tags = $tags->sortBy(
|
||||
function (Tag $tag) {
|
||||
$date = !is_null($tag->date) ? $tag->date->format('Ymd') : '000000';
|
||||
|
||||
return strtolower($date . $tag->tag);
|
||||
}
|
||||
);
|
||||
|
||||
/** @var Tag $tag */
|
||||
foreach ($tags as $tag) {
|
||||
|
||||
$year = is_null($tag->date) ? trans('firefly.no_year') : $tag->date->year;
|
||||
$monthFormatted = is_null($tag->date) ? trans('firefly.no_month') : $tag->date->formatLocalized($this->monthFormat);
|
||||
|
||||
$collection[$type][$year][$monthFormatted][] = $tag;
|
||||
}
|
||||
$now->subYear();
|
||||
}
|
||||
$count = $repository->count();
|
||||
|
||||
return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'collection', 'count'));
|
||||
return view('tags.index', compact('clouds', 'count'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,17 +187,15 @@ class TagController extends Controller
|
||||
// default values:
|
||||
$subTitle = $tag->tag;
|
||||
$subTitleIcon = 'fa-tag';
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
$count = 0;
|
||||
$loop = 0;
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = null;
|
||||
$end = null;
|
||||
$periods = new Collection;
|
||||
$apiKey = env('GOOGLE_MAPS_API_KEY', '');
|
||||
$sum = '0';
|
||||
$path = 'tags/show/' . $tag->id;
|
||||
$path = route('tags.show', [$tag->id]);
|
||||
|
||||
|
||||
// prep for "all" view.
|
||||
@@ -254,8 +203,9 @@ class TagController extends Controller
|
||||
$subTitle = trans('firefly.all_journals_for_tag', ['tag' => $tag->tag]);
|
||||
$start = $repository->firstUseDate($tag);
|
||||
$end = new Carbon;
|
||||
$sum = $repository->sumOfTag($tag);
|
||||
$path = 'tags/show/' . $tag->id . '/all';
|
||||
$sum = $repository->sumOfTag($tag, null, null);
|
||||
$result = $repository->resultOfTag($tag, null, null);
|
||||
$path = route('tags.show', [$tag->id, 'all']);
|
||||
}
|
||||
|
||||
// prep for "specific date" view.
|
||||
@@ -269,6 +219,8 @@ class TagController extends Controller
|
||||
);
|
||||
$periods = $this->getPeriodOverview($tag);
|
||||
$sum = $repository->sumOfTag($tag, $start, $end);
|
||||
$result = $repository->resultOfTag($tag, $start, $end);
|
||||
$path = route('tags.show', [$tag->id, $moment]);
|
||||
}
|
||||
|
||||
// prep for current period
|
||||
@@ -276,42 +228,25 @@ class TagController extends Controller
|
||||
$start = clone session('start', Navigation::startOfPeriod(new Carbon, $range));
|
||||
$end = clone session('end', Navigation::endOfPeriod(new Carbon, $range));
|
||||
$periods = $this->getPeriodOverview($tag);
|
||||
$subTitle = trans(
|
||||
'firefly.journals_in_period_for_tag',
|
||||
['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
// grab journals, but be prepared to jump a period back to get the right ones:
|
||||
Log::info('Now at tag loop start.');
|
||||
while ($count === 0 && $loop < 3) {
|
||||
$loop++;
|
||||
Log::info(sprintf('Count is zero, search for journals between %s and %s (pagesize %d, page %d).', $start, $end, $pageSize, $page));
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
|
||||
->setTag($tag)->withBudgetInformation()->withCategoryInformation()->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath($path);
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0 && $loop < 3) {
|
||||
$start->subDay();
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
}
|
||||
}
|
||||
|
||||
if ($moment != 'all' && $loop > 1) {
|
||||
$sum = $repository->sumOfTag($tag, $start, $end);
|
||||
$result = $repository->resultOfTag($tag, $start, $end);
|
||||
$subTitle = trans(
|
||||
'firefly.journals_in_period_for_tag',
|
||||
['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
|
||||
return view('tags.show', compact('apiKey', 'tag', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end', 'moment'));
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
|
||||
->setTag($tag)->withBudgetInformation()->withCategoryInformation()->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath($path);
|
||||
|
||||
|
||||
return view('tags.show', compact('apiKey', 'tag', 'result', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end', 'moment'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param TagFormRequest $request
|
||||
*
|
||||
@@ -408,4 +343,6 @@ class TagController extends Controller
|
||||
return $collection;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
143
app/Http/Controllers/Transaction/LinkController.php
Normal file
143
app/Http/Controllers/Transaction/LinkController.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/**
|
||||
* LinkController.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Transaction;
|
||||
|
||||
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Http\Requests\JournalLinkRequest;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionJournalLink;
|
||||
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use Log;
|
||||
use Preferences;
|
||||
use Session;
|
||||
use URL;
|
||||
use View;
|
||||
|
||||
/**
|
||||
* Class LinkController
|
||||
*
|
||||
* @package FireflyIII\Http\Controllers\Transaction
|
||||
*/
|
||||
class LinkController extends Controller
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
// some useful repositories:
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
View::share('title', trans('firefly.transactions'));
|
||||
View::share('mainTitleIcon', 'fa-repeat');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param TransactionJournalLink $link
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function delete(TransactionJournalLink $link)
|
||||
{
|
||||
$subTitleIcon = 'fa-link';
|
||||
$subTitle = trans('breadcrumbs.delete_journal_link');
|
||||
$this->rememberPreviousUri('journal_links.delete.uri');
|
||||
|
||||
return view('transactions.links.delete', compact('link', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LinkTypeRepositoryInterface $repository
|
||||
* @param TransactionJournalLink $link
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function destroy(LinkTypeRepositoryInterface $repository, TransactionJournalLink $link)
|
||||
{
|
||||
$repository->destroyLink($link);
|
||||
|
||||
Session::flash('success', strval(trans('firefly.deleted_link')));
|
||||
Preferences::mark();
|
||||
|
||||
return redirect(strval(session('journal_links.delete.uri')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalLinkRequest $request
|
||||
* @param LinkTypeRepositoryInterface $repository
|
||||
* @param JournalRepositoryInterface $journalRepository
|
||||
* @param TransactionJournal $journal
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function store(
|
||||
JournalLinkRequest $request, LinkTypeRepositoryInterface $repository, JournalRepositoryInterface $journalRepository, TransactionJournal $journal
|
||||
) {
|
||||
$linkInfo = $request->getLinkInfo();
|
||||
$linkType = $repository->find($linkInfo['link_type_id']);
|
||||
$other = $journalRepository->find($linkInfo['transaction_journal_id']);
|
||||
$alreadyLinked = $repository->findLink($journal, $other);
|
||||
if ($alreadyLinked) {
|
||||
Session::flash('error', trans('firefly.journals_error_linked'));
|
||||
|
||||
return redirect(route('transactions.show', [$journal->id]));
|
||||
}
|
||||
Log::debug(sprintf('Journal is %d, opposing is %d', $journal->id, $other->id));
|
||||
|
||||
$journalLink = new TransactionJournalLink;
|
||||
$journalLink->linkType()->associate($linkType);
|
||||
if ($linkInfo['direction'] === 'inward') {
|
||||
Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->inward, $other->id, $journal->id));
|
||||
$journalLink->source()->associate($other);
|
||||
$journalLink->destination()->associate($journal);
|
||||
}
|
||||
|
||||
if ($linkInfo['direction'] === 'outward') {
|
||||
Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->outward, $journal->id, $other->id));
|
||||
$journalLink->source()->associate($journal);
|
||||
$journalLink->destination()->associate($other);
|
||||
}
|
||||
|
||||
$journalLink->comment = $linkInfo['comments'];
|
||||
$journalLink->save();
|
||||
Session::flash('success', trans('firefly.journals_linked'));
|
||||
|
||||
return redirect(route('transactions.show', [$journal->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LinkTypeRepositoryInterface $repository
|
||||
* @param TransactionJournalLink $link
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function switch(LinkTypeRepositoryInterface $repository, TransactionJournalLink $link)
|
||||
{
|
||||
|
||||
$repository->switchLink($link);
|
||||
|
||||
return redirect(URL::previous());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -86,7 +86,7 @@ class MassController extends Controller
|
||||
foreach ($ids as $journalId) {
|
||||
/** @var TransactionJournal $journal */
|
||||
$journal = $repository->find(intval($journalId));
|
||||
if (!is_null($journal->id) && $journalId == $journal->id) {
|
||||
if (!is_null($journal->id) && intval($journalId) === $journal->id) {
|
||||
$set->push($journal);
|
||||
}
|
||||
}
|
||||
@@ -131,12 +131,11 @@ class MassController extends Controller
|
||||
$filtered = new Collection;
|
||||
$messages = [];
|
||||
/**
|
||||
* @var int $index
|
||||
* @var TransactionJournal $journal
|
||||
*/
|
||||
foreach ($journals as $index => $journal) {
|
||||
$sources = $journal->sourceAccountList($journal);
|
||||
$destinations = $journal->destinationAccountList($journal);
|
||||
foreach ($journals as $journal) {
|
||||
$sources = $journal->sourceAccountList();
|
||||
$destinations = $journal->destinationAccountList();
|
||||
if ($sources->count() > 1) {
|
||||
$messages[] = trans('firefly.cannot_edit_multiple_source', ['description' => $journal->description, 'id' => $journal->id]);
|
||||
continue;
|
||||
@@ -213,11 +212,11 @@ class MassController extends Controller
|
||||
if ($journal) {
|
||||
// get optional fields:
|
||||
$what = strtolower($journal->transactionTypeStr());
|
||||
$sourceAccountId = $request->get('source_account_id')[$journal->id] ?? 0;
|
||||
$sourceAccountId = $request->get('source_account_id')[$journal->id] ?? 0;
|
||||
$sourceAccountName = $request->get('source_account_name')[$journal->id] ?? '';
|
||||
$destAccountId = $request->get('destination_account_id')[$journal->id] ?? 0;
|
||||
$destAccountId = $request->get('destination_account_id')[$journal->id] ?? 0;
|
||||
$destAccountName = $request->get('destination_account_name')[$journal->id] ?? '';
|
||||
$budgetId = $request->get('budget_id')[$journal->id] ?? 0;
|
||||
$budgetId = $request->get('budget_id')[$journal->id] ?? 0;
|
||||
$category = $request->get('category')[$journal->id];
|
||||
$tags = $journal->tags->pluck('tag')->toArray();
|
||||
$amount = round($request->get('amount')[$journal->id], 12);
|
||||
|
||||
@@ -14,12 +14,14 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Controllers\Transaction;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use ExpandedForm;
|
||||
use FireflyIII\Events\StoredTransactionJournal;
|
||||
use FireflyIII\Events\UpdatedTransactionJournal;
|
||||
use FireflyIII\Helpers\Attachments\AttachmentHelperInterface;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Http\Requests\JournalFormRequest;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
@@ -115,7 +117,7 @@ class SingleController extends Controller
|
||||
'foreign_amount' => $foreignAmount,
|
||||
'native_amount' => $foreignAmount,
|
||||
'amount_currency_id_amount' => $transaction->foreign_currency_id ?? 0,
|
||||
'date' => $journal->date->format('Y-m-d'),
|
||||
'date' => (new Carbon())->format('Y-m-d'),
|
||||
'budget_id' => $budgetId,
|
||||
'category' => $categoryName,
|
||||
'tags' => $tags,
|
||||
@@ -142,7 +144,7 @@ class SingleController extends Controller
|
||||
{
|
||||
$what = strtolower($what);
|
||||
$uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size')));
|
||||
$assetAccounts = ExpandedForm::makeSelectList($this->accounts->getActiveAccountsByType([AccountType::DEFAULT, AccountType::ASSET]));
|
||||
$assetAccounts = $this->groupedActiveAccountList();
|
||||
$budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets());
|
||||
$piggyBanks = $this->piggyBanks->getPiggyBanksWithAmount();
|
||||
$piggies = ExpandedForm::makeSelectListWithEmpty($piggyBanks);
|
||||
@@ -174,7 +176,7 @@ class SingleController extends Controller
|
||||
*
|
||||
* @param TransactionJournal $journal
|
||||
*
|
||||
* @return View
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
|
||||
*/
|
||||
public function delete(TransactionJournal $journal)
|
||||
{
|
||||
@@ -199,10 +201,10 @@ class SingleController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalRepositoryInterface $repository
|
||||
* @param TransactionJournal $transactionJournal
|
||||
* @param TransactionJournal $transactionJournal
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @internal param JournalRepositoryInterface $repository
|
||||
*/
|
||||
public function destroy(TransactionJournal $transactionJournal)
|
||||
{
|
||||
@@ -238,7 +240,7 @@ class SingleController extends Controller
|
||||
}
|
||||
|
||||
$what = strtolower($journal->transactionTypeStr());
|
||||
$assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]));
|
||||
$assetAccounts = $this->groupedAccountList();
|
||||
$budgetList = ExpandedForm::makeSelectListWithEmpty($this->budgets->getBudgets());
|
||||
|
||||
// view related code
|
||||
@@ -408,6 +410,46 @@ class SingleController extends Controller
|
||||
return redirect($this->getPreviousUri('transactions.edit.uri'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function groupedAccountList(): array
|
||||
{
|
||||
$accounts = $this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
|
||||
$return = [];
|
||||
/** @var Account $account */
|
||||
foreach ($accounts as $account) {
|
||||
$type = $account->getMeta('accountRole');
|
||||
if (strlen($type) === 0) {
|
||||
$type = 'no_account_type';
|
||||
}
|
||||
$key = strval(trans('firefly.opt_group_' . $type));
|
||||
$return[$key][$account->id] = $account->name;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function groupedActiveAccountList(): array
|
||||
{
|
||||
$accounts = $this->accounts->getActiveAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
|
||||
$return = [];
|
||||
/** @var Account $account */
|
||||
foreach ($accounts as $account) {
|
||||
$type = $account->getMeta('accountRole');
|
||||
if (strlen($type) === 0) {
|
||||
$type = 'no_account_type';
|
||||
}
|
||||
$key = strval(trans('firefly.opt_group_' . $type));
|
||||
$return[$key][$account->id] = $account->name;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
*
|
||||
|
||||
@@ -18,6 +18,8 @@ use ExpandedForm;
|
||||
use FireflyIII\Events\UpdatedTransactionJournal;
|
||||
use FireflyIII\Helpers\Attachments\AttachmentHelperInterface;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Http\Requests\SplitJournalFormRequest;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
@@ -94,13 +96,23 @@ class SplitController extends Controller
|
||||
|
||||
$uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size')));
|
||||
$currencies = $this->currencies->get();
|
||||
$assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]));
|
||||
$accountList = $this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
|
||||
$assetAccounts = ExpandedForm::makeSelectList($accountList);
|
||||
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
|
||||
$budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets());
|
||||
$preFilled = $this->arrayFromJournal($request, $journal);
|
||||
$subTitle = trans('breadcrumbs.edit_journal', ['description' => $journal->description]);
|
||||
$subTitleIcon = 'fa-pencil';
|
||||
|
||||
$accountArray = [];
|
||||
// account array to display currency info:
|
||||
/** @var Account $account */
|
||||
foreach ($accountList as $account) {
|
||||
$accountArray[$account->id] = $account;
|
||||
$accountArray[$account->id]['currency_id'] = intval($account->getMeta('currency_id'));
|
||||
}
|
||||
|
||||
|
||||
Session::flash('gaEventCategory', 'transactions');
|
||||
Session::flash('gaEventAction', 'edit-split-' . $preFilled['what']);
|
||||
|
||||
@@ -114,26 +126,25 @@ class SplitController extends Controller
|
||||
'transactions.split.edit',
|
||||
compact(
|
||||
'subTitleIcon', 'currencies', 'optionalFields',
|
||||
'preFilled', 'subTitle', 'amount', 'sourceAccounts', 'uploadSize', 'destinationAccounts', 'assetAccounts',
|
||||
'budgets', 'journal'
|
||||
'preFilled', 'subTitle', 'uploadSize', 'assetAccounts',
|
||||
'budgets', 'journal', 'accountArray', 'previous'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param SplitJournalFormRequest $request
|
||||
* @param JournalRepositoryInterface $repository
|
||||
* @param TransactionJournal $journal
|
||||
*
|
||||
* @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function update(Request $request, JournalRepositoryInterface $repository, TransactionJournal $journal)
|
||||
public function update(SplitJournalFormRequest $request, JournalRepositoryInterface $repository, TransactionJournal $journal)
|
||||
{
|
||||
if ($this->isOpeningBalance($journal)) {
|
||||
return $this->redirectToAccount($journal);
|
||||
}
|
||||
|
||||
$data = $this->arrayFromInput($request);
|
||||
$journal = $repository->updateSplitJournal($journal, $data);
|
||||
/** @var array $files */
|
||||
@@ -167,11 +178,11 @@ class SplitController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param SplitJournalFormRequest $request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function arrayFromInput(Request $request): array
|
||||
private function arrayFromInput(SplitJournalFormRequest $request): array
|
||||
{
|
||||
$array = [
|
||||
'journal_description' => $request->get('journal_description'),
|
||||
@@ -200,8 +211,8 @@ class SplitController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param TransactionJournal $journal
|
||||
* @param SplitJournalFormRequest|Request $request
|
||||
* @param TransactionJournal $journal
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -234,6 +245,8 @@ class SplitController extends Controller
|
||||
// transactions.
|
||||
'transactions' => $this->getTransactionDataFromJournal($journal),
|
||||
];
|
||||
// update transactions array with old request data.
|
||||
$array['transactions'] = $this->updateWithPrevious($array['transactions'], $request->old());
|
||||
|
||||
return $array;
|
||||
}
|
||||
@@ -282,11 +295,11 @@ class SplitController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param SplitJournalFormRequest|Request $request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getTransactionDataFromRequest(Request $request): array
|
||||
private function getTransactionDataFromRequest(SplitJournalFormRequest $request): array
|
||||
{
|
||||
$return = [];
|
||||
$transactions = $request->get('transactions');
|
||||
@@ -312,5 +325,36 @@ class SplitController extends Controller
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $array
|
||||
* @param $old
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function updateWithPrevious($array, $old): array
|
||||
{
|
||||
if (count($old) === 0 || !isset($old['transactions'])) {
|
||||
return $array;
|
||||
}
|
||||
$old = $old['transactions'];
|
||||
foreach ($old as $index => $row) {
|
||||
if (isset($array[$index])) {
|
||||
$array[$index] = array_merge($array[$index], $row);
|
||||
continue;
|
||||
}
|
||||
// take some info from first transaction, that should at least exist.
|
||||
$array[$index] = $row;
|
||||
$array[$index]['transaction_currency_id'] = $array[0]['transaction_currency_id'];
|
||||
$array[$index]['transaction_currency_code'] = $array[0]['transaction_currency_code'];
|
||||
$array[$index]['transaction_currency_symbol'] = $array[0]['transaction_currency_symbol'];
|
||||
$array[$index]['foreign_amount'] = round($array[0]['foreign_destination_amount'] ?? '0', 12);
|
||||
$array[$index]['foreign_currency_id'] = $array[0]['foreign_currency_id'];
|
||||
$array[$index]['foreign_currency_code'] = $array[0]['foreign_currency_code'];
|
||||
$array[$index]['foreign_currency_symbol'] = $array[0]['foreign_currency_symbol'];
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\InternalTransferFilter;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
||||
use FireflyIII\Repositories\Journal\JournalTaskerInterface;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -27,7 +29,6 @@ use Log;
|
||||
use Navigation;
|
||||
use Preferences;
|
||||
use Response;
|
||||
use Steam;
|
||||
use View;
|
||||
|
||||
/**
|
||||
@@ -70,15 +71,13 @@ class TransactionController extends Controller
|
||||
// default values:
|
||||
$subTitleIcon = config('firefly.transactionIconsByWhat.' . $what);
|
||||
$types = config('firefly.transactionTypesByWhat.' . $what);
|
||||
$page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page'));
|
||||
$page = intval($request->get('page'));
|
||||
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
|
||||
$count = 0;
|
||||
$loop = 0;
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = null;
|
||||
$end = null;
|
||||
$periods = new Collection;
|
||||
$path = '/transactions/' . $what;
|
||||
$path = route('transactions.index', [$what]);
|
||||
|
||||
// prep for "all" view.
|
||||
if ($moment === 'all') {
|
||||
@@ -86,13 +85,14 @@ class TransactionController extends Controller
|
||||
$first = $repository->first();
|
||||
$start = $first->date ?? new Carbon;
|
||||
$end = new Carbon;
|
||||
$path = '/transactions/' . $what . '/all/';
|
||||
$path = route('transactions.index', [$what, 'all']);
|
||||
}
|
||||
|
||||
// prep for "specific date" view.
|
||||
if (strlen($moment) > 0 && $moment !== 'all') {
|
||||
$start = new Carbon($moment);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
$path = route('transactions.index', [$what, $moment]);
|
||||
$subTitle = trans(
|
||||
'firefly.title_' . $what . '_between',
|
||||
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
@@ -110,32 +110,14 @@ class TransactionController extends Controller
|
||||
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
// grab journals, but be prepared to jump a period back to get the right ones:
|
||||
Log::info('Now at transaction loop start.');
|
||||
while ($count === 0 && $loop < 3) {
|
||||
$loop++;
|
||||
Log::info('Count is zero, search for journals.');
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes($types)->setLimit($pageSize)->setPage($page)->withOpposingAccount();
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath($path);
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0 && $loop < 3) {
|
||||
$start->subDay();
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
}
|
||||
}
|
||||
|
||||
if ($moment != 'all' && $loop > 1) {
|
||||
$subTitle = trans(
|
||||
'firefly.title_' . $what . '_between',
|
||||
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
|
||||
);
|
||||
}
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes($types)->setLimit($pageSize)->setPage($page)->withOpposingAccount();
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath($path);
|
||||
|
||||
|
||||
return view('transactions.index', compact('subTitle', 'what', 'subTitleIcon', 'journals', 'periods', 'start', 'end', 'moment'));
|
||||
|
||||
@@ -169,23 +151,26 @@ class TransactionController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param JournalTaskerInterface $tasker
|
||||
* @param TransactionJournal $journal
|
||||
* @param JournalTaskerInterface $tasker
|
||||
*
|
||||
* @return View
|
||||
* @param LinkTypeRepositoryInterface $linkTypeRepository
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
|
||||
*/
|
||||
public function show(TransactionJournal $journal, JournalTaskerInterface $tasker)
|
||||
public function show(TransactionJournal $journal, JournalTaskerInterface $tasker, LinkTypeRepositoryInterface $linkTypeRepository)
|
||||
{
|
||||
if ($this->isOpeningBalance($journal)) {
|
||||
return $this->redirectToAccount($journal);
|
||||
}
|
||||
|
||||
$linkTypes = $linkTypeRepository->get();
|
||||
$links = $linkTypeRepository->getLinks($journal);
|
||||
$events = $tasker->getPiggyBankEvents($journal);
|
||||
$transactions = $tasker->getTransactionsOverview($journal);
|
||||
$what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type);
|
||||
$subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"';
|
||||
|
||||
return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions'));
|
||||
return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions', 'linkTypes', 'links'));
|
||||
|
||||
|
||||
}
|
||||
@@ -203,7 +188,7 @@ class TransactionController extends Controller
|
||||
$start = $first->date ?? new Carbon;
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = Navigation::startOfPeriod($start, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range);
|
||||
$end = Navigation::endOfX(new Carbon, $range, null);
|
||||
$entries = new Collection;
|
||||
$types = config('firefly.transactionTypesByWhat.' . $what);
|
||||
|
||||
@@ -229,35 +214,24 @@ class TransactionController extends Controller
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withOpposingAccount()->setTypes($types);
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$set = $collector->getJournals();
|
||||
$sum = $set->sum('transaction_amount');
|
||||
$journals = $set->count();
|
||||
$journals = $collector->getJournals();
|
||||
$sum = $journals->sum('transaction_amount');
|
||||
|
||||
// count per currency:
|
||||
$sums = $this->sumPerCurrency($journals);
|
||||
$dateStr = $end->format('Y-m-d');
|
||||
$dateName = Navigation::periodShow($end, $range);
|
||||
$array = [
|
||||
'string' => $dateStr,
|
||||
'name' => $dateName,
|
||||
'count' => $journals,
|
||||
'spent' => 0,
|
||||
'earned' => 0,
|
||||
'transferred' => 0,
|
||||
'date' => clone $end,
|
||||
'string' => $dateStr,
|
||||
'name' => $dateName,
|
||||
'sum' => $sum,
|
||||
'sums' => $sums,
|
||||
'date' => clone $end,
|
||||
];
|
||||
Log::debug(sprintf('What is %s', $what));
|
||||
switch ($what) {
|
||||
case 'withdrawal':
|
||||
$array['spent'] = $sum;
|
||||
break;
|
||||
case 'deposit':
|
||||
$array['earned'] = $sum;
|
||||
break;
|
||||
case 'transfers':
|
||||
case 'transfer':
|
||||
$array['transferred'] = Steam::positive($sum);
|
||||
break;
|
||||
|
||||
if ($journals->count() > 0) {
|
||||
$entries->push($array);
|
||||
}
|
||||
$entries->push($array);
|
||||
$end = Navigation::subtractPeriod($end, $range, 1);
|
||||
}
|
||||
Log::debug('End of loop');
|
||||
@@ -266,4 +240,41 @@ class TransactionController extends Controller
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $collection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function sumPerCurrency(Collection $collection): array
|
||||
{
|
||||
$return = [];
|
||||
/** @var Transaction $transaction */
|
||||
foreach ($collection as $transaction) {
|
||||
$currencyId = $transaction->transaction_currency_id;
|
||||
|
||||
// save currency information:
|
||||
if (!isset($return[$currencyId])) {
|
||||
$currencySymbol = $transaction->transaction_currency_symbol;
|
||||
$decimalPlaces = $transaction->transaction_currency_dp;
|
||||
$currencyCode = $transaction->transaction_currency_code;
|
||||
$return[$currencyId] = [
|
||||
'currency' => [
|
||||
'id' => $currencyId,
|
||||
'code' => $currencyCode,
|
||||
'symbol' => $currencySymbol,
|
||||
'dp' => $decimalPlaces,
|
||||
],
|
||||
'sum' => '0',
|
||||
'count' => 0,
|
||||
];
|
||||
}
|
||||
// save amount:
|
||||
$return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], $transaction->transaction_amount);
|
||||
$return[$currencyId]['count']++;
|
||||
}
|
||||
asort($return);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ class BudgetIncomeRequest extends Request
|
||||
{
|
||||
return [
|
||||
'amount' => 'numeric|required|min:0',
|
||||
'start' => 'required|date|before:end',
|
||||
'end' => 'required|date|after:start',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,9 @@ class ExportFormRequest extends Request
|
||||
public function rules()
|
||||
{
|
||||
$sessionFirst = clone session('first');
|
||||
|
||||
$first = $sessionFirst->subDay()->format('Y-m-d');
|
||||
$today = Carbon::create()->addDay()->format('Y-m-d');
|
||||
$formats = join(',', array_keys(config('firefly.export_formats')));
|
||||
$first = $sessionFirst->subDay()->format('Y-m-d');
|
||||
$today = Carbon::create()->addDay()->format('Y-m-d');
|
||||
$formats = join(',', array_keys(config('firefly.export_formats')));
|
||||
|
||||
return [
|
||||
'export_start_range' => 'required|date|after:' . $first,
|
||||
|
||||
@@ -110,7 +110,7 @@ class JournalFormRequest extends Request
|
||||
// foreign currency amounts
|
||||
'native_amount' => 'numeric|more:0',
|
||||
'source_amount' => 'numeric|more:0',
|
||||
'destination_amount' => 'numeric|more:0',
|
||||
'destination_amount' => 'numeric',
|
||||
];
|
||||
|
||||
// some rules get an upgrade depending on the type of data:
|
||||
|
||||
74
app/Http/Requests/JournalLinkRequest.php
Normal file
74
app/Http/Requests/JournalLinkRequest.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* JournalLinkRequest.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Models\LinkType;
|
||||
|
||||
/**
|
||||
* Class JournalLink
|
||||
*
|
||||
*
|
||||
* @package FireflyIII\Http\Requests
|
||||
*/
|
||||
class JournalLinkRequest extends Request
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
// Only allow logged in users
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getLinkInfo(): array
|
||||
{
|
||||
$return = [];
|
||||
$linkType = $this->get('link_type');
|
||||
$parts = explode('_', $linkType);
|
||||
$return['link_type_id'] = intval($parts[0]);
|
||||
$return['transaction_journal_id'] = $this->integer('link_journal_id');
|
||||
$return['comments'] = strlen($this->string('comments')) > 0 ? $this->string('comments') : null;
|
||||
$return['direction'] = $parts[1];
|
||||
if ($return['transaction_journal_id'] === 0 && ctype_digit($this->string('link_other'))) {
|
||||
$return['transaction_journal_id'] = $this->integer('link_other');
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
// all possible combinations of link types and inward / outward:
|
||||
$combinations = [];
|
||||
$linkTypes = LinkType::get(['id']);
|
||||
/** @var LinkType $type */
|
||||
foreach ($linkTypes as $type) {
|
||||
$combinations[] = sprintf('%d_inward', $type->id);
|
||||
$combinations[] = sprintf('%d_outward', $type->id);
|
||||
}
|
||||
$string = join(',', $combinations);
|
||||
|
||||
return [
|
||||
'link_type' => sprintf('required|in:%s', $string),
|
||||
'link_other' => 'belongsToUser:transaction_journals',
|
||||
'link_journal_id' => 'belongsToUser:transaction_journals',
|
||||
];
|
||||
}
|
||||
}
|
||||
57
app/Http/Requests/LinkTypeFormRequest.php
Normal file
57
app/Http/Requests/LinkTypeFormRequest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* LinkTypeFormRequest.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Class BillFormRequest
|
||||
*
|
||||
*
|
||||
* @package FireflyIII\Http\Requests
|
||||
*/
|
||||
class LinkTypeFormRequest extends Request
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
// Only allow logged and admins
|
||||
return auth()->check() && auth()->user()->hasRole('owner');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
/** @var LinkTypeRepositoryInterface $repository */
|
||||
$repository = app(LinkTypeRepositoryInterface::class);
|
||||
$nameRule = 'required|min:1|unique:link_types,name';
|
||||
$idRule = '';
|
||||
if (!is_null($repository->find($this->integer('id'))->id)) {
|
||||
$idRule = 'exists:link_types,id';
|
||||
$nameRule = 'required|min:1';
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'id' => $idRule,
|
||||
'name' => $nameRule,
|
||||
'inward' => 'required|min:1|different:outward',
|
||||
'outward' => 'required|min:1|different:inward',
|
||||
];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class ProfileFormRequest extends Request
|
||||
{
|
||||
return [
|
||||
'current_password' => 'required',
|
||||
'new_password' => 'required|confirmed',
|
||||
'new_password' => 'required|confirmed|secure_password',
|
||||
'new_password_confirmation' => 'required',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -28,64 +28,17 @@ class Request extends FormRequest
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function boolean(string $field): bool
|
||||
public function boolean(string $field): bool
|
||||
{
|
||||
return intval($this->input($field)) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @return Carbon|null
|
||||
*/
|
||||
protected function date(string $field)
|
||||
{
|
||||
return $this->get($field) ? new Carbon($this->get($field)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function float(string $field): float
|
||||
{
|
||||
return round($this->input($field), 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getArray(string $field, string $type): array
|
||||
{
|
||||
$original = $this->get($field);
|
||||
$return = [];
|
||||
foreach ($original as $index => $value) {
|
||||
$return[$index] = $this->$type($value);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function integer(string $field): int
|
||||
{
|
||||
return intval($this->get($field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function string(string $field): string
|
||||
public function string(string $field): string
|
||||
{
|
||||
$string = $this->get($field) ?? '';
|
||||
$search = [
|
||||
@@ -140,4 +93,51 @@ class Request extends FormRequest
|
||||
|
||||
return trim($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @return Carbon|null
|
||||
*/
|
||||
protected function date(string $field)
|
||||
{
|
||||
return $this->get($field) ? new Carbon($this->get($field)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function float(string $field): float
|
||||
{
|
||||
return round($this->input($field), 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getArray(string $field, string $type): array
|
||||
{
|
||||
$original = $this->get($field);
|
||||
$return = [];
|
||||
foreach ($original as $index => $value) {
|
||||
$return[$index] = $this->$type($value);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function integer(string $field): int
|
||||
{
|
||||
return intval($this->get($field));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
|
||||
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Class RuleFormRequest
|
||||
@@ -39,6 +39,7 @@ class RuleFormRequest extends Request
|
||||
{
|
||||
return [
|
||||
'title' => $this->string('title'),
|
||||
'rule_group_id' => $this->integer('rule_group_id'),
|
||||
'active' => $this->boolean('active'),
|
||||
'trigger' => $this->string('trigger'),
|
||||
'description' => $this->string('description'),
|
||||
@@ -57,19 +58,18 @@ class RuleFormRequest extends Request
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
/** @var RuleGroupRepositoryInterface $repository */
|
||||
$repository = app(RuleGroupRepositoryInterface::class);
|
||||
/** @var RuleRepositoryInterface $repository */
|
||||
$repository = app(RuleRepositoryInterface::class);
|
||||
$validTriggers = array_keys(config('firefly.rule-triggers'));
|
||||
$validActions = array_keys(config('firefly.rule-actions'));
|
||||
|
||||
// some actions require text:
|
||||
$contextActions = join(',', config('firefly.rule-actions-text'));
|
||||
|
||||
$titleRule = 'required|between:1,100|uniqueObjectForUser:rule_groups,title';
|
||||
$titleRule = 'required|between:1,100|uniqueObjectForUser:rules,title';
|
||||
if (!is_null($repository->find(intval($this->get('id')))->id)) {
|
||||
$titleRule = 'required|between:1,100|uniqueObjectForUser:rule_groups,title,' . intval($this->get('id'));
|
||||
$titleRule = 'required|between:1,100|uniqueObjectForUser:rules,title,' . intval($this->get('id'));
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'title' => $titleRule,
|
||||
'description' => 'between:1,5000',
|
||||
|
||||
@@ -61,23 +61,23 @@ class SplitJournalFormRequest extends Request
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'what' => 'required|in:withdrawal,deposit,transfer',
|
||||
'journal_description' => 'required|between:1,255',
|
||||
'id' => 'numeric|belongsToUser:transaction_journals,id',
|
||||
'journal_source_account_id' => 'numeric|belongsToUser:accounts,id',
|
||||
'journal_source_account_name.*' => 'between:1,255',
|
||||
'journal_currency_id' => 'required|exists:transaction_currencies,id',
|
||||
'date' => 'required|date',
|
||||
'interest_date' => 'date',
|
||||
'book_date' => 'date',
|
||||
'process_date' => 'date',
|
||||
'description.*' => 'required|between:1,255',
|
||||
'destination_account_id.*' => 'numeric|belongsToUser:accounts,id',
|
||||
'destination_account_name.*' => 'between:1,255',
|
||||
'amount.*' => 'required|numeric',
|
||||
'budget_id.*' => 'belongsToUser:budgets,id',
|
||||
'category.*' => 'between:1,255',
|
||||
'piggy_bank_id.*' => 'between:1,255',
|
||||
'what' => 'required|in:withdrawal,deposit,transfer',
|
||||
'journal_description' => 'required|between:1,255',
|
||||
'id' => 'numeric|belongsToUser:transaction_journals,id',
|
||||
'journal_source_account_id' => 'numeric|belongsToUser:accounts,id',
|
||||
'journal_source_account_name.*' => 'between:1,255',
|
||||
'journal_currency_id' => 'required|exists:transaction_currencies,id',
|
||||
'date' => 'required|date',
|
||||
'interest_date' => 'date',
|
||||
'book_date' => 'date',
|
||||
'process_date' => 'date',
|
||||
'transactions.*.description' => 'required|between:1,255',
|
||||
'transactions.*.destination_account_id' => 'numeric|belongsToUser:accounts,id',
|
||||
'transactions.*.destination_account_name' => 'between:1,255',
|
||||
'transactions.*.amount' => 'required|numeric',
|
||||
'transactions.*.budget_id' => 'belongsToUser:budgets,id',
|
||||
'transactions.*.category' => 'between:1,255',
|
||||
'transactions.*.piggy_bank_id' => 'between:1,255',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -37,14 +37,13 @@ class TagFormRequest extends Request
|
||||
*/
|
||||
public function collectTagData(): array
|
||||
{
|
||||
if ($this->get('setTag') == 'true') {
|
||||
$latitude = null;
|
||||
$longitude = null;
|
||||
$zoomLevel = null;
|
||||
if ($this->get('setTag') === 'true') {
|
||||
$latitude = $this->string('latitude');
|
||||
$longitude = $this->string('longitude');
|
||||
$zoomLevel = $this->integer('zoomLevel');
|
||||
} else {
|
||||
$latitude = null;
|
||||
$longitude = null;
|
||||
$zoomLevel = null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
@@ -54,7 +53,6 @@ class TagFormRequest extends Request
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude,
|
||||
'zoomLevel' => $zoomLevel,
|
||||
'tagMode' => $this->string('tagMode'),
|
||||
];
|
||||
|
||||
return $data;
|
||||
@@ -84,7 +82,6 @@ class TagFormRequest extends Request
|
||||
'latitude' => 'numeric|min:-90|max:90',
|
||||
'longitude' => 'numeric|min:-90|max:90',
|
||||
'zoomLevel' => 'numeric|min:0|max:80',
|
||||
'tagMode' => 'required|in:nothing,balancingAct,advancePayment',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class UserFormRequest extends Request
|
||||
return [
|
||||
'id' => 'required|exists:users,id',
|
||||
'email' => 'email|required',
|
||||
'password' => 'confirmed',
|
||||
'password' => 'confirmed|secure_password',
|
||||
'blocked_code' => 'between:0,30',
|
||||
'blocked' => 'between:0,1|numeric',
|
||||
];
|
||||
|
||||
42
app/Http/Requests/UserRegistrationRequest.php
Normal file
42
app/Http/Requests/UserRegistrationRequest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* UserRegistrationRequest.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
/**
|
||||
* Class UserRegistrationRequest
|
||||
*
|
||||
*
|
||||
* @package FireflyIII\Http\Requests
|
||||
*/
|
||||
class UserRegistrationRequest extends Request
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
// Only everybody
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'email' => 'email|required',
|
||||
'password' => 'confirmed|secure_password',
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DaveJamesMiller\Breadcrumbs\Generator as BreadCrumbGenerator;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
@@ -20,12 +21,14 @@ use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Models\LinkType;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionJournalLink;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -151,6 +154,49 @@ Breadcrumbs::register(
|
||||
);
|
||||
|
||||
|
||||
Breadcrumbs::register(
|
||||
'admin.links.index', function (BreadCrumbGenerator $breadcrumbs) {
|
||||
$breadcrumbs->parent('admin.index');
|
||||
$breadcrumbs->push(trans('firefly.journal_link_configuration'), route('admin.links.index'));
|
||||
}
|
||||
);
|
||||
|
||||
Breadcrumbs::register(
|
||||
'admin.links.create', function (BreadCrumbGenerator $breadcrumbs) {
|
||||
$breadcrumbs->parent('admin.links.index');
|
||||
$breadcrumbs->push(trans('firefly.create_new_link_type'), route('admin.links.create'));
|
||||
}
|
||||
);
|
||||
|
||||
Breadcrumbs::register(
|
||||
'admin.links.show', function (BreadCrumbGenerator $breadcrumbs, LinkType $linkType) {
|
||||
$breadcrumbs->parent('admin.links.index');
|
||||
$breadcrumbs->push(trans('firefly.overview_for_link', [$linkType->name]), route('admin.links.show', [$linkType->id]));
|
||||
}
|
||||
);
|
||||
|
||||
Breadcrumbs::register(
|
||||
'admin.links.edit', function (BreadCrumbGenerator $breadcrumbs, LinkType $linkType) {
|
||||
$breadcrumbs->parent('admin.links.index');
|
||||
$breadcrumbs->push(trans('firefly.edit_link_type', ['name' => $linkType->name]), route('admin.links.edit', [$linkType->id]));
|
||||
}
|
||||
);
|
||||
|
||||
Breadcrumbs::register(
|
||||
'admin.links.delete', function (BreadCrumbGenerator $breadcrumbs, LinkType $linkType) {
|
||||
$breadcrumbs->parent('admin.links.index');
|
||||
$breadcrumbs->push(trans('firefly.delete_link_type', ['name' => $linkType->name]), route('admin.links.delete', [$linkType->id]));
|
||||
}
|
||||
);
|
||||
|
||||
Breadcrumbs::register(
|
||||
'transactions.link.delete', function (BreadCrumbGenerator $breadcrumbs, TransactionJournalLink $link) {
|
||||
$breadcrumbs->parent('home');
|
||||
$breadcrumbs->push(trans('breadcrumbs.delete_journal_link'), route('transactions.link.delete', $link->id));
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* ATTACHMENTS
|
||||
*/
|
||||
@@ -470,16 +516,26 @@ Breadcrumbs::register(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* FILE IMPORT
|
||||
*/
|
||||
Breadcrumbs::register(
|
||||
'import.configure', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
|
||||
'import.file.index', function (BreadCrumbGenerator $breadcrumbs) {
|
||||
$breadcrumbs->parent('import.index');
|
||||
$breadcrumbs->push(trans('firefly.import_config_sub_title', ['key' => $job->key]), route('import.configure', [$job->key]));
|
||||
$breadcrumbs->push(trans('firefly.import_file'), route('import.file.index'));
|
||||
}
|
||||
);
|
||||
|
||||
Breadcrumbs::register(
|
||||
'import.file.configure', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
|
||||
$breadcrumbs->parent('import.file.index');
|
||||
$breadcrumbs->push(trans('firefly.import_config_sub_title', ['key' => $job->key]), route('import.file.configure', [$job->key]));
|
||||
}
|
||||
);
|
||||
Breadcrumbs::register(
|
||||
'import.status', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
|
||||
$breadcrumbs->parent('import.index');
|
||||
$breadcrumbs->push(trans('firefly.import_status_bread_crumb', ['key' => $job->key]), route('import.status', [$job->key]));
|
||||
'import.file.status', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
|
||||
$breadcrumbs->parent('import.file.index');
|
||||
$breadcrumbs->push(trans('firefly.import_status_bread_crumb', ['key' => $job->key]), route('import.file.status', [$job->key]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -680,7 +736,7 @@ Breadcrumbs::register(
|
||||
Breadcrumbs::register(
|
||||
'search.index', function (BreadCrumbGenerator $breadcrumbs, $query) {
|
||||
$breadcrumbs->parent('home');
|
||||
$breadcrumbs->push(trans('breadcrumbs.searchResult', ['query' => e($query)]), route('search.index'));
|
||||
$breadcrumbs->push(trans('breadcrumbs.search_result', ['query' => e($query)]), route('search.index'));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -804,7 +860,7 @@ Breadcrumbs::register(
|
||||
* MASS TRANSACTION EDIT / DELETE
|
||||
*/
|
||||
Breadcrumbs::register(
|
||||
'transactions.mass.edit', function (BreadCrumbGenerator $breadcrumbs, Collection $journals) {
|
||||
'transactions.mass.edit', function (BreadCrumbGenerator $breadcrumbs, Collection $journals): void {
|
||||
|
||||
if ($journals->count() > 0) {
|
||||
$journalIds = $journals->pluck('id')->toArray();
|
||||
@@ -816,6 +872,8 @@ Breadcrumbs::register(
|
||||
}
|
||||
|
||||
$breadcrumbs->parent('index');
|
||||
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -48,6 +48,13 @@ interface ConfiguratorInterface
|
||||
*/
|
||||
public function getNextView(): string;
|
||||
|
||||
/**
|
||||
* Return possible warning to user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWarningMessage(): string;
|
||||
|
||||
/**
|
||||
* Returns true when the initial configuration for this job is complete.
|
||||
*
|
||||
|
||||
@@ -26,8 +26,12 @@ use Log;
|
||||
*/
|
||||
class CsvConfigurator implements ConfiguratorInterface
|
||||
{
|
||||
/** @var ImportJob */
|
||||
private $job;
|
||||
|
||||
/** @var string */
|
||||
private $warning = '';
|
||||
|
||||
/**
|
||||
* ConfiguratorInterface constructor.
|
||||
*/
|
||||
@@ -50,8 +54,10 @@ class CsvConfigurator implements ConfiguratorInterface
|
||||
/** @var ConfigurationInterface $object */
|
||||
$object = new $class($this->job);
|
||||
$object->setJob($job);
|
||||
$result = $object->storeConfiguration($data);
|
||||
$this->warning = $object->getWarningMessage();
|
||||
|
||||
return $object->storeConfiguration($data);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +97,16 @@ class CsvConfigurator implements ConfiguratorInterface
|
||||
throw new FireflyException('No view for state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return possible warning to user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWarningMessage(): string
|
||||
{
|
||||
return $this->warning;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Import\Converter;
|
||||
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class RabobankDebetCredit
|
||||
*
|
||||
@@ -29,34 +31,45 @@ class Amount implements ConverterInterface
|
||||
* @param $value
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
||||
*/
|
||||
public function convert($value): string
|
||||
{
|
||||
Log::debug(sprintf('Start with amount "%s"', $value));
|
||||
$len = strlen($value);
|
||||
$decimalPosition = $len - 3;
|
||||
$decimal = null;
|
||||
|
||||
if (($len > 2 && $value{$decimalPosition} == '.') || ($len > 2 && strpos($value, '.') > $decimalPosition)) {
|
||||
if (($len > 2 && $value{$decimalPosition} === '.') || ($len > 2 && strpos($value, '.') > $decimalPosition)) {
|
||||
$decimal = '.';
|
||||
Log::debug(sprintf('Decimal character in "%s" seems to be a dot.', $value));
|
||||
}
|
||||
if ($len > 2 && $value{$decimalPosition} == ',') {
|
||||
if ($len > 2 && $value{$decimalPosition} === ',') {
|
||||
$decimal = ',';
|
||||
Log::debug(sprintf('Decimal character in "%s" seems to be a comma.', $value));
|
||||
}
|
||||
|
||||
// if decimal is dot, replace all comma's and spaces with nothing. then parse as float (round to 4 pos)
|
||||
if ($decimal === '.') {
|
||||
$search = [',', ' '];
|
||||
$value = str_replace($search, '', $value);
|
||||
$search = [',', ' '];
|
||||
$oldValue = $value;
|
||||
$value = str_replace($search, '', $value);
|
||||
Log::debug(sprintf('Converted amount from "%s" to "%s".', $oldValue, $value));
|
||||
}
|
||||
if ($decimal === ',') {
|
||||
$search = ['.', ' '];
|
||||
$value = str_replace($search, '', $value);
|
||||
$value = str_replace(',', '.', $value);
|
||||
$search = ['.', ' '];
|
||||
$oldValue = $value;
|
||||
$value = str_replace($search, '', $value);
|
||||
$value = str_replace(',', '.', $value);
|
||||
Log::debug(sprintf('Converted amount from "%s" to "%s".', $oldValue, $value));
|
||||
}
|
||||
if (is_null($decimal)) {
|
||||
// replace all:
|
||||
$search = ['.', ' ', ','];
|
||||
$value = str_replace($search, '', $value);
|
||||
$search = ['.', ' ', ','];
|
||||
$oldValue = $value;
|
||||
$value = str_replace($search, '', $value);
|
||||
Log::debug(sprintf('No decimal character found. Converted amount from "%s" to "%s".', $oldValue, $value));
|
||||
}
|
||||
|
||||
return strval(round(floatval($value), 12));
|
||||
|
||||
@@ -58,7 +58,7 @@ class CsvProcessor implements FileProcessorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the actual job:
|
||||
* Does the actual job.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -66,34 +66,46 @@ class CsvProcessor implements FileProcessorInterface
|
||||
{
|
||||
Log::debug('Now in CsvProcessor run(). Job is now running...');
|
||||
|
||||
$entries = $this->getImportArray();
|
||||
$index = 0;
|
||||
$entries = new Collection($this->getImportArray());
|
||||
Log::notice('Building importable objects from CSV file.');
|
||||
foreach ($entries as $index => $row) {
|
||||
// verify if not exists already:
|
||||
if ($this->rowAlreadyImported($row)) {
|
||||
$message = sprintf('Row #%d has already been imported.', $index);
|
||||
$this->job->addError($index, $message);
|
||||
$this->job->addStepsDone(5); // all steps.
|
||||
Log::info($message);
|
||||
continue;
|
||||
}
|
||||
$this->objects->push($this->importRow($index, $row));
|
||||
$this->job->addStepsDone(1);
|
||||
}
|
||||
// if job has no step count, set it now:
|
||||
$extended = $this->job->extended_status;
|
||||
if ($extended['steps'] === 0) {
|
||||
$extended['steps'] = $index * 5;
|
||||
$this->job->extended_status = $extended;
|
||||
$this->job->save();
|
||||
}
|
||||
Log::debug(sprintf('Number of entries: %d', $entries->count()));
|
||||
$notImported = $entries->filter(
|
||||
function (array $row, int $index) {
|
||||
if ($this->rowAlreadyImported($row)) {
|
||||
$message = sprintf('Row #%d has already been imported.', $index);
|
||||
$this->job->addError($index, $message);
|
||||
$this->job->addStepsDone(5); // all steps.
|
||||
Log::info($message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
);
|
||||
Log::debug(sprintf('Number of entries left: %d', $notImported->count()));
|
||||
|
||||
// set (new) number of steps:
|
||||
$status = $this->job->extended_status;
|
||||
$status['steps'] = $notImported->count() * 5;
|
||||
$this->job->extended_status = $status;
|
||||
$this->job->save();
|
||||
Log::debug(sprintf('Number of steps: %d', $notImported->count() * 5));
|
||||
|
||||
$notImported->each(
|
||||
function (array $row, int $index) {
|
||||
$journal = $this->importRow($index, $row);
|
||||
$this->objects->push($journal);
|
||||
$this->job->addStepsDone(1);
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set import job for this processor.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return FileProcessorInterface
|
||||
@@ -116,7 +128,6 @@ class CsvProcessor implements FileProcessorInterface
|
||||
*/
|
||||
private function annotateValue(int $index, string $value)
|
||||
{
|
||||
$value = trim($value);
|
||||
$config = $this->job->configuration;
|
||||
$role = $config['column-roles'][$index] ?? '_ignore';
|
||||
$mapped = $config['column-mapping-config'][$index][$value] ?? null;
|
||||
@@ -140,10 +151,14 @@ class CsvProcessor implements FileProcessorInterface
|
||||
*/
|
||||
private function getImportArray(): Iterator
|
||||
{
|
||||
$content = $this->job->uploadFileContents();
|
||||
$config = $this->job->configuration;
|
||||
$reader = Reader::createFromString($content);
|
||||
$reader->setDelimiter($config['delimiter']);
|
||||
$content = $this->job->uploadFileContents();
|
||||
$config = $this->job->configuration;
|
||||
$reader = Reader::createFromString($content);
|
||||
$delimiter = $config['delimiter'];
|
||||
if ($delimiter === 'tab') {
|
||||
$delimiter = "\t";
|
||||
}
|
||||
$reader->setDelimiter($delimiter);
|
||||
$start = $config['has-headers'] ? 1 : 0;
|
||||
$results = $reader->setOffset($start)->fetch();
|
||||
Log::debug(sprintf('Created a CSV reader starting at offset %d', $start));
|
||||
@@ -151,6 +166,64 @@ class CsvProcessor implements FileProcessorInterface
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return string representation of JSON error code.
|
||||
*
|
||||
* @param int $jsonError
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getJsonError(int $jsonError): string
|
||||
{
|
||||
switch ($jsonError) {
|
||||
default:
|
||||
return 'Unknown JSON error';
|
||||
case JSON_ERROR_NONE:
|
||||
return 'No JSON error';
|
||||
case JSON_ERROR_DEPTH:
|
||||
return 'JSON_ERROR_DEPTH';
|
||||
case JSON_ERROR_STATE_MISMATCH:
|
||||
return 'JSON_ERROR_STATE_MISMATCH';
|
||||
case JSON_ERROR_CTRL_CHAR:
|
||||
return 'JSON_ERROR_CTRL_CHAR';
|
||||
case JSON_ERROR_SYNTAX:
|
||||
return 'JSON_ERROR_SYNTAX';
|
||||
case JSON_ERROR_UTF8:
|
||||
return 'JSON_ERROR_UTF8';
|
||||
case JSON_ERROR_RECURSION:
|
||||
return 'JSON_ERROR_RECURSION';
|
||||
case JSON_ERROR_INF_OR_NAN:
|
||||
return 'JSON_ERROR_INF_OR_NAN';
|
||||
case JSON_ERROR_UNSUPPORTED_TYPE:
|
||||
return 'JSON_ERROR_UNSUPPORTED_TYPE';
|
||||
case JSON_ERROR_INVALID_PROPERTY_NAME:
|
||||
return 'JSON_ERROR_INVALID_PROPERTY_NAME';
|
||||
case JSON_ERROR_UTF16:
|
||||
return 'JSON_ERROR_UTF16';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash an array and return the result.
|
||||
*
|
||||
* @param array $array
|
||||
*
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getRowHash(array $array): string
|
||||
{
|
||||
$json = json_encode($array);
|
||||
$jsonError = json_last_error();
|
||||
|
||||
if ($json === false) {
|
||||
throw new FireflyException(sprintf('Error while encoding JSON for CSV row: %s', $this->getJsonError($jsonError)));
|
||||
}
|
||||
$hash = hash('sha256', $json);
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a row, build import journal by annotating each value and storing it in the import journal.
|
||||
*
|
||||
@@ -158,15 +231,22 @@ class CsvProcessor implements FileProcessorInterface
|
||||
* @param array $row
|
||||
*
|
||||
* @return ImportJournal
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function importRow(int $index, array $row): ImportJournal
|
||||
{
|
||||
Log::debug(sprintf('Now at row %d', $index));
|
||||
$row = $this->specifics($row);
|
||||
$row = $this->specifics($row);
|
||||
$hash = $this->getRowHash($row);
|
||||
|
||||
$journal = new ImportJournal;
|
||||
$journal->setUser($this->job->user);
|
||||
$journal->setHash(hash('sha256', json_encode($row)));
|
||||
$journal->setHash($hash);
|
||||
|
||||
/**
|
||||
* @var int $rowIndex
|
||||
* @var string $value
|
||||
*/
|
||||
foreach ($row as $rowIndex => $value) {
|
||||
$value = trim($value);
|
||||
if (strlen($value) > 0) {
|
||||
@@ -175,7 +255,8 @@ class CsvProcessor implements FileProcessorInterface
|
||||
$journal->setValue($annotated);
|
||||
}
|
||||
}
|
||||
Log::debug('ImportJournal complete, returning.');
|
||||
// set some extra info:
|
||||
$journal->asset->setDefaultAccountId($this->job->configuration['import-account']);
|
||||
|
||||
return $journal;
|
||||
}
|
||||
@@ -189,13 +270,12 @@ class CsvProcessor implements FileProcessorInterface
|
||||
*/
|
||||
private function rowAlreadyImported(array $array): bool
|
||||
{
|
||||
$string = json_encode($array);
|
||||
$hash = hash('sha256', json_encode($string));
|
||||
$json = json_encode($hash);
|
||||
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
|
||||
->where('data', $json)
|
||||
->where('name', 'importHash')
|
||||
->first();
|
||||
$hash = $this->getRowHash($array);
|
||||
$json = json_encode($hash);
|
||||
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
|
||||
->where('data', $json)
|
||||
->where('name', 'importHash')
|
||||
->first();
|
||||
if (!is_null($entry)) {
|
||||
return true;
|
||||
}
|
||||
@@ -215,8 +295,8 @@ class CsvProcessor implements FileProcessorInterface
|
||||
private function specifics(array $row): array
|
||||
{
|
||||
$config = $this->job->configuration;
|
||||
//
|
||||
foreach ($config['specifics'] as $name => $enabled) {
|
||||
$names = array_keys($config['specifics']);
|
||||
foreach ($names as $name) {
|
||||
|
||||
if (!in_array($name, $this->validSpecifics)) {
|
||||
throw new FireflyException(sprintf('"%s" is not a valid class name', $name));
|
||||
|
||||
@@ -36,7 +36,8 @@ class CommandHandler extends AbstractProcessingHandler
|
||||
{
|
||||
parent::__construct();
|
||||
$this->command = $command;
|
||||
$this->changeLevel(env('LOG_LEVEL', 'debug'));
|
||||
|
||||
$this->changeLevel(env('APP_LOG_LEVEL', 'info'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,9 +57,10 @@ class CommandHandler extends AbstractProcessingHandler
|
||||
*/
|
||||
private function changeLevel(string $level)
|
||||
{
|
||||
$level = strtoupper($level);
|
||||
if (defined(sprintf('Logger::%s', $level))) {
|
||||
$this->setLevel(constant(sprintf('Logger::%s', $level)));
|
||||
$level = strtoupper($level);
|
||||
$reference = sprintf('\Monolog\Logger::%s', $level);
|
||||
if (defined($reference)) {
|
||||
$this->setLevel(constant($reference));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class AssetAccountIbans implements MapperInterface
|
||||
if (strlen($iban) > 0) {
|
||||
$topList[$account->id] = $account->iban . ' (' . $account->name . ')';
|
||||
}
|
||||
if (strlen($iban) == 0) {
|
||||
if (strlen($iban) === 0) {
|
||||
$list[$account->id] = $account->name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class OpposingAccountIbans implements MapperInterface
|
||||
if (strlen($iban) > 0) {
|
||||
$topList[$account->id] = $account->iban . ' (' . $account->name . ')';
|
||||
}
|
||||
if (strlen($iban) == 0) {
|
||||
if (strlen($iban) === 0) {
|
||||
$list[$account->id] = $account->name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
*
|
||||
* Class ImportAccount
|
||||
*
|
||||
* @package FireflyIII\Import\Object
|
||||
@@ -37,8 +38,18 @@ class ImportAccount
|
||||
private $accountName = [];
|
||||
/** @var array */
|
||||
private $accountNumber = [];
|
||||
/** @var int */
|
||||
private $defaultAccountId = 0;
|
||||
/** @var string */
|
||||
private $expectedType = '';
|
||||
/**
|
||||
* This value is used to indicate the other account ID (the opposing transaction's account),
|
||||
* if it is know. If so, this particular importaccount may never return an Account with this ID.
|
||||
* If it would, this would result in a transaction from-to the same account.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $forbiddenAccountId = 0;
|
||||
/** @var AccountRepositoryInterface */
|
||||
private $repository;
|
||||
/** @var User */
|
||||
@@ -115,6 +126,22 @@ class ImportAccount
|
||||
$this->accountNumber = $accountNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $defaultAccountId
|
||||
*/
|
||||
public function setDefaultAccountId(int $defaultAccountId)
|
||||
{
|
||||
$this->defaultAccountId = $defaultAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $forbiddenAccountId
|
||||
*/
|
||||
public function setForbiddenAccountId(int $forbiddenAccountId)
|
||||
{
|
||||
$this->forbiddenAccountId = $forbiddenAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*/
|
||||
@@ -138,7 +165,9 @@ class ImportAccount
|
||||
if (count($this->accountId) === 3) {
|
||||
Log::debug(sprintf('Finding account of type %d and ID %d', $accountType->id, $this->accountId['value']));
|
||||
/** @var Account $account */
|
||||
$account = $this->user->accounts()->where('account_type_id', $accountType->id)->where('id', $this->accountId['value'])->first();
|
||||
$account = $this->user->accounts()->where('id', '!=', $this->forbiddenAccountId)->where('account_type_id', $accountType->id)->where(
|
||||
'id', $this->accountId['value']
|
||||
)->first();
|
||||
if (!is_null($account)) {
|
||||
Log::debug(sprintf('Found unmapped %s account by ID (#%d): %s', $this->expectedType, $account->id, $account->name));
|
||||
|
||||
@@ -154,7 +183,7 @@ class ImportAccount
|
||||
Log::debug(sprintf('Finding account of type %d and IBAN %s', $accountType->id, $iban));
|
||||
$filtered = $accounts->filter(
|
||||
function (Account $account) use ($iban) {
|
||||
if ($account->iban === $iban) {
|
||||
if ($account->iban === $iban && $account->id !== $this->forbiddenAccountId) {
|
||||
Log::debug(
|
||||
sprintf('Found unmapped %s account by IBAN (#%d): %s (%s)', $this->expectedType, $account->id, $account->name, $account->iban)
|
||||
);
|
||||
@@ -177,7 +206,7 @@ class ImportAccount
|
||||
Log::debug(sprintf('Finding account of type %d and name %s', $accountType->id, $name));
|
||||
$filtered = $accounts->filter(
|
||||
function (Account $account) use ($name) {
|
||||
if ($account->name === $name) {
|
||||
if ($account->name === $name && $account->id !== $this->forbiddenAccountId) {
|
||||
Log::debug(sprintf('Found unmapped %s account by name (#%d): %s', $this->expectedType, $account->id, $account->name));
|
||||
|
||||
return $account;
|
||||
@@ -249,13 +278,22 @@ class ImportAccount
|
||||
$search = intval($array['mapped']);
|
||||
$account = $this->repository->find($search);
|
||||
|
||||
if ($account->accountType->type !== $this->expectedType) {
|
||||
if (is_null($account->id)) {
|
||||
Log::error(sprintf('There is no account with id #%d. Invalid mapping will be ignored!', $search));
|
||||
|
||||
return new Account;
|
||||
}
|
||||
// must be of the same type
|
||||
// except when mapped is an asset, then it's fair game.
|
||||
// which only shows that user must map very carefully.
|
||||
if ($account->accountType->type !== $this->expectedType && $account->accountType->type !== AccountType::ASSET) {
|
||||
Log::error(
|
||||
sprintf(
|
||||
'Mapped account #%d is of type "%s" but we expect a "%s"-account. Mapping will be ignored.', $account->id, $account->accountType->type,
|
||||
$this->expectedType
|
||||
)
|
||||
);
|
||||
|
||||
return new Account;
|
||||
}
|
||||
|
||||
@@ -297,6 +335,15 @@ class ImportAccount
|
||||
}
|
||||
$this->expectedType = $oldExpectedType;
|
||||
|
||||
// 4: if search for an asset account, fall back to given "default account" (mandatory)
|
||||
if ($this->expectedType === AccountType::ASSET) {
|
||||
$this->account = $this->repository->find($this->defaultAccountId);
|
||||
Log::debug(sprintf('Fall back to default account #%d "%s"', $this->account->id, $this->account->name));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5: then maybe, create one:
|
||||
Log::debug(sprintf('Found no account of type %s so must create one ourselves.', $this->expectedType));
|
||||
|
||||
$data = [
|
||||
|
||||
@@ -180,12 +180,19 @@ class ImportBill
|
||||
|
||||
Log::debug('Finding a mapped bill based on', $array);
|
||||
|
||||
$search = intval($array['mapped']);
|
||||
$account = $this->repository->find($search);
|
||||
$search = intval($array['mapped']);
|
||||
$bill = $this->repository->find($search);
|
||||
|
||||
Log::debug(sprintf('Found bill! #%d ("%s"). Return it', $account->id, $account->name));
|
||||
if (is_null($bill->id)) {
|
||||
Log::error(sprintf('There is no bill with id #%d. Invalid mapping will be ignored!', $search));
|
||||
|
||||
return $account;
|
||||
return new Bill;
|
||||
}
|
||||
|
||||
|
||||
Log::debug(sprintf('Found bill! #%d ("%s"). Return it', $bill->id, $bill->name));
|
||||
|
||||
return $bill;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -180,12 +180,18 @@ class ImportBudget
|
||||
|
||||
Log::debug('Finding a mapped budget based on', $array);
|
||||
|
||||
$search = intval($array['mapped']);
|
||||
$account = $this->repository->find($search);
|
||||
$search = intval($array['mapped']);
|
||||
$budget = $this->repository->find($search);
|
||||
|
||||
Log::debug(sprintf('Found budget! #%d ("%s"). Return it', $account->id, $account->name));
|
||||
if (is_null($budget->id)) {
|
||||
Log::error(sprintf('There is no budget with id #%d. Invalid mapping will be ignored!', $search));
|
||||
|
||||
return $account;
|
||||
return new Budget;
|
||||
}
|
||||
|
||||
Log::debug(sprintf('Found budget! #%d ("%s"). Return it', $budget->id, $budget->name));
|
||||
|
||||
return $budget;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -174,12 +174,18 @@ class ImportCategory
|
||||
|
||||
Log::debug('Finding a mapped category based on', $array);
|
||||
|
||||
$search = intval($array['mapped']);
|
||||
$account = $this->repository->find($search);
|
||||
$search = intval($array['mapped']);
|
||||
$category = $this->repository->find($search);
|
||||
|
||||
Log::debug(sprintf('Found category! #%d ("%s"). Return it', $account->id, $account->name));
|
||||
if (is_null($category->id)) {
|
||||
Log::error(sprintf('There is no category with id #%d. Invalid mapping will be ignored!', $search));
|
||||
|
||||
return $account;
|
||||
return new Category;
|
||||
}
|
||||
|
||||
Log::debug(sprintf('Found category! #%d ("%s"). Return it', $category->id, $category->name));
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -209,6 +209,13 @@ class ImportCurrency
|
||||
$search = intval($array['mapped']);
|
||||
$currency = $this->repository->find($search);
|
||||
|
||||
|
||||
if (is_null($currency->id)) {
|
||||
Log::error(sprintf('There is no currency with id #%d. Invalid mapping will be ignored!', $search));
|
||||
|
||||
return new TransactionCurrency;
|
||||
}
|
||||
|
||||
Log::debug(sprintf('Found currency! #%d ("%s"). Return it', $currency->id, $currency->name));
|
||||
|
||||
return $currency;
|
||||
|
||||
@@ -16,9 +16,8 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Import\Converter\Amount;
|
||||
use FireflyIII\Import\Converter\ConverterInterface;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Import\MapperPreProcess\PreProcessorInterface;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Log;
|
||||
use Steam;
|
||||
@@ -38,10 +37,10 @@ class ImportJournal
|
||||
public $budget;
|
||||
/** @var ImportCategory */
|
||||
public $category;
|
||||
/** @var ImportCurrency */
|
||||
public $currency;
|
||||
/** @var string */
|
||||
public $description = '';
|
||||
/** @var Collection */
|
||||
public $errors;
|
||||
/** @var string */
|
||||
public $hash;
|
||||
/** @var array */
|
||||
@@ -50,18 +49,18 @@ class ImportJournal
|
||||
public $notes = '';
|
||||
/** @var ImportAccount */
|
||||
public $opposing;
|
||||
/** @var array */
|
||||
public $tags = [];
|
||||
/** @var string */
|
||||
private $amount = '0';
|
||||
/** @var ImportCurrency */
|
||||
private $currency;
|
||||
private $amount;
|
||||
/** @var string */
|
||||
private $convertedAmount = null;
|
||||
/** @var string */
|
||||
private $date = '';
|
||||
/** @var string */
|
||||
private $externalId = '';
|
||||
/** @var array */
|
||||
private $modifiers = [];
|
||||
/** @var array */
|
||||
private $tags = [];
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
@@ -70,7 +69,6 @@ class ImportJournal
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->errors = new Collection;
|
||||
$this->asset = new ImportAccount;
|
||||
$this->opposing = new ImportAccount;
|
||||
$this->bill = new ImportBill;
|
||||
@@ -87,43 +85,39 @@ class ImportJournal
|
||||
$this->modifiers[] = $modifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TransactionJournal
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function createTransactionJournal(): TransactionJournal
|
||||
{
|
||||
exit('does not work yet');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function getAmount(): string
|
||||
{
|
||||
|
||||
/** @var ConverterInterface $amountConverter */
|
||||
$amountConverter = app(Amount::class);
|
||||
$this->amount = $amountConverter->convert($this->amount);
|
||||
// modify
|
||||
foreach ($this->modifiers as $modifier) {
|
||||
$class = sprintf('FireflyIII\Import\Converter\%s', config(sprintf('csv.import_roles.%s.converter', $modifier['role'])));
|
||||
/** @var ConverterInterface $converter */
|
||||
$converter = app($class);
|
||||
if ($converter->convert($modifier['value']) === -1) {
|
||||
$this->amount = Steam::negative($this->amount);
|
||||
Log::debug('Now in getAmount()');
|
||||
if (is_null($this->convertedAmount)) {
|
||||
Log::debug('convertedAmount is NULL');
|
||||
/** @var ConverterInterface $amountConverter */
|
||||
$amountConverter = app(Amount::class);
|
||||
$this->convertedAmount = $amountConverter->convert($this->amount);
|
||||
Log::debug(sprintf('First attempt to convert gives "%s"', $this->convertedAmount));
|
||||
// modify
|
||||
foreach ($this->modifiers as $modifier) {
|
||||
$class = sprintf('FireflyIII\Import\Converter\%s', config(sprintf('csv.import_roles.%s.converter', $modifier['role'])));
|
||||
/** @var ConverterInterface $converter */
|
||||
$converter = app($class);
|
||||
Log::debug(sprintf('Now launching converter %s', $class));
|
||||
if ($converter->convert($modifier['value']) === -1) {
|
||||
$this->convertedAmount = Steam::negative($this->convertedAmount);
|
||||
}
|
||||
Log::debug(sprintf('convertedAmount after conversion is %s', $this->convertedAmount));
|
||||
}
|
||||
|
||||
Log::debug(sprintf('After modifiers the result is: "%s"', $this->convertedAmount));
|
||||
}
|
||||
Log::debug(sprintf('convertedAmount is: "%s"', $this->convertedAmount));
|
||||
if (bccomp($this->convertedAmount, '0') === 0) {
|
||||
throw new FireflyException('Amount is zero.');
|
||||
}
|
||||
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ImportCurrency
|
||||
*/
|
||||
public function getCurrency(): ImportCurrency
|
||||
{
|
||||
return $this->currency;
|
||||
return $this->convertedAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,6 +138,18 @@ class ImportJournal
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
if ($this->description === '') {
|
||||
return '(no description)';
|
||||
}
|
||||
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hash
|
||||
*/
|
||||
@@ -226,7 +232,7 @@ class ImportJournal
|
||||
$this->date = $array['value'];
|
||||
break;
|
||||
case 'description':
|
||||
$this->description = $array['value'];
|
||||
$this->description .= $array['value'];
|
||||
break;
|
||||
case 'sepa-ct-op':
|
||||
case 'sepa-ct-id':
|
||||
@@ -257,7 +263,7 @@ class ImportJournal
|
||||
break;
|
||||
case 'tags-comma':
|
||||
case 'tags-space':
|
||||
$this->tags[] = $array;
|
||||
$this->setTags($array);
|
||||
break;
|
||||
case 'date-interest':
|
||||
$this->metaDates['interest_date'] = $array['value'];
|
||||
@@ -270,4 +276,18 @@ class ImportJournal
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
*/
|
||||
private function setTags(array $array): void
|
||||
{
|
||||
$preProcessorClass = config(sprintf('csv.import_roles.%s.pre-process-mapper', $array['role']));
|
||||
/** @var PreProcessorInterface $preProcessor */
|
||||
$preProcessor = app(sprintf('\FireflyIII\Import\MapperPreProcess\%s', $preProcessorClass));
|
||||
$tags = $preProcessor->run($array['value']);
|
||||
$this->tags = array_merge($this->tags, $tags);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace FireflyIII\Import\Routine;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use FireflyIII\Import\FileProcessor\FileProcessorInterface;
|
||||
use FireflyIII\Import\Storage\ImportStorage;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
@@ -64,14 +64,19 @@ class ImportRoutine
|
||||
Log::info(sprintf('Returned %d valid objects from file processor', $this->lines));
|
||||
|
||||
$storage = $this->storeObjects($importObjects);
|
||||
Log::debug('Back in run()');
|
||||
|
||||
// update job:
|
||||
$this->job->status = 'finished';
|
||||
$this->job->save();
|
||||
|
||||
Log::debug('Updated job...');
|
||||
|
||||
$this->journals = $storage->journals;
|
||||
$this->errors = $storage->errors;
|
||||
|
||||
Log::debug('Going to call createImportTag()');
|
||||
|
||||
// create tag, link tag to all journals:
|
||||
$this->createImportTag();
|
||||
|
||||
@@ -101,7 +106,7 @@ class ImportRoutine
|
||||
$processor = app($class);
|
||||
$processor->setJob($this->job);
|
||||
|
||||
if ($this->job->status == 'configured') {
|
||||
if ($this->job->status === 'configured') {
|
||||
|
||||
// set job as "running"...
|
||||
$this->job->status = 'running';
|
||||
@@ -120,6 +125,7 @@ class ImportRoutine
|
||||
*/
|
||||
private function createImportTag(): Tag
|
||||
{
|
||||
Log::debug('Now in createImportTag()');
|
||||
/** @var TagRepositoryInterface $repository */
|
||||
$repository = app(TagRepositoryInterface::class);
|
||||
$repository->setUser($this->job->user);
|
||||
@@ -138,14 +144,17 @@ class ImportRoutine
|
||||
$this->job->extended_status = $extended;
|
||||
$this->job->save();
|
||||
|
||||
$this->journals->each(
|
||||
function (TransactionJournal $journal) use ($tag) {
|
||||
$journal->tags()->save($tag);
|
||||
}
|
||||
);
|
||||
Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag));
|
||||
Log::debug('Looping journals...');
|
||||
$journalIds = $this->journals->pluck('id')->toArray();
|
||||
$tagId = $tag->id;
|
||||
foreach ($journalIds as $journalId) {
|
||||
Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId));
|
||||
DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]);
|
||||
}
|
||||
Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $this->journals->count(), $tag->id, $tag->tag));
|
||||
|
||||
return $tag;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,6 +169,7 @@ class ImportRoutine
|
||||
$storage->setDateFormat($this->job->configuration['date-format']);
|
||||
$storage->setObjects($objects);
|
||||
$storage->store();
|
||||
Log::info('Back in storeObjects()');
|
||||
|
||||
return $storage;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class AbnAmroDescription implements SpecificInterface
|
||||
$this->row[8] = $matches[4]; // 'opposing-account-name'
|
||||
$this->row[7] = $matches[4]; // 'description'
|
||||
|
||||
if ($matches[1] == 'GEA') {
|
||||
if ($matches[1] === 'GEA') {
|
||||
$this->row[7] = 'GEA ' . $matches[4]; // 'description'
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,8 @@ class RabobankDescription implements SpecificInterface
|
||||
);
|
||||
$row[6] = $alternateName;
|
||||
$row[10] = '';
|
||||
} else {
|
||||
}
|
||||
if (!(strlen($oppositeAccount) < 1 && strlen($oppositeName) < 1)) {
|
||||
Log::debug('Rabobank specific: either opposite account or name are filled.');
|
||||
}
|
||||
|
||||
|
||||
@@ -11,27 +11,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Import\Storage;
|
||||
|
||||
use Amount;
|
||||
use Carbon\Carbon;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Import\Object\ImportAccount;
|
||||
use FireflyIII\Import\Object\ImportJournal;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Bill;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Rules\Processor;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
use Steam;
|
||||
|
||||
/**
|
||||
* Is capable of storing individual ImportJournal objects.
|
||||
@@ -41,15 +28,16 @@ use Steam;
|
||||
*/
|
||||
class ImportStorage
|
||||
{
|
||||
use ImportSupport;
|
||||
|
||||
/** @var Collection */
|
||||
public $errors;
|
||||
/** @var Collection */
|
||||
public $journals;
|
||||
/** @var CurrencyRepositoryInterface */
|
||||
private $currencyRepository;
|
||||
/** @var int */
|
||||
protected $defaultCurrencyId = 1;
|
||||
/** @var string */
|
||||
private $dateFormat = 'Ymd';
|
||||
/** @var TransactionCurrency */
|
||||
private $defaultCurrency;
|
||||
private $dateFormat = 'Ymd'; // yes, hard coded
|
||||
/** @var ImportJob */
|
||||
private $job;
|
||||
/** @var Collection */
|
||||
@@ -57,6 +45,9 @@ class ImportStorage
|
||||
/** @var Collection */
|
||||
private $rules;
|
||||
|
||||
/** @var array */
|
||||
private $transfers = [];
|
||||
|
||||
/**
|
||||
* ImportStorage constructor.
|
||||
*/
|
||||
@@ -80,11 +71,11 @@ class ImportStorage
|
||||
*/
|
||||
public function setJob(ImportJob $job)
|
||||
{
|
||||
$this->job = $job;
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$repository->setUser($job->user);
|
||||
$this->currencyRepository = $repository;
|
||||
$this->rules = $this->getUserRules();
|
||||
$this->job = $job;
|
||||
$currency = app('amount')->getDefaultCurrencyByUser($this->job->user);
|
||||
$this->defaultCurrencyId = $currency->id;
|
||||
$this->transfers = $this->getTransfers();
|
||||
$this->rules = $this->getRules();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,257 +87,85 @@ class ImportStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* Do storage of import objects
|
||||
*/
|
||||
public function store()
|
||||
{
|
||||
$this->defaultCurrency = Amount::getDefaultCurrencyByUser($this->job->user);
|
||||
|
||||
// routine below consists of 3 steps.
|
||||
/**
|
||||
* @var int $index
|
||||
* @var ImportJournal $object
|
||||
*/
|
||||
foreach ($this->objects as $index => $object) {
|
||||
try {
|
||||
$this->storeImportJournal($index, $object);
|
||||
} catch (FireflyException $e) {
|
||||
$this->errors->push($e->getMessage());
|
||||
Log::error(sprintf('Cannot import row #%d because: %s', $index, $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* Do storage of import objects. Is the main function.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function applyRules(TransactionJournal $journal): bool
|
||||
public function store(): bool
|
||||
{
|
||||
if ($this->rules->count() > 0) {
|
||||
|
||||
/** @var Rule $rule */
|
||||
foreach ($this->rules as $rule) {
|
||||
Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id));
|
||||
$processor = Processor::make($rule);
|
||||
$processor->handleTransactionJournal($journal);
|
||||
|
||||
if ($rule->stop_processing) {
|
||||
return true;
|
||||
$this->objects->each(
|
||||
function (ImportJournal $importJournal, int $index) {
|
||||
try {
|
||||
$this->storeImportJournal($index, $importJournal);
|
||||
} catch (FireflyException | ErrorException | Exception $e) {
|
||||
$this->errors->push($e->getMessage());
|
||||
Log::error(sprintf('Cannot import row #%d because: %s', $index, $e->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
Log::info('ImportStorage has finished.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $journalId
|
||||
* @param int $accountId
|
||||
* @param int $currencyId
|
||||
* @param string $amount
|
||||
* @param int $index
|
||||
* @param ImportJournal $importJournal
|
||||
*
|
||||
* @return bool
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function createTransaction(int $journalId, int $accountId, int $currencyId, string $amount): bool
|
||||
protected function storeImportJournal(int $index, ImportJournal $importJournal): bool
|
||||
{
|
||||
$transaction = new Transaction;
|
||||
$transaction->account_id = $accountId;
|
||||
$transaction->transaction_journal_id = $journalId;
|
||||
$transaction->transaction_currency_id = $currencyId;
|
||||
$transaction->amount = $amount;
|
||||
$transaction->save();
|
||||
if (is_null($transaction->id)) {
|
||||
$errorText = join(', ', $transaction->getErrors()->all());
|
||||
throw new FireflyException($errorText);
|
||||
}
|
||||
Log::debug(sprintf('Created transaction with ID #%d and account #%d', $transaction->id, $accountId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJournal $importJournal
|
||||
*
|
||||
* @return TransactionCurrency
|
||||
*/
|
||||
private function getCurrency(ImportJournal $importJournal, Account $account): TransactionCurrency
|
||||
{
|
||||
// start with currency pref of account, if any:
|
||||
$currency = $this->currencyRepository->find(intval($account->getMeta('currency_id')));
|
||||
if (!is_null($currency->id)) {
|
||||
return $currency;
|
||||
}
|
||||
|
||||
// use given currency
|
||||
$currency = $importJournal->getCurrency()->getTransactionCurrency();
|
||||
if (!is_null($currency->id)) {
|
||||
return $currency;
|
||||
}
|
||||
|
||||
// backup to default
|
||||
$currency = $this->defaultCurrency;
|
||||
|
||||
return $currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportAccount $account
|
||||
* @param $amount
|
||||
*
|
||||
* @return Account
|
||||
*/
|
||||
private function getOpposingAccount(ImportAccount $account, $amount): Account
|
||||
{
|
||||
if (bccomp($amount, '0') === -1) {
|
||||
Log::debug(sprintf('%s is negative, create opposing expense account.', $amount));
|
||||
$account->setExpectedType(AccountType::EXPENSE);
|
||||
|
||||
return $account->getAccount();
|
||||
}
|
||||
Log::debug(sprintf('%s is positive, create opposing revenue account.', $amount));
|
||||
// amount is positive, it's a deposit, opposing is an revenue:
|
||||
$account->setExpectedType(AccountType::REVENUE);
|
||||
|
||||
$databaseAccount = $account->getAccount();
|
||||
|
||||
return $databaseAccount;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $amount
|
||||
*
|
||||
* @return TransactionType
|
||||
*/
|
||||
private function getTransactionType(string $amount): TransactionType
|
||||
{
|
||||
$transactionType = new TransactionType();
|
||||
// amount is negative, it's a withdrawal, opposing is an expense:
|
||||
if (bccomp($amount, '0') === -1) {
|
||||
$transactionType = TransactionType::whereType(TransactionType::WITHDRAWAL)->first();
|
||||
}
|
||||
if (bccomp($amount, '0') === 1) {
|
||||
$transactionType = TransactionType::whereType(TransactionType::DEPOSIT)->first();
|
||||
}
|
||||
|
||||
return $transactionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
private function getUserRules(): Collection
|
||||
{
|
||||
$set = Rule::distinct()
|
||||
->where('rules.user_id', $this->job->user->id)
|
||||
->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id')
|
||||
->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
|
||||
->where('rule_groups.active', 1)
|
||||
->where('rule_triggers.trigger_type', 'user_action')
|
||||
->where('rule_triggers.trigger_value', 'store-journal')
|
||||
->where('rules.active', 1)
|
||||
->orderBy('rule_groups.order', 'ASC')
|
||||
->orderBy('rules.order', 'ASC')
|
||||
->get(['rules.*', 'rule_groups.order']);
|
||||
Log::debug(sprintf('Found %d user rules.', $set->count()));
|
||||
|
||||
return $set;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param Bill $bill
|
||||
*/
|
||||
private function storeBill(TransactionJournal $journal, Bill $bill)
|
||||
{
|
||||
if (!is_null($bill->id)) {
|
||||
Log::debug(sprintf('Linked bill #%d to journal #%d', $bill->id, $journal->id));
|
||||
$journal->bill()->associate($bill);
|
||||
$journal->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param Budget $budget
|
||||
*/
|
||||
private function storeBudget(TransactionJournal $journal, Budget $budget)
|
||||
{
|
||||
if (!is_null($budget->id)) {
|
||||
Log::debug(sprintf('Linked budget #%d to journal #%d', $budget->id, $journal->id));
|
||||
$journal->budgets()->save($budget);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param Category $category
|
||||
*/
|
||||
private function storeCategory(TransactionJournal $journal, Category $category)
|
||||
{
|
||||
|
||||
if (!is_null($category->id)) {
|
||||
Log::debug(sprintf('Linked category #%d to journal #%d', $category->id, $journal->id));
|
||||
$journal->categories()->save($category);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function storeImportJournal(int $index, ImportJournal $importJournal): bool
|
||||
{
|
||||
Log::debug(sprintf('Going to store object #%d with description "%s"', $index, $importJournal->description));
|
||||
|
||||
$asset = $importJournal->asset->getAccount();
|
||||
$amount = $importJournal->getAmount();
|
||||
$currency = $this->getCurrency($importJournal, $asset);
|
||||
$date = $importJournal->getDate($this->dateFormat);
|
||||
$transactionType = $this->getTransactionType($amount);
|
||||
$opposing = $this->getOpposingAccount($importJournal->opposing, $amount);
|
||||
|
||||
// if opposing is an asset account, it's a transfer:
|
||||
if ($opposing->accountType->type === AccountType::ASSET) {
|
||||
Log::debug(sprintf('Opposing account #%d %s is an asset account, make transfer.', $opposing->id, $opposing->name));
|
||||
$transactionType = TransactionType::whereType(TransactionType::TRANSFER)->first();
|
||||
}
|
||||
|
||||
// verify that opposing account is of the correct type:
|
||||
if ($opposing->accountType->type === AccountType::EXPENSE && $transactionType->type !== TransactionType::WITHDRAWAL) {
|
||||
Log::error(sprintf('Row #%d is imported as a %s but opposing is an expense account. This cannot be!', $index, $transactionType->type));
|
||||
}
|
||||
Log::debug(sprintf('Going to store object #%d with description "%s"', $index, $importJournal->getDescription()));
|
||||
$assetAccount = $importJournal->asset->getAccount();
|
||||
$amount = $importJournal->getAmount();
|
||||
$currencyId = $this->getCurrencyId($importJournal);
|
||||
$foreignCurrencyId = $this->getForeignCurrencyId($importJournal, $currencyId);
|
||||
$date = $importJournal->getDate($this->dateFormat)->format('Y-m-d');
|
||||
$opposingAccount = $this->getOpposingAccount($importJournal->opposing, $assetAccount->id, $amount);
|
||||
$transactionType = $this->getTransactionType($amount, $opposingAccount);
|
||||
$description = $importJournal->getDescription();
|
||||
|
||||
/*** First step done! */
|
||||
$this->job->addStepsDone(1);
|
||||
|
||||
// create a journal:
|
||||
$journal = new TransactionJournal;
|
||||
$journal->user_id = $this->job->user_id;
|
||||
$journal->transaction_type_id = $transactionType->id;
|
||||
$journal->transaction_currency_id = $currency->id;
|
||||
$journal->description = $importJournal->description;
|
||||
$journal->date = $date->format('Y-m-d');
|
||||
$journal->order = 0;
|
||||
$journal->tag_count = 0;
|
||||
$journal->completed = false;
|
||||
|
||||
if (!$journal->save()) {
|
||||
$errorText = join(', ', $journal->getErrors()->all());
|
||||
/**
|
||||
* Check for double transfer.
|
||||
*/
|
||||
$parameters = [
|
||||
'type' => $transactionType,
|
||||
'description' => $description,
|
||||
'amount' => $amount,
|
||||
'date' => $date,
|
||||
'asset' => $assetAccount->name,
|
||||
'opposing' => $opposingAccount->name,
|
||||
];
|
||||
if ($this->isDoubleTransfer($parameters) || $this->hashAlreadyImported($importJournal->hash)) {
|
||||
$this->job->addStepsDone(3);
|
||||
throw new FireflyException($errorText);
|
||||
// throw error
|
||||
$message = sprintf('Detected a possible duplicate, skip this one (hash: %s).', $importJournal->hash);
|
||||
Log::error($message, $parameters);
|
||||
throw new FireflyException($message);
|
||||
|
||||
}
|
||||
unset($parameters);
|
||||
|
||||
// save meta data:
|
||||
$journal->setMeta('importHash', $importJournal->hash);
|
||||
Log::debug(sprintf('Created journal with ID #%d', $journal->id));
|
||||
// store journal and create transactions:
|
||||
$parameters = [
|
||||
'type' => $transactionType,
|
||||
'currency' => $currencyId,
|
||||
'foreign_currency' => $foreignCurrencyId,
|
||||
'asset' => $assetAccount,
|
||||
'opposing' => $opposingAccount,
|
||||
'description' => $description,
|
||||
'date' => $date,
|
||||
'hash' => $importJournal->hash,
|
||||
'amount' => $amount,
|
||||
|
||||
// create transactions:
|
||||
$this->createTransaction($journal->id, $asset->id, $currency->id, $amount);
|
||||
$this->createTransaction($journal->id, $opposing->id, $currency->id, Steam::opposite($amount));
|
||||
];
|
||||
$journal = $this->storeJournal($parameters);
|
||||
unset($parameters);
|
||||
|
||||
/*** Another step done! */
|
||||
$this->job->addStepsDone(1);
|
||||
@@ -356,50 +175,64 @@ class ImportStorage
|
||||
$this->storeBudget($journal, $importJournal->budget->getBudget());
|
||||
$this->storeBill($journal, $importJournal->bill->getBill());
|
||||
$this->storeMeta($journal, $importJournal->metaDates);
|
||||
|
||||
// sepa thing as note:
|
||||
if (strlen($importJournal->notes) > 0) {
|
||||
$journal->setMeta('notes', $importJournal->notes);
|
||||
}
|
||||
$journal->setMeta('notes', $importJournal->notes);
|
||||
$this->storeTags($importJournal->tags, $journal);
|
||||
|
||||
// set journal completed:
|
||||
$journal->completed = true;
|
||||
$journal->save();
|
||||
|
||||
/*** Another step done! */
|
||||
$this->job->addStepsDone(1);
|
||||
|
||||
// run rules:
|
||||
$this->applyRules($journal);
|
||||
/*** Another step done! */
|
||||
$this->job->addStepsDone(1);
|
||||
|
||||
$this->journals->push($journal);
|
||||
|
||||
Log::info(
|
||||
sprintf(
|
||||
'Imported new journal #%d with description "%s" and amount %s %s.', $journal->id, $journal->description, $journal->transactionCurrency->code,
|
||||
$amount
|
||||
)
|
||||
);
|
||||
Log::info(sprintf('Imported new journal #%d: "%s", amount %s %s.', $journal->id, $journal->description, $journal->transactionCurrency->code, $amount));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param array $dates
|
||||
* @param array $parameters
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function storeMeta(TransactionJournal $journal, array $dates)
|
||||
private function isDoubleTransfer(array $parameters): bool
|
||||
{
|
||||
// all other date fields as meta thing:
|
||||
foreach ($dates as $name => $value) {
|
||||
try {
|
||||
$date = new Carbon($value);
|
||||
$journal->setMeta($name, $date);
|
||||
} catch (\Exception $e) {
|
||||
// don't care, ignore:
|
||||
Log::warning(sprintf('Could not parse "%s" into a valid Date object for field %s', $value, $name));
|
||||
if ($parameters['type'] !== TransactionType::TRANSFER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$amount = app('steam')->positive($parameters['amount']);
|
||||
$names = [$parameters['asset'], $parameters['opposing']];
|
||||
$transfer = [];
|
||||
$hit = false;
|
||||
sort($names);
|
||||
|
||||
foreach ($this->transfers as $transfer) {
|
||||
if ($parameters['description'] === $transfer['description']) {
|
||||
$hit = true;
|
||||
}
|
||||
if ($names === $transfer['names']) {
|
||||
$hit = true;
|
||||
}
|
||||
if (bccomp($amount, $transfer['amount']) === 0) {
|
||||
$hit = true;
|
||||
}
|
||||
if ($parameters['date'] === $transfer['date']) {
|
||||
$hit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($hit === true) {
|
||||
Log::error(
|
||||
'There already is a transfer imported with these properties. Compare existing with new. ', ['existing' => $transfer, 'new' => $parameters]
|
||||
);
|
||||
}
|
||||
|
||||
return $hit;
|
||||
}
|
||||
}
|
||||
|
||||
447
app/Import/Storage/ImportSupport.php
Normal file
447
app/Import/Storage/ImportSupport.php
Normal file
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
/**
|
||||
* ImportSupport.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Import\Storage;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Import\Object\ImportAccount;
|
||||
use FireflyIII\Import\Object\ImportJournal;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Bill;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionJournalMeta;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
|
||||
use FireflyIII\Rules\Processor;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
trait ImportSupport
|
||||
{
|
||||
/** @var int */
|
||||
protected $defaultCurrencyId = 1;
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function applyRules(TransactionJournal $journal): bool
|
||||
{
|
||||
if ($this->rules->count() > 0) {
|
||||
$this->rules->each(
|
||||
function (Rule $rule) use ($journal) {
|
||||
Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id));
|
||||
$processor = Processor::make($rule);
|
||||
$processor->handleTransactionJournal($journal);
|
||||
if ($rule->stop_processing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $parameters
|
||||
*
|
||||
* @return bool
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function createTransaction(array $parameters): bool
|
||||
{
|
||||
$transaction = new Transaction;
|
||||
$transaction->account_id = $parameters['account'];
|
||||
$transaction->transaction_journal_id = $parameters['id'];
|
||||
$transaction->transaction_currency_id = $parameters['currency'];
|
||||
$transaction->amount = $parameters['amount'];
|
||||
$transaction->foreign_currency_id = $parameters['foreign_currency'];
|
||||
$transaction->foreign_amount = $parameters['foreign_amount'];
|
||||
$transaction->save();
|
||||
if (is_null($transaction->id)) {
|
||||
$errorText = join(', ', $transaction->getErrors()->all());
|
||||
throw new FireflyException($errorText);
|
||||
}
|
||||
Log::debug(sprintf('Created transaction with ID #%d, account #%d, amount %s', $transaction->id, $parameters['account'], $parameters['amount']));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method finds out what the import journal's currency should be. The account itself
|
||||
* is favoured (and usually it stops there). If no preference is found, the journal has a say
|
||||
* and thirdly the default currency is used.
|
||||
*
|
||||
* @param ImportJournal $importJournal
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getCurrencyId(ImportJournal $importJournal): int
|
||||
{
|
||||
// start with currency pref of account, if any:
|
||||
$account = $importJournal->asset->getAccount();
|
||||
$currencyId = intval($account->getMeta('currency_id'));
|
||||
if ($currencyId > 0) {
|
||||
return $currencyId;
|
||||
}
|
||||
|
||||
// use given currency
|
||||
$currency = $importJournal->currency->getTransactionCurrency();
|
||||
if (!is_null($currency->id)) {
|
||||
return $currency->id;
|
||||
}
|
||||
|
||||
// backup to default
|
||||
$currency = $this->defaultCurrencyId;
|
||||
|
||||
return $currency;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The foreign currency is only returned when the journal has a different value from the
|
||||
* currency id (see other method).
|
||||
*
|
||||
* @param ImportJournal $importJournal
|
||||
* @param int $currencyId
|
||||
*
|
||||
* @see ImportSupport::getCurrencyId
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
private function getForeignCurrencyId(ImportJournal $importJournal, int $currencyId): ?int
|
||||
{
|
||||
// use given currency by import journal.
|
||||
$currency = $importJournal->currency->getTransactionCurrency();
|
||||
if (!is_null($currency->id) && $currency->id !== $currencyId) {
|
||||
return $currency->id;
|
||||
}
|
||||
|
||||
// return null, because no different:
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The search for the opposing account is complex. Firstly, we forbid the ImportAccount to resolve into the asset
|
||||
* account to prevent a situation where the transaction flows from A to A. Given the amount, we "expect" the opposing
|
||||
* account to be an expense or a revenue account. However, the mapping given by the user may return something else
|
||||
* entirely (usually an asset account). So whatever the expectation, the result may be anything.
|
||||
*
|
||||
* When the result does not match the expected type (a negative amount cannot be linked to a revenue account) the next step
|
||||
* will return an error.
|
||||
*
|
||||
* @param ImportAccount $account
|
||||
* @param int $forbiddenAccount
|
||||
* @param string $amount
|
||||
*
|
||||
* @see ImportSupport::getTransactionType
|
||||
*
|
||||
* @return Account
|
||||
*/
|
||||
private function getOpposingAccount(ImportAccount $account, int $forbiddenAccount, string $amount): Account
|
||||
{
|
||||
$account->setForbiddenAccountId($forbiddenAccount);
|
||||
if (bccomp($amount, '0') === -1) {
|
||||
Log::debug(sprintf('%s is negative, create opposing expense account.', $amount));
|
||||
$account->setExpectedType(AccountType::EXPENSE);
|
||||
|
||||
return $account->getAccount();
|
||||
}
|
||||
Log::debug(sprintf('%s is positive, create opposing revenue account.', $amount));
|
||||
// amount is positive, it's a deposit, opposing is an revenue:
|
||||
$account->setExpectedType(AccountType::REVENUE);
|
||||
|
||||
$databaseAccount = $account->getAccount();
|
||||
|
||||
return $databaseAccount;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
private function getRules(): Collection
|
||||
{
|
||||
$set = Rule::distinct()
|
||||
->where('rules.user_id', $this->job->user->id)
|
||||
->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id')
|
||||
->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
|
||||
->where('rule_groups.active', 1)
|
||||
->where('rule_triggers.trigger_type', 'user_action')
|
||||
->where('rule_triggers.trigger_value', 'store-journal')
|
||||
->where('rules.active', 1)
|
||||
->orderBy('rule_groups.order', 'ASC')
|
||||
->orderBy('rules.order', 'ASC')
|
||||
->get(['rules.*', 'rule_groups.order']);
|
||||
Log::debug(sprintf('Found %d user rules.', $set->count()));
|
||||
|
||||
return $set;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the amount and the opposing account its easy to define which kind of transaction type should be associated with the new
|
||||
* import. This may however fail when there is an unexpected mismatch between the transaction type and the opposing account.
|
||||
*
|
||||
* @param string $amount
|
||||
* @param Account $account
|
||||
*
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
* @see ImportSupport::getOpposingAccount()
|
||||
*/
|
||||
private function getTransactionType(string $amount, Account $account): string
|
||||
{
|
||||
$transactionType = TransactionType::WITHDRAWAL;
|
||||
// amount is negative, it's a withdrawal, opposing is an expense:
|
||||
if (bccomp($amount, '0') === -1) {
|
||||
$transactionType = TransactionType::WITHDRAWAL;
|
||||
}
|
||||
|
||||
if (bccomp($amount, '0') === 1) {
|
||||
$transactionType = TransactionType::DEPOSIT;
|
||||
}
|
||||
|
||||
// if opposing is an asset account, it's a transfer:
|
||||
if ($account->accountType->type === AccountType::ASSET) {
|
||||
Log::debug(sprintf('Opposing account #%d %s is an asset account, make transfer.', $account->id, $account->name));
|
||||
$transactionType = TransactionType::TRANSFER;
|
||||
}
|
||||
|
||||
// verify that opposing account is of the correct type:
|
||||
if ($account->accountType->type === AccountType::EXPENSE && $transactionType !== TransactionType::WITHDRAWAL) {
|
||||
$message = 'This row is imported as a withdrawal but opposing is an expense account. This cannot be!';
|
||||
Log::error($message);
|
||||
throw new FireflyException($message);
|
||||
}
|
||||
|
||||
return $transactionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a collection of the current transfers in the system and some meta data for
|
||||
* this set. This can later be used to see if the journal that firefly is trying to import
|
||||
* is not already present.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getTransfers(): array
|
||||
{
|
||||
$set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
|
||||
->leftJoin(
|
||||
'transactions AS source', function (JoinClause $join) {
|
||||
$join->on('transaction_journals.id', '=', 'source.transaction_journal_id')->where('source.amount', '<', 0);
|
||||
}
|
||||
)
|
||||
->leftJoin(
|
||||
'transactions AS destination', function (JoinClause $join) {
|
||||
$join->on('transaction_journals.id', '=', 'destination.transaction_journal_id')->where(
|
||||
'destination.amount', '>', 0
|
||||
);
|
||||
}
|
||||
)
|
||||
->leftJoin('accounts as source_accounts', 'source.account_id', '=', 'source_accounts.id')
|
||||
->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id')
|
||||
->where('transaction_journals.user_id', $this->job->user_id)
|
||||
->where('transaction_types.type', TransactionType::TRANSFER)
|
||||
->get(
|
||||
['transaction_journals.id', 'transaction_journals.encrypted', 'transaction_journals.description',
|
||||
'source_accounts.name as source_name', 'destination_accounts.name as destination_name', 'destination.amount'
|
||||
, 'transaction_journals.date']
|
||||
);
|
||||
$array = [];
|
||||
/** @var TransactionJournal $entry */
|
||||
foreach ($set as $entry) {
|
||||
$original = [app('steam')->tryDecrypt($entry->source_name), app('steam')->tryDecrypt($entry->destination_name)];
|
||||
sort($original);
|
||||
$array[] = [
|
||||
'names' => $original,
|
||||
'amount' => $entry->amount,
|
||||
'date' => $entry->date->format('Y-m-d'),
|
||||
'description' => $entry->description,
|
||||
];
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the import journal has not been imported before.
|
||||
*
|
||||
* @param string $hash
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function hashAlreadyImported(string $hash): bool
|
||||
{
|
||||
$json = json_encode($hash);
|
||||
/** @var TransactionJournalMeta $entry */
|
||||
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
|
||||
->where('data', $json)
|
||||
->where('name', 'importHash')
|
||||
->first();
|
||||
if (!is_null($entry)) {
|
||||
Log::error(sprintf('A journal with hash %s has already been imported (spoiler: it\'s journal #%d)', $hash, $entry->transaction_journal_id));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param Bill $bill
|
||||
*/
|
||||
private function storeBill(TransactionJournal $journal, Bill $bill)
|
||||
{
|
||||
if (!is_null($bill->id)) {
|
||||
Log::debug(sprintf('Linked bill #%d to journal #%d', $bill->id, $journal->id));
|
||||
$journal->bill()->associate($bill);
|
||||
$journal->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param Budget $budget
|
||||
*/
|
||||
private function storeBudget(TransactionJournal $journal, Budget $budget)
|
||||
{
|
||||
if (!is_null($budget->id)) {
|
||||
Log::debug(sprintf('Linked budget #%d to journal #%d', $budget->id, $journal->id));
|
||||
$journal->budgets()->save($budget);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param Category $category
|
||||
*/
|
||||
private function storeCategory(TransactionJournal $journal, Category $category)
|
||||
{
|
||||
|
||||
if (!is_null($category->id)) {
|
||||
Log::debug(sprintf('Linked category #%d to journal #%d', $category->id, $journal->id));
|
||||
$journal->categories()->save($category);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function storeJournal(array $parameters): TransactionJournal
|
||||
{
|
||||
// find transaction type:
|
||||
$transactionType = TransactionType::whereType($parameters['type'])->first();
|
||||
|
||||
// create a journal:
|
||||
$journal = new TransactionJournal;
|
||||
$journal->user_id = $this->job->user_id;
|
||||
$journal->transaction_type_id = $transactionType->id;
|
||||
$journal->transaction_currency_id = $parameters['currency'];
|
||||
$journal->description = $parameters['description'];
|
||||
$journal->date = $parameters['date'];
|
||||
$journal->order = 0;
|
||||
$journal->tag_count = 0;
|
||||
$journal->completed = false;
|
||||
|
||||
if (!$journal->save()) {
|
||||
$errorText = join(', ', $journal->getErrors()->all());
|
||||
// add three steps:
|
||||
$this->job->addStepsDone(3);
|
||||
// throw error
|
||||
throw new FireflyException($errorText);
|
||||
}
|
||||
// save meta data:
|
||||
$journal->setMeta('importHash', $parameters['hash']);
|
||||
Log::debug(sprintf('Created journal with ID #%d', $journal->id));
|
||||
|
||||
// create transactions:
|
||||
$one = [
|
||||
'id' => $journal->id,
|
||||
'account' => $parameters['asset']->id,
|
||||
'currency' => $parameters['currency'],
|
||||
'amount' => $parameters['amount'],
|
||||
'foreign_currency' => $parameters['foreign_currency'],
|
||||
'foreign_amount' => is_null($parameters['foreign_currency']) ? null : $parameters['amount'],
|
||||
];
|
||||
$opposite = app('steam')->opposite($parameters['amount']);
|
||||
$two = [
|
||||
'id' => $journal->id,
|
||||
'account' => $parameters['opposing']->id,
|
||||
'currency' => $parameters['currency'],
|
||||
'amount' => $opposite,
|
||||
'foreign_currency' => $parameters['foreign_currency'],
|
||||
'foreign_amount' => is_null($parameters['foreign_currency']) ? null : $opposite,
|
||||
];
|
||||
$this->createTransaction($one);
|
||||
$this->createTransaction($two);
|
||||
|
||||
return $journal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionJournal $journal
|
||||
* @param array $dates
|
||||
*/
|
||||
private function storeMeta(TransactionJournal $journal, array $dates)
|
||||
{
|
||||
// all other date fields as meta thing:
|
||||
foreach ($dates as $name => $value) {
|
||||
try {
|
||||
$date = new Carbon($value);
|
||||
$journal->setMeta($name, $date);
|
||||
} catch (Exception $e) {
|
||||
// don't care, ignore:
|
||||
Log::warning(sprintf('Could not parse "%s" into a valid Date object for field %s', $value, $name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $tags
|
||||
* @param TransactionJournal $journal
|
||||
*/
|
||||
private function storeTags(array $tags, TransactionJournal $journal): void
|
||||
{
|
||||
$repository = app(TagRepositoryInterface::class);
|
||||
$repository->setUser($journal->user);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$dbTag = $repository->findByTag($tag);
|
||||
if (is_null($dbTag->id)) {
|
||||
$dbTag = $repository->store(
|
||||
['tag' => $tag, 'date' => null, 'description' => null, 'latitude' => null, 'longitude' => null,
|
||||
'zoomLevel' => null, 'tagMode' => 'nothing']
|
||||
);
|
||||
}
|
||||
$journal->tags()->save($dbTag);
|
||||
Log::debug(sprintf('Linked tag %d ("%s") to journal #%d', $dbTag->id, $dbTag->tag, $journal->id));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
161
app/Jobs/ExecuteRuleOnExistingTransactions.php
Normal file
161
app/Jobs/ExecuteRuleOnExistingTransactions.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* ExecuteRuleOnExistingTransactions.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Jobs;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Rules\Processor;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ExecuteRuleOnExistingTransactions
|
||||
*
|
||||
* @package FireflyIII\Jobs
|
||||
*/
|
||||
class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue, SerializesModels;
|
||||
|
||||
/** @var Collection */
|
||||
private $accounts;
|
||||
/** @var Carbon */
|
||||
private $endDate;
|
||||
/** @var Rule */
|
||||
private $rule;
|
||||
/** @var Carbon */
|
||||
private $startDate;
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param Rule $rule
|
||||
*/
|
||||
public function __construct(Rule $rule)
|
||||
{
|
||||
$this->rule = $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccounts(): Collection
|
||||
{
|
||||
return $this->accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Collection $accounts
|
||||
*/
|
||||
public function setAccounts(Collection $accounts)
|
||||
{
|
||||
$this->accounts = $accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Carbon\Carbon
|
||||
*/
|
||||
public function getEndDate(): Carbon
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Carbon $date
|
||||
*/
|
||||
public function setEndDate(Carbon $date)
|
||||
{
|
||||
$this->endDate = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Carbon\Carbon
|
||||
*/
|
||||
public function getStartDate(): Carbon
|
||||
{
|
||||
return $this->startDate;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Carbon $date
|
||||
*/
|
||||
public function setStartDate(Carbon $date)
|
||||
{
|
||||
$this->startDate = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function setUser(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Lookup all journals that match the parameters specified
|
||||
$transactions = $this->collectJournals();
|
||||
$processor = Processor::make($this->rule);
|
||||
|
||||
// Execute the rules for each transaction
|
||||
foreach ($transactions as $transaction) {
|
||||
|
||||
$processor->handleTransaction($transaction);
|
||||
|
||||
// Stop processing this group if the rule specifies 'stop_processing'
|
||||
if ($processor->getRule()->stop_processing) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all journals that should be processed
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
protected function collectJournals()
|
||||
{
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setUser($this->user);
|
||||
$collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate);
|
||||
|
||||
return $collector->getJournals();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class MailError extends Job implements ShouldQueue
|
||||
Mail::send(
|
||||
['emails.error-html', 'emails.error-text'], $args,
|
||||
function (Message $message) use ($email) {
|
||||
if ($email != 'mail@example.com') {
|
||||
if ($email !== 'mail@example.com') {
|
||||
$message->to($email, $email)->subject('Caught an error in Firely III');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RegisteredUser.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
/**
|
||||
* RegisteredUser.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RequestedNewPassword.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
/**
|
||||
* RequestedNewPassword.php
|
||||
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||
* This software may be modified and distributed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
*
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
||||
@@ -95,7 +95,7 @@ class Account extends Model
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
if ($account->name == $fields['name']) {
|
||||
if ($account->name === $fields['name']) {
|
||||
return $account;
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ class Account extends Model
|
||||
{
|
||||
|
||||
if (auth()->check()) {
|
||||
if ($value->user_id == auth()->user()->id) {
|
||||
if (intval($value->user_id) === auth()->user()->id) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ class Account extends Model
|
||||
public function getMeta(string $fieldName): string
|
||||
{
|
||||
foreach ($this->accountMeta as $meta) {
|
||||
if ($meta->name == $fieldName) {
|
||||
if ($meta->name === $fieldName) {
|
||||
return strval($meta->data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class Attachment extends Model
|
||||
{
|
||||
if (auth()->check()) {
|
||||
|
||||
if ($value->user_id == auth()->user()->id) {
|
||||
if (intval($value->user_id) === auth()->user()->id) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use Crypt;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Watson\Validating\ValidatingTrait;
|
||||
|
||||
@@ -28,7 +29,7 @@ use Watson\Validating\ValidatingTrait;
|
||||
class Bill extends Model
|
||||
{
|
||||
|
||||
use ValidatingTrait;
|
||||
use SoftDeletes, ValidatingTrait;
|
||||
/**
|
||||
* The attributes that should be casted to native types.
|
||||
*
|
||||
@@ -62,7 +63,7 @@ class Bill extends Model
|
||||
public static function routeBinder(Bill $value)
|
||||
{
|
||||
if (auth()->check()) {
|
||||
if ($value->user_id == auth()->user()->id) {
|
||||
if (intval($value->user_id) === auth()->user()->id) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +78,7 @@ class Bill extends Model
|
||||
public function getMatchAttribute($value)
|
||||
{
|
||||
|
||||
if (intval($this->match_encrypted) == 1) {
|
||||
if (intval($this->match_encrypted) === 1) {
|
||||
return Crypt::decrypt($value);
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ class Bill extends Model
|
||||
public function getNameAttribute($value)
|
||||
{
|
||||
|
||||
if (intval($this->name_encrypted) == 1) {
|
||||
if (intval($this->name_encrypted) === 1) {
|
||||
return Crypt::decrypt($value);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user