mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-01-07 11:01:20 +00:00
Compare commits
1498 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 | ||
|
|
e82deddefb | ||
|
|
780bec35bb | ||
|
|
27d80fd370 | ||
|
|
952328d55c | ||
|
|
f747f3bd10 | ||
|
|
11c3aca58d | ||
|
|
5ef2067836 | ||
|
|
de9728895e | ||
|
|
19774f32c2 | ||
|
|
e62b979708 | ||
|
|
69eef30ad3 | ||
|
|
71a41bc00d | ||
|
|
dd508dbc49 | ||
|
|
13ad40402a | ||
|
|
98712f288c | ||
|
|
a29a78e199 | ||
|
|
767267c89e | ||
|
|
b3801f65cf | ||
|
|
44a1d6b3d4 | ||
|
|
7a1528d1e4 | ||
|
|
c970a25d58 | ||
|
|
0a78b0b130 | ||
|
|
69f6dc09d3 | ||
|
|
467e708617 | ||
|
|
3a84591e61 | ||
|
|
a527066216 | ||
|
|
ef9823035f | ||
|
|
513ee4c7d9 | ||
|
|
fcc9b3a588 | ||
|
|
71eed45b77 | ||
|
|
6940f00492 | ||
|
|
1c85c20fa9 | ||
|
|
ae30b02405 | ||
|
|
89b7a9d3ac | ||
|
|
dc25fc99b3 | ||
|
|
6e6957a9ab | ||
|
|
0609e7db06 | ||
|
|
f57097b79e | ||
|
|
7e988d97d3 | ||
|
|
2795042c2c | ||
|
|
c3c86545fc | ||
|
|
3fe14a3d56 | ||
|
|
bad5740728 | ||
|
|
c164553ba1 | ||
|
|
e6c72b2995 | ||
|
|
cbcefabbff | ||
|
|
97cfa3f3ae | ||
|
|
803a008821 | ||
|
|
658730928b | ||
|
|
310db374c8 | ||
|
|
dc5bd648cf | ||
|
|
b6be9e6ade | ||
|
|
04f71496e3 | ||
|
|
1eed142423 | ||
|
|
f1e0ad0df0 | ||
|
|
f8fa5f1ba5 | ||
|
|
c187563267 | ||
|
|
fcdbbd5dbc | ||
|
|
6604d04c60 | ||
|
|
63be574a14 | ||
|
|
2414f998ba | ||
|
|
90e2ecad4f | ||
|
|
87a3f5c715 | ||
|
|
a4ef81ebd8 | ||
|
|
67fc810fc2 | ||
|
|
e13a61645d | ||
|
|
aae4a78fd4 | ||
|
|
3947da5e27 | ||
|
|
4e4ce2f77c | ||
|
|
d9a2e081bd | ||
|
|
e60dde7ce5 | ||
|
|
699b138999 | ||
|
|
8f191b497f | ||
|
|
fd013d617c | ||
|
|
a8bda38035 | ||
|
|
411de9aa23 | ||
|
|
f0abe497ef | ||
|
|
9c8be83f5d | ||
|
|
45335af8cf | ||
|
|
945e433bb7 | ||
|
|
70729fdb0c | ||
|
|
4a435e5701 | ||
|
|
427747c6b8 | ||
|
|
be9407cb57 | ||
|
|
c380d3d2a0 | ||
|
|
489b0eb12d | ||
|
|
7c73629962 | ||
|
|
ff619eca1c | ||
|
|
134a39fd71 | ||
|
|
506509b1b2 | ||
|
|
02a36316be | ||
|
|
8549c7c81b | ||
|
|
5a0be7d2ad | ||
|
|
c6f44c6398 | ||
|
|
04a8a0e6a1 | ||
|
|
8a175d147b | ||
|
|
5fbc319b20 | ||
|
|
8e3ba7caf2 | ||
|
|
182aaa7d27 | ||
|
|
ec17d466f7 | ||
|
|
79c2445117 | ||
|
|
aa59db1609 | ||
|
|
814b106be2 | ||
|
|
3901ea0fbe | ||
|
|
65133107be | ||
|
|
08d15cfc5e | ||
|
|
3c89f9a5f1 | ||
|
|
dbc6bc8206 | ||
|
|
dc38291ef5 | ||
|
|
3bb634ed5b | ||
|
|
4bdcfe4d30 | ||
|
|
3b74784486 | ||
|
|
5226664a3d | ||
|
|
6d23823b63 | ||
|
|
da3a56c144 | ||
|
|
58aa54d8cf | ||
|
|
e525e673a8 | ||
|
|
445dbf8779 | ||
|
|
6a27bea2a3 | ||
|
|
13163f9c5c | ||
|
|
fde08f922b | ||
|
|
266ce00872 | ||
|
|
623a70a0d1 | ||
|
|
1d7b738040 | ||
|
|
edc8dd2601 | ||
|
|
31dc932ca4 | ||
|
|
edb355941c | ||
|
|
cddaccb7f7 | ||
|
|
a905cce2c9 | ||
|
|
b9f110ac2b | ||
|
|
7cc24417b3 | ||
|
|
b304284d70 | ||
|
|
c382fb1577 | ||
|
|
77244f4e2c | ||
|
|
8beab5f5bc | ||
|
|
902ae3f0cf | ||
|
|
519ef4e486 | ||
|
|
699e04f371 | ||
|
|
d73de0e60d | ||
|
|
8e0b7d2a73 | ||
|
|
04283cf2d6 | ||
|
|
d60b4aa56f | ||
|
|
9cfeda1b0c | ||
|
|
5e1bd8e1eb | ||
|
|
9b495d212b | ||
|
|
b6fdd0070b | ||
|
|
eea387ad0c | ||
|
|
9cdb0f173a | ||
|
|
51063230d0 | ||
|
|
1111478b7f | ||
|
|
c99c9b441f | ||
|
|
b5065a0276 | ||
|
|
c79a577060 | ||
|
|
091596e80e | ||
|
|
0b4efe4ae1 | ||
|
|
1f9b7faa60 | ||
|
|
762d7bcc34 | ||
|
|
a2145f6b49 | ||
|
|
b48de98865 | ||
|
|
a23179dd83 | ||
|
|
5c18794122 | ||
|
|
f04011f6a7 | ||
|
|
893498238e | ||
|
|
474fa9dea0 | ||
|
|
d8a8574dda | ||
|
|
935fb015d3 | ||
|
|
8bbd3063ec | ||
|
|
92c5cabd70 | ||
|
|
e5db5a7b5c | ||
|
|
28cf123da3 | ||
|
|
5069367873 | ||
|
|
234e656ff6 | ||
|
|
aeadfbdd6a | ||
|
|
39288cfb0b | ||
|
|
5e430968c1 | ||
|
|
154b74ce6f | ||
|
|
2e26303b66 | ||
|
|
51ddcd9ee1 | ||
|
|
9d5d1c0a41 | ||
|
|
6058ccff0d | ||
|
|
a8ec4fe2fd | ||
|
|
65ccb2d443 | ||
|
|
0e929602a8 | ||
|
|
5329e026dc | ||
|
|
a7412e43b3 | ||
|
|
eeae24e058 | ||
|
|
17b6cc43d5 | ||
|
|
b69a2ef0cd | ||
|
|
6d1296094e | ||
|
|
c4039b53e6 | ||
|
|
64831b4c86 | ||
|
|
1dec270907 | ||
|
|
f72f8b03df | ||
|
|
0b47e5d05d | ||
|
|
a487c7b4b2 | ||
|
|
3838b21459 | ||
|
|
7801274d33 | ||
|
|
a505406ee7 | ||
|
|
9487a95c13 | ||
|
|
3b2fe13902 | ||
|
|
db2898dfe5 | ||
|
|
e08fd399d2 | ||
|
|
e45ffba010 | ||
|
|
f7fde93ed2 | ||
|
|
3cf53604a1 | ||
|
|
e041a5e037 | ||
|
|
6c8e10255b | ||
|
|
3464ab1527 | ||
|
|
82e74a2afd | ||
|
|
771ebde295 | ||
|
|
4ce4c3138c | ||
|
|
d37b46effc | ||
|
|
0868aac750 | ||
|
|
4ff5f33966 | ||
|
|
e1aebbe12b | ||
|
|
8273f467b6 | ||
|
|
74664afa68 | ||
|
|
c05f344371 | ||
|
|
ec1507d644 | ||
|
|
8cdc1f0014 | ||
|
|
2b1ab5c6ef | ||
|
|
01fedc0bf8 | ||
|
|
1bd82d71a2 | ||
|
|
1e9dacb6e4 | ||
|
|
94939ea3d3 | ||
|
|
c3ff69d147 | ||
|
|
0fad9d4ac7 | ||
|
|
70cd8ffb72 | ||
|
|
61535bf4b8 | ||
|
|
4adcbf9e48 | ||
|
|
3eba3167fd | ||
|
|
0608fd7732 | ||
|
|
1f41f27e89 | ||
|
|
ded0df9303 | ||
|
|
2a7ba1893a | ||
|
|
6b3b19632a | ||
|
|
98bb0731df | ||
|
|
a102f7044e | ||
|
|
f1028dbaed | ||
|
|
438ce5c3db | ||
|
|
b995a1d091 | ||
|
|
e072f83507 | ||
|
|
79154bba25 | ||
|
|
613eb7522c | ||
|
|
0eb5653713 | ||
|
|
0d624f021b | ||
|
|
b263047f4e | ||
|
|
4410e1bbd7 | ||
|
|
8c3871e8de | ||
|
|
2eafd3cc15 | ||
|
|
368df66947 | ||
|
|
9f9a3ea8fd | ||
|
|
f66286105f | ||
|
|
0f0f912370 | ||
|
|
9fa0e37a5d | ||
|
|
a0cb51ff70 | ||
|
|
a6305ddea4 | ||
|
|
fb8638fe6a | ||
|
|
0009f1f865 | ||
|
|
d449c35025 | ||
|
|
1893d1a2c2 | ||
|
|
0c4539e4fa | ||
|
|
feacddd1d7 | ||
|
|
0c44fe6ce0 | ||
|
|
17fb6983d8 | ||
|
|
5fb73bdb01 | ||
|
|
7f082ea389 | ||
|
|
9856a1831a | ||
|
|
dd17a20b60 | ||
|
|
665a52b106 | ||
|
|
f68d33870b | ||
|
|
49d13b12a5 | ||
|
|
674ab7e41f | ||
|
|
5b3eb3ba82 | ||
|
|
e28d3f3b5a | ||
|
|
870d8b5008 | ||
|
|
6aa240e9a3 | ||
|
|
b55a4047d0 | ||
|
|
a07d87318c | ||
|
|
dd6555c903 | ||
|
|
e8c40b6044 | ||
|
|
91c17a0b2a | ||
|
|
2ed0ea0243 | ||
|
|
df2f92433a | ||
|
|
68b3fc72bf | ||
|
|
df5bb14758 | ||
|
|
e24199bbbe | ||
|
|
2c6099556b | ||
|
|
f98215a5da | ||
|
|
b6b6888493 | ||
|
|
f8e5b9be43 | ||
|
|
692210214f | ||
|
|
cff2546c0c | ||
|
|
f0cc1200f3 | ||
|
|
e83a9af455 | ||
|
|
fb84f9d9cf | ||
|
|
118a2515e1 | ||
|
|
63e891b0f7 | ||
|
|
9891080b57 | ||
|
|
bc7a7e55af | ||
|
|
9b8a029de1 | ||
|
|
11b5575422 | ||
|
|
06336aa580 | ||
|
|
aa8c1d6e9c | ||
|
|
9a4d5d8abf | ||
|
|
3c5631bca3 | ||
|
|
84f85f87b2 | ||
|
|
c35db5976f | ||
|
|
8d1fcf988c | ||
|
|
bbf8f2dd69 | ||
|
|
2f6436e34f | ||
|
|
9cbb03107d | ||
|
|
ee44d7fb2e | ||
|
|
6a0fcd9cf0 | ||
|
|
43cfe3b858 | ||
|
|
dba83c1c03 | ||
|
|
053e139d00 | ||
|
|
04f791a839 | ||
|
|
a4029c9490 | ||
|
|
d471dfec43 | ||
|
|
494bbd46d0 | ||
|
|
1525b9ad06 | ||
|
|
19847ee80b | ||
|
|
fddf1f146c | ||
|
|
5f19cb1c0c | ||
|
|
aaeae992e1 | ||
|
|
a0e7be9d45 | ||
|
|
78faf7e14c | ||
|
|
98f84c2c37 | ||
|
|
278805043e | ||
|
|
dc5215e41e | ||
|
|
7e11691ea4 | ||
|
|
c83dfc44d6 | ||
|
|
68a01b1735 | ||
|
|
0307b58d17 | ||
|
|
9d1508049e | ||
|
|
42322055f9 | ||
|
|
5de8fce156 | ||
|
|
29ff92f833 | ||
|
|
359007c5bf | ||
|
|
bbe40518e4 | ||
|
|
b26f3c0cc6 |
@@ -26,7 +26,7 @@ REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_DRIVER=log
|
||||
MAIL_HOST=mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_FROM=changeme@example.com
|
||||
|
||||
14
.github/CONTRIBUTING.md
vendored
14
.github/CONTRIBUTING.md
vendored
@@ -4,20 +4,12 @@
|
||||
|
||||
## Feature requests
|
||||
|
||||
If you are requesting a new feature, please check out the list of [often requested features](https://firefly-iii.github.io/requested-features/).
|
||||
|
||||
## Bugs
|
||||
|
||||
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.
|
||||
|
||||
## Installation problems
|
||||
|
||||
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!
|
||||
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/).
|
||||
|
||||
## Pull requests
|
||||
|
||||
I can only accept pull requests against the `develop` branch, never the `master` branch.
|
||||
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.
|
||||
|
||||
## Translations :us: :fr: :de:
|
||||
|
||||
If you see a spelling error, grammatical error or a weird translation in your language, please join [our CrowdIn](https://crowdin.com/project/firefly-iii) project. There, you can submit your translations and fixes. The GitHub repository will download these automatically and they will be included in the next release.
|
||||
If you see a spelling error, grammatical error or a weird translation in your language, please join [our CrowdIn](https://crowdin.com/project/firefly-iii) project. There, you can submit your translations and fixes. The GitHub repository will download these automatically and they will be included in the next release.
|
||||
|
||||
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:
|
||||
@@ -16,14 +15,14 @@ install:
|
||||
- php artisan optimize
|
||||
- php artisan env
|
||||
- cp .env.testing .env
|
||||
- mv storage/database/databasecopy.sqlite storage/database/database.sqlite
|
||||
- wget -q https://github.com/firefly-iii/test-data/raw/master/storage/database.sqlite -O storage/database/database.sqlite
|
||||
- mkdir -p build/logs
|
||||
|
||||
script:
|
||||
- phpunit -c phpunit.coverage.xml
|
||||
- phpunit -c phpunit.xml
|
||||
|
||||
after_success:
|
||||
- travis_retry php vendor/bin/coveralls -x storage/build/clover.xml
|
||||
#after_success:
|
||||
# - travis_retry php vendor/bin/coveralls -x storage/build/clover.xml
|
||||
|
||||
# safelist
|
||||
branches:
|
||||
|
||||
165
CHANGELOG.md
165
CHANGELOG.md
@@ -2,9 +2,172 @@
|
||||
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
|
||||
|
||||
### Fixed
|
||||
- Various bugs in import routine
|
||||
|
||||
## [4.6.1] - 2017-07-02
|
||||
### Fixed
|
||||
- Fixed several small issues all around.
|
||||
|
||||
## [4.6.0] - 2017-06-28
|
||||
|
||||
### Changed
|
||||
- Revamped import routine. Will be buggy.
|
||||
|
||||
### Fixed
|
||||
- Issue #667, postgresql reported by @skibbipl.
|
||||
- Issue #680 by @Xeli
|
||||
- Fixed #660
|
||||
- Fixes #672, reported by @dzaikos
|
||||
- Translation error fixed by
|
||||
- Fix a bug where the balance routine forgot to account for accounts without a currency preference.
|
||||
- Various other bugfixes.
|
||||
|
||||
## [4.5.0] - 2017-06-07
|
||||
|
||||
### Added
|
||||
- Better support for multi-currency transactions and display of transactions, accounts and everything. This requires a database overhaul (moving the currency information to specific transactions) so be careful when upgrading.
|
||||
- Translations for Spanish and Slovenian.
|
||||
- New interface for budget page, ~~stolen from~~ inspired by YNAB.
|
||||
- Expanded Docker to work with postgresql as well, thanks to @kressh
|
||||
|
||||
### Fixed
|
||||
- PostgreSQL support in database upgrade routine (#644, reported by @skibbipl)
|
||||
- Frontpage budget chart was off, fix by @nhaarman
|
||||
- Was not possible to remove opening balance.
|
||||
|
||||
## [4.4.3] - 2017-05-03
|
||||
### Added
|
||||
- Added support for Slovenian
|
||||
- Removed support for Spanish. No translations whatsoever by the guy who requested it.
|
||||
- Removed support for Russian. Same thing.
|
||||
- Removed support for Croatian. Same thing.
|
||||
- Removed support for Chinese Traditional, Hong Kong. Same thing.
|
||||
|
||||
### Changed
|
||||
- The journal collector, an internal piece of code to collect transactions, now uses a slightly different method of collecting journals. This may cause problems.
|
||||
|
||||
### Fixed
|
||||
- Issue #638 as reported by [worldworm](https://github.com/worldworm).
|
||||
- Possible fix for #624
|
||||
|
||||
## [4.4.2] - 2017-04-27
|
||||
### Fixed
|
||||
Fixed a bug where the opening balance could not be stored.
|
||||
- Fixed a bug where the opening balance could not be stored.
|
||||
|
||||
## [4.4.1] - 2017-04-27
|
||||
|
||||
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at thegrumpydictator@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
25
Dockerfile
25
Dockerfile
@@ -11,39 +11,30 @@ RUN apt-get update -y && \
|
||||
libtidy-dev \
|
||||
libxml2-dev \
|
||||
libsqlite3-dev \
|
||||
libpq-dev \
|
||||
libbz2-dev \
|
||||
gettext-base \
|
||||
locales && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN docker-php-ext-install -j$(nproc) curl gd intl json mcrypt readline tidy zip bcmath xml mbstring pdo_sqlite pdo_mysql bz2
|
||||
RUN docker-php-ext-install -j$(nproc) curl gd intl json mcrypt readline tidy zip bcmath xml mbstring pdo_sqlite pdo_mysql bz2 pdo_pgsql
|
||||
|
||||
# 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"]
|
||||
|
||||
27
README.md
27
README.md
@@ -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)
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/firefly-iii/firefly-iii/tree/master)
|
||||
|
||||
## Installation
|
||||
Firefly III can be run on Heroku. Register for a free Heroku account and instantly run Firefly III on your very own cloud instance. There is also a [demo site](https://firefly-iii.nder.be) with an example financial administration already present.
|
||||
|
||||
## Getting started
|
||||
|
||||
To install Firefly III, you'll need a web server (preferrably on Linux) and access to the command line. Then, please read the [installation guide](https://firefly-iii.github.io/using-installing.html).
|
||||
|
||||
@@ -22,7 +24,7 @@ Personal financial management is pretty difficult, and everybody has their own a
|
||||
|
||||
Firefly works on the principle that if you know where you're money is going, you can stop it from going there.
|
||||
|
||||
#### Some advantages of using Firefly
|
||||
### Some advantages of using Firefly
|
||||
|
||||
- Firefly can import any CSV file, so migrating from other systems is easy.
|
||||
- Firefly runs on your own server, so you are fully in control of your data. Remember, there is no such thing as "the cloud", it’s just somebody else’s computer!
|
||||
@@ -31,6 +33,25 @@ Firefly works on the principle that if you know where you're money is going, you
|
||||
|
||||
Firefly is pretty awesome. [You can read more about Firefly III, and its features, on the Github Pages](https://firefly-iii.github.io/).
|
||||
|
||||
### Contributing
|
||||
|
||||
Please read [CONTRIBUTING.md](https://github.com/firefly-iii/firefly-iii/blob/master/.github/CONTRIBUTING.md) for details on contributing, and the process for submitting pull requests. Please check out the [code of conduct](https://github.com/firefly-iii/firefly-iii/blob/master/CODE_OF_CONDUCT.md) as well.
|
||||
|
||||
### Versioning
|
||||
|
||||
We use [SemVer](http://semver.org/) for versioning. For the versions available, see [the tags](https://github.com/firefly-iii/firefly-iii/tags) on this repository.
|
||||
|
||||
### Authors
|
||||
|
||||
* James Cole
|
||||
* Over time, [many people have contributed to Firefly III](https://github.com/firefly-iii/firefly-iii/graphs/contributors).
|
||||
|
||||
### License
|
||||
|
||||
This work [is licensed](https://github.com/firefly-iii/firefly-iii/blob/master/LICENSE) under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
### Other stuff
|
||||
|
||||
If you like Firefly and if it helps you save lots of money, why not send me [a dime for every dollar saved](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA) (this is a joke, although the Paypal form works just fine, try it!)
|
||||
|
||||
If you want to contact me, please open an issue or [email me](mailto:thegrumpydictator@gmail.com).
|
||||
|
||||
@@ -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 = 'settings_complete';
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Console\Commands;
|
||||
|
||||
use FireflyIII\Import\ImportProcedure;
|
||||
use FireflyIII\Import\Logging\CommandHandler;
|
||||
use FireflyIII\Import\Routine\ImportRoutine;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
@@ -52,7 +51,7 @@ class Import extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Run the import routine.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
@@ -75,20 +74,25 @@ class Import extends Command
|
||||
$monolog = Log::getMonolog();
|
||||
$handler = new CommandHandler($this);
|
||||
$monolog->pushHandler($handler);
|
||||
$importProcedure = new ImportProcedure;
|
||||
$result = $importProcedure->runImport($job);
|
||||
|
||||
// display result to user:
|
||||
$this->presentResults($result);
|
||||
$this->line('The import has completed.');
|
||||
/** @var ImportRoutine $routine */
|
||||
$routine = app(ImportRoutine::class);
|
||||
$routine->setJob($job);
|
||||
$routine->run();
|
||||
|
||||
// get any errors from the importer:
|
||||
$this->presentErrors($job);
|
||||
/** @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if job is valid to be imported.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return bool
|
||||
@@ -96,12 +100,14 @@ class Import extends Command
|
||||
private function isValid(ImportJob $job): bool
|
||||
{
|
||||
if (is_null($job)) {
|
||||
Log::error('This job does not seem to exist.');
|
||||
$this->error('This job does not seem to exist.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($job->status != 'settings_complete') {
|
||||
if ($job->status !== 'configured') {
|
||||
Log::error(sprintf('This job is not ready to be imported (status is %s).', $job->status));
|
||||
$this->error('This job is not ready to be imported.');
|
||||
|
||||
return false;
|
||||
@@ -109,36 +115,4 @@ class Import extends Command
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*/
|
||||
private function presentErrors(ImportJob $job)
|
||||
{
|
||||
$extendedStatus = $job->extended_status;
|
||||
if (isset($extendedStatus['errors']) && count($extendedStatus['errors']) > 0) {
|
||||
$this->line(sprintf('The following %d error(s) occured during the import:', count($extendedStatus['errors'])));
|
||||
foreach ($extendedStatus['errors'] as $error) {
|
||||
$this->error($error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $result
|
||||
*/
|
||||
private function presentResults(Collection $result)
|
||||
{
|
||||
/**
|
||||
* @var int $index
|
||||
* @var TransactionJournal $journal
|
||||
*/
|
||||
foreach ($result as $index => $journal) {
|
||||
if (!is_null($journal->id)) {
|
||||
$this->line(sprintf('Line #%d has been imported as transaction #%d.', $index, $journal->id));
|
||||
continue;
|
||||
}
|
||||
$this->error(sprintf('Could not store line #%d', $index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,14 @@ 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;
|
||||
@@ -36,6 +35,8 @@ use Schema;
|
||||
/**
|
||||
* Class UpgradeDatabase
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) // it just touches a lot of things.
|
||||
*
|
||||
* @package FireflyIII\Console\Commands
|
||||
*/
|
||||
class UpgradeDatabase extends Command
|
||||
@@ -69,28 +70,26 @@ class UpgradeDatabase extends Command
|
||||
{
|
||||
$this->setTransactionIdentifier();
|
||||
$this->migrateRepetitions();
|
||||
$this->repairPiggyBanks();
|
||||
$this->updateAccountCurrencies();
|
||||
$this->updateJournalCurrencies();
|
||||
$this->line('Updating currency information..');
|
||||
$this->updateTransferCurrencies();
|
||||
$this->updateOtherCurrencies();
|
||||
$this->info('Firefly III database is up to date.');
|
||||
|
||||
return;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate budget repetitions to new format.
|
||||
* 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 migrateRepetitions()
|
||||
public function migrateRepetitions(): void
|
||||
{
|
||||
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)) {
|
||||
@@ -100,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)
|
||||
@@ -157,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +304,7 @@ class UpgradeDatabase extends Command
|
||||
*
|
||||
* @param int $journalId
|
||||
*/
|
||||
private function updateJournal(int $journalId)
|
||||
private function updateJournalidentifiers(int $journalId): void
|
||||
{
|
||||
$identifier = 0;
|
||||
$processed = [];
|
||||
@@ -246,92 +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
|
||||
* 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 updateJournalCurrencies()
|
||||
private function updateTransactionCurrency(Transaction $transaction): void
|
||||
{
|
||||
$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.';
|
||||
/** @var CurrencyRepositoryInterface $repository */
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$currency = $repository->find(intval($transaction->account->getMeta('currency_id')));
|
||||
|
||||
foreach ($types as $type => $operator) {
|
||||
$set = 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');
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
->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')
|
||||
->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data'))
|
||||
->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);
|
||||
}
|
||||
);
|
||||
$transaction->transaction_currency_id = $currency->id;
|
||||
$transaction->save();
|
||||
}
|
||||
/*
|
||||
* 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.*']);
|
||||
|
||||
// grab opposing transaction:
|
||||
/** @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')));
|
||||
$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 ($sourceCurrency->id !== $journal->transaction_currency_id) {
|
||||
$updated = true;
|
||||
$journal->transaction_currency_id = $sourceCurrency->id;
|
||||
$journal->save();
|
||||
}
|
||||
if (is_null($opposingCurrency->id)) {
|
||||
Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $opposing->account->id, $opposing->account->name));
|
||||
|
||||
// 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);
|
||||
}
|
||||
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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,7 +26,9 @@ class StoredTransactionJournal extends Event
|
||||
|
||||
use SerializesModels;
|
||||
|
||||
/** @var TransactionJournal */
|
||||
public $journal;
|
||||
/** @var int */
|
||||
public $piggyBankId;
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,7 @@ class UpdatedTransactionJournal extends Event
|
||||
|
||||
use SerializesModels;
|
||||
|
||||
/** @var TransactionJournal */
|
||||
public $journal;
|
||||
|
||||
/**
|
||||
|
||||
@@ -303,7 +303,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac
|
||||
->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
|
||||
->leftJoin('accounts AS opposing_accounts', 'opposing.account_id', '=', 'opposing_accounts.id')
|
||||
->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', 'transaction_types.id')
|
||||
->leftJoin('transaction_currencies', 'transaction_journals.transaction_currency_id', '=', 'transaction_currencies.id')
|
||||
->leftJoin('transaction_currencies', 'transactions.transaction_currency_id', '=', 'transaction_currencies.id')
|
||||
->whereIn('transactions.account_id', $accountIds)
|
||||
->where('transaction_journals.user_id', $this->job->user_id)
|
||||
->where('transaction_journals.date', '>=', $this->start->format('Y-m-d'))
|
||||
@@ -338,7 +338,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac
|
||||
'transaction_journals.encrypted as journal_encrypted',
|
||||
'transaction_journals.transaction_type_id',
|
||||
'transaction_types.type as transaction_type',
|
||||
'transaction_journals.transaction_currency_id',
|
||||
'transactions.transaction_currency_id',
|
||||
'transaction_currencies.code AS transaction_currency_code',
|
||||
|
||||
]
|
||||
|
||||
@@ -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,9 +110,16 @@ 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;
|
||||
$index = 0;
|
||||
foreach ($data as $key => $value) {
|
||||
|
||||
// make larger than 0
|
||||
|
||||
@@ -22,10 +22,15 @@ interface GeneratorInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Will generate a (ChartJS) compatible array from the given input. Expects this format:
|
||||
* Will generate a Chart JS compatible array from the given input. Expects this format
|
||||
*
|
||||
* Will take labels for all from first set.
|
||||
*
|
||||
* 0: [
|
||||
* 'label' => 'label of set',
|
||||
* 'type' => bar or line, optional
|
||||
* 'yAxisID' => ID of yAxis, optional, will not be included when unused.
|
||||
* 'fill' => if to fill a line? optional, will not be included when unused.
|
||||
* 'entries' =>
|
||||
* [
|
||||
* 'label-of-entry' => 'value'
|
||||
@@ -33,12 +38,16 @@ interface GeneratorInterface
|
||||
* ]
|
||||
* 1: [
|
||||
* 'label' => 'label of another set',
|
||||
* 'type' => bar or line, optional
|
||||
* 'yAxisID' => ID of yAxis, optional, will not be included when unused.
|
||||
* 'fill' => if to fill a line? optional, will not be included when unused.
|
||||
* 'entries' =>
|
||||
* [
|
||||
* 'label-of-entry' => 'value'
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
|
||||
@@ -19,6 +19,7 @@ use FireflyIII\Generator\Report\ReportGeneratorInterface;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Steam;
|
||||
|
||||
@@ -147,6 +148,8 @@ class MonthReportGenerator implements ReportGeneratorInterface
|
||||
*/
|
||||
private function getAuditReport(Account $account, Carbon $date): array
|
||||
{
|
||||
/** @var CurrencyRepositoryInterface $currencyRepos */
|
||||
$currencyRepos = app(CurrencyRepositoryInterface::class);
|
||||
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
@@ -155,15 +158,21 @@ class MonthReportGenerator implements ReportGeneratorInterface
|
||||
$journals = $journals->reverse();
|
||||
$dayBeforeBalance = Steam::balance($account, $date);
|
||||
$startBalance = $dayBeforeBalance;
|
||||
|
||||
$currency = $currencyRepos->find(intval($account->getMeta('currency_id')));
|
||||
|
||||
/** @var Transaction $journal */
|
||||
foreach ($journals as $transaction) {
|
||||
$transaction->before = $startBalance;
|
||||
$transactionAmount = $transaction->transaction_amount;
|
||||
$newBalance = bcadd($startBalance, $transactionAmount);
|
||||
$transaction->after = $newBalance;
|
||||
$startBalance = $newBalance;
|
||||
|
||||
if ($currency->id === $transaction->foreign_currency_id) {
|
||||
$transactionAmount = $transaction->transaction_foreign_amount;
|
||||
}
|
||||
|
||||
$newBalance = bcadd($startBalance, $transactionAmount);
|
||||
$transaction->after = $newBalance;
|
||||
$startBalance = $newBalance;
|
||||
$transaction->currency = $currency;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -18,6 +18,9 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Generator\Report\ReportGeneratorInterface;
|
||||
use FireflyIII\Generator\Report\Support;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -156,11 +159,13 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
|
||||
->setTypes([TransactionType::WITHDRAWAL])
|
||||
->setBudgets($this->budgets)->withOpposingAccount()->disableFilter();
|
||||
->setBudgets($this->budgets)->withOpposingAccount();
|
||||
$collector->removeFilter(TransferFilter::class);
|
||||
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(PositiveAmountFilter::class);
|
||||
|
||||
$accountIds = $this->accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$transactions = self::filterExpenses($transactions, $accountIds);
|
||||
$this->expenses = $transactions;
|
||||
|
||||
return $transactions;
|
||||
|
||||
@@ -18,6 +18,10 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Generator\Report\ReportGeneratorInterface;
|
||||
use FireflyIII\Generator\Report\Support;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -166,11 +170,13 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
|
||||
->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
|
||||
->setCategories($this->categories)->withOpposingAccount()->disableFilter();
|
||||
->setCategories($this->categories)->withOpposingAccount();
|
||||
$collector->removeFilter(TransferFilter::class);
|
||||
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(PositiveAmountFilter::class);
|
||||
|
||||
$accountIds = $this->accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$transactions = self::filterExpenses($transactions, $accountIds);
|
||||
$this->expenses = $transactions;
|
||||
|
||||
return $transactions;
|
||||
@@ -190,9 +196,11 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
|
||||
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
|
||||
->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
|
||||
->setCategories($this->categories)->withOpposingAccount();
|
||||
$accountIds = $this->accounts->pluck('id')->toArray();
|
||||
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(NegativeAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
$transactions = self::filterIncome($transactions, $accountIds);
|
||||
$this->income = $transactions;
|
||||
|
||||
return $transactions;
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace FireflyIII\Generator\Report;
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
|
||||
/**
|
||||
@@ -25,61 +24,6 @@ use Log;
|
||||
*/
|
||||
class Support
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Collection $collection
|
||||
* @param array $accounts
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public static function filterExpenses(Collection $collection, array $accounts): Collection
|
||||
{
|
||||
return self::filterTransactions($collection, $accounts, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $collection
|
||||
* @param array $accounts
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public static function filterIncome(Collection $collection, array $accounts): Collection
|
||||
{
|
||||
return self::filterTransactions($collection, $accounts, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $collection
|
||||
* @param array $accounts
|
||||
* @param int $modifier
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public static function filterTransactions(Collection $collection, array $accounts, int $modifier): Collection
|
||||
{
|
||||
$result = $collection->filter(
|
||||
function (Transaction $transaction) use ($accounts, $modifier) {
|
||||
$opposing = $transaction->opposing_account_id;
|
||||
// remove internal transfer
|
||||
if (in_array($opposing, $accounts)) {
|
||||
Log::debug(sprintf('Filtered #%d because its opposite is in accounts.', $transaction->id));
|
||||
|
||||
return null;
|
||||
}
|
||||
// remove positive amount
|
||||
if (bccomp($transaction->transaction_amount, '0') === $modifier) {
|
||||
Log::debug(sprintf('Filtered #%d because amount is %f.', $transaction->id, $transaction->transaction_amount));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,10 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Generator\Report\ReportGeneratorInterface;
|
||||
use FireflyIII\Generator\Report\Support;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
@@ -162,11 +166,14 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
|
||||
->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
|
||||
->setTags($this->tags)->withOpposingAccount()->disableFilter();
|
||||
->setTags($this->tags)->withOpposingAccount();
|
||||
$collector->removeFilter(TransferFilter::class);
|
||||
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(PositiveAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
|
||||
$accountIds = $this->accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$transactions = self::filterExpenses($transactions, $accountIds);
|
||||
$this->expenses = $transactions;
|
||||
|
||||
return $transactions;
|
||||
@@ -186,9 +193,11 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
|
||||
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
|
||||
->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
|
||||
->setTags($this->tags)->withOpposingAccount();
|
||||
$accountIds = $this->accounts->pluck('id')->toArray();
|
||||
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(NegativeAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
$transactions = self::filterIncome($transactions, $accountIds);
|
||||
$this->income = $transactions;
|
||||
|
||||
return $transactions;
|
||||
|
||||
@@ -14,76 +14,95 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Handlers\Events;
|
||||
|
||||
use FireflyIII\Events\StoredTransactionJournal;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\PiggyBankEvent;
|
||||
use FireflyIII\Models\PiggyBankRepetition;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Journal\JournalRepositoryInterface as JRI;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface as PRI;
|
||||
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface as RGRI;
|
||||
use FireflyIII\Rules\Processor;
|
||||
use FireflyIII\Support\Events\BillScanner;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* Class StoredJournalEventHandler
|
||||
*
|
||||
* @package FireflyIII\Handlers\Events
|
||||
*/
|
||||
class StoredJournalEventHandler
|
||||
{
|
||||
/** @var JRI */
|
||||
public $journalRepository;
|
||||
/** @var PRI */
|
||||
public $repository;
|
||||
|
||||
/** @var RGRI */
|
||||
public $ruleGroupRepository;
|
||||
|
||||
/**
|
||||
* StoredJournalEventHandler constructor.
|
||||
*
|
||||
* @param PRI $repository
|
||||
* @param JRI $journalRepository
|
||||
* @param RGRI $ruleGroupRepository
|
||||
*/
|
||||
public function __construct(PRI $repository, JRI $journalRepository, RGRI $ruleGroupRepository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->journalRepository = $journalRepository;
|
||||
$this->ruleGroupRepository = $ruleGroupRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method connects a new transfer to a piggy bank.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param StoredTransactionJournal $event
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function connectToPiggyBank(StoredTransactionJournal $event): bool
|
||||
{
|
||||
/** @var TransactionJournal $journal */
|
||||
$journal = $event->journal;
|
||||
$piggyBankId = $event->piggyBankId;
|
||||
Log::debug(sprintf('Trying to connect journal %d to piggy bank %d.', $journal->id, $piggyBankId));
|
||||
$piggyBank = $this->repository->find($piggyBankId);
|
||||
|
||||
/*
|
||||
* Will only continue when journal is a transfer.
|
||||
*/
|
||||
Log::debug(sprintf('Journal transaction type is %s', $journal->transactionType->type));
|
||||
if ($journal->transactionType->type !== TransactionType::TRANSFER) {
|
||||
// is a transfer?
|
||||
if (!$this->journalRepository->isTransfer($journal)) {
|
||||
Log::info(sprintf('Will not connect %s #%d to a piggy bank.', $journal->transactionType->type, $journal->id));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify existence of piggy bank:
|
||||
*/
|
||||
if (!$this->verifyExistence($event)) {
|
||||
Log::error(sprintf('No such piggy bank or no repetition on %s', $journal->date->format('Y-m-d')));
|
||||
// piggy exists?
|
||||
if (is_null($piggyBank->id)) {
|
||||
Log::error(sprintf('There is no piggy bank with ID #%d', $piggyBankId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get relevant data:
|
||||
*/
|
||||
$piggyBank = $journal->user->piggyBanks()->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']);
|
||||
$repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first();
|
||||
$amount = $this->getExactAmount($journal, $piggyBank, $repetition);
|
||||
// repetition exists?
|
||||
$repetition = $this->repository->getRepetition($piggyBank, $journal->date);
|
||||
if (is_null($repetition->id)) {
|
||||
Log::error(sprintf('No piggy bank repetition on %s!', $journal->date->format('Y-m-d')));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// get the amount
|
||||
$amount = $this->repository->getExactAmount($piggyBank, $repetition, $journal);
|
||||
if (bccomp($amount, '0') === 0) {
|
||||
Log::debug('Amount is zero, will not create event.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$repetition->currentamount = bcadd($repetition->currentamount, $amount);
|
||||
$repetition->save();
|
||||
// update amount
|
||||
$this->repository->addAmountToRepetition($repetition, $amount);
|
||||
$event = $this->repository->createEventWithJournal($piggyBank, $amount, $journal);
|
||||
|
||||
/** @var PiggyBankEvent $event */
|
||||
$event = PiggyBankEvent::create(
|
||||
['piggy_bank_id' => $piggyBank->id, 'transaction_journal_id' => $journal->id, 'date' => $journal->date, 'amount' => $amount]
|
||||
);
|
||||
Log::debug(sprintf('Created piggy bank event #%d', $event->id));
|
||||
|
||||
return true;
|
||||
@@ -100,16 +119,11 @@ class StoredJournalEventHandler
|
||||
{
|
||||
// get all the user's rule groups, with the rules, order by 'order'.
|
||||
$journal = $storedJournalEvent->journal;
|
||||
$groups = $journal->user->ruleGroups()->where('rule_groups.active', 1)->orderBy('order', 'ASC')->get();
|
||||
//
|
||||
$groups = $this->ruleGroupRepository->getActiveGroups($journal->user);
|
||||
|
||||
/** @var RuleGroup $group */
|
||||
foreach ($groups as $group) {
|
||||
$rules = $group->rules()
|
||||
->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
|
||||
->where('rule_triggers.trigger_type', 'user_action')
|
||||
->where('rule_triggers.trigger_value', 'store-journal')
|
||||
->where('rules.active', 1)
|
||||
->get(['rules.*']);
|
||||
$rules = $this->ruleGroupRepository->getActiveStoreRules($group);
|
||||
/** @var Rule $rule */
|
||||
foreach ($rules as $rule) {
|
||||
|
||||
@@ -117,9 +131,8 @@ class StoredJournalEventHandler
|
||||
$processor->handleTransactionJournal($journal);
|
||||
|
||||
if ($rule->stop_processing) {
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,81 +153,4 @@ class StoredJournalEventHandler
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 6 but I can live with it.
|
||||
* @param TransactionJournal $journal
|
||||
* @param PiggyBank $piggyBank
|
||||
* @param PiggyBankRepetition $repetition
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getExactAmount(TransactionJournal $journal, PiggyBank $piggyBank, PiggyBankRepetition $repetition): string
|
||||
{
|
||||
$amount = $journal->amountPositive();
|
||||
$sources = $journal->sourceAccountList()->pluck('id')->toArray();
|
||||
$room = bcsub(strval($piggyBank->targetamount), strval($repetition->currentamount));
|
||||
$compare = bcmul($repetition->currentamount, '-1');
|
||||
|
||||
Log::debug(sprintf('Will add/remove %f to piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name));
|
||||
|
||||
// if piggy account matches source account, the amount is positive
|
||||
if (in_array($piggyBank->account_id, $sources)) {
|
||||
$amount = bcmul($amount, '-1');
|
||||
Log::debug(sprintf('Account #%d is the source, so will remove amount from piggy bank.', $piggyBank->account_id));
|
||||
}
|
||||
|
||||
|
||||
// if the amount is positive, make sure it fits in piggy bank:
|
||||
if (bccomp($amount, '0') === 1 && bccomp($room, $amount) === -1) {
|
||||
// amount is positive and $room is smaller than $amount
|
||||
Log::debug(sprintf('Room in piggy bank for extra money is %f', $room));
|
||||
Log::debug(sprintf('There is NO room to add %f to piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name));
|
||||
Log::debug(sprintf('New amount is %f', $room));
|
||||
|
||||
return $room;
|
||||
}
|
||||
|
||||
// amount is negative and $currentamount is smaller than $amount
|
||||
if (bccomp($amount, '0') === -1 && bccomp($compare, $amount) === 1) {
|
||||
Log::debug(sprintf('Max amount to remove is %f', $repetition->currentamount));
|
||||
Log::debug(sprintf('Cannot remove %f from piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name));
|
||||
Log::debug(sprintf('New amount is %f', $compare));
|
||||
|
||||
return $compare;
|
||||
}
|
||||
|
||||
return $amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StoredTransactionJournal $event
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function verifyExistence(StoredTransactionJournal $event): bool
|
||||
{
|
||||
/** @var TransactionJournal $journal */
|
||||
$journal = $event->journal;
|
||||
$piggyBankId = $event->piggyBankId;
|
||||
|
||||
/** @var PiggyBank $piggyBank */
|
||||
$piggyBank = $journal->user->piggyBanks()->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']);
|
||||
|
||||
if (is_null($piggyBank)) {
|
||||
Log::error('No such piggy bank!');
|
||||
|
||||
return false;
|
||||
}
|
||||
Log::debug(sprintf('Found piggy bank #%d: "%s"', $piggyBank->id, $piggyBank->name));
|
||||
// update piggy bank rep for date of transaction journal.
|
||||
$repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first();
|
||||
if (is_null($repetition)) {
|
||||
Log::error(sprintf('No piggy bank repetition on %s!', $journal->date->format('Y-m-d')));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,31 @@ namespace FireflyIII\Handlers\Events;
|
||||
use FireflyIII\Events\UpdatedTransactionJournal;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
|
||||
use FireflyIII\Rules\Processor;
|
||||
use FireflyIII\Support\Events\BillScanner;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* Class UpdatedJournalEventHandler
|
||||
*
|
||||
* @package FireflyIII\Handlers\Events
|
||||
*/
|
||||
class UpdatedJournalEventHandler
|
||||
{
|
||||
/** @var RuleGroupRepositoryInterface */
|
||||
public $repository;
|
||||
|
||||
/**
|
||||
* StoredJournalEventHandler constructor.
|
||||
*
|
||||
* @param RuleGroupRepositoryInterface $ruleGroupRepository
|
||||
*/
|
||||
public function __construct(RuleGroupRepositoryInterface $ruleGroupRepository)
|
||||
{
|
||||
$this->repository = $ruleGroupRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will check all the rules when a journal is updated.
|
||||
@@ -39,16 +54,11 @@ class UpdatedJournalEventHandler
|
||||
{
|
||||
// get all the user's rule groups, with the rules, order by 'order'.
|
||||
$journal = $updatedJournalEvent->journal;
|
||||
$groups = $journal->user->ruleGroups()->where('rule_groups.active', 1)->orderBy('order', 'ASC')->get();
|
||||
//
|
||||
$groups = $this->repository->getActiveGroups($journal->user);
|
||||
|
||||
/** @var RuleGroup $group */
|
||||
foreach ($groups as $group) {
|
||||
$rules = $group->rules()
|
||||
->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
|
||||
->where('rule_triggers.trigger_type', 'user_action')
|
||||
->where('rule_triggers.trigger_value', 'update-journal')
|
||||
->where('rules.active', 1)
|
||||
->get(['rules.*']);
|
||||
$rules = $this->repository->getActiveUpdateRules($group);
|
||||
/** @var Rule $rule */
|
||||
foreach ($rules as $rule) {
|
||||
$processor = Processor::make($rule);
|
||||
|
||||
@@ -15,8 +15,9 @@ namespace FireflyIII\Handlers\Events;
|
||||
|
||||
use FireflyIII\Events\RegisteredUser;
|
||||
use FireflyIII\Events\RequestedNewPassword;
|
||||
use FireflyIII\Mail\RegisteredUser as RegisteredUserMail;
|
||||
use FireflyIII\Mail\RequestedNewPassword as RequestedNewPasswordMail;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use Illuminate\Mail\Message;
|
||||
use Log;
|
||||
use Mail;
|
||||
use Swift_TransportException;
|
||||
@@ -68,15 +69,14 @@ class UserEventHandler
|
||||
|
||||
// send email.
|
||||
try {
|
||||
Mail::send(
|
||||
['emails.password-html', 'emails.password-text'], ['url' => $url, 'ip' => $ipAddress], function (Message $message) use ($email) {
|
||||
$message->to($email, $email)->subject('Your password reset request');
|
||||
}
|
||||
);
|
||||
Mail::to($email)->send(new RequestedNewPasswordMail($url, $ipAddress));
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Swift_TransportException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -93,23 +93,23 @@ class UserEventHandler
|
||||
|
||||
$sendMail = env('SEND_REGISTRATION_MAIL', true);
|
||||
if (!$sendMail) {
|
||||
return true;
|
||||
return true; // @codeCoverageIgnore
|
||||
}
|
||||
// get the email address
|
||||
$email = $event->user->email;
|
||||
$address = route('index');
|
||||
$uri = route('index');
|
||||
$ipAddress = $event->ipAddress;
|
||||
|
||||
// send email.
|
||||
try {
|
||||
Mail::send(
|
||||
['emails.registered-html', 'emails.registered-text'], ['address' => $address, 'ip' => $ipAddress], function (Message $message) use ($email) {
|
||||
$message->to($email, $email)->subject('Welcome to Firefly III!');
|
||||
}
|
||||
);
|
||||
Mail::to($email)->send(new RegisteredUserMail($uri, $ipAddress));
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Swift_TransportException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace FireflyIII\Helpers\Attachments;
|
||||
use Crypt;
|
||||
use FireflyIII\Models\Attachment;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Log;
|
||||
use Storage;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
@@ -28,6 +30,8 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
class AttachmentHelper implements AttachmentHelperInterface
|
||||
{
|
||||
|
||||
/** @var Collection */
|
||||
public $attachments;
|
||||
/** @var MessageBag */
|
||||
public $errors;
|
||||
/** @var MessageBag */
|
||||
@@ -49,6 +53,7 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
$this->allowedMimes = (array)config('firefly.allowedMimes');
|
||||
$this->errors = new MessageBag;
|
||||
$this->messages = new MessageBag;
|
||||
$this->attachments = new Collection;
|
||||
$this->uploadDisk = Storage::disk('upload');
|
||||
}
|
||||
|
||||
@@ -64,6 +69,14 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAttachments(): Collection
|
||||
{
|
||||
return $this->attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MessageBag
|
||||
*/
|
||||
@@ -86,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) {
|
||||
@@ -110,7 +123,7 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
$md5 = md5_file($file->getRealPath());
|
||||
$name = $file->getClientOriginalName();
|
||||
$class = get_class($model);
|
||||
$count = auth()->user()->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count();
|
||||
$count = $model->user->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count();
|
||||
|
||||
if ($count > 0) {
|
||||
$msg = (string)trans('validation.file_already_attached', ['name' => $name]);
|
||||
@@ -137,7 +150,7 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
}
|
||||
|
||||
$attachment = new Attachment; // create Attachment object.
|
||||
$attachment->user()->associate(auth()->user());
|
||||
$attachment->user()->associate($model->user);
|
||||
$attachment->attachable()->associate($model);
|
||||
$attachment->md5 = md5_file($file->getRealPath());
|
||||
$attachment->filename = $file->getClientOriginalName();
|
||||
@@ -145,17 +158,21 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
$attachment->size = $file->getSize();
|
||||
$attachment->uploaded = 0;
|
||||
$attachment->save();
|
||||
Log::debug('Created attachment:', $attachment->toArray());
|
||||
|
||||
$fileObject = $file->openFile('r');
|
||||
$fileObject->rewind();
|
||||
$content = $fileObject->fread($file->getSize());
|
||||
$encrypted = Crypt::encrypt($content);
|
||||
Log::debug(sprintf('Full file length is %d and upload size is %d.', strlen($content), $file->getSize()));
|
||||
Log::debug(sprintf('Encrypted content is %d', strlen($encrypted)));
|
||||
|
||||
// store it:
|
||||
$this->uploadDisk->put($attachment->fileName(), $encrypted);
|
||||
|
||||
$attachment->uploaded = 1; // update attachment
|
||||
$attachment->save();
|
||||
$this->attachments->push($attachment);
|
||||
|
||||
$name = e($file->getClientOriginalName()); // add message:
|
||||
$msg = (string)trans('validation.file_attached', ['name' => $name]);
|
||||
@@ -188,6 +205,8 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
*
|
||||
* @return bool
|
||||
@@ -218,7 +237,7 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
return false;
|
||||
}
|
||||
if (!$this->validSize($file)) {
|
||||
return false;
|
||||
return false; // @codeCoverageIgnore
|
||||
}
|
||||
if ($this->hasFile($file, $model)) {
|
||||
return false;
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace FireflyIII\Helpers\Attachments;
|
||||
|
||||
use FireflyIII\Models\Attachment;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\MessageBag;
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,11 @@ interface AttachmentHelperInterface
|
||||
*/
|
||||
public function getAttachmentLocation(Attachment $attachment): string;
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAttachments(): Collection;
|
||||
|
||||
/**
|
||||
* @return MessageBag
|
||||
*/
|
||||
@@ -49,6 +55,6 @@ interface AttachmentHelperInterface
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function saveAttachmentsForModel(Model $model, array $files = null): bool;
|
||||
public function saveAttachmentsForModel(Model $model, ?array $files): bool;
|
||||
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Helpers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Generator\Report\Support;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
@@ -91,6 +94,7 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
if ($this->collectOtherObjects && $direction === 'expense') {
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setUser($this->user);
|
||||
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)->setTypes([TransactionType::WITHDRAWAL]);
|
||||
$journals = $collector->getJournals();
|
||||
$sum = strval($journals->sum('transaction_amount'));
|
||||
@@ -102,6 +106,7 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
if ($this->collectOtherObjects && $direction === 'income') {
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setUser($this->user);
|
||||
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)->setTypes([TransactionType::DEPOSIT]);
|
||||
$journals = $collector->getJournals();
|
||||
$sum = strval($journals->sum('transaction_amount'));
|
||||
@@ -114,6 +119,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Collection $accounts
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -126,6 +133,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Collection $budgets
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -138,6 +147,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Collection $categories
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -150,6 +161,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param bool $collectOtherObjects
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -162,6 +175,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -174,6 +189,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Carbon $start
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -186,6 +203,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Collection $tags
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -198,6 +217,8 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return MetaPieChartInterface
|
||||
@@ -209,23 +230,32 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getTransactions(string $direction)
|
||||
/**
|
||||
* @param string $direction
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
protected function getTransactions(string $direction): Collection
|
||||
{
|
||||
$types = [TransactionType::DEPOSIT, TransactionType::TRANSFER];
|
||||
$modifier = -1;
|
||||
if ($direction === 'expense') {
|
||||
$types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER];
|
||||
$modifier = 1;
|
||||
}
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$types = [TransactionType::DEPOSIT, TransactionType::TRANSFER];
|
||||
$collector->addFilter(NegativeAmountFilter::class);
|
||||
if ($direction === 'expense') {
|
||||
$types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER];
|
||||
$collector->addFilter(PositiveAmountFilter::class);
|
||||
$collector->removeFilter(NegativeAmountFilter::class);
|
||||
}
|
||||
|
||||
$collector->setUser($this->user);
|
||||
$collector->setAccounts($this->accounts);
|
||||
$collector->setRange($this->start, $this->end);
|
||||
$collector->setTypes($types);
|
||||
$collector->withOpposingAccount();
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
|
||||
if ($direction === 'income') {
|
||||
$collector->disableFilter();
|
||||
$collector->removeFilter(TransferFilter::class);
|
||||
}
|
||||
|
||||
if ($this->budgets->count() > 0) {
|
||||
@@ -240,11 +270,7 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
$collector->withBudgetInformation();
|
||||
}
|
||||
|
||||
$accountIds = $this->accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$set = Support::filterTransactions($transactions, $accountIds, $modifier);
|
||||
|
||||
return $set;
|
||||
return $collector->getJournals();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,6 +312,7 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
$chartData = [];
|
||||
$names = [];
|
||||
$repository = app($this->repositories[$type]);
|
||||
$repository->setUser($this->user);
|
||||
foreach ($array as $objectId => $amount) {
|
||||
if (!isset($names[$objectId])) {
|
||||
$object = $repository->find(intval($objectId));
|
||||
@@ -300,6 +327,11 @@ class MetaPieChart implements MetaPieChartInterface
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function groupByTag(Collection $set): array
|
||||
{
|
||||
$grouped = [];
|
||||
|
||||
@@ -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,18 +15,21 @@ namespace FireflyIII\Helpers\Collector;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Crypt;
|
||||
use DB;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Filter\FilterInterface;
|
||||
use FireflyIII\Helpers\Filter\InternalTransferFilter;
|
||||
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
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;
|
||||
@@ -55,25 +58,41 @@ class JournalCollector implements JournalCollectorInterface
|
||||
'transaction_journals.description',
|
||||
'transaction_journals.date',
|
||||
'transaction_journals.encrypted',
|
||||
'transaction_currencies.code as transaction_currency_code',
|
||||
'transaction_types.type as transaction_type_type',
|
||||
'transaction_journals.bill_id',
|
||||
'bills.name as bill_name',
|
||||
'bills.name_encrypted as bill_name_encrypted',
|
||||
|
||||
'transactions.id as id',
|
||||
'transactions.amount as transaction_amount',
|
||||
'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];
|
||||
|
||||
/** @var bool */
|
||||
private $joinedBudget = false;
|
||||
/** @var bool */
|
||||
@@ -95,6 +114,22 @@ class JournalCollector implements JournalCollectorInterface
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
*
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function addFilter(string $filter): JournalCollectorInterface
|
||||
{
|
||||
$interfaces = class_implements($filter);
|
||||
if (in_array(FilterInterface::class, $interfaces)) {
|
||||
Log::debug(sprintf('Enabled filter %s', $filter));
|
||||
$this->filters[] = $filter;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @throws FireflyException
|
||||
@@ -119,36 +154,6 @@ class JournalCollector implements JournalCollectorInterface
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function disableFilter(): JournalCollectorInterface
|
||||
{
|
||||
$this->filterTransfers = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function disableInternalFilter(): JournalCollectorInterface
|
||||
{
|
||||
$this->filterInternalTransfers = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function enableInternalFilter(): JournalCollectorInterface
|
||||
{
|
||||
$this->filterInternalTransfers = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
@@ -157,14 +162,9 @@ class JournalCollector implements JournalCollectorInterface
|
||||
$this->run = true;
|
||||
/** @var Collection $set */
|
||||
$set = $this->query->get(array_values($this->fields));
|
||||
Log::debug(sprintf('Count of set is %d', $set->count()));
|
||||
$set = $this->filterTransfers($set);
|
||||
Log::debug(sprintf('Count of set after filterTransfers() is %d', $set->count()));
|
||||
|
||||
// possibly filter "internal" transfers:
|
||||
$set = $this->filterInternalTransfers($set);
|
||||
Log::debug(sprintf('Count of set after filterInternalTransfers() is %d', $set->count()));
|
||||
|
||||
// run all filters:
|
||||
$set = $this->filter($set);
|
||||
|
||||
// loop for decryption.
|
||||
$set->each(
|
||||
@@ -175,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.
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
@@ -204,6 +202,22 @@ class JournalCollector implements JournalCollectorInterface
|
||||
return $journals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
*
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function removeFilter(string $filter): JournalCollectorInterface
|
||||
{
|
||||
$key = array_search($filter, $this->filters, true);
|
||||
if (!($key === false)) {
|
||||
Log::debug(sprintf('Removed filter %s', $filter));
|
||||
unset($this->filters[$key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $accounts
|
||||
*
|
||||
@@ -219,6 +233,7 @@ class JournalCollector implements JournalCollectorInterface
|
||||
}
|
||||
|
||||
if ($accounts->count() > 1) {
|
||||
$this->addFilter(TransferFilter::class);
|
||||
$this->filterTransfers = true;
|
||||
}
|
||||
|
||||
@@ -242,6 +257,7 @@ class JournalCollector implements JournalCollectorInterface
|
||||
}
|
||||
|
||||
if ($accounts->count() > 1) {
|
||||
$this->addFilter(TransferFilter::class);
|
||||
$this->filterTransfers = true;
|
||||
}
|
||||
|
||||
@@ -381,6 +397,10 @@ class JournalCollector implements JournalCollectorInterface
|
||||
*/
|
||||
public function setPage(int $page): JournalCollectorInterface
|
||||
{
|
||||
if ($page < 1) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$this->page = $page;
|
||||
|
||||
if ($page > 0) {
|
||||
@@ -464,7 +484,9 @@ class JournalCollector implements JournalCollectorInterface
|
||||
*/
|
||||
public function setUser(User $user)
|
||||
{
|
||||
Log::debug(sprintf('Journal collector now collecting for user #%d', $user->id));
|
||||
$this->user = $user;
|
||||
$this->startQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,19 +494,22 @@ class JournalCollector implements JournalCollectorInterface
|
||||
*/
|
||||
public function startQuery()
|
||||
{
|
||||
Log::debug('journalCollector::startQuery');
|
||||
/** @var EloquentBuilder $query */
|
||||
$query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transaction_journals.transaction_currency_id')
|
||||
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
|
||||
->leftJoin('bills', 'bills.id', 'transaction_journals.bill_id')
|
||||
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
|
||||
->leftJoin('account_types', 'accounts.account_type_id', 'account_types.id')
|
||||
->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transactions.transaction_currency_id')
|
||||
->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', 'transactions.foreign_currency_id')
|
||||
->whereNull('transactions.deleted_at')
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->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;
|
||||
|
||||
@@ -560,79 +585,23 @@ class JournalCollector implements JournalCollectorInterface
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function filterInternalTransfers(Collection $set): Collection
|
||||
private function filter(Collection $set): Collection
|
||||
{
|
||||
if ($this->filterInternalTransfers === false) {
|
||||
Log::debug('Did NO filtering for internal transfers on given set.');
|
||||
|
||||
return $set;
|
||||
}
|
||||
if ($this->joinedOpposing === false) {
|
||||
Log::info('Cannot filter internal transfers because no opposing information is present.');
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
||||
$accountIds = $this->accountIds;
|
||||
$set = $set->filter(
|
||||
function (Transaction $transaction) use ($accountIds) {
|
||||
// both id's in $accountids?
|
||||
if (in_array($transaction->account_id, $accountIds) && in_array($transaction->opposing_account_id, $accountIds)) {
|
||||
Log::debug(
|
||||
sprintf(
|
||||
'Transaction #%d has #%d and #%d in set, so removed',
|
||||
$transaction->id, $transaction->account_id, $transaction->opposing_account_id
|
||||
), $accountIds
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
|
||||
// create all possible filters:
|
||||
$filters = [
|
||||
InternalTransferFilter::class => new InternalTransferFilter($this->accountIds),
|
||||
OpposingAccountFilter::class => new OpposingAccountFilter($this->accountIds),
|
||||
TransferFilter::class => new TransferFilter,
|
||||
PositiveAmountFilter::class => new PositiveAmountFilter,
|
||||
NegativeAmountFilter::class => new NegativeAmountFilter,
|
||||
];
|
||||
Log::debug(sprintf('Will run %d filters on the set.', count($this->filters)));
|
||||
foreach ($this->filters as $enabled) {
|
||||
if (isset($filters[$enabled])) {
|
||||
Log::debug(sprintf('Before filter %s: %d', $enabled, $set->count()));
|
||||
$set = $filters[$enabled]->filter($set);
|
||||
Log::debug(sprintf('After filter %s: %d', $enabled, $set->count()));
|
||||
}
|
||||
);
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the set of accounts used by the collector includes more than one asset
|
||||
* account, chances are the set include double entries: transfers get selected
|
||||
* on both the source, and then again on the destination account.
|
||||
*
|
||||
* This method filters them out by removing transfers that have been selected twice.
|
||||
*
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function filterTransfers(Collection $set): Collection
|
||||
{
|
||||
if ($this->filterTransfers) {
|
||||
$count = [];
|
||||
$new = new Collection;
|
||||
/** @var Transaction $transaction */
|
||||
foreach ($set as $transaction) {
|
||||
if ($transaction->transaction_type_type !== TransactionType::TRANSFER) {
|
||||
$new->push($transaction);
|
||||
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)];
|
||||
sort($accountIds);
|
||||
$key = $journalId . '-' . join(',', $accountIds) . '-' . $amount;
|
||||
Log::debug(sprintf('Key is %s', $key));
|
||||
if (!isset($count[$key])) {
|
||||
// not yet counted? add to new set and count it:
|
||||
$new->push($transaction);
|
||||
$count[$key] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
return $set;
|
||||
@@ -650,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';
|
||||
@@ -706,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!');
|
||||
|
||||
@@ -28,26 +28,18 @@ use Illuminate\Support\Collection;
|
||||
*/
|
||||
interface JournalCollectorInterface
|
||||
{
|
||||
/**
|
||||
* @param string $filter
|
||||
*
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function addFilter(string $filter): JournalCollectorInterface;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int;
|
||||
|
||||
/**
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function disableFilter(): JournalCollectorInterface;
|
||||
|
||||
/**
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function disableInternalFilter(): JournalCollectorInterface;
|
||||
|
||||
/**
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function enableInternalFilter(): JournalCollectorInterface;
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
@@ -58,6 +50,13 @@ interface JournalCollectorInterface
|
||||
*/
|
||||
public function getPaginatedJournals(): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
*
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
public function removeFilter(string $filter): JournalCollectorInterface;
|
||||
|
||||
/**
|
||||
* @param Collection $accounts
|
||||
*
|
||||
|
||||
56
app/Helpers/Filter/AmountFilter.php
Normal file
56
app/Helpers/Filter/AmountFilter.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* AmountFilter.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\Helpers\Filter;
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class AmountFilter
|
||||
*
|
||||
* This filter removes transactions with either a positive amount ($parameters = 1) or a negative amount
|
||||
* ($parameter = -1). This is helpful when a Collection has you with both transactions in a journal.
|
||||
*
|
||||
* @package FireflyIII\Helpers\Filter
|
||||
*/
|
||||
class AmountFilter implements FilterInterface
|
||||
{
|
||||
/** @var int */
|
||||
private $modifier = 0;
|
||||
|
||||
public function __construct(int $modifier)
|
||||
{
|
||||
$this->modifier = $modifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection
|
||||
{
|
||||
return $set->filter(
|
||||
function (Transaction $transaction) {
|
||||
// remove by amount
|
||||
if (bccomp($transaction->transaction_amount, '0') === $this->modifier) {
|
||||
Log::debug(sprintf('Filtered #%d because amount is %f.', $transaction->id, $transaction->transaction_amount));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
34
app/Helpers/Filter/EmptyFilter.php
Normal file
34
app/Helpers/Filter/EmptyFilter.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* EmptyFilter.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\Helpers\Filter;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class EmptyFilter
|
||||
*
|
||||
* @package FireflyIII\Helpers\Filter
|
||||
*/
|
||||
class EmptyFilter implements FilterInterface
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection
|
||||
{
|
||||
return $set;
|
||||
}
|
||||
}
|
||||
26
app/Helpers/Filter/FilterInterface.php
Normal file
26
app/Helpers/Filter/FilterInterface.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* FilterInterface.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\Helpers\Filter;
|
||||
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
interface FilterInterface
|
||||
{
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection;
|
||||
|
||||
}
|
||||
73
app/Helpers/Filter/InternalTransferFilter.php
Normal file
73
app/Helpers/Filter/InternalTransferFilter.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* InternalTransferFilter.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\Helpers\Filter;
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class InternalTransferFilter
|
||||
*
|
||||
* This filter removes any filters that are from A to B or from B to A given a set of
|
||||
* account id's (in $parameters) where A and B are mentioned. So transfers between the mentioned
|
||||
* accounts will be removed.
|
||||
*
|
||||
* @package FireflyIII\Helpers\Filter
|
||||
*/
|
||||
class InternalTransferFilter implements FilterInterface
|
||||
{
|
||||
/** @var array */
|
||||
private $accounts = [];
|
||||
|
||||
/**
|
||||
* InternalTransferFilter constructor.
|
||||
*
|
||||
* @param array $accounts
|
||||
*/
|
||||
public function __construct(array $accounts)
|
||||
{
|
||||
$this->accounts = $accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection
|
||||
{
|
||||
return $set->filter(
|
||||
function (Transaction $transaction) {
|
||||
if (is_null($transaction->opposing_account_id)) {
|
||||
return $transaction;
|
||||
}
|
||||
// both id's in $parameters?
|
||||
if (in_array($transaction->account_id, $this->accounts) && in_array($transaction->opposing_account_id, $this->accounts)) {
|
||||
Log::debug(
|
||||
sprintf(
|
||||
'Transaction #%d has #%d and #%d in set, so removed',
|
||||
$transaction->id, $transaction->account_id, $transaction->opposing_account_id
|
||||
), $this->accounts
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
47
app/Helpers/Filter/NegativeAmountFilter.php
Normal file
47
app/Helpers/Filter/NegativeAmountFilter.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* NegativeAmountFilter.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\Helpers\Filter;
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class NegativeAmountFilter
|
||||
*
|
||||
* This filter removes entries with a negative amount (the original modifier is -1).
|
||||
*
|
||||
* @package FireflyIII\Helpers\Filter
|
||||
*/
|
||||
class NegativeAmountFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection
|
||||
{
|
||||
return $set->filter(
|
||||
function (Transaction $transaction) {
|
||||
// remove by amount
|
||||
if (bccomp($transaction->transaction_amount, '0') === -1) {
|
||||
Log::debug(sprintf('Filtered #%d because amount is %f.', $transaction->id, $transaction->transaction_amount));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
63
app/Helpers/Filter/OpposingAccountFilter.php
Normal file
63
app/Helpers/Filter/OpposingAccountFilter.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* OpposingAccountFilter.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\Helpers\Filter;
|
||||
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class OpposingAccountFilter
|
||||
*
|
||||
* This filter is similar to the internal transfer filter but only removes transactions when the opposing account is
|
||||
* amongst $parameters (list of account ID's).
|
||||
*
|
||||
* @package FireflyIII\Helpers\Filter
|
||||
*/
|
||||
class OpposingAccountFilter implements FilterInterface
|
||||
{
|
||||
/** @var array */
|
||||
private $accounts = [];
|
||||
|
||||
/**
|
||||
* InternalTransferFilter constructor.
|
||||
*
|
||||
* @param array $accounts
|
||||
*/
|
||||
public function __construct(array $accounts)
|
||||
{
|
||||
$this->accounts = $accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection
|
||||
{
|
||||
return $set->filter(
|
||||
function (Transaction $transaction) {
|
||||
$opposing = $transaction->opposing_account_id;
|
||||
// remove internal transfer
|
||||
if (in_array($opposing, $this->accounts)) {
|
||||
Log::debug(sprintf('Filtered #%d because its opposite is in accounts.', $transaction->id), $this->accounts);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
50
app/Helpers/Filter/PositiveAmountFilter.php
Normal file
50
app/Helpers/Filter/PositiveAmountFilter.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* PositiveAmountFilter.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\Helpers\Filter;
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class PositiveAmountFilter
|
||||
*
|
||||
* This filter removes entries with a negative amount (the original modifier is -1).
|
||||
*
|
||||
* This filter removes transactions with either a positive amount ($parameters = 1) or a negative amount
|
||||
* ($parameter = -1). This is helpful when a Collection has you with both transactions in a journal.
|
||||
*
|
||||
* @package FireflyIII\Helpers\Filter
|
||||
*/
|
||||
class PositiveAmountFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection
|
||||
{
|
||||
return $set->filter(
|
||||
function (Transaction $transaction) {
|
||||
// remove by amount
|
||||
if (bccomp($transaction->transaction_amount, '0') === 1) {
|
||||
Log::debug(sprintf('Filtered #%d because amount is %f.', $transaction->id, $transaction->transaction_amount));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
60
app/Helpers/Filter/TransferFilter.php
Normal file
60
app/Helpers/Filter/TransferFilter.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/**
|
||||
* TransferFilter.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\Helpers\Filter;
|
||||
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use Illuminate\Support\Collection;
|
||||
use Steam;
|
||||
|
||||
/**
|
||||
* Class TransferFilter
|
||||
*
|
||||
* This filter removes any transfers that are in the collection twice (from A to B and from B to A).
|
||||
*
|
||||
* @package FireflyIII\Helpers\Filter
|
||||
*/
|
||||
class TransferFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function filter(Collection $set): Collection
|
||||
{
|
||||
$count = [];
|
||||
$new = new Collection;
|
||||
/** @var Transaction $transaction */
|
||||
foreach ($set as $transaction) {
|
||||
if ($transaction->transaction_type_type !== TransactionType::TRANSFER) {
|
||||
$new->push($transaction);
|
||||
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)];
|
||||
$transactionIds = [$transaction->id, intval($transaction->opposing_id)];
|
||||
sort($accountIds);
|
||||
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);
|
||||
$count[$key] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -187,7 +187,7 @@ class PopupReport implements PopupReportInterface
|
||||
$journals = $journals->filter(
|
||||
function (Transaction $transaction) use ($report) {
|
||||
// get the destinations:
|
||||
$destinations = $transaction->destinationAccountList($transaction->transactionJournal)->pluck('id')->toArray();
|
||||
$destinations = $transaction->transactionJournal->destinationAccountList()->pluck('id')->toArray();
|
||||
|
||||
// do these intersect with the current list?
|
||||
return !empty(array_intersect($report, $destinations));
|
||||
@@ -196,4 +196,4 @@ class PopupReport implements PopupReportInterface
|
||||
|
||||
return $journals;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +80,4 @@ interface PopupReportInterface
|
||||
* @return Collection
|
||||
*/
|
||||
public function byIncome(Account $account, array $attributes): Collection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -219,8 +219,8 @@ class AccountController extends Controller
|
||||
$start->subDay();
|
||||
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
$startBalances = Steam::balancesById($ids, $start);
|
||||
$endBalances = Steam::balancesById($ids, $end);
|
||||
$startBalances = Steam::balancesByAccounts($accounts, $start);
|
||||
$endBalances = Steam::balancesByAccounts($accounts, $end);
|
||||
$activities = Steam::getLastActivities($ids);
|
||||
|
||||
$accounts->each(
|
||||
@@ -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) {
|
||||
$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'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -98,10 +98,13 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function download(AttachmentRepositoryInterface $repository, Attachment $attachment)
|
||||
{
|
||||
|
||||
|
||||
if ($repository->exists($attachment)) {
|
||||
$content = $repository->getContent($attachment);
|
||||
$quoted = sprintf('"%s"', addcslashes(basename($attachment->filename), '"\\'));
|
||||
|
||||
|
||||
/** @var LaravelResponse $response */
|
||||
$response = response($content, 200);
|
||||
$response
|
||||
@@ -149,7 +152,8 @@ 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;
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Amount;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Http\Requests\BudgetFormRequest;
|
||||
@@ -74,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();
|
||||
@@ -166,16 +165,38 @@ class BudgetController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $moment
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function index()
|
||||
public function index(string $moment = null)
|
||||
{
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = session('start', new Carbon);
|
||||
$end = session('end', new Carbon);
|
||||
|
||||
// make date if present:
|
||||
if (!is_null($moment) || strlen(strval($moment)) !== 0) {
|
||||
try {
|
||||
$start = new Carbon($moment);
|
||||
$end = Navigation::endOfPeriod($start, $range);
|
||||
} catch (Exception $e) {
|
||||
// start and end are already defined.
|
||||
|
||||
}
|
||||
}
|
||||
$next = clone $end;
|
||||
$next->addDay();
|
||||
$prev = clone $start;
|
||||
$prev->subDay();
|
||||
$prev = Navigation::startOfPeriod($prev, $range);
|
||||
|
||||
|
||||
$this->repository->cleanupBudgets();
|
||||
|
||||
|
||||
$budgets = $this->repository->getActiveBudgets();
|
||||
$inactive = $this->repository->getInactiveBudgets();
|
||||
$start = session('start', new Carbon);
|
||||
$end = session('end', new Carbon);
|
||||
$periodStart = $start->formatLocalized($this->monthAndDayFormat);
|
||||
$periodEnd = $end->formatLocalized($this->monthAndDayFormat);
|
||||
$budgetInformation = $this->collectBudgetInformation($budgets, $start, $end);
|
||||
@@ -184,9 +205,44 @@ class BudgetController extends Controller
|
||||
$spent = array_sum(array_column($budgetInformation, 'spent'));
|
||||
$budgeted = array_sum(array_column($budgetInformation, 'budgeted'));
|
||||
|
||||
// select thing for last 12 periods:
|
||||
$previousLoop = [];
|
||||
$previousDate = clone $start;
|
||||
$count = 0;
|
||||
while ($count < 12) {
|
||||
$previousDate->subDay();
|
||||
$previousDate = Navigation::startOfPeriod($previousDate, $range);
|
||||
$format = $previousDate->format('Y-m-d');
|
||||
$previousLoop[$format] = Navigation::periodShow($previousDate, $range);
|
||||
$count++;
|
||||
}
|
||||
|
||||
// select thing for next 12 periods:
|
||||
$nextLoop = [];
|
||||
$nextDate = clone $end;
|
||||
$nextDate->addDay();
|
||||
$count = 0;
|
||||
|
||||
while ($count < 12) {
|
||||
$format = $nextDate->format('Y-m-d');
|
||||
$nextLoop[$format] = Navigation::periodShow($nextDate, $range);
|
||||
$nextDate = Navigation::endOfPeriod($nextDate, $range);
|
||||
$count++;
|
||||
$nextDate->addDay();
|
||||
}
|
||||
|
||||
// display info
|
||||
$currentMonth = Navigation::periodShow($start, $range);
|
||||
$nextText = Navigation::periodShow($next, $range);
|
||||
$prevText = Navigation::periodShow($prev, $range);
|
||||
|
||||
return view(
|
||||
'budgets.index',
|
||||
compact('available', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', 'spent', 'budgeted')
|
||||
compact(
|
||||
'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText',
|
||||
'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets',
|
||||
'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start', 'end'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,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('Count is zero, search for journals.');
|
||||
/** @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) {
|
||||
$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'));
|
||||
}
|
||||
@@ -277,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')]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,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;
|
||||
@@ -308,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]);
|
||||
@@ -326,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', [
|
||||
@@ -346,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());
|
||||
@@ -407,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'));
|
||||
}
|
||||
@@ -509,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
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\InternalTransferFilter;
|
||||
use FireflyIII\Http\Requests\CategoryFormRequest;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Category;
|
||||
@@ -163,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') {
|
||||
@@ -198,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->disableInternalFilter();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('/categories/list/no-category');
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0) {
|
||||
$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'));
|
||||
}
|
||||
@@ -246,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;
|
||||
@@ -286,33 +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()->disableInternalFilter();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('categories/show/' . $category->id);
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0) {
|
||||
$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'));
|
||||
}
|
||||
@@ -380,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
|
||||
@@ -402,14 +361,17 @@ class CategoryController extends Controller
|
||||
// count journals without budget in this period:
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()->withOpposingAccount()->disableInternalFilter();
|
||||
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()
|
||||
->withOpposingAccount();
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$count = $collector->getJournals()->count();
|
||||
|
||||
// amount transferred
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()
|
||||
->withOpposingAccount()->setTypes([TransactionType::TRANSFER])->disableInternalFilter();
|
||||
->withOpposingAccount()->setTypes([TransactionType::TRANSFER]);
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$transferred = Steam::positive($collector->getJournals()->sum('transaction_amount'));
|
||||
|
||||
// amount spent
|
||||
@@ -458,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.
|
||||
@@ -488,7 +450,8 @@ class CategoryController extends Controller
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->setCategory($category)
|
||||
->withOpposingAccount()->setTypes([TransactionType::TRANSFER])->disableInternalFilter();
|
||||
->withOpposingAccount()->setTypes([TransactionType::TRANSFER]);
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$transferred = Steam::positive($collector->getJournals()->sum('transaction_amount'));
|
||||
|
||||
$entries->push(
|
||||
|
||||
@@ -14,7 +14,6 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
@@ -115,9 +114,8 @@ class AccountController extends Controller
|
||||
$start->subDay();
|
||||
|
||||
$accounts = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]);
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
$startBalances = Steam::balancesById($ids, $start);
|
||||
$endBalances = Steam::balancesById($ids, $end);
|
||||
$startBalances = Steam::balancesByAccounts($accounts, $start);
|
||||
$endBalances = Steam::balancesByAccounts($accounts, $end);
|
||||
$chartData = [];
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
@@ -129,6 +127,7 @@ class AccountController extends Controller
|
||||
$chartData[$account->name] = $diff;
|
||||
}
|
||||
}
|
||||
|
||||
arsort($chartData);
|
||||
$data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData);
|
||||
$cache->store($data);
|
||||
@@ -336,7 +335,7 @@ class AccountController extends Controller
|
||||
|
||||
/**
|
||||
* @param Account $account
|
||||
* @param Carbon $start
|
||||
* @param Carbon $start
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws FireflyException
|
||||
@@ -411,9 +410,8 @@ class AccountController extends Controller
|
||||
$accounts = $repository->getAccountsByType([AccountType::REVENUE]);
|
||||
|
||||
$start->subDay();
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
$startBalances = Steam::balancesById($ids, $start);
|
||||
$endBalances = Steam::balancesById($ids, $end);
|
||||
$startBalances = Steam::balancesByAccounts($accounts, $start);
|
||||
$endBalances = Steam::balancesByAccounts($accounts, $end);
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$id = $account->id;
|
||||
@@ -427,7 +425,7 @@ class AccountController extends Controller
|
||||
}
|
||||
|
||||
arsort($chartData);
|
||||
$data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData);
|
||||
$data = $this->generator->singleSet(strval(trans('firefly.earned')), $chartData);
|
||||
$cache->store($data);
|
||||
|
||||
return Response::json($data);
|
||||
|
||||
@@ -31,6 +31,7 @@ use Illuminate\Support\Collection;
|
||||
use Navigation;
|
||||
use Preferences;
|
||||
use Response;
|
||||
use Steam;
|
||||
|
||||
/**
|
||||
* Class BudgetController
|
||||
@@ -123,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.');
|
||||
}
|
||||
|
||||
@@ -162,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);
|
||||
@@ -207,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);
|
||||
@@ -254,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);
|
||||
@@ -320,12 +321,12 @@ class BudgetController extends Controller
|
||||
['label' => strval(trans('firefly.overspent')), 'entries' => [], 'type' => 'bar',],
|
||||
];
|
||||
|
||||
|
||||
/** @var Budget $budget */
|
||||
foreach ($budgets as $budget) {
|
||||
// get relevant repetitions:
|
||||
$limits = $this->repository->getBudgetLimits($budget, $start, $end);
|
||||
$expenses = $this->getExpensesForBudget($limits, $budget, $start, $end);
|
||||
|
||||
foreach ($expenses as $name => $row) {
|
||||
$chartData[0]['entries'][$name] = $row['spent'];
|
||||
$chartData[1]['entries'][$name] = $row['left'];
|
||||
@@ -529,9 +530,7 @@ class BudgetController extends Controller
|
||||
$rows = $this->spentInPeriodMulti($budget, $limits);
|
||||
foreach ($rows as $name => $row) {
|
||||
if (bccomp($row['spent'], '0') !== 0 || bccomp($row['left'], '0') !== 0) {
|
||||
$return[$name]['spent'] = bcmul($row['spent'], '-1');
|
||||
$return[$name]['left'] = $row['left'];
|
||||
$return[$name]['overspent'] = bcmul($row['overspent'], '-1');
|
||||
$return[$name] = $row;
|
||||
}
|
||||
}
|
||||
unset($rows, $row);
|
||||
@@ -563,6 +562,7 @@ class BudgetController extends Controller
|
||||
/** @var BudgetLimit $budgetLimit */
|
||||
foreach ($limits as $budgetLimit) {
|
||||
$expenses = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $budgetLimit->start_date, $budgetLimit->end_date);
|
||||
$expenses = Steam::positive($expenses);
|
||||
|
||||
if ($limits->count() > 1) {
|
||||
$name = $budget->name . ' ' . trans(
|
||||
@@ -578,10 +578,14 @@ class BudgetController extends Controller
|
||||
* left: amount of budget limit min spent, or 0 when < 0.
|
||||
* spent: spent, or amount of budget limit when > amount
|
||||
*/
|
||||
$amount = $budgetLimit->amount;
|
||||
$left = bccomp(bcadd($amount, $expenses), '0') < 1 ? '0' : bcadd($amount, $expenses);
|
||||
$spent = bccomp($expenses, $amount) === 1 ? $expenses : bcmul($amount, '-1');
|
||||
$overspent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcadd($amount, $expenses) : '0';
|
||||
$amount = $budgetLimit->amount;
|
||||
$leftInLimit = bcsub($amount, $expenses);
|
||||
$hasOverspent = bccomp($leftInLimit, '0') === -1;
|
||||
|
||||
$left = $hasOverspent ? '0' : bcsub($amount, $expenses);
|
||||
$spent = $hasOverspent ? $amount : $expenses;
|
||||
$overspent = $hasOverspent ? Steam::positive($leftInLimit) : '0';
|
||||
|
||||
$return[$name] = [
|
||||
'left' => $left,
|
||||
'overspent' => $overspent,
|
||||
|
||||
@@ -16,9 +16,11 @@ namespace FireflyIII\Http\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
|
||||
use FireflyIII\Generator\Report\Support;
|
||||
use FireflyIII\Helpers\Chart\MetaPieChartInterface;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
@@ -234,12 +236,15 @@ class BudgetReportController extends Controller
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
|
||||
->setBudgets($budgets)->withOpposingAccount()->disableFilter();
|
||||
$accountIds = $accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$set = Support::filterExpenses($transactions, $accountIds);
|
||||
->setBudgets($budgets)->withOpposingAccount();
|
||||
$collector->removeFilter(TransferFilter::class);
|
||||
|
||||
return $set;
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(PositiveAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,7 +67,7 @@ class CategoryController extends Controller
|
||||
|
||||
$start = $repository->firstUseDate($category);
|
||||
|
||||
if ($start->year == 1900) {
|
||||
if ($start->year === 1900) {
|
||||
$start = new Carbon;
|
||||
}
|
||||
|
||||
@@ -277,10 +277,10 @@ class CategoryController extends Controller
|
||||
*/
|
||||
public function specificPeriod(CategoryRepositoryInterface $repository, Category $category, Carbon $date)
|
||||
{
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = Navigation::startOfPeriod($date, $range);
|
||||
$end = Navigation::endOfPeriod($date, $range);
|
||||
$data = $this->makePeriodChart($repository, $category, $start, $end);
|
||||
$range = Preferences::get('viewRange', '1M')->data;
|
||||
$start = Navigation::startOfPeriod($date, $range);
|
||||
$end = Navigation::endOfPeriod($date, $range);
|
||||
$data = $this->makePeriodChart($repository, $category, $start, $end);
|
||||
|
||||
return Response::json($data);
|
||||
}
|
||||
@@ -336,9 +336,9 @@ class CategoryController extends Controller
|
||||
$sum = bcadd($spent, $earned);
|
||||
$label = trim(Navigation::periodShow($start, '1D'));
|
||||
|
||||
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'),12);
|
||||
$chartData[1]['entries'][$label] = round($earned,12);
|
||||
$chartData[2]['entries'][$label] = round($sum,12);
|
||||
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12);
|
||||
$chartData[1]['entries'][$label] = round($earned, 12);
|
||||
$chartData[2]['entries'][$label] = round($sum, 12);
|
||||
|
||||
|
||||
$start->addDay();
|
||||
|
||||
@@ -16,9 +16,12 @@ namespace FireflyIII\Http\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
|
||||
use FireflyIII\Generator\Report\Category\MonthReportGenerator;
|
||||
use FireflyIII\Helpers\Chart\MetaPieChartInterface;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\Transaction;
|
||||
@@ -244,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];
|
||||
}
|
||||
}
|
||||
@@ -271,12 +274,15 @@ class CategoryReportController extends Controller
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
|
||||
->setCategories($categories)->withOpposingAccount()->disableFilter();
|
||||
$accountIds = $accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$set = MonthReportGenerator::filterExpenses($transactions, $accountIds);
|
||||
->setCategories($categories)->withOpposingAccount();
|
||||
$collector->removeFilter(TransferFilter::class);
|
||||
|
||||
return $set;
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(PositiveAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,11 +299,13 @@ class CategoryReportController extends Controller
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
|
||||
->setCategories($categories)->withOpposingAccount();
|
||||
$accountIds = $accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$set = MonthReportGenerator::filterIncome($transactions, $accountIds);
|
||||
|
||||
return $set;
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(NegativeAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,11 +67,10 @@ class ReportController extends Controller
|
||||
if ($cache->has()) {
|
||||
return Response::json($cache->get()); // @codeCoverageIgnore
|
||||
}
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
$current = clone $start;
|
||||
$chartData = [];
|
||||
while ($current < $end) {
|
||||
$balances = Steam::balancesById($ids, $current);
|
||||
$balances = Steam::balancesByAccounts($accounts, $current);
|
||||
$sum = $this->arraySum($balances);
|
||||
$label = $current->formatLocalized(strval(trans('config.month_and_day')));
|
||||
$chartData[$label] = $sum;
|
||||
@@ -104,7 +103,7 @@ class ReportController extends Controller
|
||||
$cache->addProperty($accounts);
|
||||
$cache->addProperty($end);
|
||||
if ($cache->has()) {
|
||||
//return Response::json($cache->get()); // @codeCoverageIgnore
|
||||
return Response::json($cache->get()); // @codeCoverageIgnore
|
||||
}
|
||||
Log::debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray());
|
||||
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
|
||||
@@ -250,7 +249,7 @@ class ReportController extends Controller
|
||||
$cache->addProperty($accounts);
|
||||
$cache->addProperty($end);
|
||||
if ($cache->has()) {
|
||||
// return $cache->get(); // @codeCoverageIgnore
|
||||
return $cache->get(); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$currentStart = clone $start;
|
||||
|
||||
@@ -14,9 +14,12 @@ namespace FireflyIII\Http\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
|
||||
use FireflyIII\Generator\Report\Tag\MonthReportGenerator;
|
||||
use FireflyIII\Helpers\Chart\MetaPieChartInterface;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
|
||||
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
|
||||
use FireflyIII\Helpers\Filter\TransferFilter;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\Transaction;
|
||||
@@ -228,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];
|
||||
}
|
||||
}
|
||||
@@ -303,12 +306,15 @@ class TagReportController extends Controller
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
|
||||
->setTags($tags)->withOpposingAccount()->disableFilter();
|
||||
$accountIds = $accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$set = MonthReportGenerator::filterExpenses($transactions, $accountIds);
|
||||
->setTags($tags)->withOpposingAccount();
|
||||
$collector->removeFilter(TransferFilter::class);
|
||||
|
||||
return $set;
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(PositiveAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,11 +331,13 @@ class TagReportController extends Controller
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
|
||||
->setTags($tags)->withOpposingAccount();
|
||||
$accountIds = $accounts->pluck('id')->toArray();
|
||||
$transactions = $collector->getJournals();
|
||||
$set = MonthReportGenerator::filterIncome($transactions, $accountIds);
|
||||
|
||||
return $set;
|
||||
$collector->addFilter(OpposingAccountFilter::class);
|
||||
$collector->addFilter(NegativeAmountFilter::class);
|
||||
|
||||
$transactions = $collector->getJournals();
|
||||
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,4 +364,4 @@ class TagReportController extends Controller
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,32 +12,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Crypt;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Http\Requests\ImportUploadRequest;
|
||||
use FireflyIII\Import\ImportProcedureInterface;
|
||||
use FireflyIII\Import\Setup\SetupInterface;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
|
||||
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response as LaravelResponse;
|
||||
use Log;
|
||||
use Response;
|
||||
use Session;
|
||||
use SplFileObject;
|
||||
use Storage;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use View;
|
||||
|
||||
/**
|
||||
* Class ImportController
|
||||
* Class ImportController.
|
||||
*
|
||||
* @package FireflyIII\Http\Controllers
|
||||
*/
|
||||
class ImportController extends Controller
|
||||
{
|
||||
/** @var ImportJobRepositoryInterface */
|
||||
public $repository;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -48,121 +35,23 @@ class ImportController extends Controller
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
View::share('mainTitleIcon', 'fa-archive');
|
||||
View::share('title', trans('firefly.import_data_full'));
|
||||
View::share('title', trans('firefly.import_index_title'));
|
||||
$this->repository = app(ImportJobRepositoryInterface::class);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the last step before the import starts.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
|
||||
*/
|
||||
public function complete(ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in complete()', ['job' => $job->key]);
|
||||
if (!$this->jobInCorrectStep($job, 'complete')) {
|
||||
return $this->redirectToCorrectStep($job);
|
||||
}
|
||||
$subTitle = trans('firefly.import_complete');
|
||||
$subTitleIcon = 'fa-star';
|
||||
|
||||
return view('import.complete', compact('job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 3.
|
||||
* This is the first step in configuring the job. It can only be executed
|
||||
* when the job is set to "import_status_never_started".
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return View
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function configure(ImportJob $job)
|
||||
{
|
||||
Log::debug('Now at start of configure()');
|
||||
if (!$this->jobInCorrectStep($job, 'configure')) {
|
||||
return $this->redirectToCorrectStep($job);
|
||||
}
|
||||
|
||||
// actual code
|
||||
$importer = $this->makeImporter($job);
|
||||
$importer->configure();
|
||||
$data = $importer->getConfigurationData();
|
||||
$subTitle = trans('firefly.configure_import');
|
||||
$subTitleIcon = 'fa-wrench';
|
||||
|
||||
return view('import.' . $job->file_type . '.configure', compact('data', 'job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON file of the job's config and send it to the user.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function download(ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in download()', ['job' => $job->key]);
|
||||
$config = $job->configuration;
|
||||
$config['column-roles-complete'] = false;
|
||||
$config['column-mapping-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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function finished(ImportJob $job)
|
||||
{
|
||||
if (!$this->jobInCorrectStep($job, 'finished')) {
|
||||
return $this->redirectToCorrectStep($job);
|
||||
}
|
||||
|
||||
// if there is a tag (there might not be), we can link to it:
|
||||
$tagId = $job->extended_status['importTag'] ?? 0;
|
||||
|
||||
$subTitle = trans('firefly.import_finished');
|
||||
$subTitleIcon = 'fa-star';
|
||||
|
||||
return view('import.finished', compact('job', 'subTitle', 'subTitleIcon', 'tagId'));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is step 1. Upload a file.
|
||||
* General import index
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
Log::debug('Now at index');
|
||||
$subTitle = trans('firefly.import_data_index');
|
||||
$subTitle = trans('firefly.import_index_sub_title');
|
||||
$subTitleIcon = 'fa-home';
|
||||
$importFileTypes = [];
|
||||
$defaultImportType = config('firefly.default_import_format');
|
||||
@@ -174,331 +63,4 @@ class ImportController extends Controller
|
||||
return view('import.index', compact('subTitle', 'subTitleIcon', 'importFileTypes', 'defaultImportType'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function json(ImportJob $job)
|
||||
{
|
||||
$result = [
|
||||
'showPercentage' => false,
|
||||
'started' => false,
|
||||
'finished' => false,
|
||||
'running' => false,
|
||||
'errors' => $job->extended_status['errors'],
|
||||
'percentage' => 0,
|
||||
'steps' => $job->extended_status['total_steps'],
|
||||
'stepsDone' => $job->extended_status['steps_done'],
|
||||
'statusText' => trans('firefly.import_status_' . $job->status),
|
||||
'finishedText' => '',
|
||||
];
|
||||
$percentage = 0;
|
||||
if ($job->extended_status['total_steps'] !== 0) {
|
||||
$percentage = round(($job->extended_status['steps_done'] / $job->extended_status['total_steps']) * 100, 0);
|
||||
}
|
||||
if ($job->status === 'import_complete') {
|
||||
$tagId = $job->extended_status['importTag'];
|
||||
/** @var TagRepositoryInterface $repository */
|
||||
$repository = app(TagRepositoryInterface::class);
|
||||
$tag = $repository->find($tagId);
|
||||
$result['finished'] = true;
|
||||
$result['finishedText'] = trans('firefly.import_finished_link', ['link' => route('tags.show', [$tag->id]), 'tag' => $tag->tag]);
|
||||
}
|
||||
|
||||
if ($job->status === 'import_running') {
|
||||
$result['started'] = true;
|
||||
$result['running'] = true;
|
||||
$result['showPercentage'] = true;
|
||||
$result['percentage'] = $percentage;
|
||||
}
|
||||
|
||||
return Response::json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4. Save the configuration.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param ImportJobRepositoryInterface $repository
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function postConfigure(Request $request, ImportJobRepositoryInterface $repository, ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in postConfigure()', ['job' => $job->key]);
|
||||
if (!$this->jobInCorrectStep($job, 'process')) {
|
||||
return $this->redirectToCorrectStep($job);
|
||||
}
|
||||
Log::debug('Continue postConfigure()', ['job' => $job->key]);
|
||||
|
||||
// actual code
|
||||
$importer = $this->makeImporter($job);
|
||||
$data = $request->all();
|
||||
$files = $request->files;
|
||||
$importer->saveImportConfiguration($data, $files);
|
||||
|
||||
// update job:
|
||||
$repository->updateStatus($job, 'import_configuration_saved');
|
||||
|
||||
// return redirect to settings.
|
||||
// this could loop until the user is done.
|
||||
return redirect(route('import.settings', [$job->key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* This step 6. Depending on the importer, this will process the
|
||||
* settings given and store them.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function postSettings(Request $request, ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in postSettings()', ['job' => $job->key]);
|
||||
if (!$this->jobInCorrectStep($job, 'store-settings')) {
|
||||
return $this->redirectToCorrectStep($job);
|
||||
}
|
||||
$importer = $this->makeImporter($job);
|
||||
$importer->storeSettings($request);
|
||||
|
||||
// return redirect to settings (for more settings perhaps)
|
||||
return redirect(route('import.settings', [$job->key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 5. Depending on the importer, this will show the user settings to
|
||||
* fill in.
|
||||
*
|
||||
* @param ImportJobRepositoryInterface $repository
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function settings(ImportJobRepositoryInterface $repository, ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in settings()', ['job' => $job->key]);
|
||||
if (!$this->jobInCorrectStep($job, 'settings')) {
|
||||
return $this->redirectToCorrectStep($job);
|
||||
}
|
||||
Log::debug('Continue in settings()');
|
||||
$importer = $this->makeImporter($job);
|
||||
$subTitle = trans('firefly.settings_for_import');
|
||||
$subTitleIcon = 'fa-wrench';
|
||||
|
||||
// now show settings screen to user.
|
||||
if ($importer->requireUserSettings()) {
|
||||
Log::debug('Job requires user config.');
|
||||
$data = $importer->getDataForSettings();
|
||||
$view = $importer->getViewForSettings();
|
||||
|
||||
return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
Log::debug('Job does NOT require user config.');
|
||||
|
||||
$repository->updateStatus($job, 'settings_complete');
|
||||
|
||||
// if no more settings, save job and continue to process thing.
|
||||
return redirect(route('import.complete', [$job->key]));
|
||||
|
||||
// ask the importer for the requested action.
|
||||
// for example pick columns or map data.
|
||||
// depends of course on the data in the job.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportProcedureInterface $importProcedure
|
||||
* @param ImportJob $job
|
||||
*/
|
||||
public function start(ImportProcedureInterface $importProcedure, ImportJob $job)
|
||||
{
|
||||
set_time_limit(0);
|
||||
if ($job->status == 'settings_complete') {
|
||||
$importProcedure->runImport($job);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the last step before the import starts.
|
||||
*
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
|
||||
*/
|
||||
public function status(ImportJob $job)
|
||||
{ //
|
||||
Log::debug('Now in status()', ['job' => $job->key]);
|
||||
if (!$this->jobInCorrectStep($job, 'status')) {
|
||||
return $this->redirectToCorrectStep($job);
|
||||
}
|
||||
$subTitle = trans('firefly.import_status');
|
||||
$subTitleIcon = 'fa-star';
|
||||
|
||||
return view('import.status', compact('job', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is step 2. It creates an Import Job. Stores the import.
|
||||
*
|
||||
* @param ImportUploadRequest $request
|
||||
* @param ImportJobRepositoryInterface $repository
|
||||
* @param UserRepositoryInterface $userRepository
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function upload(ImportUploadRequest $request, ImportJobRepositoryInterface $repository, UserRepositoryInterface $userRepository)
|
||||
{
|
||||
Log::debug('Now in upload()');
|
||||
// create import job:
|
||||
$type = $request->get('import_file_type');
|
||||
$job = $repository->create($type);
|
||||
Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]);
|
||||
|
||||
/** @var UploadedFile $upload */
|
||||
$upload = $request->files->get('import_file');
|
||||
$newName = $job->key . '.upload';
|
||||
$uploaded = new SplFileObject($upload->getRealPath());
|
||||
$content = $uploaded->fread($uploaded->getSize());
|
||||
$contentEncrypted = Crypt::encrypt($content);
|
||||
$disk = Storage::disk('upload');
|
||||
|
||||
// user is demo user, replace upload with prepared file.
|
||||
if ($userRepository->hasRole(auth()->user(), 'demo')) {
|
||||
$stubsDisk = Storage::disk('stubs');
|
||||
$content = $stubsDisk->get('demo-import.csv');
|
||||
$contentEncrypted = Crypt::encrypt($content);
|
||||
$disk->put($newName, $contentEncrypted);
|
||||
Log::debug('Replaced upload with demo file.');
|
||||
|
||||
// also set up prepared configuration.
|
||||
$configuration = json_decode($stubsDisk->get('demo-configuration.json'), true);
|
||||
$repository->setConfiguration($job, $configuration);
|
||||
Log::debug('Set configuration for demo user', $configuration);
|
||||
|
||||
// also flash info
|
||||
Session::flash('info', trans('demo.import-configure-security'));
|
||||
}
|
||||
if (!$userRepository->hasRole(auth()->user(), 'demo')) {
|
||||
// user is not demo, process original upload:
|
||||
$disk->put($newName, $contentEncrypted);
|
||||
Log::debug('Uploaded file', ['name' => $upload->getClientOriginalName(), 'size' => $upload->getSize(), 'mime' => $upload->getClientMimeType()]);
|
||||
}
|
||||
|
||||
// store configuration file's content into the job's configuration thing. Otherwise, leave it empty.
|
||||
// demo user's configuration upload is ignored completely.
|
||||
if ($request->files->has('configuration_file') && !auth()->user()->hasRole('demo')) {
|
||||
/** @var UploadedFile $configFile */
|
||||
$configFile = $request->files->get('configuration_file');
|
||||
Log::debug(
|
||||
'Uploaded configuration file',
|
||||
['name' => $configFile->getClientOriginalName(), 'size' => $configFile->getSize(), 'mime' => $configFile->getClientMimeType()]
|
||||
);
|
||||
|
||||
$configFileObject = new SplFileObject($configFile->getRealPath());
|
||||
$configRaw = $configFileObject->fread($configFileObject->getSize());
|
||||
$configuration = json_decode($configRaw, true);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if (!is_null($configuration) && is_array($configuration)) {
|
||||
Log::debug('Found configuration', $configuration);
|
||||
$repository->setConfiguration($job, $configuration);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return redirect(route('import.configure', [$job->key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
* @param string $method
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function jobInCorrectStep(ImportJob $job, string $method): bool
|
||||
{
|
||||
Log::debug('Now in jobInCorrectStep()', ['job' => $job->key, 'method' => $method]);
|
||||
switch ($method) {
|
||||
case 'configure':
|
||||
case 'process':
|
||||
return $job->status === 'import_status_never_started';
|
||||
case 'settings':
|
||||
case 'store-settings':
|
||||
Log::debug(sprintf('Job %d with key %s has status %s', $job->id, $job->key, $job->status));
|
||||
|
||||
return $job->status === 'import_configuration_saved';
|
||||
case 'finished':
|
||||
return $job->status === 'import_complete';
|
||||
case 'complete':
|
||||
return $job->status === 'settings_complete';
|
||||
case 'status':
|
||||
return ($job->status === 'settings_complete') || ($job->status === 'import_running');
|
||||
}
|
||||
|
||||
return false; // @codeCoverageIgnore
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return SetupInterface
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function makeImporter(ImportJob $job): SetupInterface
|
||||
{
|
||||
// create proper importer (depends on job)
|
||||
$type = strtolower($job->file_type);
|
||||
|
||||
// validate type:
|
||||
$validTypes = array_keys(config('firefly.import_formats'));
|
||||
|
||||
|
||||
if (in_array($type, $validTypes)) {
|
||||
/** @var SetupInterface $importer */
|
||||
$importer = app('FireflyIII\Import\Setup\\' . ucfirst($type) . 'Setup');
|
||||
$importer->setJob($job);
|
||||
|
||||
return $importer;
|
||||
}
|
||||
throw new FireflyException(sprintf('"%s" is not a valid file type', $type)); // @codeCoverageIgnore
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImportJob $job
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function redirectToCorrectStep(ImportJob $job)
|
||||
{
|
||||
Log::debug('Now in redirectToCorrectStep()', ['job' => $job->key]);
|
||||
switch ($job->status) {
|
||||
case 'import_status_never_started':
|
||||
Log::debug('Will redirect to configure()');
|
||||
|
||||
return redirect(route('import.configure', [$job->key]));
|
||||
case 'import_configuration_saved':
|
||||
Log::debug('Will redirect to settings()');
|
||||
|
||||
return redirect(route('import.settings', [$job->key]));
|
||||
case 'settings_complete':
|
||||
Log::debug('Will redirect to complete()');
|
||||
|
||||
return redirect(route('import.complete', [$job->key]));
|
||||
case 'import_complete':
|
||||
Log::debug('Will redirect to finished()');
|
||||
|
||||
return redirect(route('import.finished', [$job->key]));
|
||||
}
|
||||
|
||||
throw new FireflyException('Cannot redirect for job state ' . $job->status); // @codeCoverageIgnore
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -34,7 +35,7 @@ class JavascriptController extends Controller
|
||||
* @param AccountRepositoryInterface $repository
|
||||
* @param CurrencyRepositoryInterface $currencyRepository
|
||||
*
|
||||
* @return $this
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function accounts(AccountRepositoryInterface $repository, CurrencyRepositoryInterface $currencyRepository)
|
||||
{
|
||||
@@ -63,7 +64,7 @@ class JavascriptController extends Controller
|
||||
/**
|
||||
* @param CurrencyRepositoryInterface $repository
|
||||
*
|
||||
* @return $this
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function currencies(CurrencyRepositoryInterface $repository)
|
||||
{
|
||||
@@ -71,8 +72,8 @@ class JavascriptController extends Controller
|
||||
$data = ['currencies' => [],];
|
||||
/** @var TransactionCurrency $currency */
|
||||
foreach ($currencies as $currency) {
|
||||
$currencyId = $currency->id;
|
||||
$entry = ['name' => $currency->name, 'code' => $currency->code, 'symbol' => $currency->symbol];
|
||||
$currencyId = $currency->id;
|
||||
$entry = ['name' => $currency->name, 'code' => $currency->code, 'symbol' => $currency->symbol];
|
||||
$data['currencies'][$currencyId] = $entry;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,7 +41,6 @@ class ExchangeController extends Controller
|
||||
/** @var CurrencyRepositoryInterface $repository */
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$rate = $repository->getExchangeRate($fromCurrency, $toCurrency, $date);
|
||||
$amount = null;
|
||||
if (is_null($rate->id)) {
|
||||
Log::debug(sprintf('No cached exchange rate in database for %s to %s on %s', $fromCurrency->code, $toCurrency->code, $date->format('Y-m-d')));
|
||||
$preferred = env('EXCHANGE_RATE_SERVICE', config('firefly.preferred_exchange_service'));
|
||||
@@ -63,4 +62,4 @@ class ExchangeController extends Controller
|
||||
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,12 +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\Account\AccountTaskerInterface;
|
||||
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
|
||||
@@ -28,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;
|
||||
|
||||
/**
|
||||
@@ -65,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
|
||||
*
|
||||
@@ -116,10 +74,11 @@ class JsonController extends Controller
|
||||
* Since both this method and the chart use the exact same data, we can suffice
|
||||
* with calling the one method in the bill repository that will get this amount.
|
||||
*/
|
||||
$amount = $repository->getBillsPaidInRange($start, $end); // will be a negative amount.
|
||||
$amount = bcmul($amount, '-1');
|
||||
$amount = $repository->getBillsPaidInRange($start, $end); // will be a negative amount.
|
||||
$amount = bcmul($amount, '-1');
|
||||
$currency = Amount::getDefaultCurrency();
|
||||
|
||||
$data = ['box' => 'bills-paid', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount];
|
||||
$data = ['box' => 'bills-paid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount];
|
||||
|
||||
return Response::json($data);
|
||||
}
|
||||
@@ -131,19 +90,19 @@ class JsonController extends Controller
|
||||
*/
|
||||
public function boxBillsUnpaid(BillRepositoryInterface $repository)
|
||||
{
|
||||
$start = session('start', Carbon::now()->startOfMonth());
|
||||
$end = session('end', Carbon::now()->endOfMonth());
|
||||
$amount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount.
|
||||
$data = ['box' => 'bills-unpaid', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount];
|
||||
$start = session('start', Carbon::now()->startOfMonth());
|
||||
$end = session('end', Carbon::now()->endOfMonth());
|
||||
$amount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount.
|
||||
$currency = Amount::getDefaultCurrency();
|
||||
$data = ['box' => 'bills-unpaid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount];
|
||||
|
||||
return Response::json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccountTaskerInterface $accountTasker
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @internal param AccountTaskerInterface $accountTasker
|
||||
* @internal param AccountRepositoryInterface $repository
|
||||
*
|
||||
*/
|
||||
public function boxIn()
|
||||
@@ -167,18 +126,19 @@ class JsonController extends Controller
|
||||
->setTypes([TransactionType::DEPOSIT])
|
||||
->withOpposingAccount();
|
||||
|
||||
$amount = strval($collector->getJournals()->sum('transaction_amount'));
|
||||
$data = ['box' => 'in', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount];
|
||||
$amount = strval($collector->getJournals()->sum('transaction_amount'));
|
||||
$currency = Amount::getDefaultCurrency();
|
||||
$data = ['box' => 'in', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount];
|
||||
$cache->store($data);
|
||||
|
||||
return Response::json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccountTaskerInterface $accountTasker
|
||||
* @param AccountRepositoryInterface $repository
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @internal param AccountTaskerInterface $accountTasker
|
||||
* @internal param AccountRepositoryInterface $repository
|
||||
*
|
||||
*/
|
||||
public function boxOut()
|
||||
{
|
||||
@@ -200,9 +160,9 @@ class JsonController extends Controller
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)
|
||||
->setTypes([TransactionType::WITHDRAWAL])
|
||||
->withOpposingAccount();
|
||||
$amount = strval($collector->getJournals()->sum('transaction_amount'));
|
||||
|
||||
$data = ['box' => 'out', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount];
|
||||
$amount = strval($collector->getJournals()->sum('transaction_amount'));
|
||||
$currency = Amount::getDefaultCurrency();
|
||||
$data = ['box' => 'out', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount];
|
||||
$cache->store($data);
|
||||
|
||||
return Response::json($data);
|
||||
@@ -236,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.
|
||||
*
|
||||
@@ -292,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
|
||||
*
|
||||
@@ -364,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);
|
||||
|
||||
@@ -276,18 +276,29 @@ class PiggyBankController extends Controller
|
||||
*/
|
||||
public function postAdd(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank)
|
||||
{
|
||||
$amount = $request->get('amount');
|
||||
|
||||
$amount = $request->get('amount');
|
||||
$currency = Amount::getDefaultCurrency();
|
||||
if ($repository->canAddAmount($piggyBank, $amount)) {
|
||||
$repository->addAmount($piggyBank, $amount);
|
||||
Session::flash('success', strval(trans('firefly.added_amount_to_piggy', ['amount' => Amount::format($amount, false), 'name' => $piggyBank->name])));
|
||||
Session::flash(
|
||||
'success', strval(
|
||||
trans(
|
||||
'firefly.added_amount_to_piggy',
|
||||
['amount' => Amount::formatAnything($currency, $amount, false), 'name' => $piggyBank->name]
|
||||
)
|
||||
)
|
||||
);
|
||||
Preferences::mark();
|
||||
|
||||
return redirect(route('piggy-banks.index'));
|
||||
}
|
||||
|
||||
Log::error('Cannot add ' . $amount . ' because canAddAmount returned false.');
|
||||
Session::flash('error', strval(trans('firefly.cannot_add_amount_piggy', ['amount' => Amount::format($amount, false), 'name' => e($piggyBank->name)])));
|
||||
Session::flash(
|
||||
'error', strval(
|
||||
trans('firefly.cannot_add_amount_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)])
|
||||
)
|
||||
);
|
||||
|
||||
return redirect(route('piggy-banks.index'));
|
||||
}
|
||||
@@ -301,11 +312,13 @@ class PiggyBankController extends Controller
|
||||
*/
|
||||
public function postRemove(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank)
|
||||
{
|
||||
$amount = $request->get('amount');
|
||||
$amount = $request->get('amount');
|
||||
$currency = Amount::getDefaultCurrency();
|
||||
if ($repository->canRemoveAmount($piggyBank, $amount)) {
|
||||
$repository->removeAmount($piggyBank, $amount);
|
||||
Session::flash(
|
||||
'success', strval(trans('firefly.removed_amount_from_piggy', ['amount' => Amount::format($amount, false), 'name' => $piggyBank->name]))
|
||||
'success',
|
||||
strval(trans('firefly.removed_amount_from_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => $piggyBank->name]))
|
||||
);
|
||||
Preferences::mark();
|
||||
|
||||
@@ -314,7 +327,11 @@ class PiggyBankController extends Controller
|
||||
|
||||
$amount = strval(round($request->get('amount'), 12));
|
||||
|
||||
Session::flash('error', strval(trans('firefly.cannot_remove_from_piggy', ['amount' => Amount::format($amount, false), 'name' => e($piggyBank->name)])));
|
||||
Session::flash(
|
||||
'error', strval(
|
||||
trans('firefly.cannot_remove_from_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)])
|
||||
)
|
||||
);
|
||||
|
||||
return redirect(route('piggy-banks.index'));
|
||||
}
|
||||
@@ -380,7 +397,7 @@ class PiggyBankController extends Controller
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return redirect($this->getPreviousUri('piggy-banks.edit.uri'));
|
||||
return redirect($this->getPreviousUri('piggy-banks.create.uri'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,12 +56,8 @@ class PreferencesController extends Controller
|
||||
*/
|
||||
public function code(Google2FA $google2fa)
|
||||
{
|
||||
$domain = $this->getDomain();
|
||||
$secretKey = 'FIREFLYIII';
|
||||
$secretKey = str_pad($secretKey, intval(pow(2, ceil(log(strlen($secretKey), 2)))), 'X');
|
||||
|
||||
/** @noinspection PhpMethodParametersCountMismatchInspection */
|
||||
$secret = $google2fa->generateSecretKey(16, $secretKey);
|
||||
$domain = $this->getDomain();
|
||||
$secret = $google2fa->generateSecretKey(16);
|
||||
Session::flash('two-factor-secret', $secret);
|
||||
$image = $google2fa->getQRCodeInline('Firefly III at ' . $domain, auth()->user()->email, $secret, 150);
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class CategoryController extends Controller
|
||||
foreach ($categories as $category) {
|
||||
$spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $end);
|
||||
if (bccomp($spent, '0') !== 0) {
|
||||
$report[$category->id] = ['name' => $category->name, 'spent' => $spent];
|
||||
$report[$category->id] = ['name' => $category->name, 'spent' => $spent, 'id' => $category->id];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ namespace FireflyIII\Http\Controllers\Report;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Account\AccountTaskerInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -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,19 +362,66 @@ 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-tasker', ['transactions' => $matchingTransactions])->render();
|
||||
$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]);
|
||||
}
|
||||
|
||||
// Return json response
|
||||
$view = view('list.journals-tiny', ['transactions' => $matchingTransactions])->render();
|
||||
|
||||
return Response::json(['html' => $view, 'warning' => $warning]);
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||
use FireflyIII\Helpers\Filter\InternalTransferFilter;
|
||||
use FireflyIII\Http\Requests\TagFormRequest;
|
||||
use FireflyIII\Models\Tag;
|
||||
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;
|
||||
@@ -43,9 +43,6 @@ use View;
|
||||
class TagController extends Controller
|
||||
{
|
||||
|
||||
/** @var array */
|
||||
public $tagOptions = [];
|
||||
|
||||
/** @var TagRepositoryInterface */
|
||||
protected $repository;
|
||||
|
||||
@@ -60,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);
|
||||
}
|
||||
@@ -78,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');
|
||||
@@ -106,6 +88,8 @@ class TagController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tag
|
||||
*
|
||||
* @param Tag $tag
|
||||
*
|
||||
* @return View
|
||||
@@ -140,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');
|
||||
@@ -180,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'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,16 +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 = route('tags.show', [$tag->id]);
|
||||
|
||||
|
||||
// prep for "all" view.
|
||||
@@ -252,7 +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);
|
||||
$sum = $repository->sumOfTag($tag, null, null);
|
||||
$result = $repository->resultOfTag($tag, null, null);
|
||||
$path = route('tags.show', [$tag->id, 'all']);
|
||||
}
|
||||
|
||||
// prep for "specific date" view.
|
||||
@@ -266,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
|
||||
@@ -273,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('Count is zero, search for journals.');
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
|
||||
->setTag($tag)->withBudgetInformation()->withCategoryInformation();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('tags/show/' . $tag->id);
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0) {
|
||||
$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
|
||||
*
|
||||
@@ -405,4 +343,6 @@ class TagController extends Controller
|
||||
return $collection;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -174,14 +174,23 @@ class ConvertController extends Controller
|
||||
switch ($joined) {
|
||||
default:
|
||||
throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT:
|
||||
// one
|
||||
$destination = $sourceAccount;
|
||||
break;
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: // two
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER:
|
||||
// two
|
||||
$destination = $accountRepository->find(intval($data['destination_account_asset']));
|
||||
break;
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: // three
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: // five
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL:
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL:
|
||||
// three and five
|
||||
if ($data['destination_account_expense'] === '') {
|
||||
// destination is a cash account.
|
||||
$destination = $accountRepository->getCashAccount();
|
||||
|
||||
return $destination;
|
||||
}
|
||||
$data = [
|
||||
'name' => $data['destination_account_expense'],
|
||||
'accountType' => 'expense',
|
||||
@@ -191,8 +200,9 @@ class ConvertController extends Controller
|
||||
];
|
||||
$destination = $accountRepository->store($data);
|
||||
break;
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: // four
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: // six
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER:
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT:
|
||||
// four and six
|
||||
$destination = $destinationAccount;
|
||||
break;
|
||||
}
|
||||
@@ -219,8 +229,16 @@ class ConvertController extends Controller
|
||||
switch ($joined) {
|
||||
default:
|
||||
throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: // six
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT:
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT:
|
||||
|
||||
if ($data['source_account_revenue'] === '') {
|
||||
// destination is a cash account.
|
||||
$destination = $accountRepository->getCashAccount();
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $data['source_account_revenue'],
|
||||
'accountType' => 'revenue',
|
||||
@@ -230,14 +248,14 @@ class ConvertController extends Controller
|
||||
];
|
||||
$source = $accountRepository->store($data);
|
||||
break;
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: // two
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: // five
|
||||
case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER:
|
||||
case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL:
|
||||
$source = $sourceAccount;
|
||||
break;
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: // three
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL:
|
||||
$source = $destinationAccount;
|
||||
break;
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: // four
|
||||
case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER:
|
||||
$source = $accountRepository->find(intval($data['source_account_asset']));
|
||||
break;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use FireflyIII\Http\Requests\MassDeleteJournalRequest;
|
||||
use FireflyIII\Http\Requests\MassEditJournalRequest;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
||||
@@ -85,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);
|
||||
}
|
||||
}
|
||||
@@ -126,17 +127,15 @@ class MassController extends Controller
|
||||
$budgetRepository = app(BudgetRepositoryInterface::class);
|
||||
$budgets = $budgetRepository->getBudgets();
|
||||
|
||||
// skip transactions that have multiple destinations
|
||||
// or multiple sources:
|
||||
// skip transactions that have multiple destinations, multiple sources or are an opening balance.
|
||||
$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;
|
||||
@@ -146,6 +145,10 @@ class MassController extends Controller
|
||||
$messages[] = trans('firefly.cannot_edit_multiple_dest', ['description' => $journal->description, 'id' => $journal->id]);
|
||||
continue;
|
||||
}
|
||||
if ($journal->transactionType->type === TransactionType::OPENING_BALANCE) {
|
||||
$messages[] = trans('firefly.cannot_edit_opening_balance');
|
||||
continue;
|
||||
}
|
||||
$filtered->push($journal);
|
||||
}
|
||||
|
||||
@@ -158,13 +161,21 @@ class MassController extends Controller
|
||||
Session::flash('gaEventCategory', 'transactions');
|
||||
Session::flash('gaEventAction', 'mass-edit');
|
||||
|
||||
// set some values to be used in the edit routine:
|
||||
// collect some useful meta data for the mass edit:
|
||||
$filtered->each(
|
||||
function (TransactionJournal $journal) {
|
||||
$journal->amount = $journal->amountPositive();
|
||||
$sources = $journal->sourceAccountList();
|
||||
$destinations = $journal->destinationAccountList();
|
||||
$journal->transaction_count = $journal->transactions()->count();
|
||||
$transaction = $journal->positiveTransaction();
|
||||
$currency = $transaction->transactionCurrency;
|
||||
$journal->amount = floatval($transaction->amount);
|
||||
$sources = $journal->sourceAccountList();
|
||||
$destinations = $journal->destinationAccountList();
|
||||
$journal->transaction_count = $journal->transactions()->count();
|
||||
$journal->currency_symbol = $currency->symbol;
|
||||
$journal->transaction_type_type = $journal->transactionType->type;
|
||||
|
||||
$journal->foreign_amount = floatval($transaction->foreign_amount);
|
||||
$journal->foreign_currency = $transaction->foreignCurrency;
|
||||
|
||||
if (!is_null($sources->first())) {
|
||||
$journal->source_account_id = $sources->first()->id;
|
||||
$journal->source_account_name = $sources->first()->editname;
|
||||
@@ -201,13 +212,17 @@ 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);
|
||||
$foreignAmount = isset($request->get('foreign_amount')[$journal->id]) ? round($request->get('foreign_amount')[$journal->id], 12) : null;
|
||||
$foreignCurrencyId = isset($request->get('foreign_currency_id')[$journal->id]) ?
|
||||
intval($request->get('foreign_currency_id')[$journal->id]) : null;
|
||||
|
||||
// build data array
|
||||
$data = [
|
||||
@@ -218,16 +233,19 @@ class MassController extends Controller
|
||||
'source_account_name' => $sourceAccountName,
|
||||
'destination_account_id' => intval($destAccountId),
|
||||
'destination_account_name' => $destAccountName,
|
||||
'amount' => round($request->get('amount')[$journal->id], 12),
|
||||
'currency_id' => $journal->transaction_currency_id,
|
||||
'amount' => $foreignAmount,
|
||||
'native_amount' => $amount,
|
||||
'source_amount' => $amount,
|
||||
'date' => new Carbon($request->get('date')[$journal->id]),
|
||||
'interest_date' => $journal->interest_date,
|
||||
'book_date' => $journal->book_date,
|
||||
'process_date' => $journal->process_date,
|
||||
'budget_id' => intval($budgetId),
|
||||
'currency_id' => $foreignCurrencyId,
|
||||
'foreign_amount' => $foreignAmount,
|
||||
'destination_amount' => $foreignAmount,
|
||||
'category' => $category,
|
||||
'tags' => $tags,
|
||||
|
||||
];
|
||||
// call repository update function.
|
||||
$repository->update($journal, $data);
|
||||
@@ -235,6 +253,7 @@ class MassController extends Controller
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Preferences::mark();
|
||||
Session::flash('success', trans('firefly.mass_edited_transactions_success', ['amount' => $count]));
|
||||
|
||||
@@ -14,13 +14,16 @@ 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;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
@@ -54,6 +57,9 @@ class SingleController extends Controller
|
||||
/** @var PiggyBankRepositoryInterface */
|
||||
private $piggyBanks;
|
||||
|
||||
/** @var JournalRepositoryInterface */
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -74,6 +80,7 @@ class SingleController extends Controller
|
||||
$this->piggyBanks = app(PiggyBankRepositoryInterface::class);
|
||||
$this->attachments = app(AttachmentHelperInterface::class);
|
||||
$this->currency = app(CurrencyRepositoryInterface::class);
|
||||
$this->repository = app(JournalRepositoryInterface::class);
|
||||
|
||||
View::share('title', trans('firefly.transactions'));
|
||||
View::share('mainTitleIcon', 'fa-repeat');
|
||||
@@ -93,27 +100,35 @@ class SingleController extends Controller
|
||||
$category = $journal->categories()->first();
|
||||
$categoryName = is_null($category) ? '' : $category->name;
|
||||
$tags = join(',', $journal->tags()->get()->pluck('tag')->toArray());
|
||||
|
||||
/** @var Transaction $transaction */
|
||||
$transaction = $journal->transactions()->first();
|
||||
$amount = Steam::positive($transaction->amount);
|
||||
$foreignAmount = is_null($transaction->foreign_amount) ? null : Steam::positive($transaction->foreign_amount);
|
||||
|
||||
$preFilled = [
|
||||
'description' => $journal->description,
|
||||
'source_account_id' => $source->id,
|
||||
'source_account_name' => $source->name,
|
||||
'destination_account_id' => $destination->id,
|
||||
'destination_account_name' => $destination->name,
|
||||
'amount' => $journal->amountPositive(),
|
||||
'date' => $journal->date->format('Y-m-d'),
|
||||
'budget_id' => $budgetId,
|
||||
'category' => $categoryName,
|
||||
'tags' => $tags,
|
||||
'interest_date' => $journal->getMeta('interest_date'),
|
||||
'book_date' => $journal->getMeta('book_date'),
|
||||
'process_date' => $journal->getMeta('process_date'),
|
||||
'due_date' => $journal->getMeta('due_date'),
|
||||
'payment_date' => $journal->getMeta('payment_date'),
|
||||
'invoice_date' => $journal->getMeta('invoice_date'),
|
||||
'internal_reference' => $journal->getMeta('internal_reference'),
|
||||
'notes' => $journal->getMeta('notes'),
|
||||
'description' => $journal->description,
|
||||
'source_account_id' => $source->id,
|
||||
'source_account_name' => $source->name,
|
||||
'destination_account_id' => $destination->id,
|
||||
'destination_account_name' => $destination->name,
|
||||
'amount' => $amount,
|
||||
'source_amount' => $amount,
|
||||
'destination_amount' => $foreignAmount,
|
||||
'foreign_amount' => $foreignAmount,
|
||||
'native_amount' => $foreignAmount,
|
||||
'amount_currency_id_amount' => $transaction->foreign_currency_id ?? 0,
|
||||
'date' => (new Carbon())->format('Y-m-d'),
|
||||
'budget_id' => $budgetId,
|
||||
'category' => $categoryName,
|
||||
'tags' => $tags,
|
||||
'interest_date' => $journal->getMeta('interest_date'),
|
||||
'book_date' => $journal->getMeta('book_date'),
|
||||
'process_date' => $journal->getMeta('process_date'),
|
||||
'due_date' => $journal->getMeta('due_date'),
|
||||
'payment_date' => $journal->getMeta('payment_date'),
|
||||
'invoice_date' => $journal->getMeta('invoice_date'),
|
||||
'internal_reference' => $journal->getMeta('internal_reference'),
|
||||
'notes' => $journal->getMeta('notes'),
|
||||
];
|
||||
Session::flash('preFilled', $preFilled);
|
||||
|
||||
@@ -129,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);
|
||||
@@ -161,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)
|
||||
{
|
||||
@@ -186,12 +201,12 @@ class SingleController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param JournalRepositoryInterface $repository
|
||||
* @param TransactionJournal $transactionJournal
|
||||
* @param TransactionJournal $transactionJournal
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @internal param JournalRepositoryInterface $repository
|
||||
*/
|
||||
public function destroy(JournalRepositoryInterface $repository, TransactionJournal $transactionJournal)
|
||||
public function destroy(TransactionJournal $transactionJournal)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($this->isOpeningBalance($transactionJournal)) {
|
||||
@@ -201,7 +216,7 @@ class SingleController extends Controller
|
||||
$type = $transactionJournal->transactionTypeStr();
|
||||
Session::flash('success', strval(trans('firefly.deleted_' . strtolower($type), ['description' => e($transactionJournal->description)])));
|
||||
|
||||
$repository->delete($transactionJournal);
|
||||
$this->repository->delete($transactionJournal);
|
||||
|
||||
Preferences::mark();
|
||||
|
||||
@@ -220,15 +235,12 @@ class SingleController extends Controller
|
||||
return $this->redirectToAccount($journal);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$count = $journal->transactions()->count();
|
||||
|
||||
if ($count > 2) {
|
||||
if ($this->isSplitJournal($journal)) {
|
||||
return redirect(route('transactions.split.edit', [$journal->id]));
|
||||
}
|
||||
|
||||
$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
|
||||
@@ -238,6 +250,8 @@ class SingleController extends Controller
|
||||
$sourceAccounts = $journal->sourceAccountList();
|
||||
$destinationAccounts = $journal->destinationAccountList();
|
||||
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
|
||||
$pTransaction = $journal->positiveTransaction();
|
||||
$foreignCurrency = !is_null($pTransaction->foreignCurrency) ? $pTransaction->foreignCurrency : $pTransaction->transactionCurrency;
|
||||
$preFilled = [
|
||||
'date' => $journal->dateAsString(),
|
||||
'interest_date' => $journal->dateAsString('interest_date'),
|
||||
@@ -250,8 +264,6 @@ class SingleController extends Controller
|
||||
'source_account_name' => $sourceAccounts->first()->edit_name,
|
||||
'destination_account_id' => $destinationAccounts->first()->id,
|
||||
'destination_account_name' => $destinationAccounts->first()->edit_name,
|
||||
'amount' => $journal->amountPositive(),
|
||||
'currency' => $journal->transactionCurrency,
|
||||
|
||||
// new custom fields:
|
||||
'due_date' => $journal->dateAsString('due_date'),
|
||||
@@ -260,30 +272,25 @@ class SingleController extends Controller
|
||||
'interal_reference' => $journal->getMeta('internal_reference'),
|
||||
'notes' => $journal->getMeta('notes'),
|
||||
|
||||
// exchange rate fields
|
||||
'native_amount' => $journal->amountPositive(),
|
||||
'native_currency' => $journal->transactionCurrency,
|
||||
// amount fields
|
||||
'amount' => $pTransaction->amount,
|
||||
'source_amount' => $pTransaction->amount,
|
||||
'native_amount' => $pTransaction->amount,
|
||||
'destination_amount' => $pTransaction->foreign_amount,
|
||||
'currency' => $pTransaction->transactionCurrency,
|
||||
'source_currency' => $pTransaction->transactionCurrency,
|
||||
'native_currency' => $pTransaction->transactionCurrency,
|
||||
'foreign_currency' => $foreignCurrency,
|
||||
'destination_currency' => $foreignCurrency,
|
||||
];
|
||||
|
||||
// if user has entered a foreign currency, update some fields
|
||||
$foreignCurrencyId = intval($journal->getMeta('foreign_currency_id'));
|
||||
if ($foreignCurrencyId > 0) {
|
||||
// update some fields in pre-filled.
|
||||
// @codeCoverageIgnoreStart
|
||||
$preFilled['amount'] = $journal->getMeta('foreign_amount');
|
||||
$preFilled['currency'] = $this->currency->find(intval($journal->getMeta('foreign_currency_id')));
|
||||
// @codeCoverageIgnoreEnd
|
||||
// amounts for withdrawals and deposits:
|
||||
// amount, native_amount, source_amount, destination_amount
|
||||
if (($journal->isWithdrawal() || $journal->isDeposit()) && !is_null($pTransaction->foreign_amount)) {
|
||||
$preFilled['amount'] = $pTransaction->foreign_amount;
|
||||
$preFilled['currency'] = $pTransaction->foreignCurrency;
|
||||
}
|
||||
|
||||
if ($journal->isWithdrawal() && $destinationAccounts->first()->accountType->type == AccountType::CASH) {
|
||||
$preFilled['destination_account_name'] = '';
|
||||
}
|
||||
|
||||
if ($journal->isDeposit() && $sourceAccounts->first()->accountType->type == AccountType::CASH) {
|
||||
$preFilled['source_account_name'] = '';
|
||||
}
|
||||
|
||||
|
||||
Session::flash('preFilled', $preFilled);
|
||||
Session::flash('gaEventCategory', 'transactions');
|
||||
Session::flash('gaEventAction', 'edit-' . $what);
|
||||
@@ -319,6 +326,7 @@ class SingleController extends Controller
|
||||
|
||||
return redirect(route('transactions.create', [$request->input('what')]))->withInput();
|
||||
}
|
||||
|
||||
/** @var array $files */
|
||||
$files = $request->hasFile('attachments') ? $request->file('attachments') : null;
|
||||
$this->attachments->saveAttachmentsForModel($journal, $files);
|
||||
@@ -401,4 +409,60 @@ class SingleController extends Controller
|
||||
// redirect to previous URL.
|
||||
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
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isSplitJournal(TransactionJournal $journal): bool
|
||||
{
|
||||
$count = $this->repository->countTransactions($journal);
|
||||
|
||||
if ($count > 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -93,14 +95,24 @@ class SplitController extends Controller
|
||||
}
|
||||
|
||||
$uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size')));
|
||||
$currencies = ExpandedForm::makeSelectList($this->currencies->get());
|
||||
$assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]));
|
||||
$currencies = $this->currencies->get();
|
||||
$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,27 +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 */
|
||||
@@ -168,18 +178,17 @@ 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'),
|
||||
'journal_source_account_id' => $request->get('journal_source_account_id'),
|
||||
'journal_source_account_name' => $request->get('journal_source_account_name'),
|
||||
'journal_destination_account_id' => $request->get('journal_destination_account_id'),
|
||||
'currency_id' => $request->get('currency_id'),
|
||||
'what' => $request->get('what'),
|
||||
'date' => $request->get('date'),
|
||||
// all custom fields:
|
||||
@@ -202,8 +211,8 @@ class SplitController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param TransactionJournal $journal
|
||||
* @param SplitJournalFormRequest|Request $request
|
||||
* @param TransactionJournal $journal
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -218,10 +227,9 @@ class SplitController extends Controller
|
||||
'journal_source_account_id' => $request->old('journal_source_account_id', $sourceAccounts->first()->id),
|
||||
'journal_source_account_name' => $request->old('journal_source_account_name', $sourceAccounts->first()->name),
|
||||
'journal_destination_account_id' => $request->old('journal_destination_account_id', $destinationAccounts->first()->id),
|
||||
'currency_id' => $request->old('currency_id', $journal->transaction_currency_id),
|
||||
'destinationAccounts' => $destinationAccounts,
|
||||
'what' => strtolower($journal->transactionTypeStr()),
|
||||
'date' => $request->old('date', $journal->date),
|
||||
'date' => $request->old('date', $journal->date->format('Y-m-d')),
|
||||
'tags' => join(',', $journal->tags->pluck('tag')->toArray()),
|
||||
|
||||
// all custom fields:
|
||||
@@ -237,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;
|
||||
}
|
||||
@@ -253,14 +263,22 @@ class SplitController extends Controller
|
||||
/** @var array $transaction */
|
||||
foreach ($transactions as $index => $transaction) {
|
||||
$set = [
|
||||
'description' => $transaction['description'],
|
||||
'source_account_id' => $transaction['source_account_id'],
|
||||
'source_account_name' => $transaction['source_account_name'],
|
||||
'destination_account_id' => $transaction['destination_account_id'],
|
||||
'destination_account_name' => $transaction['destination_account_name'],
|
||||
'amount' => round($transaction['destination_amount'], 12),
|
||||
'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0,
|
||||
'category' => $transaction['category'],
|
||||
'description' => $transaction['description'],
|
||||
'source_account_id' => $transaction['source_account_id'],
|
||||
'source_account_name' => $transaction['source_account_name'],
|
||||
'destination_account_id' => $transaction['destination_account_id'],
|
||||
'destination_account_name' => $transaction['destination_account_name'],
|
||||
'amount' => round($transaction['destination_amount'], 12),
|
||||
'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0,
|
||||
'category' => $transaction['category'],
|
||||
'transaction_currency_id' => $transaction['transaction_currency_id'],
|
||||
'transaction_currency_code' => $transaction['transaction_currency_code'],
|
||||
'transaction_currency_symbol' => $transaction['transaction_currency_symbol'],
|
||||
'foreign_amount' => round($transaction['foreign_destination_amount'], 12),
|
||||
'foreign_currency_id' => $transaction['foreign_currency_id'],
|
||||
'foreign_currency_code' => $transaction['foreign_currency_code'],
|
||||
'foreign_currency_symbol' => $transaction['foreign_currency_symbol'],
|
||||
|
||||
];
|
||||
|
||||
// set initial category and/or budget:
|
||||
@@ -277,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');
|
||||
@@ -294,8 +312,12 @@ class SplitController extends Controller
|
||||
'destination_account_id' => $transaction['destination_account_id'] ?? 0,
|
||||
'destination_account_name' => $transaction['destination_account_name'] ?? '',
|
||||
'amount' => round($transaction['amount'] ?? 0, 12),
|
||||
'foreign_amount' => !isset($transaction['foreign_amount']) ? null : round($transaction['foreign_amount'] ?? 0, 12),
|
||||
'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0,
|
||||
'category' => $transaction['category'] ?? '',
|
||||
'transaction_currency_id' => intval($transaction['transaction_currency_id']),
|
||||
'foreign_currency_id' => $transaction['foreign_currency_id'] ?? null,
|
||||
|
||||
];
|
||||
}
|
||||
Log::debug(sprintf('Found %d splits in request data.', count($return)));
|
||||
@@ -303,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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,10 +16,12 @@ namespace FireflyIII\Http\Controllers;
|
||||
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\Currency\CurrencyRepositoryInterface;
|
||||
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,14 +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 = route('transactions.index', [$what]);
|
||||
|
||||
// prep for "all" view.
|
||||
if ($moment === 'all') {
|
||||
@@ -85,12 +85,14 @@ class TransactionController extends Controller
|
||||
$first = $repository->first();
|
||||
$start = $first->date ?? new Carbon;
|
||||
$end = new Carbon;
|
||||
$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)]
|
||||
@@ -108,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()
|
||||
->disableInternalFilter();
|
||||
$journals = $collector->getPaginatedJournals();
|
||||
$journals->setPath('/budgets/list/no-budget');
|
||||
$count = $journals->getCollection()->count();
|
||||
if ($count === 0) {
|
||||
$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'));
|
||||
|
||||
@@ -167,32 +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) . '"';
|
||||
|
||||
$events = $tasker->getPiggyBankEvents($journal);
|
||||
$transactions = $tasker->getTransactionsOverview($journal);
|
||||
$what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type);
|
||||
$subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"';
|
||||
$foreignCurrency = null;
|
||||
|
||||
if ($journal->hasMeta('foreign_currency_id')) {
|
||||
// @codeCoverageIgnoreStart
|
||||
/** @var CurrencyRepositoryInterface $repository */
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$foreignCurrency = $repository->find(intval($journal->getMeta('foreign_currency_id')));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions', 'foreignCurrency'));
|
||||
return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions', 'linkTypes', 'links'));
|
||||
|
||||
|
||||
}
|
||||
@@ -210,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);
|
||||
|
||||
@@ -234,36 +212,26 @@ class TransactionController extends Controller
|
||||
// count journals without budget in this period:
|
||||
/** @var JournalCollectorInterface $collector */
|
||||
$collector = app(JournalCollectorInterface::class);
|
||||
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withOpposingAccount()->setTypes($types)->disableInternalFilter();
|
||||
$set = $collector->getJournals();
|
||||
$sum = $set->sum('transaction_amount');
|
||||
$journals = $set->count();
|
||||
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withOpposingAccount()->setTypes($types);
|
||||
$collector->removeFilter(InternalTransferFilter::class);
|
||||
$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');
|
||||
@@ -272,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,19 +38,19 @@ class AccountFormRequest extends Request
|
||||
public function getAccountData(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->string('name'),
|
||||
'active' => $this->boolean('active'),
|
||||
'accountType' => $this->string('what'),
|
||||
'currency_id' => $this->integer('currency_id'),
|
||||
'virtualBalance' => $this->float('virtualBalance'),
|
||||
'iban' => $this->string('iban'),
|
||||
'BIC' => $this->string('BIC'),
|
||||
'accountNumber' => $this->string('accountNumber'),
|
||||
'accountRole' => $this->string('accountRole'),
|
||||
'openingBalance' => $this->float('openingBalance'),
|
||||
'openingBalanceDate' => $this->date('openingBalanceDate'),
|
||||
'ccType' => $this->string('ccType'),
|
||||
'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'),
|
||||
'name' => $this->string('name'),
|
||||
'active' => $this->boolean('active'),
|
||||
'accountType' => $this->string('what'),
|
||||
'currency_id' => $this->integer('currency_id'),
|
||||
'virtualBalance' => $this->float('virtualBalance'),
|
||||
'iban' => $this->string('iban'),
|
||||
'BIC' => $this->string('BIC'),
|
||||
'accountNumber' => $this->string('accountNumber'),
|
||||
'accountRole' => $this->string('accountRole'),
|
||||
'openingBalance' => $this->float('openingBalance'),
|
||||
'openingBalanceDate' => $this->date('openingBalanceDate'),
|
||||
'ccType' => $this->string('ccType'),
|
||||
'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -38,8 +38,9 @@ class ImportUploadRequest extends Request
|
||||
$types = array_keys(config('firefly.import_formats'));
|
||||
|
||||
return [
|
||||
'import_file' => 'required|file',
|
||||
'import_file_type' => 'required|in:' . join(',', $types),
|
||||
'import_file' => 'required|file',
|
||||
'import_file_type' => 'required|in:' . join(',', $types),
|
||||
'configuration_file' => 'file',
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user